diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index aa8d926..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,83 +0,0 @@ - -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- -FROM mcr.microsoft.com/dotnet/sdk:7.0 - - -# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" -# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs -# will be updated to match your local UID/GID (when using the dockerFile property). -# See https://aka.ms/vscode-remote/containers/non-root-user for details. -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -# [Optional] Version of Node.js to install. -ARG INSTALL_NODE="false" -ARG NODE_VERSION="lts/*" -ENV NVM_DIR=/usr/local/share/nvm - -# [Optional] Install the Azure CLI -ARG INSTALL_AZURE_CLI="false" - -# Configure apt and install packages -RUN curl -fsSL https://aka.ms/install-azd.sh | bash \ - && apt-get update \ - && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \ - # - # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed - && apt-get -y install git openssh-client less iproute2 procps apt-transport-https gnupg2 curl lsb-release \ - # - # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. - && groupadd --gid $USER_GID $USERNAME \ - && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ - # [Optional] Add sudo support for the non-root user - && apt-get install -y sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ - && chmod 0440 /etc/sudoers.d/$USERNAME \ - # - # [Optional] Install Node.js for ASP.NET Core Web Applicationss - && if [ "$INSTALL_NODE" = "true" ]; then \ - # - # Install nvm and Node - mkdir -p ${NVM_DIR} \ - && curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 2>&1 \ - && chown -R ${USER_UID}:${USER_GID} ${NVM_DIR} \ - && /bin/bash -c "source $NVM_DIR/nvm.sh \ - && nvm alias default ${NODE_VERSION}" 2>&1 \ - && echo '[ -s "$NVM_DIR/nvm.sh" ] && \\. "$NVM_DIR/nvm.sh" && [ -s "$NVM_DIR/bash_completion" ] && \\. "$NVM_DIR/bash_completion"' \ - | tee -a /home/${USERNAME}/.bashrc /home/${USERNAME}/.zshrc >> /root/.zshrc \ - && echo "if [ \"\$(stat -c '%U' ${NVM_DIR})\" != \"${USERNAME}\" ]; then sudo chown -R ${USER_UID}:root ${NVM_DIR}; fi" \ - | tee -a /root/.bashrc /root/.zshrc /home/${USERNAME}/.bashrc >> /home/${USERNAME}/.zshrc \ - && chown ${USER_UID}:${USER_GID} /home/${USERNAME}/.bashrc /home/${USERNAME}/.zshrc \ - && chown -R ${USER_UID}:root ${NVM_DIR} \ - # - # Install yarn - && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ - && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ - && apt-get update \ - && apt-get -y install --no-install-recommends yarn; \ - fi \ - # - # [Optional] Install the Azure CLI - && if [ "$INSTALL_AZURE_CLI" = "true" ]; then \ - echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list \ - && curl -sL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - 2>/dev/null \ - && apt-get update \ - && apt-get install -y azure-cli; \ - fi \ - # - # Install EF Core dotnet tool - && dotnet tool install dotnet-ef --tool-path /home/$USERNAME/.dotnet/tools \ - && chown -R $USERNAME /home/$USERNAME/.dotnet \ - # - # Clean up - && apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* - -# Set PATH for dotnet tools -ENV PATH "$PATH:/home/$USERNAME/.dotnet/tools" \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1606d8e..2f003b0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,74 +1,43 @@ -// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.112.0/containers/dotnetcore-3.1 +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet { - "name": "eShopOnWeb", - "build": { - "dockerfile": "Dockerfile", - "args": { - "USERNAME": "vscode", - "INSTALL_NODE": "false", - "NODE_VERSION": "lts/*", - "INSTALL_AZURE_CLI": "false" - } - }, - "features": { - "ghcr.io/devcontainers/features/azure-cli:1": { - "version": "2.38" - }, - "ghcr.io/devcontainers/features/docker-from-docker:1": { - "version": "20.10" - }, - "ghcr.io/devcontainers/features/github-cli:1": { - "version": "2" - } - }, - // Comment out to connect as root user. See https://aka.ms/vscode-remote/containers/non-root. - // make sure this is the same as USERNAME above - "remoteUser": "vscode", - "runArgs": [ - "-v", - "/var/run/docker.sock:/var/run/docker.sock" - ], - // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.profiles.linux": { - "bash": { - "path": "bash", - "icon": "terminal-bash" - } - } + "name": "eShopOnWeb", + "image": "mcr.microsoft.com/devcontainers/dotnet:0-7.0", + + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-dotnettools.csharp", + "formulahendry.dotnet-test-explorer", + "ms-vscode.vscode-node-azure-pack", + "ms-kubernetes-tools.vscode-kubernetes-tools", + "redhat.vscode-yaml" + ] + } }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-dotnettools.csharp", - "formulahendry.dotnet-test-explorer", - "ms-vscode.vscode-node-azure-pack", - "ms-kubernetes-tools.vscode-kubernetes-tools", - "redhat.vscode-yaml" - ], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [5000, 5001], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "dotnet dev-certs https --trust" - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [5000, 5001], - - // [Optional] To reuse of your local HTTPS dev cert, first export it locally using this command: - // * Windows PowerShell: - // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" - // * macOS/Linux terminal: - // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" - // - // Next, after running the command above, uncomment lines in the 'mounts' and 'remoteEnv' lines below, - // and open / rebuild the container so the settings take effect. - // - "mounts": [ - // "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" - ], - "remoteEnv": { - // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", - // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", - } - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "dotnet restore" + // [Optional] To reuse of your local HTTPS dev cert, first export it locally using this command: + // * Windows PowerShell: + // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" + // * macOS/Linux terminal: + // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" + // + // Next, after running the command above, uncomment lines in the 'mounts' and 'remoteEnv' lines below, + // and open / rebuild the container so the settings take effect. + // + // "mounts": [ + // // "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" + // ], + // "remoteEnv": { + // // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", + // // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", + // }, } diff --git a/.devcontainer/devcontainerreadme.md b/.devcontainer/devcontainerreadme.md new file mode 100644 index 0000000..ff6ee13 --- /dev/null +++ b/.devcontainer/devcontainerreadme.md @@ -0,0 +1,36 @@ +# Dev container + +This project includes a [dev container](https://containers.dev/), which lets you use a container as a full-featured dev environment. + +You can use the dev container configuration in this folder to build and run the app without needing to install any of its tools locally! You can use it in [GitHub Codespaces](https://github.com/features/codespaces) or the [VS Code Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). + +## GitHub Codespaces +Follow these steps to open this sample in a Codespace: +1. Click the **Code** drop-down menu at the top of https://github.com/dotnet-architecture/eShopOnWeb. +1. Click on the **Codespaces** tab. +1. Click **Create codespace on main** . + +For more info, check out the [GitHub documentation](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace#creating-a-codespace). + +## VS Code Dev Containers + +If you already have VS Code and Docker installed, you can click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/dotnet-architecture/eShopOnWeb) to get started. This will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +You can also follow these steps to open this sample in a container using the VS Code Dev Containers extension: + +1. If this is your first time using a development container, please ensure your system meets the pre-reqs (i.e. have Docker installed) in the [getting started steps](https://aka.ms/vscode-remote/containers/getting-started). + +2. Open a locally cloned copy of the code: + + - Clone this repository to your local filesystem. + - Press F1 and select the **Dev Containers: Open Folder in Container...** command. + - Select the cloned copy of this folder, wait for the container to start, and try things out! + +You can learn more in the [Dev Containers documentation](https://code.visualstudio.com/docs/devcontainers/containers). + +## Tips and tricks + +* Since the dev container is Linux-based, you won't be able to use LocalDB. Add ` "UseOnlyInMemoryDatabase": true,` to the [appsettings.json](../src/Web/appsettings.json) file (there's additional context on this [in the app's readme](../README.md#configuring-the-sample-to-use-sql-server)). +* If you get a `502 bad gateway` error, you may need to set your port to the https protocol. You can do this by opening the Ports view in VS Code (**Ports: Focus on Ports View**), right-clicking on the port you're using, select **Change Port Protocol**, and set **https**. +* If you are working with the same repository folder in a container and Windows, you'll want consistent line endings (otherwise you may see hundreds of changes in the SCM view). The `.gitattributes` file in the root of this repo disables line ending conversion and should prevent this. See [tips and tricks](https://code.visualstudio.com/docs/devcontainers/tips-and-tricks#_resolving-git-line-ending-issues-in-containers-resulting-in-many-modified-files) for more info. +* If you'd like to review the contents of the image used in this dev container, you can check it out in the [devcontainers/images](https://github.com/devcontainers/images/tree/main/src/dotnet) repo. diff --git a/.editorconfig b/.editorconfig index 88b30b0..459d752 100644 --- a/.editorconfig +++ b/.editorconfig @@ -141,4 +141,10 @@ csharp_preserve_single_line_blocks = true ############################### [*.vb] # Modifier preferences -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion \ No newline at end of file +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion############################### +###################################### +# Configure Nullable Reference Types # +###################################### +[{**/*Dto.cs,**/*Request.cs,**/*Response.cs}] +# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5dc46e6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 802924a..b1b48b7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,56 +1,56 @@ true + true net7.0 - + - + - - - - - - - - - - - - + + + + + + + + + + + - + - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - - + + - - + + all diff --git a/Everything.sln b/Everything.sln new file mode 100644 index 0000000..037da0d --- /dev/null +++ b/Everything.sln @@ -0,0 +1,92 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationCore", "src\ApplicationCore\ApplicationCore.csproj", "{1A5759FF-9990-4CF5-AD78-528452C5EFCC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorAdmin", "src\BlazorAdmin\BlazorAdmin.csproj", "{7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorShared", "src\BlazorShared\BlazorShared.csproj", "{6FD75683-D186-4BE3-ABD0-2324650B46B5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{35457566-83CE-44FC-A650-265CC9C544DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApi", "src\PublicApi\PublicApi.csproj", "{7F226129-E8B0-4274-87A7-347AA4F7D374}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "src\Web\Web.csproj", "{7559FA9E-7CFC-4615-8D09-3CDEFC765455}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BAA5312D-B54C-42D6-A3B9-504DD12F8250}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionalTests", "tests\FunctionalTests\FunctionalTests.csproj", "{020545FF-D985-4274-9FDB-FD8B9B32D2ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{D6829485-DD9C-42CE-BEDE-4EB0E81021AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicApiIntegrationTests", "tests\PublicApiIntegrationTests\PublicApiIntegrationTests.csproj", "{698594AE-78D3-429F-B5CC-3A6F6BCE397A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{EAD6CF0B-2979-462C-BBB9-AF723B1EB570}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A5759FF-9990-4CF5-AD78-528452C5EFCC}.Release|Any CPU.Build.0 = Release|Any CPU + {7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970}.Release|Any CPU.Build.0 = Release|Any CPU + {6FD75683-D186-4BE3-ABD0-2324650B46B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FD75683-D186-4BE3-ABD0-2324650B46B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FD75683-D186-4BE3-ABD0-2324650B46B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FD75683-D186-4BE3-ABD0-2324650B46B5}.Release|Any CPU.Build.0 = Release|Any CPU + {35457566-83CE-44FC-A650-265CC9C544DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35457566-83CE-44FC-A650-265CC9C544DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35457566-83CE-44FC-A650-265CC9C544DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35457566-83CE-44FC-A650-265CC9C544DC}.Release|Any CPU.Build.0 = Release|Any CPU + {7F226129-E8B0-4274-87A7-347AA4F7D374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F226129-E8B0-4274-87A7-347AA4F7D374}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F226129-E8B0-4274-87A7-347AA4F7D374}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F226129-E8B0-4274-87A7-347AA4F7D374}.Release|Any CPU.Build.0 = Release|Any CPU + {7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7559FA9E-7CFC-4615-8D09-3CDEFC765455}.Release|Any CPU.Build.0 = Release|Any CPU + {020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {020545FF-D985-4274-9FDB-FD8B9B32D2ED}.Release|Any CPU.Build.0 = Release|Any CPU + {D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6829485-DD9C-42CE-BEDE-4EB0E81021AC}.Release|Any CPU.Build.0 = Release|Any CPU + {698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {698594AE-78D3-429F-B5CC-3A6F6BCE397A}.Release|Any CPU.Build.0 = Release|Any CPU + {EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAD6CF0B-2979-462C-BBB9-AF723B1EB570}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1A5759FF-9990-4CF5-AD78-528452C5EFCC} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7} + {7D7D0B73-4153-4E9B-BBD1-C9D7C8AEE970} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7} + {6FD75683-D186-4BE3-ABD0-2324650B46B5} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7} + {35457566-83CE-44FC-A650-265CC9C544DC} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7} + {7F226129-E8B0-4274-87A7-347AA4F7D374} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7} + {7559FA9E-7CFC-4615-8D09-3CDEFC765455} = {8FF16BDB-352E-42A2-A25F-0B5BC3A17FD7} + {020545FF-D985-4274-9FDB-FD8B9B32D2ED} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250} + {D6829485-DD9C-42CE-BEDE-4EB0E81021AC} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250} + {698594AE-78D3-429F-B5CC-3A6F6BCE397A} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250} + {EAD6CF0B-2979-462C-BBB9-AF723B1EB570} = {BAA5312D-B54C-42D6-A3B9-504DD12F8250} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 7dd7726..c4db165 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A list of Frequently Asked Questions about this repository can be found [here](h ## eBook -This reference application is meant to support the free .PDF download ebook: [Architecting Modern Web Applications with ASP.NET Core and Azure](https://aka.ms/webappebook), updated to **ASP.NET Core 6.0**. [Also available in ePub/mobi formats](https://dotnet.microsoft.com/learn/web/aspnet-architecture). +This reference application is meant to support the free .PDF download ebook: [Architecting Modern Web Applications with ASP.NET Core and Azure](https://aka.ms/webappebook), updated to **ASP.NET Core 7.0**. [Also available in ePub/mobi formats](https://dotnet.microsoft.com/learn/web/aspnet-architecture). You can also read the book in online pages at the .NET docs here: https://docs.microsoft.com/dotnet/architecture/modern-web-apps-azure/ @@ -100,13 +100,12 @@ You can also run the samples in Docker (see below). ### Configuring the sample to use SQL Server -1. By default, the project uses a real database. If you want an in memory database, you can add in `appsettings.json` +1. By default, the project uses a real database. If you want an in memory database, you can add in the `appsettings.json` file in the Web folder ```json { "UseOnlyInMemoryDatabase": true } - ``` 1. Ensure your connection strings in `appsettings.json` point to a local SQL Server instance. @@ -140,6 +139,14 @@ You can also run the samples in Docker (see below). dotnet ef migrations add InitialIdentityModel --context appidentitydbcontext -p ../Infrastructure/Infrastructure.csproj -s Web.csproj -o Identity/Migrations ``` +## Running the sample in the dev container + +This project includes a `.devcontainer` folder with a [dev container configuration](https://containers.dev/), which lets you use a container as a full-featured dev environment. + +You can use the dev container to build and run the app without needing to install any of its tools locally! You can work in GitHub Codespaces or the VS Code Dev Containers extension. + +Learn more about using the dev container in its [readme](/.devcontainer/devcontainerreadme.md). + ## Running the sample using Docker You can run the Web sample by running these commands from the root folder (where the .sln file is located): diff --git a/src/Infrastructure/Dependencies.cs b/src/Infrastructure/Dependencies.cs index d049a96..9645676 100644 --- a/src/Infrastructure/Dependencies.cs +++ b/src/Infrastructure/Dependencies.cs @@ -10,10 +10,10 @@ public static class Dependencies { public static void ConfigureServices(IConfiguration configuration, IServiceCollection services) { - var useOnlyInMemoryDatabase = false; + bool useOnlyInMemoryDatabase = false; if (configuration["UseOnlyInMemoryDatabase"] != null) { - useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]); + useOnlyInMemoryDatabase = bool.Parse(configuration["UseOnlyInMemoryDatabase"]!); } if (useOnlyInMemoryDatabase) diff --git a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs index 3531005..8be12f6 100644 --- a/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs +++ b/src/Infrastructure/Identity/AppIdentityDbContextSeed.cs @@ -24,6 +24,9 @@ public class AppIdentityDbContextSeed var adminUser = new ApplicationUser { UserName = adminUserName, Email = adminUserName }; await userManager.CreateAsync(adminUser, AuthorizationConstants.DEFAULT_PASSWORD); adminUser = await userManager.FindByNameAsync(adminUserName); - await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS); + if (adminUser != null) + { + await userManager.AddToRoleAsync(adminUser, BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS); + } } } diff --git a/src/Infrastructure/Identity/IdentityTokenClaimService.cs b/src/Infrastructure/Identity/IdentityTokenClaimService.cs index c45f355..36de7ae 100644 --- a/src/Infrastructure/Identity/IdentityTokenClaimService.cs +++ b/src/Infrastructure/Identity/IdentityTokenClaimService.cs @@ -25,6 +25,7 @@ public class IdentityTokenClaimService : ITokenClaimsService var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(AuthorizationConstants.JWT_SECRET_KEY); var user = await _userManager.FindByNameAsync(userName); + if (user == null) throw new UserNotFoundException(userName); var roles = await _userManager.GetRolesAsync(user); var claims = new List { new Claim(ClaimTypes.Name, userName) }; diff --git a/src/Infrastructure/Identity/UserNotFoundException.cs b/src/Infrastructure/Identity/UserNotFoundException.cs new file mode 100644 index 0000000..0a98b9e --- /dev/null +++ b/src/Infrastructure/Identity/UserNotFoundException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Microsoft.eShopWeb.Infrastructure.Identity; + +public class UserNotFoundException : Exception +{ + public UserNotFoundException(string userName) : base($"No user found with username: {userName}") + { + } +} diff --git a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs index 5296e71..9571c3a 100644 --- a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.ClaimValue.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; +namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; public class ClaimValue { @@ -17,6 +12,6 @@ public class ClaimValue Value = value; } - public string Type { get; set; } - public string Value { get; set; } + public string Type { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; } diff --git a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs index 8c55fbb..8a4eaf8 100644 --- a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.UserInfo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; namespace Microsoft.eShopWeb.PublicApi.AuthEndpoints; @@ -9,7 +6,7 @@ public class UserInfo { public static readonly UserInfo Anonymous = new UserInfo(); public bool IsAuthenticated { get; set; } - public string NameClaimType { get; set; } - public string RoleClaimType { get; set; } - public IEnumerable Claims { get; set; } + public string NameClaimType { get; set; } = string.Empty; + public string RoleClaimType { get; set; } = string.Empty; + public IEnumerable Claims { get; set; } = new List(); } diff --git a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs index 0d5bece..c5cce65 100644 --- a/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs +++ b/src/PublicApi/AuthEndpoints/AuthenticateEndpoint.cs @@ -33,7 +33,8 @@ public class AuthenticateEndpoint : EndpointBaseAsync OperationId = "auth.authenticate", Tags = new[] { "AuthEndpoints" }) ] - public override async Task> HandleAsync(AuthenticateRequest request, CancellationToken cancellationToken = default) + public override async Task> HandleAsync(AuthenticateRequest request, + CancellationToken cancellationToken = default) { var response = new AuthenticateResponse(request.CorrelationId()); diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs index e9744d8..19691af 100644 --- a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.ListPagedCatalogItemRequest.cs @@ -2,8 +2,8 @@ public class ListPagedCatalogItemRequest : BaseRequest { - public int? PageSize { get; init; } - public int? PageIndex { get; init; } + public int PageSize { get; init; } + public int PageIndex { get; init; } public int? CatalogBrandId { get; init; } public int? CatalogTypeId { get; init; } diff --git a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs index 920fe4f..3e36d2f 100644 --- a/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs @@ -46,8 +46,8 @@ public class CatalogItemListPagedEndpoint : IEndpoint 0) { - response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize.Value).ToString()); + response.PageCount = int.Parse(Math.Ceiling((decimal)totalItems / request.PageSize).ToString()); } else { diff --git a/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs index b923322..15efa68 100644 --- a/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs +++ b/src/PublicApi/CatalogItemEndpoints/UpdateCatalogItemEndpoint.cs @@ -39,6 +39,10 @@ public class UpdateCatalogItemEndpoint : IEndpoint() builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)); builder.Services.Configure(builder.Configuration); -builder.Services.AddSingleton(new UriComposer(builder.Configuration.Get())); +var catalogSettings = builder.Configuration.Get() ?? new CatalogSettings(); +builder.Services.AddSingleton(new UriComposer(catalogSettings)); builder.Services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); builder.Services.AddScoped(); @@ -73,12 +73,12 @@ const string CORS_POLICY = "CorsPolicy"; builder.Services.AddCors(options => { options.AddPolicy(name: CORS_POLICY, - corsPolicyBuilder => - { - corsPolicyBuilder.WithOrigins(baseUrlConfig.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); - corsPolicyBuilder.AllowAnyMethod(); - corsPolicyBuilder.AllowAnyHeader(); - }); + corsPolicyBuilder => + { + corsPolicyBuilder.WithOrigins(baseUrlConfig!.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/')); + corsPolicyBuilder.AllowAnyMethod(); + corsPolicyBuilder.AllowAnyHeader(); + }); }); builder.Services.AddControllers(); @@ -172,12 +172,9 @@ app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); -app.UseEndpoints(endpoints => -{ - endpoints.MapControllers(); -}); - +app.MapControllers(); app.MapEndpoints(); + app.Logger.LogInformation("LAUNCHING PublicApi"); app.Run(); diff --git a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs index fdf4b1f..fe22ef7 100644 --- a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -25,7 +25,7 @@ public class LoginModel : PageModel } [BindProperty] - public InputModel? Input { get; set; } + public required InputModel Input { get; set; } public IList? ExternalLogins { get; set; } @@ -74,7 +74,8 @@ public class LoginModel : PageModel // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true //var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); - var result = await _signInManager.PasswordSignInAsync(Input?.Email, Input?.Password, false, true); + var result = await _signInManager.PasswordSignInAsync(Input!.Email!, Input!.Password!, + false, true); if (result.Succeeded) { diff --git a/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs index ce66e9e..f0165fa 100644 --- a/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/src/Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -35,7 +35,7 @@ public class RegisterModel : PageModel } [BindProperty] - public InputModel? Input { get; set; } + public required InputModel Input { get; set; } public string? ReturnUrl { get; set; } @@ -69,7 +69,7 @@ public class RegisterModel : PageModel if (ModelState.IsValid) { var user = new ApplicationUser { UserName = Input?.Email, Email = Input?.Email }; - var result = await _userManager.CreateAsync(user, Input?.Password); + var result = await _userManager.CreateAsync(user, Input?.Password!); if (result.Succeeded) { _logger.LogInformation("User created a new account with password."); @@ -82,7 +82,7 @@ public class RegisterModel : PageModel protocol: Request.Scheme); Guard.Against.Null(callbackUrl, nameof(callbackUrl)); - await _emailSender.SendEmailAsync(Input?.Email, "Confirm your email", + await _emailSender.SendEmailAsync(Input!.Email!, "Confirm your email", $"Please confirm your account by clicking here."); await _signInManager.SignInAsync(user, isPersistent: false); diff --git a/src/Web/Configuration/ConfigureCoreServices.cs b/src/Web/Configuration/ConfigureCoreServices.cs index f38657d..cf39e9a 100644 --- a/src/Web/Configuration/ConfigureCoreServices.cs +++ b/src/Web/Configuration/ConfigureCoreServices.cs @@ -4,8 +4,6 @@ using Microsoft.eShopWeb.Infrastructure.Data; using Microsoft.eShopWeb.Infrastructure.Data.Queries; using Microsoft.eShopWeb.Infrastructure.Logging; using Microsoft.eShopWeb.Infrastructure.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.eShopWeb.Web.Configuration; @@ -20,7 +18,10 @@ public static class ConfigureCoreServices services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddSingleton(new UriComposer(configuration.Get())); + + var catalogSettings = configuration.Get() ?? new CatalogSettings(); + services.AddSingleton(new UriComposer(catalogSettings)); + services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); services.AddTransient(); diff --git a/src/Web/Configuration/ConfigureWebServices.cs b/src/Web/Configuration/ConfigureWebServices.cs index f89a99a..e282276 100644 --- a/src/Web/Configuration/ConfigureWebServices.cs +++ b/src/Web/Configuration/ConfigureWebServices.cs @@ -8,7 +8,8 @@ public static class ConfigureWebServices { public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration) { - services.AddMediatR(typeof(BasketViewModelService).Assembly); + services.AddMediatR(cfg => + cfg.RegisterServicesFromAssembly(typeof(BasketViewModelService).Assembly)); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Web/Controllers/ManageController.cs b/src/Web/Controllers/ManageController.cs index 779fa36..d97473c 100644 --- a/src/Web/Controllers/ManageController.cs +++ b/src/Web/Controllers/ManageController.cs @@ -122,6 +122,11 @@ public class ManageController : Controller var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); Guard.Against.Null(callbackUrl, nameof(callbackUrl)); var email = user.Email; + if (email == null) + { + throw new ApplicationException($"No email associated with user {user.UserName}'."); + } + await _emailSender.SendEmailConfirmationAsync(email, callbackUrl); StatusMessage = "Verification email sent. Please check your email."; @@ -162,7 +167,8 @@ public class ManageController : Controller throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + var changePasswordResult = await _userManager + .ChangePasswordAsync(user, model.OldPassword!, model.NewPassword!); if (!changePasswordResult.Succeeded) { AddErrors(changePasswordResult); @@ -211,7 +217,7 @@ public class ManageController : Controller throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } - var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword); + var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword!); if (!addPasswordResult.Succeeded) { AddErrors(addPasswordResult); @@ -293,6 +299,10 @@ public class ManageController : Controller { throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } + if (!ModelState.IsValid) + { + return View(model); + } var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); if (!result.Succeeded) @@ -407,7 +417,7 @@ public class ManageController : Controller } // Strip spaces and hypens - var verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty); + string verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty) ?? ""; var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); @@ -421,7 +431,7 @@ public class ManageController : Controller await _userManager.SetTwoFactorEnabledAsync(user, true); _logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id); - var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List(); TempData[RecoveryCodesKey] = recoveryCodes.ToArray(); return RedirectToAction(nameof(ShowRecoveryCodes)); @@ -465,7 +475,7 @@ public class ManageController : Controller throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); } - var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); + var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10) ?? new List(); _logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id); var model = new ShowRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() }; @@ -533,8 +543,8 @@ public class ManageController : Controller unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } - model.SharedKey = FormatKey(unformattedKey); - model.AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); + model.SharedKey = FormatKey(unformattedKey!); + model.AuthenticatorUri = GenerateQrCodeUri(user.Email!, unformattedKey!); } } diff --git a/src/Web/Pages/Index.cshtml.cs b/src/Web/Pages/Index.cshtml.cs index f2dd2cf..ad6a81e 100644 --- a/src/Web/Pages/Index.cshtml.cs +++ b/src/Web/Pages/Index.cshtml.cs @@ -13,7 +13,7 @@ public class IndexModel : PageModel _catalogViewModelService = catalogViewModelService; } - public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); + public required CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel(); public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId) { diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 9e35944..d67117a 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -106,7 +106,7 @@ var baseUrlConfig = configSection.Get(); // Blazor Admin Required Services for Prerendering builder.Services.AddScoped(s => new HttpClient { - BaseAddress = new Uri(baseUrlConfig.WebBase) + BaseAddress = new Uri(baseUrlConfig!.WebBase) }); // add blazor services @@ -195,15 +195,13 @@ app.UseCookiePolicy(); app.UseAuthentication(); app.UseAuthorization(); -app.UseEndpoints(endpoints => -{ - endpoints.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}"); - endpoints.MapRazorPages(); - endpoints.MapHealthChecks("home_page_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("homePageHealthCheck") }); - endpoints.MapHealthChecks("api_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("apiHealthCheck") }); - //endpoints.MapBlazorHub("/admin"); - endpoints.MapFallbackToFile("index.html"); -}); + +app.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}"); +app.MapRazorPages(); +app.MapHealthChecks("home_page_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("homePageHealthCheck") }); +app.MapHealthChecks("api_health_check", new HealthCheckOptions { Predicate = check => check.Tags.Contains("apiHealthCheck") }); +//endpoints.MapBlazorHub("/admin"); +app.MapFallbackToFile("index.html"); app.Logger.LogInformation("LAUNCHING"); app.Run(); diff --git a/src/Web/Services/CachedCatalogViewModelService.cs b/src/Web/Services/CachedCatalogViewModelService.cs index d190d43..e7a506b 100644 --- a/src/Web/Services/CachedCatalogViewModelService.cs +++ b/src/Web/Services/CachedCatalogViewModelService.cs @@ -21,30 +21,30 @@ public class CachedCatalogViewModelService : ICatalogViewModelService public async Task> GetBrands() { - return await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry => + return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetBrands(); - }); + })) ?? new List(); } public async Task GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId) { var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId); - return await _cache.GetOrCreateAsync(cacheKey, async entry => + return (await _cache.GetOrCreateAsync(cacheKey, async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId); - }); + })) ?? new CatalogIndexViewModel(); } public async Task> GetTypes() { - return await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry => + return (await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry => { entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration; return await _catalogViewModelService.GetTypes(); - }); + })) ?? new List(); } } diff --git a/src/Web/ViewModels/CatalogIndexViewModel.cs b/src/Web/ViewModels/CatalogIndexViewModel.cs index 247afe5..69e09e3 100644 --- a/src/Web/ViewModels/CatalogIndexViewModel.cs +++ b/src/Web/ViewModels/CatalogIndexViewModel.cs @@ -1,13 +1,12 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.Rendering; namespace Microsoft.eShopWeb.Web.ViewModels; public class CatalogIndexViewModel { - public List? CatalogItems { get; set; } - public List? Brands { get; set; } - public List? Types { get; set; } + public List CatalogItems { get; set; } = new List(); + public List? Brands { get; set; } = new List(); + public List? Types { get; set; } = new List(); public int? BrandFilterApplied { get; set; } public int? TypesFilterApplied { get; set; } public PaginationInfoViewModel? PaginationInfo { get; set; } diff --git a/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs b/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs index 45a5975..78ddac1 100644 --- a/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs +++ b/src/Web/ViewModels/Manage/RemoveLoginViewModel.cs @@ -1,7 +1,11 @@ -namespace Microsoft.eShopWeb.Web.ViewModels.Manage; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.eShopWeb.Web.ViewModels.Manage; public class RemoveLoginViewModel { - public string? LoginProvider { get; set; } - public string? ProviderKey { get; set; } + [Required] + public string LoginProvider { get; set; } = string.Empty; + [Required] + public string ProviderKey { get; set; } = string.Empty; } diff --git a/src/Web/ViewModels/OrderViewModel.cs b/src/Web/ViewModels/OrderViewModel.cs index 7ecfd90..582a504 100644 --- a/src/Web/ViewModels/OrderViewModel.cs +++ b/src/Web/ViewModels/OrderViewModel.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; +using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate; namespace Microsoft.eShopWeb.Web.ViewModels; @@ -13,5 +11,5 @@ public class OrderViewModel public decimal Total { get; set; } public string Status => DEFAULT_STATUS; public Address? ShippingAddress { get; set; } - public List OrderItems { get; set; } = new List(); + public List OrderItems { get; set; } = new(); } diff --git a/src/Web/Views/Manage/ShowRecoverCodes.cshtml b/src/Web/Views/Manage/ShowRecoverCodes.cshtml index ed6bc95..0d012e2 100644 --- a/src/Web/Views/Manage/ShowRecoverCodes.cshtml +++ b/src/Web/Views/Manage/ShowRecoverCodes.cshtml @@ -16,10 +16,13 @@
- @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + @if (Model.RecoveryCodes != null) { - @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ @for (var row = 0; row < Model.RecoveryCodes.Length; row += 2) + { + @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
+ } }
-© 2021 GitHub, Inc. \ No newline at end of file +© 2023 GitHub, Inc. \ No newline at end of file diff --git a/src/Web/Views/Order/Detail.cshtml b/src/Web/Views/Order/Detail.cshtml index cb5db3f..097d17e 100644 --- a/src/Web/Views/Order/Detail.cshtml +++ b/src/Web/Views/Order/Detail.cshtml @@ -30,15 +30,15 @@
-
@Model.ShippingAddress.Street
+
@Model.ShippingAddress?.Street
-
@Model.ShippingAddress.City
+
@Model.ShippingAddress?.City
-
@Model.ShippingAddress.Country
+
@Model.ShippingAddress?.Country
diff --git a/src/Web/Views/Shared/_LoginPartial.cshtml b/src/Web/Views/Shared/_LoginPartial.cshtml index ac3f106..0e52fea 100644 --- a/src/Web/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/Views/Shared/_LoginPartial.cshtml @@ -1,4 +1,4 @@ -@if (Context.User.Identity.IsAuthenticated) +@if (Context!.User!.Identity!.IsAuthenticated) {
diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 35c6f0a..6ed273b 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Web/appsettings.Docker.json b/src/Web/appsettings.Docker.json index bac53d6..07ea75e 100644 --- a/src/Web/appsettings.Docker.json +++ b/src/Web/appsettings.Docker.json @@ -1,7 +1,7 @@ { "ConnectionStrings": { - "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;", - "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;" + "CatalogConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;", + "IdentityConnection": "Server=sqlserver,1433;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;User Id=sa;Password=@someThingComplicated1234;Trusted_Connection=false;TrustServerCertificate=true;" }, "baseUrls": { "apiBase": "http://localhost:5200/api/", diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index 70989a6..c2bc659 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -17,4 +17,4 @@ }, "AllowedHosts": "*" } -} \ No newline at end of file +} diff --git a/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs b/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs index 72d1f23..88b37bf 100644 --- a/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs +++ b/tests/FunctionalTests/Web/Controllers/OrderControllerIndex.cs @@ -23,7 +23,7 @@ public class OrderIndexOnGet : IClassFixture public async Task ReturnsRedirectGivenAnonymousUser() { var response = await Client.GetAsync("/order/my-orders"); - var redirectLocation = response.Headers.Location.OriginalString; + var redirectLocation = response!.Headers.Location!.OriginalString; Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Contains("/Account/Login", redirectLocation); diff --git a/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs b/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs index dd19a71..be2dbb0 100644 --- a/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs +++ b/tests/FunctionalTests/Web/Pages/Basket/BasketPageCheckout.cs @@ -45,6 +45,6 @@ public class BasketPageCheckout : IClassFixture formContent = new FormUrlEncodedContent(keyValues); var postResponse2 = await Client.PostAsync("/Basket/Checkout", formContent); - Assert.Contains("/Identity/Account/Login", postResponse2.RequestMessage.RequestUri.ToString()); + Assert.Contains("/Identity/Account/Login", postResponse2!.RequestMessage!.RequestUri!.ToString()!); } } diff --git a/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs b/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs index 4657d77..8d6c0be 100644 --- a/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs +++ b/tests/FunctionalTests/Web/Pages/Basket/CheckoutTest.cs @@ -62,7 +62,7 @@ public class CheckoutTest : IClassFixture var checkOutResponse = await Client.PostAsync("/basket/checkout", checkOutContent); var stringCheckOutResponse = await checkOutResponse.Content.ReadAsStringAsync(); - Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage.RequestUri.ToString()); + Assert.Contains("/Basket/Success", checkOutResponse.RequestMessage!.RequestUri!.ToString()); Assert.Contains("Thanks for your Order!", stringCheckOutResponse); } } diff --git a/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs b/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs index cd36458..1f68da7 100644 --- a/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs +++ b/tests/FunctionalTests/Web/Pages/Basket/IndexTest.cs @@ -52,7 +52,7 @@ public class IndexTest : IClassFixture var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync(); - Assert.Contains("/basket/update", updateResponse.RequestMessage.RequestUri.ToString()); + Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!); decimal expectedTotalAmount = 416.50M; Assert.Contains(expectedTotalAmount.ToString("N2"), stringUpdateResponse); } @@ -92,7 +92,7 @@ public class IndexTest : IClassFixture var stringUpdateResponse = await updateResponse.Content.ReadAsStringAsync(); - Assert.Contains("/basket/update", updateResponse.RequestMessage.RequestUri.ToString()); + Assert.Contains("/basket/update", updateResponse!.RequestMessage!.RequestUri!.ToString()!); Assert.Contains("Basket is empty", stringUpdateResponse); } } diff --git a/tests/FunctionalTests/Web/WebPageHelpers.cs b/tests/FunctionalTests/Web/WebPageHelpers.cs index d858bfb..1259c9e 100644 --- a/tests/FunctionalTests/Web/WebPageHelpers.cs +++ b/tests/FunctionalTests/Web/WebPageHelpers.cs @@ -22,6 +22,6 @@ public static class WebPageHelpers { var regex = new Regex(regexpression); var match = regex.Match(input); - return match.Groups.Values.LastOrDefault().Value; + return match!.Groups!.Values!.LastOrDefault()!.Value; } } diff --git a/tests/FunctionalTests/Web/WebTestFixture.cs b/tests/FunctionalTests/Web/WebTestFixture.cs index 55b2e74..d9bb491 100644 --- a/tests/FunctionalTests/Web/WebTestFixture.cs +++ b/tests/FunctionalTests/Web/WebTestFixture.cs @@ -23,6 +23,16 @@ public class TestApplication : WebApplicationFactory // Add mock/test services to the builder here builder.ConfigureServices(services => { + var descriptors = services.Where(d => + d.ServiceType == typeof(DbContextOptions) || + d.ServiceType == typeof(DbContextOptions)) + .ToList(); + + foreach (var descriptor in descriptors) + { + services.Remove(descriptor); + } + services.AddScoped(sp => { // Replace SQLite with in-memory database for tests diff --git a/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs b/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs index 03a969c..62550e6 100644 --- a/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/AuthEndpoints/AuthenticateEndpointTest.cs @@ -7,29 +7,28 @@ using Microsoft.eShopWeb.ApplicationCore.Constants; using Microsoft.eShopWeb.PublicApi.AuthEndpoints; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace PublicApiIntegrationTests.AuthEndpoints -{ - [TestClass] - public class AuthenticateEndpoint - { - [TestMethod] - [DataRow("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] - [DataRow("demouser@microsoft.com", "badpassword", false)] - [DataRow("baduser@microsoft.com", "badpassword", false)] - public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) - { - var request = new AuthenticateRequest() - { - Username = testUsername, - Password = testPassword - }; - var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - var response = await ProgramTest.NewClient.PostAsync("api/authenticate", jsonContent); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); +namespace PublicApiIntegrationTests.AuthEndpoints; - Assert.AreEqual(expectedResult, model.Result); - } +[TestClass] +public class AuthenticateEndpoint +{ + [TestMethod] + [DataRow("demouser@microsoft.com", AuthorizationConstants.DEFAULT_PASSWORD, true)] + [DataRow("demouser@microsoft.com", "badpassword", false)] + [DataRow("baduser@microsoft.com", "badpassword", false)] + public async Task ReturnsExpectedResultGivenCredentials(string testUsername, string testPassword, bool expectedResult) + { + var request = new AuthenticateRequest() + { + Username = testUsername, + Password = testPassword + }; + var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); + var response = await ProgramTest.NewClient.PostAsync("api/authenticate", jsonContent); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(expectedResult, model!.Result); } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs index 5882db0..9baefde 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemGetByIdEndpointTest.cs @@ -4,29 +4,28 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.CatalogItemEndpoints +namespace PublicApiIntegrationTests.CatalogItemEndpoints; + +[TestClass] +public class CatalogItemGetByIdEndpointTest { - [TestClass] - public class CatalogItemGetByIdEndpointTest + [TestMethod] + public async Task ReturnsItemGivenValidId() { - [TestMethod] - public async Task ReturnsItemGivenValidId() - { - var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); + var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/5"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); - Assert.AreEqual(5, model.CatalogItem.Id); - Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name); - } + Assert.AreEqual(5, model!.CatalogItem.Id); + Assert.AreEqual("Roslyn Red Sheet", model.CatalogItem.Name); + } - [TestMethod] - public async Task ReturnsNotFoundGivenInvalidId() - { - var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0"); + [TestMethod] + public async Task ReturnsNotFoundGivenInvalidId() + { + var response = await ProgramTest.NewClient.GetAsync("api/catalog-items/0"); - Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); - } + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs index 5eb3036..5470111 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CatalogItemListPagedEndpoint.cs @@ -8,66 +8,65 @@ using System.Net.Http; using System.Net; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.CatalogItemEndpoints +namespace PublicApiIntegrationTests.CatalogItemEndpoints; + +[TestClass] +public class CatalogItemListPagedEndpoint { - [TestClass] - public class CatalogItemListPagedEndpoint + [TestMethod] + public async Task ReturnsFirst10CatalogItems() { - [TestMethod] - public async Task ReturnsFirst10CatalogItems() + var client = ProgramTest.NewClient; + var response = await client.GetAsync("/api/catalog-items?pageSize=10"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(10, model!.CatalogItems.Count()); + } + + [TestMethod] + public async Task ReturnsCorrectCatalogItemsGivenPageIndex1() + { + + var pageSize = 10; + var pageIndex = 1; + + var client = ProgramTest.NewClient; + var response = await client.GetAsync($"/api/catalog-items"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + var totalItem = model!.CatalogItems.Count(); + + var response2 = await client.GetAsync($"/api/catalog-items?pageSize={pageSize}&pageIndex={pageIndex}"); + response.EnsureSuccessStatusCode(); + var stringResponse2 = await response2.Content.ReadAsStringAsync(); + var model2 = stringResponse2.FromJson(); + + var totalExpected = totalItem - (pageSize * pageIndex); + + Assert.AreEqual(totalExpected, model2!.CatalogItems.Count()); + } + + [DataTestMethod] + [DataRow("catalog-items")] + [DataRow("catalog-brands")] + [DataRow("catalog-types")] + [DataRow("catalog-items/1")] + public async Task SuccessFullMutipleParallelCall(string endpointName) + { + var client = ProgramTest.NewClient; + var tasks = new List>(); + + for (int i = 0; i < 100; i++) { - var client = ProgramTest.NewClient; - var response = await client.GetAsync("/api/catalog-items?pageSize=10"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.AreEqual(10, model.CatalogItems.Count()); + var task = client.GetAsync($"/api/{endpointName}"); + tasks.Add(task); } + await Task.WhenAll(tasks.ToList()); + var totalKO = tasks.Count(t => t.Result.StatusCode != HttpStatusCode.OK); - [TestMethod] - public async Task ReturnsCorrectCatalogItemsGivenPageIndex1() - { - - var pageSize = 10; - var pageIndex = 1; - - var client = ProgramTest.NewClient; - var response = await client.GetAsync($"/api/catalog-items"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - var totalItem = model.CatalogItems.Count(); - - var response2 = await client.GetAsync($"/api/catalog-items?pageSize={pageSize}&pageIndex={pageIndex}"); - response.EnsureSuccessStatusCode(); - var stringResponse2 = await response2.Content.ReadAsStringAsync(); - var model2 = stringResponse2.FromJson(); - - var totalExpected = totalItem - (pageSize * pageIndex); - - Assert.AreEqual(totalExpected, model2.CatalogItems.Count()); - } - - [DataTestMethod] - [DataRow("catalog-items")] - [DataRow("catalog-brands")] - [DataRow("catalog-types")] - [DataRow("catalog-items/1")] - public async Task SuccessFullMutipleParallelCall(string endpointName) - { - var client = ProgramTest.NewClient; - var tasks = new List>(); - - for (int i = 0; i < 100; i++) - { - var task = client.GetAsync($"/api/{endpointName}"); - tasks.Add(task); - } - await Task.WhenAll(tasks.ToList()); - var totalKO = tasks.Count(t => t.Result.StatusCode != HttpStatusCode.OK); - - Assert.AreEqual(0, totalKO); - } + Assert.AreEqual(0, totalKO); } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs index a85923d..6c5d79e 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/CreateCatalogItemEndpointTest.cs @@ -8,62 +8,61 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.AuthEndpoints +namespace PublicApiIntegrationTests.AuthEndpoints; + +[TestClass] +public class CreateCatalogItemEndpointTest { - [TestClass] - public class CreateCatalogItemEndpointTest + private int _testBrandId = 1; + private int _testTypeId = 2; + private string _testDescription = "test description"; + private string _testName = "test name"; + private decimal _testPrice = 1.23m; + + + [TestMethod] + public async Task ReturnsNotAuthorizedGivenNormalUserToken() { - private int _testBrandId = 1; - private int _testTypeId = 2; - private string _testDescription = "test description"; - private string _testName = "test name"; - private decimal _testPrice = 1.23m; + var jsonContent = GetValidNewItemJson(); + var token = ApiTokenHelper.GetNormalUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await client.PostAsync("api/catalog-items", jsonContent); + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } - [TestMethod] - public async Task ReturnsNotAuthorizedGivenNormalUserToken() + [TestMethod] + public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken() + { + var jsonContent = GetValidNewItemJson(); + var adminToken = ApiTokenHelper.GetAdminUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); + var response = await client.PostAsync("api/catalog-items", jsonContent); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); + + Assert.AreEqual(_testBrandId, model!.CatalogItem.CatalogBrandId); + Assert.AreEqual(_testTypeId, model.CatalogItem.CatalogTypeId); + Assert.AreEqual(_testDescription, model.CatalogItem.Description); + Assert.AreEqual(_testName, model.CatalogItem.Name); + Assert.AreEqual(_testPrice, model.CatalogItem.Price); + } + + private StringContent GetValidNewItemJson() + { + var request = new CreateCatalogItemRequest() { - var jsonContent = GetValidNewItemJson(); - var token = ApiTokenHelper.GetNormalUserToken(); - var client = ProgramTest.NewClient; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - var response = await client.PostAsync("api/catalog-items", jsonContent); + CatalogBrandId = _testBrandId, + CatalogTypeId = _testTypeId, + Description = _testDescription, + Name = _testName, + Price = _testPrice + }; + var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); - } - - [TestMethod] - public async Task ReturnsSuccessGivenValidNewItemAndAdminUserToken() - { - var jsonContent = GetValidNewItemJson(); - var adminToken = ApiTokenHelper.GetAdminUserToken(); - var client = ProgramTest.NewClient; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); - var response = await client.PostAsync("api/catalog-items", jsonContent); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); - - Assert.AreEqual(_testBrandId, model.CatalogItem.CatalogBrandId); - Assert.AreEqual(_testTypeId, model.CatalogItem.CatalogTypeId); - Assert.AreEqual(_testDescription, model.CatalogItem.Description); - Assert.AreEqual(_testName, model.CatalogItem.Name); - Assert.AreEqual(_testPrice, model.CatalogItem.Price); - } - - private StringContent GetValidNewItemJson() - { - var request = new CreateCatalogItemRequest() - { - CatalogBrandId = _testBrandId, - CatalogTypeId = _testTypeId, - Description = _testDescription, - Name = _testName, - Price = _testPrice - }; - var jsonContent = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json"); - - return jsonContent; - } + return jsonContent; } } diff --git a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs index f41976e..98c8212 100644 --- a/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs +++ b/tests/PublicApiIntegrationTests/CatalogItemEndpoints/DeleteCatalogItemEndpointTest.cs @@ -5,34 +5,33 @@ using System.Net; using System.Net.Http.Headers; using System.Threading.Tasks; -namespace PublicApiIntegrationTests.CatalogItemEndpoints +namespace PublicApiIntegrationTests.CatalogItemEndpoints; + +[TestClass] +public class DeleteCatalogItemEndpointTest { - [TestClass] - public class DeleteCatalogItemEndpointTest + [TestMethod] + public async Task ReturnsSuccessGivenValidIdAndAdminUserToken() { - [TestMethod] - public async Task ReturnsSuccessGivenValidIdAndAdminUserToken() - { - var adminToken = ApiTokenHelper.GetAdminUserToken(); - var client = ProgramTest.NewClient; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); - var response = await client.DeleteAsync("api/catalog-items/12"); - response.EnsureSuccessStatusCode(); - var stringResponse = await response.Content.ReadAsStringAsync(); - var model = stringResponse.FromJson(); + var adminToken = ApiTokenHelper.GetAdminUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); + var response = await client.DeleteAsync("api/catalog-items/12"); + response.EnsureSuccessStatusCode(); + var stringResponse = await response.Content.ReadAsStringAsync(); + var model = stringResponse.FromJson(); - Assert.AreEqual("Deleted", model.Status); - } + Assert.AreEqual("Deleted", model!.Status); + } - [TestMethod] - public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken() - { - var adminToken = ApiTokenHelper.GetAdminUserToken(); - var client = ProgramTest.NewClient; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); - var response = await client.DeleteAsync("api/catalog-items/0"); + [TestMethod] + public async Task ReturnsNotFoundGivenInvalidIdAndAdminUserToken() + { + var adminToken = ApiTokenHelper.GetAdminUserToken(); + var client = ProgramTest.NewClient; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", adminToken); + var response = await client.DeleteAsync("api/catalog-items/0"); - Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); - } + Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/PublicApiIntegrationTests/ProgramTest.cs b/tests/PublicApiIntegrationTests/ProgramTest.cs index ca92234..3f13136 100644 --- a/tests/PublicApiIntegrationTests/ProgramTest.cs +++ b/tests/PublicApiIntegrationTests/ProgramTest.cs @@ -2,26 +2,25 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Net.Http; -namespace PublicApiIntegrationTests +namespace PublicApiIntegrationTests; + +[TestClass] +public class ProgramTest { - [TestClass] - public class ProgramTest + private static WebApplicationFactory _application = new(); + + public static HttpClient NewClient { - private static WebApplicationFactory _application; - - public static HttpClient NewClient + get { - get - { - return _application.CreateClient(); - } - } - - [AssemblyInitialize] - public static void AssemblyInitialize(TestContext _) - { - _application = new WebApplicationFactory(); - + return _application.CreateClient(); } } + + [AssemblyInitialize] + public static void AssemblyInitialize(TestContext _) + { + _application = new WebApplicationFactory(); + + } }