├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .template.config └── template.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Amantinband.CleanArchitecture.Template.nuspec ├── CleanArchitecture.sln ├── Directory.Build.props ├── Directory.Packages.props ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── Clean Architecture Diagram.svg ├── Clean Architecture Template Application Layer Unit Tests.png ├── Clean Architecture Template Application Layer Unit Tests.svg ├── Clean Architecture Template Domain Layer Unit Tests.png ├── Clean Architecture Template Domain Layer Unit Tests.svg ├── Clean Architecture Template Integration Tests.png ├── Clean Architecture Template Integration Tests.svg ├── Clean Architecture Template Promo Code.png ├── Clean Architecture Template Subcutaneous Tests.png ├── Clean Architecture Template Subcutaneous Tests.svg ├── Clean Architecture Template Testing Suite.png ├── Clean Architecture Template Testing Suite.svg ├── Clean Architecture Template Title.png ├── Clean Architecture Template.png ├── Clean Architecture Template.svg └── icon.png ├── docker-compose.yml ├── global.json ├── requests ├── Reminders │ ├── CreateReminder.http │ ├── DeleteReminder.http │ ├── DismissReminder.http │ ├── GetReminder.http │ └── ListReminders.http ├── Subscriptions │ ├── CreateSubscription.http │ ├── DeleteSubscription.http │ └── GetSubscription.http └── Tokens │ └── GenerateToken.http ├── src ├── CleanArchitecture.Api │ ├── CleanArchitecture.Api.csproj │ ├── CleanArchitecture.sqlite │ ├── Controllers │ │ ├── ApiController.cs │ │ ├── RemindersController.cs │ │ ├── SubscriptionsController.cs │ │ └── TokensController.cs │ ├── DependencyInjection.cs │ ├── IAssemblyMarker.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── CleanArchitecture.Application │ ├── CleanArchitecture.Application.csproj │ ├── Common │ │ ├── Behaviors │ │ │ ├── AuthorizationBehavior.cs │ │ │ └── ValidationBehavior.cs │ │ ├── Interfaces │ │ │ ├── IAuthorizationService.cs │ │ │ ├── IDateTimeProvider.cs │ │ │ ├── IJwtTokenGenerator.cs │ │ │ ├── IRemindersRepository.cs │ │ │ └── IUsersRepository.cs │ │ └── Security │ │ │ ├── Permissions │ │ │ ├── Permission.Reminder.cs │ │ │ └── Permission.Subscription.cs │ │ │ ├── Policies │ │ │ └── Policy.cs │ │ │ ├── Request │ │ │ ├── AuthorizeAttribute.cs │ │ │ └── IAuthorizeableRequest.cs │ │ │ └── Roles │ │ │ └── Role.cs │ ├── DependencyInjection.cs │ ├── Reminders │ │ ├── Commands │ │ │ ├── DeleteReminder │ │ │ │ ├── DeleteReminderCommand.cs │ │ │ │ └── DeleteReminderCommandHandler.cs │ │ │ ├── DismissReminder │ │ │ │ ├── DismissReminderCommand.cs │ │ │ │ └── DismissReminderCommandHandler.cs │ │ │ └── SetReminder │ │ │ │ ├── SetReminderCommand.cs │ │ │ │ ├── SetReminderCommandHandler.cs │ │ │ │ └── SetReminderCommandValidator.cs │ │ ├── Events │ │ │ ├── ReminderDeletedEventHandler.cs │ │ │ ├── ReminderDismissedEventHandler.cs │ │ │ └── ReminderSetEventHandler.cs │ │ └── Queries │ │ │ ├── GetReminder │ │ │ ├── GetReminderQuery.cs │ │ │ └── GetReminderQueryHandler.cs │ │ │ └── ListReminders │ │ │ ├── ListRemindersQuery.cs │ │ │ └── ListRemindersQueryHandler.cs │ ├── Subscriptions │ │ ├── Commands │ │ │ ├── CancelSubscription │ │ │ │ ├── CancelSubscriptionCommand.cs │ │ │ │ └── CancelSubscriptionCommandHandler.cs │ │ │ └── CreateSubscription │ │ │ │ ├── CreateSubscriptionCommand.cs │ │ │ │ ├── CreateSubscriptionCommandHandler.cs │ │ │ │ └── CreateSubscriptionCommandValidator.cs │ │ ├── Common │ │ │ └── SubscriptionResult.cs │ │ ├── Events │ │ │ └── SubscriptionCanceledEventHandler.cs │ │ └── Queries │ │ │ └── GetSubscription │ │ │ ├── GetSubscriptionQuery.cs │ │ │ └── GetSubscriptionQueryHandler.cs │ └── Tokens │ │ └── Queries │ │ └── Generate │ │ ├── GenerateTokenQuery.cs │ │ ├── GenerateTokenQueryHandler.cs │ │ └── GenerateTokenResult.cs ├── CleanArchitecture.Contracts │ ├── CleanArchitecture.Contracts.csproj │ ├── Common │ │ └── SubscriptionType.cs │ ├── Reminders │ │ ├── CreateReminderRequest.cs │ │ └── ReminderResponse.cs │ ├── Subscriptions │ │ ├── CreateSubscriptionRequest.cs │ │ └── SubscriptionResponse.cs │ └── Tokens │ │ ├── GenerateTokenRequest.cs │ │ └── TokenResponse.cs ├── CleanArchitecture.Domain │ ├── CleanArchitecture.Domain.csproj │ ├── Common │ │ ├── Entity.cs │ │ └── IDomainEvent.cs │ ├── Reminders │ │ └── Reminder.cs │ └── Users │ │ ├── Calendar.cs │ │ ├── Events │ │ ├── ReminderDeletedEvent.cs │ │ ├── ReminderDismissedEvent.cs │ │ ├── ReminderSetEvent.cs │ │ └── SubscriptionCanceledEvent.cs │ │ ├── Subscription.cs │ │ ├── SubscriptionType.cs │ │ ├── User.cs │ │ └── UserErrors.cs └── CleanArchitecture.Infrastructure │ ├── CleanArchitecture.Infrastructure.csproj │ ├── Common │ ├── Middleware │ │ └── EventualConsistencyMiddleware.cs │ └── Persistence │ │ ├── AppDbContext.cs │ │ ├── FluentApiExtensions.cs │ │ ├── ListOfIdsConverter.cs │ │ └── ValueJsonConverter.cs │ ├── DependencyInjection.cs │ ├── Migrations │ ├── 20231226150412_InitialCreate.Designer.cs │ ├── 20231226150412_InitialCreate.cs │ └── AppDbContextModelSnapshot.cs │ ├── Reminders │ ├── BackgroundServices │ │ ├── EmailSettings.cs │ │ ├── ReminderEmailBackgroundService.cs │ │ └── SmtpSettings.cs │ └── Persistence │ │ ├── ReminderConfigurations.cs │ │ └── RemindersRepository.cs │ ├── RequestPipeline.cs │ ├── Security │ ├── AuthorizationService.cs │ ├── CurrentUserProvider │ │ ├── CurrentUser.cs │ │ ├── CurrentUserProvider.cs │ │ └── ICurrentUserProvider.cs │ ├── PolicyEnforcer │ │ ├── IPolicyEnforcer.cs │ │ └── PolicyEnforcer.cs │ ├── TokenGenerator │ │ ├── JwtSettings.cs │ │ └── JwtTokenGenerator.cs │ └── TokenValidation │ │ └── JwtBearerTokenValidationConfiguration.cs │ ├── Services │ └── SystemDateTimeProvider.cs │ └── Users │ └── Persistence │ ├── UserConfigurations.cs │ └── UsersRepository.cs └── tests ├── .editorconfig ├── CleanArchitecture.Api.IntegrationTests ├── CleanArchitecture.Api.IntegrationTests.csproj ├── Common │ ├── AppHttpClient.cs │ ├── Subscriptions │ │ └── SubscriptionRequestFactory.cs │ ├── Tokens │ │ └── TokenRequestFactory.cs │ └── WebApplicationFactory │ │ ├── SqliteTestDatabase.cs │ │ ├── WebAppFactory.cs │ │ └── WebAppFactoryCollection.cs ├── Controllers │ └── SubscriptionControllers.CreateSubscriptionTests.cs └── GlobalUsings.cs ├── CleanArchitecture.Application.SubcutaneousTests ├── CleanArchitecture.Application.SubcutaneousTests.csproj ├── Common │ ├── SqliteTestDatabase.cs │ ├── WebAppFactory.cs │ └── WebAppFactoryCollection.cs ├── GlobalUsings.cs ├── Reminders │ ├── Commands │ │ ├── DeleteReminder │ │ │ ├── DeleteReminder.AuthorizationTests.cs │ │ │ └── DeleteReminderTests.cs │ │ ├── DismissReminder │ │ │ ├── DismissReminder.AuthorizationTests.cs │ │ │ └── DismissReminderTests.cs │ │ └── SetReminder │ │ │ ├── SetReminder.AuthorizationTests.cs │ │ │ ├── SetReminder.ValidationTests.cs │ │ │ └── SetReminderTests.cs │ └── Queries │ │ ├── GetReminder │ │ ├── GetReminder.AuthorizationTests.cs │ │ └── GetReminderTests.cs │ │ └── ListReminders │ │ ├── ListReminders.AuthorizationTests.cs │ │ └── ListRemindersTests.cs └── Subscriptions │ ├── Commands │ ├── CancelSubscription │ │ ├── CancelSubscription.AuthorizationTests.cs │ │ └── CancelSubscriptionTests.cs │ └── CreateSubscription │ │ ├── CreateSubscription.AuthorizationTests.cs │ │ ├── CreateSubscription.ValidationTests.cs │ │ └── CreateSubscriptionTests.cs │ └── Queries │ └── GetSubscription │ ├── GetSubscription.AuthorizationTests.cs │ └── GetSubscriptionTests.cs ├── CleanArchitecture.Application.UnitTests ├── CleanArchitecture.Application.UnitTests.csproj ├── Common │ └── Behaviors │ │ ├── AuthorizationBehaviorTests.cs │ │ └── ValidationBehaviorTests.cs └── GlobalUsings.cs ├── CleanArchitecture.Domain.UnitTests ├── CleanArchitecture.Domain.UnitTests.csproj ├── GlobalUsings.cs ├── Reminders │ └── ReminderTests.cs └── Users │ └── UserTests.cs └── TestCommon ├── Reminders ├── MediatorExtensions.cs ├── ReminderCommandFactory.cs ├── ReminderFactory.cs ├── ReminderQueyFactory.cs └── ReminderValidationsExtensions.cs ├── Security ├── CurrentUserFactory.cs └── TestCurrentUserProvider.cs ├── Subscriptions ├── MediatorExtensions.cs ├── SubscriptionCommandFactory.cs ├── SubscriptionFactory.cs ├── SubscriptionQueryFactory.cs └── SubscriptionValidationExtensions.cs ├── TestCommon.csproj ├── TestConstants ├── Constants.Reminder.cs ├── Constants.Subscription.cs └── Constants.User.cs ├── TestUtilities └── NSubstitute │ └── Must.cs └── Users └── UserFactory.cs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [amantinband] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: amantinband # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 8.0.x 19 | - name: Restore dependencies 20 | run: dotnet restore 21 | - name: Build 22 | run: dotnet build -c Release --no-restore 23 | - name: Test 24 | run: dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" 25 | - name: Upload coverage to Codecov 26 | uses: codecov/codecov-action@v3 27 | with: 28 | token: ${{ secrets.CODE_COV_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish template to NuGet 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | name: Package & Publish template 11 | runs-on: windows-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup NuGet 15 | uses: NuGet/setup-nuget@v1 16 | with: 17 | nuget-version: "5.x" 18 | 19 | - name: Package 20 | run: nuget pack Amantinband.CleanArchitecture.Template.nuspec -OutputDirectory artifacts -NoDefaultExcludes 21 | 22 | - name: Publish 23 | run: nuget push .\artifacts\*.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json -SkipDuplicate 24 | -------------------------------------------------------------------------------- /.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "amantinband", 4 | "classifications": [ 5 | "Clean Architecture", 6 | "ASP.NET", 7 | "ASP.NET Core", 8 | "Web API", 9 | "Project Template" 10 | ], 11 | "name": "Clean Architecture Template", 12 | "shortName": "clean-arch", 13 | "tags": { 14 | "language": "C#", 15 | "type": "project" 16 | }, 17 | "identity": "Amantinband.CleanArchitecture.Template", 18 | "description": ".NET 8 Clean Architecture Template", 19 | "sourceName": "CleanArchitecture", 20 | "sources": [ 21 | { 22 | "source": "./", 23 | "target": "./", 24 | "exclude": [ 25 | ".git/**", 26 | ".gitignore", 27 | ".template.config/**/*" 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/CleanArchitecture.Api/bin/Debug/net8.0/CleanArchitecture.Api.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/CleanArchitecture.Api", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rest-client.environmentVariables": { 3 | "$shared": { 4 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiTGlvciIsImZhbWlseV9uYW1lIjoiRGFnYW4iLCJlbWFpbCI6Imxpb3JAZGFnYW4uY29tIiwiaWQiOiJhYWU5M2JmNS05ZTNjLTQ3YjMtYWFjZS0zMDM0NjUzYjZiYjIiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsInBlcm1pc3Npb25zIjpbInNldDpyZW1pbmRlciIsImdldDpyZW1pbmRlciIsImRpc21pc3M6cmVtaW5kZXIiLCJkZWxldGU6cmVtaW5kZXIiLCJjcmVhdGU6c3Vic2NyaXB0aW9uIiwiZGVsZXRlOnN1YnNjcmlwdGlvbiIsImdldDpzdWJzY3JpcHRpb24iXSwiZXhwIjoxNzA0MTM4MzQ4LCJpc3MiOiJSZW1pbmRlclNlcnZpY2UiLCJhdWQiOiJSZW1pbmRlclNlcnZpY2UifQ.g6pQDvZD8-9BmXLRWyKAaD_wTf632GIGNApCCnYV6Jc", 5 | "userId": "aae93bf5-9e3c-47b3-aace-3034653b6bb2", 6 | "subscriptionId": "c8ee11f0-d4bb-4b43-a448-d511924b520e", 7 | "reminderId": "08233bb1-ce29-49e2-b346-5f8b7cf61593" 8 | }, 9 | "dev": { 10 | "host": "http://localhost:5001", 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/CleanArchitecture.sln", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/CleanArchitecture.sln", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/CleanArchitecture.sln" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Amantinband.CleanArchitecture.Template.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Amantinband.CleanArchitecture.Template 5 | 1.1.1 6 | amantinband 7 | .NET 8 Clean Architecture Template 8 | https://github.com/amantinband/clean-architecture 9 | 10 | clean-architecture asp-net asp-net-core clean architecture template 11 | MIT 12 | false 13 | content/assets/icon.png 14 | content/README.md 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | enable 7 | enable 8 | preview 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 2 | WORKDIR /src 3 | COPY ["src/CleanArchitecture.Api/CleanArchitecture.Api.csproj", "CleanArchitecture.Api/"] 4 | COPY ["src/CleanArchitecture.Application/CleanArchitecture.Application.csproj", "CleanArchitecture.Application/"] 5 | COPY ["src/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj", "CleanArchitecture.Domain/"] 6 | COPY ["src/CleanArchitecture.Contracts/CleanArchitecture.Contracts.csproj", "CleanArchitecture.Contracts/"] 7 | COPY ["src/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj", "CleanArchitecture.Infrastructure/"] 8 | COPY ["Directory.Packages.props", "./"] 9 | COPY ["Directory.Build.props", "./"] 10 | RUN dotnet restore "CleanArchitecture.Api/CleanArchitecture.Api.csproj" 11 | COPY . ../ 12 | WORKDIR /src/CleanArchitecture.Api 13 | RUN dotnet build "CleanArchitecture.Api.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish --no-restore -c Release -o /app/publish 17 | 18 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 19 | ENV ASPNETCORE_HTTP_PORTS=5001 20 | EXPOSE 5001 21 | WORKDIR /app 22 | COPY --from=publish /app/publish . 23 | ENTRYPOINT ["dotnet", "CleanArchitecture.Api.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amichai Mantinband 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/Clean Architecture Template Application Layer Unit Tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Application Layer Unit Tests.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template Domain Layer Unit Tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Domain Layer Unit Tests.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template Integration Tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Integration Tests.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template Promo Code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Promo Code.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template Subcutaneous Tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Subcutaneous Tests.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template Testing Suite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Testing Suite.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template Title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template Title.png -------------------------------------------------------------------------------- /assets/Clean Architecture Template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/Clean Architecture Template.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/assets/icon.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | api: 5 | container_name: clean-architecture-api 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | ports: 10 | - "5001:5001" 11 | environment: 12 | - ASPNETCORE_ENVIRONMENT=Development 13 | restart: on-failure 14 | volumes: 15 | - ./src/CleanArchitecture.Api/CleanArchitecture.sqlite:/app/CleanArchitecture.sqlite -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "rollForward": "latestMinor", 4 | "version": "8.0.100" 5 | } 6 | } -------------------------------------------------------------------------------- /requests/Reminders/CreateReminder.http: -------------------------------------------------------------------------------- 1 | POST {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders 2 | Content-Type: application/json 3 | Authorization: Bearer {{token}} 4 | 5 | { 6 | "text": "Tell my husband I love him", 7 | "dateTime": "2024-02-16T22:20:09" 8 | } -------------------------------------------------------------------------------- /requests/Reminders/DeleteReminder.http: -------------------------------------------------------------------------------- 1 | DELETE {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders/{{reminderId}} 2 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /requests/Reminders/DismissReminder.http: -------------------------------------------------------------------------------- 1 | POST {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders/{{reminderId}}/dismiss 2 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /requests/Reminders/GetReminder.http: -------------------------------------------------------------------------------- 1 | GET {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders/{{reminderId}} 2 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /requests/Reminders/ListReminders.http: -------------------------------------------------------------------------------- 1 | GET {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}}/reminders 2 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /requests/Subscriptions/CreateSubscription.http: -------------------------------------------------------------------------------- 1 | POST {{host}}/users/{{userId}}/subscriptions 2 | Content-Type: application/json 3 | Authorization: Bearer {{token}} 4 | 5 | { 6 | "FirstName": "Lior", 7 | "LastName": "Dagan", 8 | "Email": "lior@dagan.com", 9 | "SubscriptionType": "Basic" 10 | // "SubscriptionType": "Pro" 11 | } -------------------------------------------------------------------------------- /requests/Subscriptions/DeleteSubscription.http: -------------------------------------------------------------------------------- 1 | DELETE {{host}}/users/{{userId}}/subscriptions/{{subscriptionId}} 2 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /requests/Subscriptions/GetSubscription.http: -------------------------------------------------------------------------------- 1 | GET {{host}}/users/{{userId}}/subscriptions 2 | Authorization: Bearer {{token}} -------------------------------------------------------------------------------- /requests/Tokens/GenerateToken.http: -------------------------------------------------------------------------------- 1 | POST {{host}}/tokens/generate 2 | Content-Type: application/json 3 | 4 | { 5 | "Id": "aae93bf5-9e3c-47b3-aace-3034653b6bb2", 6 | "FirstName": "Lior", 7 | "LastName": "Dagan", 8 | "Email": "lior@dagan.com", 9 | "Permissions": [ 10 | "set:reminder", 11 | "get:reminder", 12 | "dismiss:reminder", 13 | "delete:reminder", 14 | "create:subscription", 15 | "delete:subscription", 16 | "get:subscription" 17 | ], 18 | "Roles": [ 19 | "Admin" 20 | ] 21 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/CleanArchitecture.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | true 5 | 36d3aabc-1075-47d7-9b3b-c1feebf8f3bd 6 | 7 | 8 | 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | all 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/CleanArchitecture.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amantinband/clean-architecture/582281115489ac69d048e47c7363c7832e5b425a/src/CleanArchitecture.Api/CleanArchitecture.sqlite -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/Controllers/ApiController.cs: -------------------------------------------------------------------------------- 1 | using ErrorOr; 2 | 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.ModelBinding; 6 | 7 | namespace CleanArchitecture.Api.Controllers; 8 | 9 | [ApiController] 10 | [Authorize] 11 | public class ApiController : ControllerBase 12 | { 13 | protected ActionResult Problem(List errors) 14 | { 15 | if (errors.Count is 0) 16 | { 17 | return Problem(); 18 | } 19 | 20 | if (errors.All(error => error.Type == ErrorType.Validation)) 21 | { 22 | return ValidationProblem(errors); 23 | } 24 | 25 | return Problem(errors[0]); 26 | } 27 | 28 | private ObjectResult Problem(Error error) 29 | { 30 | var statusCode = error.Type switch 31 | { 32 | ErrorType.Conflict => StatusCodes.Status409Conflict, 33 | ErrorType.Validation => StatusCodes.Status400BadRequest, 34 | ErrorType.NotFound => StatusCodes.Status404NotFound, 35 | ErrorType.Unauthorized => StatusCodes.Status403Forbidden, 36 | _ => StatusCodes.Status500InternalServerError, 37 | }; 38 | 39 | return Problem(statusCode: statusCode, title: error.Description); 40 | } 41 | 42 | private ActionResult ValidationProblem(List errors) 43 | { 44 | var modelStateDictionary = new ModelStateDictionary(); 45 | 46 | errors.ForEach(error => modelStateDictionary.AddModelError(error.Code, error.Description)); 47 | 48 | return ValidationProblem(modelStateDictionary); 49 | } 50 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/Controllers/RemindersController.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Reminders.Commands.DeleteReminder; 2 | using CleanArchitecture.Application.Reminders.Commands.DismissReminder; 3 | using CleanArchitecture.Application.Reminders.Commands.SetReminder; 4 | using CleanArchitecture.Application.Reminders.Queries.GetReminder; 5 | using CleanArchitecture.Application.Reminders.Queries.ListReminders; 6 | using CleanArchitecture.Contracts.Reminders; 7 | using CleanArchitecture.Domain.Reminders; 8 | 9 | using MediatR; 10 | 11 | using Microsoft.AspNetCore.Mvc; 12 | 13 | namespace CleanArchitecture.Api.Controllers; 14 | 15 | [Route("users/{userId:guid}/subscriptions/{subscriptionId:guid}/reminders")] 16 | public class RemindersController(ISender _mediator) : ApiController 17 | { 18 | [HttpPost] 19 | public async Task CreateReminder(Guid userId, Guid subscriptionId, CreateReminderRequest request) 20 | { 21 | var command = new SetReminderCommand(userId, subscriptionId, request.Text, request.DateTime.UtcDateTime); 22 | 23 | var result = await _mediator.Send(command); 24 | 25 | return result.Match( 26 | reminder => CreatedAtAction( 27 | actionName: nameof(GetReminder), 28 | routeValues: new { UserId = userId, SubscriptionId = subscriptionId, ReminderId = reminder.Id }, 29 | value: ToDto(reminder)), 30 | Problem); 31 | } 32 | 33 | [HttpPost("{reminderId:guid}/dismiss")] 34 | public async Task DismissReminder(Guid userId, Guid subscriptionId, Guid reminderId) 35 | { 36 | var command = new DismissReminderCommand(userId, subscriptionId, reminderId); 37 | 38 | var result = await _mediator.Send(command); 39 | 40 | return result.Match( 41 | _ => NoContent(), 42 | Problem); 43 | } 44 | 45 | [HttpDelete("{reminderId:guid}")] 46 | public async Task DeleteReminder(Guid userId, Guid subscriptionId, Guid reminderId) 47 | { 48 | var command = new DeleteReminderCommand(userId, subscriptionId, reminderId); 49 | 50 | var result = await _mediator.Send(command); 51 | 52 | return result.Match( 53 | _ => NoContent(), 54 | Problem); 55 | } 56 | 57 | [HttpGet("{reminderId:guid}")] 58 | public async Task GetReminder(Guid userId, Guid subscriptionId, Guid reminderId) 59 | { 60 | var query = new GetReminderQuery(userId, subscriptionId, reminderId); 61 | 62 | var result = await _mediator.Send(query); 63 | 64 | return result.Match( 65 | reminder => Ok(ToDto(reminder)), 66 | Problem); 67 | } 68 | 69 | [HttpGet] 70 | public async Task ListReminders(Guid userId, Guid subscriptionId) 71 | { 72 | var query = new ListRemindersQuery(userId, subscriptionId); 73 | 74 | var result = await _mediator.Send(query); 75 | 76 | return result.Match( 77 | reminders => Ok(reminders.ConvertAll(ToDto)), 78 | Problem); 79 | } 80 | 81 | private ReminderResponse ToDto(Reminder reminder) => 82 | new(reminder.Id, reminder.Text, reminder.DateTime, reminder.IsDismissed); 83 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/Controllers/SubscriptionsController.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Subscriptions.Commands.CancelSubscription; 2 | using CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 3 | using CleanArchitecture.Application.Subscriptions.Common; 4 | using CleanArchitecture.Application.Subscriptions.Queries.GetSubscription; 5 | using CleanArchitecture.Contracts.Subscriptions; 6 | 7 | using MediatR; 8 | 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | using DomainSubscriptionType = CleanArchitecture.Domain.Users.SubscriptionType; 12 | using SubscriptionType = CleanArchitecture.Contracts.Common.SubscriptionType; 13 | 14 | namespace CleanArchitecture.Api.Controllers; 15 | 16 | [Route("users/{userId:guid}/subscriptions")] 17 | public class SubscriptionsController(IMediator _mediator) : ApiController 18 | { 19 | [HttpPost] 20 | public async Task CreateSubscription(Guid userId, CreateSubscriptionRequest request) 21 | { 22 | if (!DomainSubscriptionType.TryFromName(request.SubscriptionType.ToString(), out var subscriptionType)) 23 | { 24 | return Problem( 25 | statusCode: StatusCodes.Status400BadRequest, 26 | detail: "Invalid plan type"); 27 | } 28 | 29 | var command = new CreateSubscriptionCommand( 30 | userId, 31 | request.FirstName, 32 | request.LastName, 33 | request.Email, 34 | subscriptionType); 35 | 36 | var result = await _mediator.Send(command); 37 | 38 | return result.Match( 39 | subscription => CreatedAtAction( 40 | actionName: nameof(GetSubscription), 41 | routeValues: new { UserId = userId }, 42 | value: ToDto(subscription)), 43 | Problem); 44 | } 45 | 46 | [HttpDelete("{subscriptionId:guid}")] 47 | public async Task DeleteSubscription(Guid userId, Guid subscriptionId) 48 | { 49 | var command = new CancelSubscriptionCommand(userId, subscriptionId); 50 | 51 | var result = await _mediator.Send(command); 52 | 53 | return result.Match( 54 | _ => NoContent(), 55 | Problem); 56 | } 57 | 58 | [HttpGet] 59 | public async Task GetSubscription(Guid userId) 60 | { 61 | var query = new GetSubscriptionQuery(userId); 62 | 63 | var result = await _mediator.Send(query); 64 | 65 | return result.Match( 66 | user => Ok(ToDto(user)), 67 | Problem); 68 | } 69 | 70 | private static SubscriptionType ToDto(DomainSubscriptionType subscriptionType) => 71 | subscriptionType.Name switch 72 | { 73 | nameof(DomainSubscriptionType.Basic) => SubscriptionType.Basic, 74 | nameof(DomainSubscriptionType.Pro) => SubscriptionType.Pro, 75 | _ => throw new InvalidOperationException(), 76 | }; 77 | 78 | private static SubscriptionResponse ToDto(SubscriptionResult subscriptionResult) => 79 | new( 80 | subscriptionResult.Id, 81 | subscriptionResult.UserId, 82 | ToDto(subscriptionResult.SubscriptionType)); 83 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/Controllers/TokensController.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Authentication.Queries.Login; 2 | using CleanArchitecture.Application.Tokens.Queries.Generate; 3 | using CleanArchitecture.Contracts.Common; 4 | using CleanArchitecture.Contracts.Tokens; 5 | 6 | using MediatR; 7 | 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | using DomainSubscriptionType = CleanArchitecture.Domain.Users.SubscriptionType; 12 | 13 | namespace CleanArchitecture.Api.Controllers; 14 | 15 | [Route("tokens")] 16 | [AllowAnonymous] 17 | public class TokensController(ISender _mediator) : ApiController 18 | { 19 | [HttpPost("generate")] 20 | public async Task GenerateToken(GenerateTokenRequest request) 21 | { 22 | if (!DomainSubscriptionType.TryFromName(request.SubscriptionType.ToString(), out var plan)) 23 | { 24 | return Problem( 25 | statusCode: StatusCodes.Status400BadRequest, 26 | detail: "Invalid subscription type"); 27 | } 28 | 29 | var query = new GenerateTokenQuery( 30 | request.Id, 31 | request.FirstName, 32 | request.LastName, 33 | request.Email, 34 | plan, 35 | request.Permissions, 36 | request.Roles); 37 | 38 | var result = await _mediator.Send(query); 39 | 40 | return result.Match( 41 | generateTokenResult => Ok(ToDto(generateTokenResult)), 42 | Problem); 43 | } 44 | 45 | private static TokenResponse ToDto(GenerateTokenResult authResult) 46 | { 47 | return new TokenResponse( 48 | authResult.Id, 49 | authResult.FirstName, 50 | authResult.LastName, 51 | authResult.Email, 52 | ToDto(authResult.SubscriptionType), 53 | authResult.Token); 54 | } 55 | 56 | private static SubscriptionType ToDto(DomainSubscriptionType subscriptionType) => 57 | subscriptionType.Name switch 58 | { 59 | nameof(DomainSubscriptionType.Basic) => SubscriptionType.Basic, 60 | nameof(DomainSubscriptionType.Pro) => SubscriptionType.Pro, 61 | _ => throw new InvalidOperationException(), 62 | }; 63 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Api; 2 | 3 | public static class DependencyInjection 4 | { 5 | public static IServiceCollection AddPresentation(this IServiceCollection services) 6 | { 7 | services.AddControllers(); 8 | services.AddEndpointsApiExplorer(); 9 | services.AddSwaggerGen(); 10 | services.AddProblemDetails(); 11 | 12 | return services; 13 | } 14 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/IAssemblyMarker.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Api; 2 | 3 | public interface IAssemblyMarker { } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Api; 2 | using CleanArchitecture.Application; 3 | using CleanArchitecture.Infrastructure; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | { 7 | builder.Services 8 | .AddPresentation() 9 | .AddApplication() 10 | .AddInfrastructure(builder.Configuration); 11 | } 12 | 13 | var app = builder.Build(); 14 | { 15 | app.UseExceptionHandler(); 16 | app.UseInfrastructure(); 17 | 18 | if (app.Environment.IsDevelopment()) 19 | { 20 | app.UseSwagger(); 21 | app.UseSwaggerUI(); 22 | } 23 | 24 | app.UseHttpsRedirection(); 25 | app.UseAuthorization(); 26 | app.MapControllers(); 27 | 28 | app.Run(); 29 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5519", 8 | "sslPort": 44365 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5001", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7250;http://localhost:5001", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | }, 40 | "Docker": { 41 | "commandName": "Docker", 42 | "launchBrowser": false, 43 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 44 | "environmentVariables": { 45 | "ASPNETCORE_ENVIRONMENT": "Development", 46 | }, 47 | "publishAllPorts": true, 48 | "httpPort": 5001, 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "JwtSettings": { 9 | "Secret": "a-very-super-secret-key-that-is-long-enough", 10 | "TokenExpirationInMinutes": 60, 11 | "Issuer": "ReminderService", 12 | "Audience": "ReminderService" 13 | }, 14 | "EmailSettings": { 15 | "EnableEmailNotifications": false, 16 | "DefaultFromEmail": "your-email@gmail.com (also, change EnableEmailNotifications to true 👆)", 17 | "SmtpSettings": { 18 | "Server": "smtp.gmail.com", 19 | "Port": 587, 20 | "Username": "your-email@gmail.com", 21 | "Password": "your-password" 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "JwtSettings": { 10 | "Secret": "", 11 | "TokenExpirationInMinutes": 0, 12 | "Issuer": "", 13 | "Audience": "" 14 | }, 15 | "EmailSettings": { 16 | "EnableEmailNotifications": false, 17 | "DefaultFromEmail": "", 18 | "SmtpSettings": { 19 | "Server": "", 20 | "Port": 0, 21 | "Username": "", 22 | "Password": "" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/CleanArchitecture.Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Behaviors/AuthorizationBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using CleanArchitecture.Application.Common.Interfaces; 4 | using CleanArchitecture.Application.Common.Security.Request; 5 | 6 | using ErrorOr; 7 | 8 | using MediatR; 9 | 10 | namespace CleanArchitecture.Application.Common.Behaviors; 11 | 12 | public class AuthorizationBehavior( 13 | IAuthorizationService _authorizationService) 14 | : IPipelineBehavior 15 | where TRequest : IAuthorizeableRequest 16 | where TResponse : IErrorOr 17 | { 18 | public async Task Handle( 19 | TRequest request, 20 | RequestHandlerDelegate next, 21 | CancellationToken cancellationToken) 22 | { 23 | var authorizationAttributes = request.GetType() 24 | .GetCustomAttributes() 25 | .ToList(); 26 | 27 | if (authorizationAttributes.Count == 0) 28 | { 29 | return await next(); 30 | } 31 | 32 | var requiredPermissions = authorizationAttributes 33 | .SelectMany(authorizationAttribute => authorizationAttribute.Permissions?.Split(',') ?? []) 34 | .ToList(); 35 | 36 | var requiredRoles = authorizationAttributes 37 | .SelectMany(authorizationAttribute => authorizationAttribute.Roles?.Split(',') ?? []) 38 | .ToList(); 39 | 40 | var requiredPolicies = authorizationAttributes 41 | .SelectMany(authorizationAttribute => authorizationAttribute.Policies?.Split(',') ?? []) 42 | .ToList(); 43 | 44 | var authorizationResult = _authorizationService.AuthorizeCurrentUser( 45 | request, 46 | requiredRoles, 47 | requiredPermissions, 48 | requiredPolicies); 49 | 50 | return authorizationResult.IsError 51 | ? (dynamic)authorizationResult.Errors 52 | : await next(); 53 | } 54 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Behaviors/ValidationBehavior.cs: -------------------------------------------------------------------------------- 1 | using ErrorOr; 2 | 3 | using FluentValidation; 4 | 5 | using MediatR; 6 | 7 | namespace CleanArchitecture.Application.Common.Behaviors; 8 | 9 | public class ValidationBehavior(IValidator? validator = null) 10 | : IPipelineBehavior 11 | where TRequest : IRequest 12 | where TResponse : IErrorOr 13 | { 14 | private readonly IValidator? _validator = validator; 15 | 16 | public async Task Handle( 17 | TRequest request, 18 | RequestHandlerDelegate next, 19 | CancellationToken cancellationToken) 20 | { 21 | if (_validator is null) 22 | { 23 | return await next(); 24 | } 25 | 26 | var validationResult = await _validator.ValidateAsync(request, cancellationToken); 27 | 28 | if (validationResult.IsValid) 29 | { 30 | return await next(); 31 | } 32 | 33 | var errors = validationResult.Errors 34 | .ConvertAll(error => Error.Validation( 35 | code: error.PropertyName, 36 | description: error.ErrorMessage)); 37 | 38 | return (dynamic)errors; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Interfaces/IAuthorizationService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Request; 2 | 3 | using ErrorOr; 4 | 5 | namespace CleanArchitecture.Application.Common.Interfaces; 6 | 7 | public interface IAuthorizationService 8 | { 9 | ErrorOr AuthorizeCurrentUser( 10 | IAuthorizeableRequest request, 11 | List requiredRoles, 12 | List requiredPermissions, 13 | List requiredPolicies); 14 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Interfaces/IDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.Common.Interfaces; 2 | 3 | public interface IDateTimeProvider 4 | { 5 | public DateTime UtcNow { get; } 6 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Interfaces/IJwtTokenGenerator.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | 3 | namespace CleanArchitecture.Application.Common.Interfaces; 4 | 5 | public interface IJwtTokenGenerator 6 | { 7 | string GenerateToken( 8 | Guid id, 9 | string firstName, 10 | string lastName, 11 | string email, 12 | SubscriptionType subscriptionType, 13 | List permissions, 14 | List roles); 15 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Interfaces/IRemindersRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Reminders; 2 | 3 | namespace CleanArchitecture.Application.Common.Interfaces; 4 | 5 | public interface IRemindersRepository 6 | { 7 | Task AddAsync(Reminder reminder, CancellationToken cancellationToken); 8 | Task GetByIdAsync(Guid reminderId, CancellationToken cancellationToken); 9 | Task> ListBySubscriptionIdAsync(Guid subscriptionId, CancellationToken cancellationToken); 10 | Task RemoveAsync(Reminder reminder, CancellationToken cancellationToken); 11 | Task RemoveRangeAsync(List reminders, CancellationToken cancellationToken); 12 | Task UpdateAsync(Reminder reminder, CancellationToken cancellationToken); 13 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Interfaces/IUsersRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | 3 | namespace CleanArchitecture.Application.Common.Interfaces; 4 | 5 | public interface IUsersRepository 6 | { 7 | Task AddAsync(User user, CancellationToken cancellationToken); 8 | Task GetByIdAsync(Guid userId, CancellationToken cancellationToken); 9 | Task GetBySubscriptionIdAsync(Guid subscriptionId, CancellationToken cancellationToken); 10 | Task RemoveAsync(User user, CancellationToken cancellationToken); 11 | Task UpdateAsync(User user, CancellationToken cancellationToken); 12 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Security/Permissions/Permission.Reminder.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.Common.Security.Permissions; 2 | 3 | public static partial class Permission 4 | { 5 | public static class Reminder 6 | { 7 | public const string Set = "set:reminder"; 8 | public const string Get = "get:reminder"; 9 | public const string Dismiss = "dismiss:reminder"; 10 | public const string Delete = "delete:reminder"; 11 | } 12 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Security/Permissions/Permission.Subscription.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.Common.Security.Permissions; 2 | 3 | public static partial class Permission 4 | { 5 | public static class Subscription 6 | { 7 | public const string Create = "create:subscription"; 8 | public const string Delete = "delete:subscription"; 9 | public const string Get = "get:subscription"; 10 | } 11 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Security/Policies/Policy.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.Common.Security.Policies; 2 | 3 | public static class Policy 4 | { 5 | public const string SelfOrAdmin = "SelfOrAdminPolicy"; 6 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Security/Request/AuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.Common.Security.Request; 2 | 3 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] 4 | public class AuthorizeAttribute : Attribute 5 | { 6 | public string? Permissions { get; set; } 7 | public string? Roles { get; set; } 8 | public string? Policies { get; set; } 9 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Security/Request/IAuthorizeableRequest.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace CleanArchitecture.Application.Common.Security.Request; 4 | 5 | public interface IAuthorizeableRequest : IRequest 6 | { 7 | Guid UserId { get; } 8 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Common/Security/Roles/Role.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.Common.Security.Roles; 2 | 3 | public static class Role 4 | { 5 | public const string Admin = "Admin"; 6 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Behaviors; 2 | 3 | using FluentValidation; 4 | 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace CleanArchitecture.Application; 8 | 9 | public static class DependencyInjection 10 | { 11 | public static IServiceCollection AddApplication(this IServiceCollection services) 12 | { 13 | services.AddMediatR(options => 14 | { 15 | options.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly); 16 | 17 | options.AddOpenBehavior(typeof(AuthorizationBehavior<,>)); 18 | options.AddOpenBehavior(typeof(ValidationBehavior<,>)); 19 | }); 20 | 21 | services.AddValidatorsFromAssemblyContaining(typeof(DependencyInjection)); 22 | return services; 23 | } 24 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/DeleteReminder/DeleteReminderCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | 5 | using ErrorOr; 6 | 7 | namespace CleanArchitecture.Application.Reminders.Commands.DeleteReminder; 8 | 9 | [Authorize(Permissions = Permission.Reminder.Delete, Policies = Policy.SelfOrAdmin)] 10 | public record DeleteReminderCommand(Guid UserId, Guid SubscriptionId, Guid ReminderId) 11 | : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/DeleteReminder/DeleteReminderCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | 3 | using ErrorOr; 4 | 5 | using MediatR; 6 | 7 | namespace CleanArchitecture.Application.Reminders.Commands.DeleteReminder; 8 | 9 | public class DeleteReminderCommandHandler( 10 | IRemindersRepository _remindersRepository, 11 | IUsersRepository _usersRepository) : IRequestHandler> 12 | { 13 | public async Task> Handle(DeleteReminderCommand request, CancellationToken cancellationToken) 14 | { 15 | var reminder = await _remindersRepository.GetByIdAsync(request.ReminderId, cancellationToken); 16 | 17 | var user = await _usersRepository.GetByIdAsync(request.UserId, cancellationToken); 18 | 19 | if (reminder is null || user is null) 20 | { 21 | return Error.NotFound(description: "Reminder not found"); 22 | } 23 | 24 | var deleteReminderResult = user.DeleteReminder(reminder); 25 | 26 | if (deleteReminderResult.IsError) 27 | { 28 | return deleteReminderResult.Errors; 29 | } 30 | 31 | await _usersRepository.UpdateAsync(user, cancellationToken); 32 | 33 | return Result.Success; 34 | } 35 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/DismissReminder/DismissReminderCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | 5 | using ErrorOr; 6 | 7 | namespace CleanArchitecture.Application.Reminders.Commands.DismissReminder; 8 | 9 | [Authorize(Permissions = Permission.Reminder.Dismiss, Policies = Policy.SelfOrAdmin)] 10 | public record DismissReminderCommand(Guid UserId, Guid SubscriptionId, Guid ReminderId) 11 | : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/DismissReminder/DismissReminderCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | 3 | using ErrorOr; 4 | 5 | using MediatR; 6 | 7 | namespace CleanArchitecture.Application.Reminders.Commands.DismissReminder; 8 | 9 | public class DismissReminderCommandHandler( 10 | IUsersRepository _usersRepository) 11 | : IRequestHandler> 12 | { 13 | public async Task> Handle(DismissReminderCommand request, CancellationToken cancellationToken) 14 | { 15 | var user = await _usersRepository.GetByIdAsync(request.UserId, cancellationToken); 16 | 17 | if (user is null) 18 | { 19 | return Error.NotFound(description: "Reminder not found"); 20 | } 21 | 22 | var dismissReminderResult = user.DismissReminder(request.ReminderId); 23 | 24 | if (dismissReminderResult.IsError) 25 | { 26 | return dismissReminderResult.Errors; 27 | } 28 | 29 | await _usersRepository.UpdateAsync(user, cancellationToken); 30 | 31 | return Result.Success; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/SetReminder/SetReminderCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | using CleanArchitecture.Domain.Reminders; 5 | 6 | using ErrorOr; 7 | 8 | namespace CleanArchitecture.Application.Reminders.Commands.SetReminder; 9 | 10 | [Authorize(Permissions = Permission.Reminder.Set, Policies = Policy.SelfOrAdmin)] 11 | public record SetReminderCommand(Guid UserId, Guid SubscriptionId, string Text, DateTime DateTime) 12 | : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/SetReminder/SetReminderCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Reminders; 3 | 4 | using ErrorOr; 5 | 6 | using MediatR; 7 | 8 | namespace CleanArchitecture.Application.Reminders.Commands.SetReminder; 9 | 10 | public class SetReminderCommandHandler(IUsersRepository _usersRepository) 11 | : IRequestHandler> 12 | { 13 | public async Task> Handle(SetReminderCommand command, CancellationToken cancellationToken) 14 | { 15 | var reminder = new Reminder( 16 | command.UserId, 17 | command.SubscriptionId, 18 | command.Text, 19 | command.DateTime); 20 | 21 | var user = await _usersRepository.GetBySubscriptionIdAsync(command.SubscriptionId, cancellationToken); 22 | 23 | if (user is null) 24 | { 25 | return Error.NotFound(description: "Subscription not found"); 26 | } 27 | 28 | var setReminderResult = user.SetReminder(reminder); 29 | 30 | if (setReminderResult.IsError) 31 | { 32 | return setReminderResult.Errors; 33 | } 34 | 35 | await _usersRepository.UpdateAsync(user, cancellationToken); 36 | 37 | return reminder; 38 | } 39 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Commands/SetReminder/SetReminderCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | 3 | using FluentValidation; 4 | 5 | namespace CleanArchitecture.Application.Reminders.Commands.SetReminder; 6 | 7 | public class SetReminderCommandValidator : AbstractValidator 8 | { 9 | public SetReminderCommandValidator(IDateTimeProvider dateTimeProvider) 10 | { 11 | RuleFor(x => x.DateTime).GreaterThan(dateTimeProvider.UtcNow); 12 | RuleFor(x => x.Text).MinimumLength(3).MaximumLength(10000); 13 | } 14 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Events/ReminderDeletedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Users.Events; 3 | 4 | using MediatR; 5 | 6 | namespace CleanArchitecture.Application.Reminders.Events; 7 | 8 | public class ReminderDeletedEventHandler(IRemindersRepository _remindersRepository) : INotificationHandler 9 | { 10 | public async Task Handle(ReminderDeletedEvent notification, CancellationToken cancellationToken) 11 | { 12 | var reminder = await _remindersRepository.GetByIdAsync(notification.ReminderId, cancellationToken) 13 | ?? throw new InvalidOperationException(); 14 | 15 | await _remindersRepository.RemoveAsync(reminder, cancellationToken); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Events/ReminderDismissedEventHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Users.Events; 3 | 4 | using MediatR; 5 | 6 | namespace CleanArchitecture.Application.Reminders.Events; 7 | 8 | public class ReminderDismissedEventHandler(IRemindersRepository _remindersRepository) : INotificationHandler 9 | { 10 | public async Task Handle(ReminderDismissedEvent notification, CancellationToken cancellationToken) 11 | { 12 | var reminder = await _remindersRepository.GetByIdAsync(notification.ReminderId, cancellationToken) 13 | ?? throw new InvalidOperationException(); 14 | 15 | reminder.Dismiss(); 16 | 17 | await _remindersRepository.UpdateAsync(reminder, cancellationToken); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Events/ReminderSetEventHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Users.Events; 3 | 4 | using MediatR; 5 | 6 | namespace CleanArchitecture.Application.Reminders.Events; 7 | 8 | public class ReminderSetEventHandler(IRemindersRepository _remindersRepository) : INotificationHandler 9 | { 10 | public async Task Handle(ReminderSetEvent @event, CancellationToken cancellationToken) 11 | { 12 | await _remindersRepository.AddAsync(@event.Reminder, cancellationToken); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Queries/GetReminder/GetReminderQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | using CleanArchitecture.Domain.Reminders; 5 | 6 | using ErrorOr; 7 | 8 | namespace CleanArchitecture.Application.Reminders.Queries.GetReminder; 9 | 10 | [Authorize(Permissions = Permission.Reminder.Get, Policies = Policy.SelfOrAdmin)] 11 | public record GetReminderQuery(Guid UserId, Guid SubscriptionId, Guid ReminderId) : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Queries/GetReminder/GetReminderQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Reminders; 3 | 4 | using ErrorOr; 5 | 6 | using MediatR; 7 | 8 | namespace CleanArchitecture.Application.Reminders.Queries.GetReminder; 9 | 10 | public class GetReminderQueryHandler(IRemindersRepository _remindersRepository) 11 | : IRequestHandler> 12 | { 13 | public async Task> Handle(GetReminderQuery query, CancellationToken cancellationToken) 14 | { 15 | var reminder = await _remindersRepository.GetByIdAsync(query.ReminderId, cancellationToken); 16 | 17 | if (reminder?.UserId != query.UserId) 18 | { 19 | return Error.NotFound(description: "Reminder not found"); 20 | } 21 | 22 | return reminder; 23 | } 24 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Queries/ListReminders/ListRemindersQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | using CleanArchitecture.Domain.Reminders; 5 | 6 | using ErrorOr; 7 | 8 | namespace CleanArchitecture.Application.Reminders.Queries.ListReminders; 9 | 10 | [Authorize(Permissions = Permission.Reminder.Get, Policies = Policy.SelfOrAdmin)] 11 | public record ListRemindersQuery(Guid UserId, Guid SubscriptionId) : IAuthorizeableRequest>>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Reminders/Queries/ListReminders/ListRemindersQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Reminders; 3 | 4 | using ErrorOr; 5 | 6 | using MediatR; 7 | 8 | namespace CleanArchitecture.Application.Reminders.Queries.ListReminders; 9 | 10 | public class ListRemindersQueryHandler(IRemindersRepository _remindersRepository) : IRequestHandler>> 11 | { 12 | public async Task>> Handle(ListRemindersQuery request, CancellationToken cancellationToken) 13 | { 14 | return await _remindersRepository.ListBySubscriptionIdAsync(request.SubscriptionId, cancellationToken); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Commands/CancelSubscription/CancelSubscriptionCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Request; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | using ErrorOr; 5 | 6 | namespace CleanArchitecture.Application.Subscriptions.Commands.CancelSubscription; 7 | 8 | [Authorize(Roles = Role.Admin)] 9 | public record CancelSubscriptionCommand(Guid UserId, Guid SubscriptionId) : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Commands/CancelSubscription/CancelSubscriptionCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | 3 | using ErrorOr; 4 | 5 | using MediatR; 6 | 7 | namespace CleanArchitecture.Application.Subscriptions.Commands.CancelSubscription; 8 | 9 | public class CancelSubscriptionCommandHandler(IUsersRepository _usersRepository) 10 | : IRequestHandler> 11 | { 12 | public async Task> Handle(CancelSubscriptionCommand request, CancellationToken cancellationToken) 13 | { 14 | var user = await _usersRepository.GetByIdAsync(request.UserId, cancellationToken); 15 | 16 | if (user is null) 17 | { 18 | return Error.NotFound(description: "User not found"); 19 | } 20 | 21 | var deleteSubscriptionResult = user.CancelSubscription(request.SubscriptionId); 22 | 23 | if (deleteSubscriptionResult.IsError) 24 | { 25 | return deleteSubscriptionResult.Errors; 26 | } 27 | 28 | await _usersRepository.UpdateAsync(user, cancellationToken); 29 | 30 | return Result.Success; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Commands/CreateSubscription/CreateSubscriptionCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | using CleanArchitecture.Application.Subscriptions.Common; 5 | using CleanArchitecture.Domain.Users; 6 | 7 | using ErrorOr; 8 | 9 | namespace CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 10 | 11 | [Authorize(Permissions = Permission.Subscription.Create, Policies = Policy.SelfOrAdmin)] 12 | public record CreateSubscriptionCommand( 13 | Guid UserId, 14 | string FirstName, 15 | string LastName, 16 | string Email, 17 | SubscriptionType SubscriptionType) 18 | : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Commands/CreateSubscription/CreateSubscriptionCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Application.Subscriptions.Common; 3 | using CleanArchitecture.Domain.Subscriptions; 4 | using CleanArchitecture.Domain.Users; 5 | 6 | using ErrorOr; 7 | 8 | using MediatR; 9 | 10 | namespace CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 11 | 12 | public class CreateSubscriptionCommandHandler( 13 | IUsersRepository _usersRepository) : IRequestHandler> 14 | { 15 | public async Task> Handle(CreateSubscriptionCommand request, CancellationToken cancellationToken) 16 | { 17 | if (await _usersRepository.GetByIdAsync(request.UserId, cancellationToken) is not null) 18 | { 19 | return Error.Conflict(description: "User already has an active subscription"); 20 | } 21 | 22 | var subscription = new Subscription(request.SubscriptionType); 23 | 24 | var user = new User( 25 | request.UserId, 26 | request.FirstName, 27 | request.LastName, 28 | request.Email, 29 | subscription); 30 | 31 | await _usersRepository.AddAsync(user, cancellationToken); 32 | 33 | return SubscriptionResult.FromUser(user); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Commands/CreateSubscription/CreateSubscriptionCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 4 | 5 | public class CreateSubscriptionCommandValidator : AbstractValidator 6 | { 7 | public CreateSubscriptionCommandValidator() 8 | { 9 | RuleFor(x => x.FirstName) 10 | .MinimumLength(2) 11 | .MaximumLength(10000); 12 | 13 | RuleFor(x => x.LastName) 14 | .MinimumLength(2) 15 | .MaximumLength(10000); 16 | 17 | RuleFor(x => x.Email).EmailAddress(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Common/SubscriptionResult.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | 3 | using Throw; 4 | 5 | namespace CleanArchitecture.Application.Subscriptions.Common; 6 | 7 | public record SubscriptionResult( 8 | Guid Id, 9 | Guid UserId, 10 | SubscriptionType SubscriptionType) 11 | { 12 | public static SubscriptionResult FromUser(User user) 13 | { 14 | user.Subscription.ThrowIfNull(); 15 | 16 | return new SubscriptionResult( 17 | user.Subscription.Id, 18 | user.Id, 19 | user.Subscription.SubscriptionType); 20 | } 21 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Events/SubscriptionCanceledEventHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Users.Events; 3 | 4 | using MediatR; 5 | 6 | namespace CleanArchitecture.Application.Subscriptions.Events; 7 | 8 | public class SubscriptionCanceledEventHandler(IUsersRepository _usersRepository) 9 | : INotificationHandler 10 | { 11 | public async Task Handle(SubscriptionCanceledEvent notification, CancellationToken cancellationToken) 12 | { 13 | notification.User.DeleteAllReminders(); 14 | 15 | await _usersRepository.RemoveAsync(notification.User, cancellationToken); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Queries/GetSubscription/GetSubscriptionQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Policies; 3 | using CleanArchitecture.Application.Common.Security.Request; 4 | using CleanArchitecture.Application.Subscriptions.Common; 5 | 6 | using ErrorOr; 7 | 8 | namespace CleanArchitecture.Application.Subscriptions.Queries.GetSubscription; 9 | 10 | [Authorize(Permissions = Permission.Subscription.Get, Policies = Policy.SelfOrAdmin)] 11 | public record GetSubscriptionQuery(Guid UserId) 12 | : IAuthorizeableRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Subscriptions/Queries/GetSubscription/GetSubscriptionQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Application.Subscriptions.Common; 3 | using CleanArchitecture.Domain.Users; 4 | 5 | using ErrorOr; 6 | 7 | using MediatR; 8 | 9 | namespace CleanArchitecture.Application.Subscriptions.Queries.GetSubscription; 10 | 11 | public class GetSubscriptionQueryHandler(IUsersRepository _usersRepository) 12 | : IRequestHandler> 13 | { 14 | public async Task> Handle(GetSubscriptionQuery request, CancellationToken cancellationToken) 15 | { 16 | return await _usersRepository.GetByIdAsync(request.UserId, cancellationToken) is User user 17 | ? SubscriptionResult.FromUser(user) 18 | : Error.NotFound(description: "Subscription not found."); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Tokens/Queries/Generate/GenerateTokenQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Authentication.Queries.Login; 2 | using CleanArchitecture.Domain.Users; 3 | 4 | using ErrorOr; 5 | 6 | using MediatR; 7 | 8 | namespace CleanArchitecture.Application.Tokens.Queries.Generate; 9 | 10 | public record GenerateTokenQuery( 11 | Guid? Id, 12 | string FirstName, 13 | string LastName, 14 | string Email, 15 | SubscriptionType SubscriptionType, 16 | List Permissions, 17 | List Roles) : IRequest>; -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Tokens/Queries/Generate/GenerateTokenQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Authentication.Queries.Login; 2 | using CleanArchitecture.Application.Common.Interfaces; 3 | 4 | using ErrorOr; 5 | 6 | using MediatR; 7 | 8 | namespace CleanArchitecture.Application.Tokens.Queries.Generate; 9 | 10 | public class GenerateTokenQueryHandler( 11 | IJwtTokenGenerator _jwtTokenGenerator) 12 | : IRequestHandler> 13 | { 14 | public Task> Handle(GenerateTokenQuery query, CancellationToken cancellationToken) 15 | { 16 | var id = query.Id ?? Guid.NewGuid(); 17 | 18 | var token = _jwtTokenGenerator.GenerateToken( 19 | id, 20 | query.FirstName, 21 | query.LastName, 22 | query.Email, 23 | query.SubscriptionType, 24 | query.Permissions, 25 | query.Roles); 26 | 27 | var authResult = new GenerateTokenResult( 28 | id, 29 | query.FirstName, 30 | query.LastName, 31 | query.Email, 32 | query.SubscriptionType, 33 | token); 34 | 35 | return Task.FromResult(ErrorOrFactory.From(authResult)); 36 | } 37 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Application/Tokens/Queries/Generate/GenerateTokenResult.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | 3 | namespace CleanArchitecture.Application.Authentication.Queries.Login; 4 | 5 | public record GenerateTokenResult( 6 | Guid Id, 7 | string FirstName, 8 | string LastName, 9 | string Email, 10 | SubscriptionType SubscriptionType, 11 | string Token); -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/CleanArchitecture.Contracts.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Common/SubscriptionType.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace CleanArchitecture.Contracts.Common; 4 | 5 | [JsonConverter(typeof(JsonStringEnumConverter))] 6 | public enum SubscriptionType 7 | { 8 | Basic, 9 | Pro, 10 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Reminders/CreateReminderRequest.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Contracts.Reminders; 2 | 3 | public record CreateReminderRequest(string Text, DateTimeOffset DateTime); -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Reminders/ReminderResponse.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Contracts.Reminders; 2 | 3 | public record ReminderResponse( 4 | Guid Id, 5 | string Text, 6 | DateTimeOffset DateTime, 7 | bool IsDismissed); 8 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Subscriptions/CreateSubscriptionRequest.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Contracts.Common; 2 | 3 | namespace CleanArchitecture.Contracts.Subscriptions; 4 | 5 | public record CreateSubscriptionRequest( 6 | string FirstName, 7 | string LastName, 8 | string Email, 9 | SubscriptionType SubscriptionType); -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Subscriptions/SubscriptionResponse.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Contracts.Common; 2 | 3 | namespace CleanArchitecture.Contracts.Subscriptions; 4 | 5 | public record SubscriptionResponse( 6 | Guid Id, 7 | Guid UserId, 8 | SubscriptionType SubscriptionType); -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Tokens/GenerateTokenRequest.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Contracts.Common; 2 | 3 | namespace CleanArchitecture.Contracts.Tokens; 4 | 5 | public record GenerateTokenRequest( 6 | Guid? Id, 7 | string FirstName, 8 | string LastName, 9 | string Email, 10 | SubscriptionType SubscriptionType, 11 | List Permissions, 12 | List Roles); -------------------------------------------------------------------------------- /src/CleanArchitecture.Contracts/Tokens/TokenResponse.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Contracts.Common; 2 | 3 | namespace CleanArchitecture.Contracts.Tokens; 4 | 5 | public record TokenResponse( 6 | Guid Id, 7 | string FirstName, 8 | string LastName, 9 | string Email, 10 | SubscriptionType SubscriptionType, 11 | string Token); -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/CleanArchitecture.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Common/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Domain.Common; 2 | 3 | public abstract class Entity 4 | { 5 | public Guid Id { get; private init; } 6 | 7 | protected readonly List _domainEvents = []; 8 | 9 | protected Entity(Guid id) 10 | { 11 | Id = id; 12 | } 13 | 14 | public List PopDomainEvents() 15 | { 16 | var copy = _domainEvents.ToList(); 17 | _domainEvents.Clear(); 18 | 19 | return copy; 20 | } 21 | 22 | protected Entity() { } 23 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Common/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace CleanArchitecture.Domain.Common; 4 | 5 | public interface IDomainEvent : INotification 6 | { 7 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Reminders/Reminder.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | 3 | using ErrorOr; 4 | 5 | namespace CleanArchitecture.Domain.Reminders; 6 | 7 | public class Reminder : Entity 8 | { 9 | public Guid UserId { get; } 10 | 11 | public Guid SubscriptionId { get; } 12 | 13 | public DateTime DateTime { get; } 14 | 15 | public DateOnly Date => DateOnly.FromDateTime(DateTime.Date); 16 | 17 | public string Text { get; } = null!; 18 | 19 | public bool IsDismissed { get; private set; } 20 | 21 | public Reminder( 22 | Guid userId, 23 | Guid subscriptionId, 24 | string text, 25 | DateTime dateTime, 26 | Guid? id = null) 27 | : base(id ?? Guid.NewGuid()) 28 | { 29 | UserId = userId; 30 | SubscriptionId = subscriptionId; 31 | Text = text; 32 | DateTime = dateTime; 33 | } 34 | 35 | public ErrorOr Dismiss() 36 | { 37 | if (IsDismissed) 38 | { 39 | return Error.Conflict(description: "Reminder already dismissed"); 40 | } 41 | 42 | IsDismissed = true; 43 | 44 | return Result.Success; 45 | } 46 | 47 | private Reminder() 48 | { 49 | } 50 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/Calendar.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Domain.Users; 2 | 3 | public class Calendar 4 | { 5 | /// 6 | /// day -> num events. 7 | /// 8 | private readonly Dictionary _calendar = []; 9 | 10 | public static Calendar Empty() 11 | { 12 | return new Calendar(); 13 | } 14 | 15 | public void IncrementEventCount(DateOnly date) 16 | { 17 | if (!_calendar.ContainsKey(date)) 18 | { 19 | _calendar[date] = 0; 20 | } 21 | 22 | _calendar[date]++; 23 | } 24 | 25 | public void DecrementEventCount(DateOnly date) 26 | { 27 | if (!_calendar.ContainsKey(date)) 28 | { 29 | return; 30 | } 31 | 32 | _calendar[date]--; 33 | } 34 | 35 | public void SetEventCount(DateOnly date, int numEvents) 36 | { 37 | _calendar[date] = numEvents; 38 | } 39 | 40 | public int GetNumEventsOnDay(DateTimeOffset dateTime) 41 | { 42 | return _calendar.TryGetValue(DateOnly.FromDateTime(dateTime.Date), out var numEvents) 43 | ? numEvents 44 | : 0; 45 | } 46 | 47 | private Calendar() 48 | { 49 | } 50 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/Events/ReminderDeletedEvent.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | 3 | namespace CleanArchitecture.Domain.Users.Events; 4 | 5 | public record ReminderDeletedEvent(Guid ReminderId) : IDomainEvent; -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/Events/ReminderDismissedEvent.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | 3 | namespace CleanArchitecture.Domain.Users.Events; 4 | 5 | public record ReminderDismissedEvent(Guid ReminderId) : IDomainEvent; -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/Events/ReminderSetEvent.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | using CleanArchitecture.Domain.Reminders; 3 | 4 | namespace CleanArchitecture.Domain.Users.Events; 5 | 6 | public record ReminderSetEvent(Reminder Reminder) : IDomainEvent; -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/Events/SubscriptionCanceledEvent.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | 3 | namespace CleanArchitecture.Domain.Users.Events; 4 | 5 | public record SubscriptionCanceledEvent(User User, Guid SubscriptionId) : IDomainEvent; -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/Subscription.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | using CleanArchitecture.Domain.Users; 3 | 4 | namespace CleanArchitecture.Domain.Subscriptions; 5 | 6 | public class Subscription : Entity 7 | { 8 | public SubscriptionType SubscriptionType { get; } = null!; 9 | 10 | public Subscription(SubscriptionType subscriptionType, Guid? id = null) 11 | : base(id ?? Guid.NewGuid()) 12 | { 13 | SubscriptionType = subscriptionType; 14 | } 15 | 16 | public static readonly Subscription Canceled = new(new SubscriptionType("Canceled", -1), Guid.Empty); 17 | 18 | private Subscription() 19 | { 20 | } 21 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/SubscriptionType.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.SmartEnum; 2 | 3 | namespace CleanArchitecture.Domain.Users; 4 | 5 | public class SubscriptionType(string name, int value) 6 | : SmartEnum(name, value) 7 | { 8 | public static readonly SubscriptionType Basic = new(nameof(Basic), 0); 9 | public static readonly SubscriptionType Pro = new(nameof(Pro), 1); 10 | 11 | public int GetMaxDailyReminders() => Name switch 12 | { 13 | nameof(Basic) => 3, 14 | nameof(Pro) => int.MaxValue, 15 | _ => throw new InvalidOperationException(), 16 | }; 17 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/User.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | using CleanArchitecture.Domain.Reminders; 3 | using CleanArchitecture.Domain.Subscriptions; 4 | using CleanArchitecture.Domain.Users.Events; 5 | 6 | using ErrorOr; 7 | 8 | using Throw; 9 | 10 | namespace CleanArchitecture.Domain.Users; 11 | 12 | public class User : Entity 13 | { 14 | private readonly Calendar _calendar = null!; 15 | 16 | private readonly List _reminderIds = []; 17 | 18 | private readonly List _dismissedReminderIds = []; 19 | 20 | public Subscription Subscription { get; private set; } = null!; 21 | 22 | public string Email { get; } = null!; 23 | 24 | public string FirstName { get; } = null!; 25 | 26 | public string LastName { get; } = null!; 27 | 28 | public User( 29 | Guid id, 30 | string firstName, 31 | string lastName, 32 | string email, 33 | Subscription subscription, 34 | Calendar? calendar = null) 35 | : base(id) 36 | { 37 | FirstName = firstName; 38 | LastName = lastName; 39 | Email = email; 40 | Subscription = subscription; 41 | _calendar = calendar ?? Calendar.Empty(); 42 | } 43 | 44 | public ErrorOr SetReminder(Reminder reminder) 45 | { 46 | if (Subscription == Subscription.Canceled) 47 | { 48 | return Error.NotFound(description: "Subscription not found"); 49 | } 50 | 51 | reminder.SubscriptionId.Throw().IfNotEquals(Subscription.Id); 52 | 53 | if (HasReachedDailyReminderLimit(reminder.DateTime)) 54 | { 55 | return UserErrors.CannotCreateMoreRemindersThanSubscriptionAllows; 56 | } 57 | 58 | _calendar.IncrementEventCount(reminder.Date); 59 | 60 | _reminderIds.Add(reminder.Id); 61 | 62 | _domainEvents.Add(new ReminderSetEvent(reminder)); 63 | 64 | return Result.Success; 65 | } 66 | 67 | public ErrorOr DismissReminder(Guid reminderId) 68 | { 69 | if (Subscription == Subscription.Canceled) 70 | { 71 | return Error.NotFound(description: "Subscription not found"); 72 | } 73 | 74 | if (!_reminderIds.Contains(reminderId)) 75 | { 76 | return Error.NotFound(description: "Reminder not found"); 77 | } 78 | 79 | if (_dismissedReminderIds.Contains(reminderId)) 80 | { 81 | return Error.Conflict(description: "Reminder already dismissed"); 82 | } 83 | 84 | _dismissedReminderIds.Add(reminderId); 85 | 86 | _domainEvents.Add(new ReminderDismissedEvent(reminderId)); 87 | 88 | return Result.Success; 89 | } 90 | 91 | public ErrorOr CancelSubscription(Guid subscriptionId) 92 | { 93 | if (subscriptionId != Subscription.Id) 94 | { 95 | return Error.NotFound(description: "Subscription not found"); 96 | } 97 | 98 | Subscription = Subscription.Canceled; 99 | 100 | _domainEvents.Add(new SubscriptionCanceledEvent(this, subscriptionId)); 101 | 102 | return Result.Success; 103 | } 104 | 105 | public ErrorOr DeleteReminder(Reminder reminder) 106 | { 107 | if (Subscription == Subscription.Canceled) 108 | { 109 | return Error.NotFound(description: "Subscription not found"); 110 | } 111 | 112 | if (!_reminderIds.Remove(reminder.Id)) 113 | { 114 | return Error.NotFound(description: "Reminder not found"); 115 | } 116 | 117 | _dismissedReminderIds.Remove(reminder.Id); 118 | 119 | _calendar.DecrementEventCount(reminder.Date); 120 | 121 | _domainEvents.Add(new ReminderDeletedEvent(reminder.Id)); 122 | 123 | return Result.Success; 124 | } 125 | 126 | public void DeleteAllReminders() 127 | { 128 | _reminderIds.ForEach(reminderId => _domainEvents.Add(new ReminderDeletedEvent(reminderId))); 129 | 130 | _reminderIds.Clear(); 131 | } 132 | 133 | private bool HasReachedDailyReminderLimit(DateTimeOffset dateTime) 134 | { 135 | var dailyReminderCount = _calendar.GetNumEventsOnDay(dateTime.Date); 136 | 137 | return dailyReminderCount >= Subscription.SubscriptionType.GetMaxDailyReminders() 138 | || dailyReminderCount == int.MaxValue; 139 | } 140 | 141 | private User() 142 | { 143 | } 144 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Domain/Users/UserErrors.cs: -------------------------------------------------------------------------------- 1 | using ErrorOr; 2 | 3 | namespace CleanArchitecture.Domain.Users; 4 | 5 | public static class UserErrors 6 | { 7 | public static Error CannotCreateMoreRemindersThanSubscriptionAllows { get; } = Error.Validation( 8 | code: "UserErrors.CannotCreateMoreRemindersThanSubscriptionAllows", 9 | description: "Cannot create more reminders than subscription allows"); 10 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | net8.0 26 | enable 27 | enable 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Common/Middleware/EventualConsistencyMiddleware.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | 3 | using MediatR; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace CleanArchitecture.Infrastructure.Common.Middleware; 8 | 9 | public class EventualConsistencyMiddleware(RequestDelegate _next) 10 | { 11 | public const string DomainEventsKey = "DomainEventsKey"; 12 | 13 | public async Task InvokeAsync(HttpContext context, IPublisher publisher, AppDbContext dbContext) 14 | { 15 | var transaction = await dbContext.Database.BeginTransactionAsync(); 16 | context.Response.OnCompleted(async () => 17 | { 18 | try 19 | { 20 | if (context.Items.TryGetValue(DomainEventsKey, out var value) && value is Queue domainEvents) 21 | { 22 | while (domainEvents.TryDequeue(out var nextEvent)) 23 | { 24 | await publisher.Publish(nextEvent); 25 | } 26 | } 27 | 28 | await transaction.CommitAsync(); 29 | } 30 | catch (Exception) 31 | { 32 | } 33 | finally 34 | { 35 | await transaction.DisposeAsync(); 36 | } 37 | }); 38 | 39 | await _next(context); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Common/Persistence/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Common; 2 | using CleanArchitecture.Domain.Reminders; 3 | using CleanArchitecture.Domain.Users; 4 | using CleanArchitecture.Infrastructure.Common.Middleware; 5 | 6 | using MediatR; 7 | 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.EntityFrameworkCore; 10 | 11 | namespace CleanArchitecture.Infrastructure.Common; 12 | 13 | public class AppDbContext(DbContextOptions options, IHttpContextAccessor _httpContextAccessor, IPublisher _publisher) : DbContext(options) 14 | { 15 | public DbSet Reminders { get; set; } = null!; 16 | 17 | public DbSet Users { get; set; } = null!; 18 | 19 | public async override Task SaveChangesAsync(CancellationToken cancellationToken = default) 20 | { 21 | var domainEvents = ChangeTracker.Entries() 22 | .SelectMany(entry => entry.Entity.PopDomainEvents()) 23 | .ToList(); 24 | 25 | if (IsUserWaitingOnline()) 26 | { 27 | AddDomainEventsToOfflineProcessingQueue(domainEvents); 28 | return await base.SaveChangesAsync(cancellationToken); 29 | } 30 | 31 | await PublishDomainEvents(domainEvents); 32 | return await base.SaveChangesAsync(cancellationToken); 33 | } 34 | 35 | protected override void OnModelCreating(ModelBuilder modelBuilder) 36 | { 37 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); 38 | 39 | base.OnModelCreating(modelBuilder); 40 | } 41 | 42 | private bool IsUserWaitingOnline() => _httpContextAccessor.HttpContext is not null; 43 | 44 | private async Task PublishDomainEvents(List domainEvents) 45 | { 46 | foreach (var domainEvent in domainEvents) 47 | { 48 | await _publisher.Publish(domainEvent); 49 | } 50 | } 51 | 52 | private void AddDomainEventsToOfflineProcessingQueue(List domainEvents) 53 | { 54 | Queue domainEventsQueue = _httpContextAccessor.HttpContext!.Items.TryGetValue(EventualConsistencyMiddleware.DomainEventsKey, out var value) && 55 | value is Queue existingDomainEvents 56 | ? existingDomainEvents 57 | : new(); 58 | 59 | domainEvents.ForEach(domainEventsQueue.Enqueue); 60 | _httpContextAccessor.HttpContext.Items[EventualConsistencyMiddleware.DomainEventsKey] = domainEventsQueue; 61 | } 62 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Common/Persistence/FluentApiExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace CleanArchitecture.Infrastructure.Common.Persistence; 4 | 5 | public static class FluentApiExtensions 6 | { 7 | // FYI: SQLite doesn't support JSON columns yet. Otherwise, we'd prefer calling .ToJson() on the owned entity instead. 8 | public static PropertyBuilder HasValueJsonConverter(this PropertyBuilder propertyBuilder) 9 | { 10 | return propertyBuilder.HasConversion( 11 | new ValueJsonConverter(), 12 | new ValueJsonComparer()); 13 | } 14 | 15 | public static PropertyBuilder HasListOfIdsConverter(this PropertyBuilder propertyBuilder) 16 | { 17 | return propertyBuilder.HasConversion( 18 | new ListOfIdsConverter(), 19 | new ListOfIdsComparer()); 20 | } 21 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Common/Persistence/ListOfIdsConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using Microsoft.EntityFrameworkCore.ChangeTracking; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | 6 | namespace CleanArchitecture.Infrastructure.Common.Persistence; 7 | 8 | public class ListOfIdsConverter(ConverterMappingHints? mappingHints = null) 9 | : ValueConverter, string>( 10 | v => string.Join(',', v), 11 | v => v.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(Guid.Parse).ToList(), 12 | mappingHints) 13 | { 14 | } 15 | 16 | public class ListOfIdsComparer : ValueComparer> 17 | { 18 | public ListOfIdsComparer() 19 | : base( 20 | (t1, t2) => t1!.SequenceEqual(t2!), 21 | t => t.Select(x => x!.GetHashCode()).Aggregate((x, y) => x ^ y), 22 | t => t) 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Common/Persistence/ValueJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | using Microsoft.EntityFrameworkCore.ChangeTracking; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | 6 | namespace CleanArchitecture.Infrastructure.Common.Persistence; 7 | 8 | public class ValueJsonConverter(ConverterMappingHints? mappingHints = null) 9 | : ValueConverter( 10 | v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), 11 | v => JsonSerializer.Deserialize(v, JsonSerializerOptions.Default)!, 12 | mappingHints) 13 | { 14 | } 15 | 16 | public class ValueJsonComparer : ValueComparer 17 | { 18 | public ValueJsonComparer() 19 | : base( 20 | (l, r) => JsonSerializer.Serialize(l, JsonSerializerOptions.Default) == JsonSerializer.Serialize(r, JsonSerializerOptions.Default), 21 | v => v == null ? 0 : JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(), 22 | v => JsonSerializer.Deserialize(JsonSerializer.Serialize(v, JsonSerializerOptions.Default), JsonSerializerOptions.Default)!) 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Mail; 3 | 4 | using CleanArchitecture.Application.Common.Interfaces; 5 | using CleanArchitecture.Infrastructure.Common; 6 | using CleanArchitecture.Infrastructure.Reminders.BackgroundServices; 7 | using CleanArchitecture.Infrastructure.Reminders.Persistence; 8 | using CleanArchitecture.Infrastructure.Security; 9 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 10 | using CleanArchitecture.Infrastructure.Security.PolicyEnforcer; 11 | using CleanArchitecture.Infrastructure.Security.TokenGenerator; 12 | using CleanArchitecture.Infrastructure.Security.TokenValidation; 13 | using CleanArchitecture.Infrastructure.Services; 14 | using CleanArchitecture.Infrastructure.Users.Persistence; 15 | 16 | using Microsoft.AspNetCore.Authentication.JwtBearer; 17 | using Microsoft.EntityFrameworkCore; 18 | using Microsoft.Extensions.Configuration; 19 | using Microsoft.Extensions.DependencyInjection; 20 | 21 | namespace CleanArchitecture.Infrastructure; 22 | 23 | public static class DependencyInjection 24 | { 25 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) 26 | { 27 | services 28 | .AddHttpContextAccessor() 29 | .AddServices() 30 | .AddBackgroundServices(configuration) 31 | .AddAuthentication(configuration) 32 | .AddAuthorization() 33 | .AddPersistence(); 34 | 35 | return services; 36 | } 37 | 38 | private static IServiceCollection AddBackgroundServices(this IServiceCollection services, IConfiguration configuration) 39 | { 40 | services.AddEmailNotifications(configuration); 41 | 42 | return services; 43 | } 44 | 45 | private static IServiceCollection AddEmailNotifications( 46 | this IServiceCollection services, 47 | IConfiguration configuration) 48 | { 49 | EmailSettings emailSettings = new(); 50 | configuration.Bind(EmailSettings.Section, emailSettings); 51 | 52 | if (!emailSettings.EnableEmailNotifications) 53 | { 54 | return services; 55 | } 56 | 57 | services.AddHostedService(); 58 | 59 | services 60 | .AddFluentEmail(emailSettings.DefaultFromEmail) 61 | .AddSmtpSender(new SmtpClient(emailSettings.SmtpSettings.Server) 62 | { 63 | Port = emailSettings.SmtpSettings.Port, 64 | Credentials = new NetworkCredential( 65 | emailSettings.SmtpSettings.Username, 66 | emailSettings.SmtpSettings.Password), 67 | }); 68 | 69 | return services; 70 | } 71 | 72 | private static IServiceCollection AddServices(this IServiceCollection services) 73 | { 74 | services.AddSingleton(); 75 | 76 | return services; 77 | } 78 | 79 | private static IServiceCollection AddPersistence(this IServiceCollection services) 80 | { 81 | services.AddDbContext(options => options.UseSqlite("Data Source = CleanArchitecture.sqlite")); 82 | 83 | services.AddScoped(); 84 | services.AddScoped(); 85 | 86 | return services; 87 | } 88 | 89 | private static IServiceCollection AddAuthorization(this IServiceCollection services) 90 | { 91 | services.AddScoped(); 92 | services.AddScoped(); 93 | services.AddSingleton(); 94 | 95 | return services; 96 | } 97 | 98 | private static IServiceCollection AddAuthentication(this IServiceCollection services, IConfiguration configuration) 99 | { 100 | services.Configure(configuration.GetSection(JwtSettings.Section)); 101 | 102 | services.AddSingleton(); 103 | 104 | services 105 | .ConfigureOptions() 106 | .AddAuthentication(defaultScheme: JwtBearerDefaults.AuthenticationScheme) 107 | .AddJwtBearer(); 108 | 109 | return services; 110 | } 111 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Migrations/20231226150412_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace CleanArchitecture.Infrastructure.Migrations 7 | { 8 | /// 9 | public partial class InitialCreate : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Reminders", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "TEXT", nullable: false), 19 | UserId = table.Column(type: "TEXT", nullable: false), 20 | SubscriptionId = table.Column(type: "TEXT", nullable: false), 21 | DateTime = table.Column(type: "TEXT", nullable: false), 22 | Text = table.Column(type: "TEXT", nullable: false), 23 | IsDismissed = table.Column(type: "INTEGER", nullable: false) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_Reminders", x => x.Id); 28 | }); 29 | 30 | migrationBuilder.CreateTable( 31 | name: "Users", 32 | columns: table => new 33 | { 34 | Id = table.Column(type: "TEXT", nullable: false), 35 | Subscription_SubscriptionType = table.Column(type: "TEXT", nullable: false), 36 | SubscriptionId = table.Column(type: "TEXT", nullable: false), 37 | Email = table.Column(type: "TEXT", nullable: false), 38 | FirstName = table.Column(type: "TEXT", nullable: false), 39 | LastName = table.Column(type: "TEXT", nullable: false), 40 | DismissedReminderIds = table.Column(type: "TEXT", nullable: false), 41 | ReminderIds = table.Column(type: "TEXT", nullable: false), 42 | CalendarDictionary = table.Column(type: "TEXT", nullable: true) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_Users", x => x.Id); 47 | }); 48 | } 49 | 50 | /// 51 | protected override void Down(MigrationBuilder migrationBuilder) 52 | { 53 | migrationBuilder.DropTable( 54 | name: "Reminders"); 55 | 56 | migrationBuilder.DropTable( 57 | name: "Users"); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Reminders/BackgroundServices/EmailSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Infrastructure.Reminders.BackgroundServices; 2 | 3 | public class EmailSettings 4 | { 5 | public const string Section = "EmailSettings"; 6 | 7 | public bool EnableEmailNotifications { get; init; } 8 | 9 | public string DefaultFromEmail { get; init; } = null!; 10 | 11 | public SmtpSettings SmtpSettings { get; init; } = null!; 12 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Reminders/BackgroundServices/ReminderEmailBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Users; 3 | using CleanArchitecture.Infrastructure.Common; 4 | 5 | using FluentEmail.Core; 6 | 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | 10 | namespace CleanArchitecture.Infrastructure.Reminders.BackgroundServices; 11 | 12 | public class ReminderEmailBackgroundService( 13 | IServiceScopeFactory serviceScopeFactory, 14 | IDateTimeProvider _dateTimeProvider, 15 | IFluentEmail _fluentEmail) : IHostedService, IDisposable 16 | { 17 | private readonly AppDbContext _dbContext = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService(); 18 | private Timer _timer = null!; 19 | 20 | public Task StartAsync(CancellationToken cancellationToken) 21 | { 22 | _timer = new Timer(SendEmailNotifications, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); 23 | return Task.CompletedTask; 24 | } 25 | 26 | public Task StopAsync(CancellationToken cancellationToken) 27 | { 28 | _timer?.Change(Timeout.Infinite, 0); 29 | return Task.CompletedTask; 30 | } 31 | 32 | public void Dispose() 33 | { 34 | _timer?.Dispose(); 35 | } 36 | 37 | /// 38 | /// TODO: there are many edge cases that aren't caught here. This is an immediate nice to have implementation for now. 39 | /// 40 | private async void SendEmailNotifications(object? state) 41 | { 42 | var now = _dateTimeProvider.UtcNow; 43 | var oneMinuteFromNow = now.AddMinutes(1); 44 | 45 | var dueRemindersBySubscription = _dbContext.Reminders 46 | .Where(reminder => reminder.DateTime >= now && reminder.DateTime <= oneMinuteFromNow && !reminder.IsDismissed) 47 | .GroupBy(reminder => reminder.SubscriptionId) 48 | .ToList(); 49 | 50 | var subscriptionToBeNotified = dueRemindersBySubscription.ConvertAll(x => x.Key); 51 | 52 | var usersToBeNotified = _dbContext.Users 53 | .Where(user => subscriptionToBeNotified.Contains(user.Subscription.Id)) 54 | .ToList(); 55 | 56 | foreach (User? user in usersToBeNotified) 57 | { 58 | var dueReminders = dueRemindersBySubscription 59 | .Single(x => x.Key == user.Subscription.Id) 60 | .ToList(); 61 | 62 | await _fluentEmail 63 | .To(user.Email) 64 | .Subject($"{dueReminders.Count} reminders due!") 65 | .Body($""" 66 | Dear {user.FirstName} {user.LastName} from the present. 67 | 68 | I hope this email finds you well. 69 | 70 | I'm writing you this email to remind you about the following reminders: 71 | {string.Join('\n', dueReminders.Select((reminder, i) => $"{i + 1}. {reminder.Text}"))} 72 | 73 | Best, 74 | {user.FirstName} from the past. 75 | """) 76 | .SendAsync(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Reminders/BackgroundServices/SmtpSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Infrastructure.Reminders.BackgroundServices; 2 | 3 | public class SmtpSettings 4 | { 5 | public string Server { get; init; } = null!; 6 | public int Port { get; init; } 7 | public string Username { get; init; } = null!; 8 | public string Password { get; init; } = null!; 9 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Reminders/Persistence/ReminderConfigurations.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Reminders; 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace CleanArchitecture.Infrastructure.Reminders.Persistence; 7 | 8 | public class ReminderConfigurations : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.HasKey(r => r.Id); 13 | 14 | builder.Property(r => r.Id) 15 | .ValueGeneratedNever(); 16 | 17 | builder.Property(r => r.UserId); 18 | 19 | builder.Property(r => r.SubscriptionId); 20 | 21 | builder.Property(r => r.DateTime); 22 | 23 | builder.Property(r => r.Text); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Reminders/Persistence/RemindersRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Reminders; 3 | using CleanArchitecture.Infrastructure.Common; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace CleanArchitecture.Infrastructure.Reminders.Persistence; 8 | 9 | public class RemindersRepository(AppDbContext _dbContext) : IRemindersRepository 10 | { 11 | public async Task AddAsync(Reminder reminder, CancellationToken cancellationToken) 12 | { 13 | await _dbContext.AddAsync(reminder, cancellationToken); 14 | await _dbContext.SaveChangesAsync(cancellationToken); 15 | } 16 | 17 | public async Task GetByIdAsync(Guid reminderId, CancellationToken cancellationToken) 18 | { 19 | return await _dbContext.Reminders.FindAsync(reminderId, cancellationToken); 20 | } 21 | 22 | public async Task> ListBySubscriptionIdAsync(Guid subscriptionId, CancellationToken cancellationToken) 23 | { 24 | return await _dbContext.Reminders 25 | .AsNoTracking() 26 | .Where(reminder => reminder.SubscriptionId == subscriptionId) 27 | .ToListAsync(cancellationToken); 28 | } 29 | 30 | public async Task RemoveAsync(Reminder reminder, CancellationToken cancellationToken) 31 | { 32 | _dbContext.Remove(reminder); 33 | await _dbContext.SaveChangesAsync(cancellationToken); 34 | } 35 | 36 | public async Task RemoveRangeAsync(List reminders, CancellationToken cancellationToken) 37 | { 38 | _dbContext.RemoveRange(reminders, cancellationToken); 39 | await _dbContext.SaveChangesAsync(cancellationToken); 40 | } 41 | 42 | public async Task UpdateAsync(Reminder reminder, CancellationToken cancellationToken) 43 | { 44 | _dbContext.Update(reminder); 45 | await _dbContext.SaveChangesAsync(cancellationToken); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/RequestPipeline.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Common.Middleware; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | namespace CleanArchitecture.Infrastructure; 6 | 7 | public static class RequestPipeline 8 | { 9 | public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) 10 | { 11 | app.UseMiddleware(); 12 | return app; 13 | } 14 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/AuthorizationService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Application.Common.Security.Request; 3 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 4 | using CleanArchitecture.Infrastructure.Security.PolicyEnforcer; 5 | 6 | using ErrorOr; 7 | 8 | namespace CleanArchitecture.Infrastructure.Security; 9 | 10 | public class AuthorizationService( 11 | IPolicyEnforcer _policyEnforcer, 12 | ICurrentUserProvider _currentUserProvider) 13 | : IAuthorizationService 14 | { 15 | public ErrorOr AuthorizeCurrentUser( 16 | IAuthorizeableRequest request, 17 | List requiredRoles, 18 | List requiredPermissions, 19 | List requiredPolicies) 20 | { 21 | var currentUser = _currentUserProvider.GetCurrentUser(); 22 | 23 | if (requiredPermissions.Except(currentUser.Permissions).Any()) 24 | { 25 | return Error.Unauthorized(description: "User is missing required permissions for taking this action"); 26 | } 27 | 28 | if (requiredRoles.Except(currentUser.Roles).Any()) 29 | { 30 | return Error.Unauthorized(description: "User is missing required roles for taking this action"); 31 | } 32 | 33 | foreach (var policy in requiredPolicies) 34 | { 35 | var authorizationAgainstPolicyResult = _policyEnforcer.Authorize(request, currentUser, policy); 36 | 37 | if (authorizationAgainstPolicyResult.IsError) 38 | { 39 | return authorizationAgainstPolicyResult.Errors; 40 | } 41 | } 42 | 43 | return Result.Success; 44 | } 45 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/CurrentUserProvider/CurrentUser.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 2 | 3 | public record CurrentUser( 4 | Guid Id, 5 | string FirstName, 6 | string LastName, 7 | string Email, 8 | IReadOnlyList Permissions, 9 | IReadOnlyList Roles); -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/CurrentUserProvider/CurrentUserProvider.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | 4 | using Microsoft.AspNetCore.Http; 5 | 6 | using Throw; 7 | 8 | namespace CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 9 | 10 | public class CurrentUserProvider(IHttpContextAccessor _httpContextAccessor) : ICurrentUserProvider 11 | { 12 | public CurrentUser GetCurrentUser() 13 | { 14 | _httpContextAccessor.HttpContext.ThrowIfNull(); 15 | 16 | var id = Guid.Parse(GetSingleClaimValue("id")); 17 | var permissions = GetClaimValues("permissions"); 18 | var roles = GetClaimValues(ClaimTypes.Role); 19 | var firstName = GetSingleClaimValue(JwtRegisteredClaimNames.Name); 20 | var lastName = GetSingleClaimValue(ClaimTypes.Surname); 21 | var email = GetSingleClaimValue(ClaimTypes.Email); 22 | 23 | return new CurrentUser(id, firstName, lastName, email, permissions, roles); 24 | } 25 | 26 | private List GetClaimValues(string claimType) => 27 | _httpContextAccessor.HttpContext!.User.Claims 28 | .Where(claim => claim.Type == claimType) 29 | .Select(claim => claim.Value) 30 | .ToList(); 31 | 32 | private string GetSingleClaimValue(string claimType) => 33 | _httpContextAccessor.HttpContext!.User.Claims 34 | .Single(claim => claim.Type == claimType) 35 | .Value; 36 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/CurrentUserProvider/ICurrentUserProvider.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 2 | 3 | public interface ICurrentUserProvider 4 | { 5 | CurrentUser GetCurrentUser(); 6 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/PolicyEnforcer/IPolicyEnforcer.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Request; 2 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 3 | 4 | using ErrorOr; 5 | 6 | namespace CleanArchitecture.Infrastructure.Security.PolicyEnforcer; 7 | 8 | public interface IPolicyEnforcer 9 | { 10 | public ErrorOr Authorize( 11 | IAuthorizeableRequest request, 12 | CurrentUser currentUser, 13 | string policy); 14 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/PolicyEnforcer/PolicyEnforcer.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Policies; 2 | using CleanArchitecture.Application.Common.Security.Request; 3 | using CleanArchitecture.Application.Common.Security.Roles; 4 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 5 | 6 | using ErrorOr; 7 | 8 | namespace CleanArchitecture.Infrastructure.Security.PolicyEnforcer; 9 | 10 | public class PolicyEnforcer : IPolicyEnforcer 11 | { 12 | public ErrorOr Authorize( 13 | IAuthorizeableRequest request, 14 | CurrentUser currentUser, 15 | string policy) 16 | { 17 | return policy switch 18 | { 19 | Policy.SelfOrAdmin => SelfOrAdminPolicy(request, currentUser), 20 | _ => Error.Unexpected(description: "Unknown policy name"), 21 | }; 22 | } 23 | 24 | private static ErrorOr SelfOrAdminPolicy(IAuthorizeableRequest request, CurrentUser currentUser) => 25 | request.UserId == currentUser.Id || currentUser.Roles.Contains(Role.Admin) 26 | ? Result.Success 27 | : Error.Unauthorized(description: "Requesting user failed policy requirement"); 28 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/TokenGenerator/JwtSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Infrastructure.Security.TokenGenerator; 2 | 3 | public class JwtSettings 4 | { 5 | public const string Section = "JwtSettings"; 6 | 7 | public string Audience { get; set; } = null!; 8 | public string Issuer { get; set; } = null!; 9 | public string Secret { get; set; } = null!; 10 | public int TokenExpirationInMinutes { get; set; } 11 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/TokenGenerator/JwtTokenGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | using System.Text; 4 | 5 | using CleanArchitecture.Application.Common.Interfaces; 6 | using CleanArchitecture.Domain.Users; 7 | 8 | using Microsoft.Extensions.Options; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace CleanArchitecture.Infrastructure.Security.TokenGenerator; 12 | 13 | public class JwtTokenGenerator(IOptions jwtOptions) : IJwtTokenGenerator 14 | { 15 | private readonly JwtSettings _jwtSettings = jwtOptions.Value; 16 | 17 | public string GenerateToken( 18 | Guid id, 19 | string firstName, 20 | string lastName, 21 | string email, 22 | SubscriptionType subscriptionType, 23 | List permissions, 24 | List roles) 25 | { 26 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Secret)); 27 | var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 28 | 29 | var claims = new List 30 | { 31 | new(JwtRegisteredClaimNames.Name, firstName), 32 | new(JwtRegisteredClaimNames.FamilyName, lastName), 33 | new(JwtRegisteredClaimNames.Email, email), 34 | new("id", id.ToString()), 35 | }; 36 | 37 | roles.ForEach(role => claims.Add(new(ClaimTypes.Role, role))); 38 | permissions.ForEach(permission => claims.Add(new("permissions", permission))); 39 | 40 | var token = new JwtSecurityToken( 41 | _jwtSettings.Issuer, 42 | _jwtSettings.Audience, 43 | claims, 44 | expires: DateTime.UtcNow.AddMinutes(_jwtSettings.TokenExpirationInMinutes), 45 | signingCredentials: credentials); 46 | 47 | return new JwtSecurityTokenHandler().WriteToken(token); 48 | } 49 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Security/TokenValidation/JwtBearerTokenValidationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using CleanArchitecture.Infrastructure.Security.TokenGenerator; 4 | 5 | using Microsoft.AspNetCore.Authentication.JwtBearer; 6 | using Microsoft.Extensions.Options; 7 | using Microsoft.IdentityModel.Tokens; 8 | 9 | namespace CleanArchitecture.Infrastructure.Security.TokenValidation; 10 | 11 | public sealed class JwtBearerTokenValidationConfiguration(IOptions jwtSettings) 12 | : IConfigureNamedOptions 13 | { 14 | private readonly JwtSettings _jwtSettings = jwtSettings.Value; 15 | 16 | public void Configure(string? name, JwtBearerOptions options) => Configure(options); 17 | 18 | public void Configure(JwtBearerOptions options) 19 | { 20 | options.TokenValidationParameters = new TokenValidationParameters 21 | { 22 | ValidateIssuer = true, 23 | ValidateAudience = true, 24 | ValidateLifetime = true, 25 | ValidateIssuerSigningKey = true, 26 | ValidIssuer = _jwtSettings.Issuer, 27 | ValidAudience = _jwtSettings.Audience, 28 | IssuerSigningKey = new SymmetricSecurityKey( 29 | Encoding.UTF8.GetBytes(_jwtSettings.Secret)), 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Services/SystemDateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | 3 | namespace CleanArchitecture.Infrastructure.Services; 4 | 5 | public class SystemDateTimeProvider : IDateTimeProvider 6 | { 7 | public DateTime UtcNow => DateTime.UtcNow; 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Users/Persistence/UserConfigurations.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | using CleanArchitecture.Infrastructure.Common.Persistence; 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | 7 | namespace CleanArchitecture.Infrastructure.Users.Persistence; 8 | 9 | public class UserConfigurations : IEntityTypeConfiguration 10 | { 11 | public void Configure(EntityTypeBuilder builder) 12 | { 13 | builder.HasKey(u => u.Id); 14 | 15 | builder.Property(u => u.Id) 16 | .ValueGeneratedNever(); 17 | 18 | builder.OwnsOne("_calendar", cb => 19 | cb.Property>("_calendar") 20 | .HasColumnName("CalendarDictionary") 21 | .HasValueJsonConverter()); 22 | 23 | builder.Property>("_reminderIds") 24 | .HasColumnName("ReminderIds") 25 | .HasListOfIdsConverter(); 26 | 27 | builder.Property>("_dismissedReminderIds") 28 | .HasColumnName("DismissedReminderIds") 29 | .HasListOfIdsConverter(); 30 | 31 | builder.OwnsOne(u => u.Subscription, sb => 32 | { 33 | sb.Property(s => s.Id) 34 | .HasColumnName("SubscriptionId"); 35 | 36 | sb.Property(s => s.SubscriptionType) 37 | .HasConversion( 38 | v => v.Name, 39 | v => SubscriptionType.FromName(v, false)); 40 | }); 41 | 42 | builder.Property(u => u.Email); 43 | 44 | builder.Property(u => u.LastName); 45 | 46 | builder.Property(u => u.FirstName); 47 | } 48 | } -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Users/Persistence/UsersRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Interfaces; 2 | using CleanArchitecture.Domain.Users; 3 | using CleanArchitecture.Infrastructure.Common; 4 | 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace CleanArchitecture.Infrastructure.Users.Persistence; 8 | 9 | public class UsersRepository(AppDbContext _dbContext) : IUsersRepository 10 | { 11 | public async Task AddAsync(User user, CancellationToken cancellationToken) 12 | { 13 | await _dbContext.AddAsync(user, cancellationToken); 14 | await _dbContext.SaveChangesAsync(cancellationToken); 15 | } 16 | 17 | public async Task GetByIdAsync(Guid userId, CancellationToken cancellationToken) 18 | { 19 | return await _dbContext.Users.FindAsync(userId, cancellationToken); 20 | } 21 | 22 | public async Task GetBySubscriptionIdAsync(Guid subscriptionId, CancellationToken cancellationToken) 23 | { 24 | return await _dbContext.Users.FirstOrDefaultAsync(user => user.Subscription.Id == subscriptionId, cancellationToken); 25 | } 26 | 27 | public async Task RemoveAsync(User user, CancellationToken cancellationToken) 28 | { 29 | _dbContext.Remove(user); 30 | await _dbContext.SaveChangesAsync(cancellationToken); 31 | } 32 | 33 | public async Task UpdateAsync(User user, CancellationToken cancellationToken) 34 | { 35 | _dbContext.Update(user); 36 | await _dbContext.SaveChangesAsync(cancellationToken); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/.editorconfig: -------------------------------------------------------------------------------- 1 | # Test configurations 2 | 3 | [*.{cs,vb}] 4 | 5 | # SA1649: File name should match first type name 6 | dotnet_diagnostic.SA1649.severity = none 7 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/CleanArchitecture.Api.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/Common/AppHttpClient.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | 3 | using CleanArchitecture.Api.IntegrationTests.Common.Subscriptions; 4 | using CleanArchitecture.Api.IntegrationTests.Common.Tokens; 5 | using CleanArchitecture.Contracts.Subscriptions; 6 | using CleanArchitecture.Contracts.Tokens; 7 | 8 | namespace CleanArchitecture.Api.IntegrationTests.Common; 9 | 10 | public class AppHttpClient(HttpClient _httpClient) 11 | { 12 | public async Task CreateSubscriptionAndExpectSuccessAsync( 13 | Guid? userId = null, 14 | CreateSubscriptionRequest? createSubscriptionRequest = null, 15 | string? token = null) 16 | { 17 | var response = await CreateSubscriptionAsync(userId, createSubscriptionRequest, token); 18 | 19 | response.StatusCode.Should().Be(HttpStatusCode.Created); 20 | 21 | var subscriptionResponse = await response.Content.ReadFromJsonAsync(); 22 | 23 | subscriptionResponse.Should().NotBeNull(); 24 | 25 | return subscriptionResponse!; 26 | } 27 | 28 | public async Task GenerateTokenAsync( 29 | GenerateTokenRequest? generateTokenRequest = null) 30 | { 31 | generateTokenRequest ??= TokenRequestFactory.CreateGenerateTokenRequest(); 32 | 33 | var response = await _httpClient.PostAsJsonAsync("tokens/generate", generateTokenRequest); 34 | 35 | response.Should().BeSuccessful(); 36 | 37 | var tokenResponse = await response.Content.ReadFromJsonAsync(); 38 | 39 | tokenResponse.Should().NotBeNull(); 40 | 41 | return tokenResponse!.Token; 42 | } 43 | 44 | public async Task CreateSubscriptionAsync( 45 | Guid? userId = null, 46 | CreateSubscriptionRequest? createSubscriptionRequest = null, 47 | string? token = null) 48 | { 49 | userId ??= Constants.User.Id; 50 | createSubscriptionRequest ??= SubscriptionRequestFactory.CreateCreateSubscriptionRequest(); 51 | token ??= await GenerateTokenAsync(); 52 | 53 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 54 | 55 | return await _httpClient.PostAsJsonAsync($"users/{userId}/subscriptions", createSubscriptionRequest); 56 | } 57 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/Common/Subscriptions/SubscriptionRequestFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Contracts.Subscriptions; 2 | 3 | namespace CleanArchitecture.Api.IntegrationTests.Common.Subscriptions; 4 | 5 | public static class SubscriptionRequestFactory 6 | { 7 | public static CreateSubscriptionRequest CreateCreateSubscriptionRequest( 8 | string firstName = Constants.User.FirstName, 9 | string lastName = Constants.User.LastName, 10 | string emailName = Constants.User.Email, 11 | SubscriptionType? subscriptionType = null) 12 | { 13 | return new( 14 | firstName, 15 | lastName, 16 | emailName, 17 | subscriptionType ?? SubscriptionType.Basic); 18 | } 19 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/Common/Tokens/TokenRequestFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Contracts.Tokens; 2 | 3 | namespace CleanArchitecture.Api.IntegrationTests.Common.Tokens; 4 | 5 | public static class TokenRequestFactory 6 | { 7 | public static GenerateTokenRequest CreateGenerateTokenRequest( 8 | Guid? id = null, 9 | string firstName = Constants.User.FirstName, 10 | string lastName = Constants.User.LastName, 11 | string email = Constants.User.Email, 12 | SubscriptionType? subscriptionType = null, 13 | List? permissions = null, 14 | List? roles = null) 15 | { 16 | return new( 17 | id ?? Constants.User.Id, 18 | firstName, 19 | lastName, 20 | email, 21 | subscriptionType ?? SubscriptionType.Basic, 22 | permissions ?? Constants.User.Permissions, 23 | roles ?? Constants.User.Roles); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/Common/WebApplicationFactory/SqliteTestDatabase.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Common; 2 | 3 | using Microsoft.Data.Sqlite; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace CleanArchitecture.Api.IntegrationTests.Common.WebApplicationFactory; 7 | 8 | /// 9 | /// We're using SQLite so no need to spin an actual database. 10 | /// 11 | public class SqliteTestDatabase : IDisposable 12 | { 13 | public SqliteConnection Connection { get; } 14 | 15 | public static SqliteTestDatabase CreateAndInitialize() 16 | { 17 | var testDatabase = new SqliteTestDatabase("DataSource=:memory:"); 18 | 19 | testDatabase.InitializeDatabase(); 20 | 21 | return testDatabase; 22 | } 23 | 24 | public void InitializeDatabase() 25 | { 26 | Connection.Open(); 27 | var options = new DbContextOptionsBuilder() 28 | .UseSqlite(Connection) 29 | .Options; 30 | 31 | using var context = new AppDbContext(options, null!, null!); 32 | context.Database.EnsureDeleted(); 33 | context.Database.EnsureCreated(); 34 | } 35 | 36 | public void ResetDatabase() 37 | { 38 | Connection.Close(); 39 | 40 | InitializeDatabase(); 41 | } 42 | 43 | public void Dispose() 44 | { 45 | Connection.Close(); 46 | } 47 | 48 | private SqliteTestDatabase(string connectionString) 49 | { 50 | Connection = new SqliteConnection(connectionString); 51 | } 52 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/Common/WebApplicationFactory/WebAppFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Common; 2 | 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Mvc.Testing; 5 | using Microsoft.AspNetCore.TestHost; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.DependencyInjection.Extensions; 10 | 11 | namespace CleanArchitecture.Api.IntegrationTests.Common.WebApplicationFactory; 12 | 13 | public class WebAppFactory : WebApplicationFactory, IAsyncLifetime 14 | { 15 | private SqliteTestDatabase _testDatabase = null!; 16 | 17 | public AppHttpClient CreateAppHttpClient() 18 | { 19 | return new AppHttpClient(CreateClient()); 20 | } 21 | 22 | public Task InitializeAsync() => Task.CompletedTask; 23 | 24 | public new Task DisposeAsync() 25 | { 26 | _testDatabase.Dispose(); 27 | 28 | return Task.CompletedTask; 29 | } 30 | 31 | public void ResetDatabase() 32 | { 33 | _testDatabase.ResetDatabase(); 34 | } 35 | 36 | protected override void ConfigureWebHost(IWebHostBuilder builder) 37 | { 38 | _testDatabase = SqliteTestDatabase.CreateAndInitialize(); 39 | 40 | builder.ConfigureTestServices(services => services 41 | .RemoveAll>() 42 | .AddDbContext((sp, options) => options.UseSqlite(_testDatabase.Connection))); 43 | 44 | builder.ConfigureAppConfiguration((context, conf) => conf.AddInMemoryCollection(new Dictionary 45 | { 46 | { "EmailSettings:EnableEmailNotifications", "false" }, 47 | })); 48 | } 49 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/Common/WebApplicationFactory/WebAppFactoryCollection.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Api.IntegrationTests.Common.WebApplicationFactory; 2 | 3 | [CollectionDefinition(CollectionName)] 4 | public class WebAppFactoryCollection : ICollectionFixture 5 | { 6 | public const string CollectionName = "WebAppFactoryCollection"; 7 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Api.IntegrationTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Net; 2 | global using System.Net.Http.Json; 3 | 4 | global using CleanArchitecture.Api.IntegrationTests.Common; 5 | global using CleanArchitecture.Contracts.Common; 6 | 7 | global using FluentAssertions; 8 | 9 | global using TestCommon.TestConstants; 10 | 11 | global using Xunit; -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/CleanArchitecture.Application.SubcutaneousTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Common/SqliteTestDatabase.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Common; 2 | 3 | using Microsoft.Data.Sqlite; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace CleanArchitecture.Application.SubcutaneousTests.Common; 7 | 8 | /// 9 | /// In Subcutaneous tests we aren't testing integration with a real database, 10 | /// so even if we weren't using SQLite we would use some in-memory database. 11 | /// 12 | public class SqliteTestDatabase : IDisposable 13 | { 14 | public SqliteConnection Connection { get; } 15 | 16 | public static SqliteTestDatabase CreateAndInitialize() 17 | { 18 | var testDatabase = new SqliteTestDatabase("DataSource=:memory:"); 19 | 20 | testDatabase.InitializeDatabase(); 21 | 22 | return testDatabase; 23 | } 24 | 25 | public void InitializeDatabase() 26 | { 27 | Connection.Open(); 28 | var options = new DbContextOptionsBuilder() 29 | .UseSqlite(Connection) 30 | .Options; 31 | 32 | using var context = new AppDbContext(options, null!, null!); 33 | context.Database.EnsureDeleted(); 34 | context.Database.EnsureCreated(); 35 | } 36 | 37 | public void ResetDatabase() 38 | { 39 | Connection.Close(); 40 | 41 | InitializeDatabase(); 42 | } 43 | 44 | public void Dispose() 45 | { 46 | Connection.Close(); 47 | } 48 | 49 | private SqliteTestDatabase(string connectionString) 50 | { 51 | Connection = new SqliteConnection(connectionString); 52 | } 53 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Common/WebAppFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Api; 2 | using CleanArchitecture.Infrastructure.Common; 3 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 4 | 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Mvc.Testing; 7 | using Microsoft.AspNetCore.TestHost; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.DependencyInjection.Extensions; 12 | 13 | namespace CleanArchitecture.Application.SubcutaneousTests.Common; 14 | 15 | public class WebAppFactory : WebApplicationFactory, IAsyncLifetime 16 | { 17 | public TestCurrentUserProvider TestCurrentUserProvider { get; private set; } = new(); 18 | public SqliteTestDatabase TestDatabase { get; set; } = null!; 19 | 20 | public IMediator CreateMediator() 21 | { 22 | var serviceScope = Services.CreateScope(); 23 | 24 | TestDatabase.ResetDatabase(); 25 | 26 | return serviceScope.ServiceProvider.GetRequiredService(); 27 | } 28 | 29 | public Task InitializeAsync() => Task.CompletedTask; 30 | 31 | public new Task DisposeAsync() 32 | { 33 | TestDatabase.Dispose(); 34 | 35 | return Task.CompletedTask; 36 | } 37 | 38 | protected override void ConfigureWebHost(IWebHostBuilder builder) 39 | { 40 | TestDatabase = SqliteTestDatabase.CreateAndInitialize(); 41 | 42 | builder.ConfigureTestServices(services => 43 | { 44 | services 45 | .RemoveAll() 46 | .AddScoped(_ => TestCurrentUserProvider); 47 | 48 | services 49 | .RemoveAll>() 50 | .AddDbContext((sp, options) => options.UseSqlite(TestDatabase.Connection)); 51 | }); 52 | 53 | builder.ConfigureAppConfiguration((context, conf) => conf.AddInMemoryCollection(new Dictionary 54 | { 55 | { "EmailSettings:EnableEmailNotifications", "false" }, 56 | })); 57 | } 58 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Common/WebAppFactoryCollection.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Common; 2 | 3 | [CollectionDefinition(CollectionName)] 4 | public class WebAppFactoryCollection : ICollectionFixture 5 | { 6 | public const string CollectionName = "WebAppFactoryCollection"; 7 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using CleanArchitecture.Application.SubcutaneousTests.Common; 2 | 3 | global using ErrorOr; 4 | 5 | global using FluentAssertions; 6 | 7 | global using MediatR; 8 | 9 | global using TestCommon.Reminders; 10 | global using TestCommon.Security; 11 | global using TestCommon.Subscriptions; 12 | 13 | global using Xunit; -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Commands/DeleteReminder/DeleteReminder.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Commands.DeleteReminder; 5 | 6 | public class DeleteReminderAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public DeleteReminderAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task DeleteReminderForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var command = ReminderCommandFactory.CreateDeleteReminderCommand(); 29 | 30 | // Act 31 | var result = await _mediator.Send(command); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task DeleteReminderForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var command = ReminderCommandFactory.CreateDeleteReminderCommand(); 48 | 49 | // Act 50 | var result = await _mediator.Send(command); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task DeleteReminderForSelf_WhenHasRequiredPermissions_ShouldAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | permissions: [Permission.Reminder.Delete], 62 | roles: []); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var command = ReminderCommandFactory.CreateDeleteReminderCommand(); 67 | 68 | // Act 69 | var result = await _mediator.Send(command); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task DeleteReminderForSelf_WhenDoesNotHaveRequiredPermissions_ShouldNotAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | permissions: [], 81 | roles: []); 82 | 83 | _currentUserProvider.Returns(currentUser); 84 | 85 | var command = ReminderCommandFactory.CreateDeleteReminderCommand(); 86 | 87 | // Act 88 | var result = await _mediator.Send(command); 89 | 90 | // Assert 91 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Commands/DeleteReminder/DeleteReminderTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Commands.DeleteReminder; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class DeleteReminderTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task DeleteReminder_WhenSubscriptionDoesNotExists_ShouldReturnNotFound() 10 | { 11 | // Arrange 12 | var command = ReminderCommandFactory.CreateDeleteReminderCommand(); 13 | 14 | // Act 15 | var result = await _mediator.Send(command); 16 | 17 | // Assert 18 | result.IsError.Should().BeTrue(); 19 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 20 | } 21 | 22 | [Fact] 23 | public async Task DeleteReminder_WhenReminderDoesNotExists_ShouldReturnNotFound() 24 | { 25 | // Arrange 26 | var subscription = await _mediator.CreateSubscriptionAsync(); 27 | var command = ReminderCommandFactory.CreateDeleteReminderCommand(subscriptionId: subscription.Id); 28 | 29 | // Act 30 | var result = await _mediator.Send(command); 31 | 32 | // Assert 33 | result.IsError.Should().BeTrue(); 34 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 35 | } 36 | 37 | [Fact] 38 | public async Task DeleteReminder_WhenValidCommand_ShouldDeleteReminder() 39 | { 40 | // Arrange 41 | var subscription = await _mediator.CreateSubscriptionAsync(); 42 | var reminder = await _mediator.SetReminderAsync( 43 | ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 44 | 45 | var command = ReminderCommandFactory.CreateDeleteReminderCommand( 46 | subscriptionId: subscription.Id, 47 | reminderId: reminder.Id); 48 | 49 | // Act 50 | var result = await _mediator.Send(command); 51 | 52 | // Assert 53 | result.IsError.Should().BeFalse(); 54 | result.Value.Should().Be(Result.Success); 55 | 56 | // Assert side effects took place 57 | var getReminderResult = await _mediator.GetReminderAsync( 58 | ReminderQueryFactory.CreateGetReminderQuery( 59 | subscriptionId: subscription.Id, 60 | reminderId: reminder.Id)); 61 | 62 | getReminderResult.IsError.Should().BeTrue(); 63 | getReminderResult.FirstError.Type.Should().Be(ErrorType.NotFound); 64 | } 65 | 66 | [Fact] 67 | public async Task DeleteReminder_WhenReminderAlreadyDeleted_ShouldReturnNotFound() 68 | { 69 | // Arrange 70 | var subscription = await _mediator.CreateSubscriptionAsync(); 71 | var reminder = await _mediator.SetReminderAsync( 72 | ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 73 | 74 | var command = ReminderCommandFactory.CreateDeleteReminderCommand( 75 | subscriptionId: subscription.Id, 76 | reminderId: reminder.Id); 77 | 78 | // Act 79 | var firstDeleteReminderResult = await _mediator.Send(command); 80 | var secondDeleteReminderResult = await _mediator.Send(command); 81 | 82 | // Assert 83 | firstDeleteReminderResult.IsError.Should().BeFalse(); 84 | 85 | secondDeleteReminderResult.IsError.Should().BeTrue(); 86 | secondDeleteReminderResult.FirstError.Type.Should().Be(ErrorType.NotFound); 87 | 88 | // Assert side effects took place 89 | var getReminderResult = await _mediator.GetReminderAsync( 90 | ReminderQueryFactory.CreateGetReminderQuery( 91 | subscriptionId: subscription.Id, 92 | reminderId: reminder.Id)); 93 | 94 | getReminderResult.IsError.Should().BeTrue(); 95 | getReminderResult.FirstError.Type.Should().Be(ErrorType.NotFound); 96 | } 97 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Commands/DismissReminder/DismissReminder.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Commands.DismissReminder; 5 | 6 | public class DismissReminderAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public DismissReminderAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task DismissReminderForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var command = ReminderCommandFactory.CreateDismissReminderCommand(); 29 | 30 | // Act 31 | var result = await _mediator.Send(command); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task DismissReminderForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var command = ReminderCommandFactory.CreateDismissReminderCommand(); 48 | 49 | // Act 50 | var result = await _mediator.Send(command); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task DismissReminderForSelf_WhenHasRequiredPermissions_ShouldAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | permissions: [Permission.Reminder.Dismiss], 62 | roles: []); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var command = ReminderCommandFactory.CreateDismissReminderCommand(); 67 | 68 | // Act 69 | var result = await _mediator.Send(command); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task DismissReminderForSelf_WhenDoesNotHaveRequiredPermissions_ShouldNotAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | permissions: [], 81 | roles: []); 82 | 83 | _currentUserProvider.Returns(currentUser); 84 | 85 | var command = ReminderCommandFactory.CreateDismissReminderCommand(); 86 | 87 | // Act 88 | var result = await _mediator.Send(command); 89 | 90 | // Assert 91 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Commands/DismissReminder/DismissReminderTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Commands.DismissReminder; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class DismissReminderTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task DismissReminder_WhenSubscriptionDoesNotExists_ShouldReturnNotFound() 10 | { 11 | // Arrange 12 | var command = ReminderCommandFactory.CreateDismissReminderCommand(); 13 | 14 | // Act 15 | var result = await _mediator.Send(command); 16 | 17 | // Assert 18 | result.IsError.Should().BeTrue(); 19 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 20 | } 21 | 22 | [Fact] 23 | public async Task DismissReminder_WhenValidCommand_ShouldDismissReminder() 24 | { 25 | // Arrange 26 | var subscription = await _mediator.CreateSubscriptionAsync(); 27 | var reminder = await _mediator.SetReminderAsync( 28 | ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 29 | 30 | var command = ReminderCommandFactory.CreateDismissReminderCommand( 31 | subscriptionId: subscription.Id, 32 | reminderId: reminder.Id); 33 | 34 | // Act 35 | var result = await _mediator.Send(command); 36 | 37 | // Assert 38 | result.IsError.Should().BeFalse(); 39 | result.Value.Should().Be(Result.Success); 40 | 41 | // Assert side effects took place 42 | var getReminderResult = await _mediator.GetReminderAsync( 43 | ReminderQueryFactory.CreateGetReminderQuery( 44 | subscriptionId: subscription.Id, 45 | reminderId: reminder.Id)); 46 | 47 | getReminderResult.IsError.Should().BeFalse(); 48 | getReminderResult.Value.IsDismissed.Should().BeTrue(); 49 | } 50 | 51 | [Fact] 52 | public async Task DismissReminder_WhenReminderAlreadyDismissed_ShouldReturnConflict() 53 | { 54 | // Arrange 55 | var subscription = await _mediator.CreateSubscriptionAsync(); 56 | var reminder = await _mediator.SetReminderAsync( 57 | ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 58 | 59 | var command = ReminderCommandFactory.CreateDismissReminderCommand( 60 | subscriptionId: subscription.Id, 61 | reminderId: reminder.Id); 62 | 63 | // Act 64 | var firstDismissReminderResult = await _mediator.Send(command); 65 | var secondDismissReminderResult = await _mediator.Send(command); 66 | 67 | // Assert 68 | firstDismissReminderResult.IsError.Should().BeFalse(); 69 | 70 | secondDismissReminderResult.IsError.Should().BeTrue(); 71 | secondDismissReminderResult.FirstError.Type.Should().Be(ErrorType.Conflict); 72 | 73 | // Assert side effects took place 74 | var getReminderResult = await _mediator.GetReminderAsync( 75 | ReminderQueryFactory.CreateGetReminderQuery( 76 | subscriptionId: subscription.Id, 77 | reminderId: reminder.Id)); 78 | 79 | getReminderResult.IsError.Should().BeFalse(); 80 | getReminderResult.Value.IsDismissed.Should().BeTrue(); 81 | } 82 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Commands/SetReminder/SetReminder.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Commands.SetReminder; 5 | 6 | public class SetReminderAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public SetReminderAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task SetReminderForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var command = ReminderCommandFactory.CreateSetReminderCommand(); 29 | 30 | // Act 31 | var result = await _mediator.Send(command); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task SetReminderForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var command = ReminderCommandFactory.CreateSetReminderCommand(); 48 | 49 | // Act 50 | var result = await _mediator.Send(command); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task SetReminderForSelf_WhenHasRequiredPermissions_ShouldAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | permissions: [Permission.Reminder.Set], 62 | roles: []); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var command = ReminderCommandFactory.CreateSetReminderCommand(); 67 | 68 | // Act 69 | var result = await _mediator.Send(command); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task SetReminderForSelf_WhenDoesNotHaveRequiredPermissions_ShouldNotAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | permissions: [], 81 | roles: []); 82 | 83 | _currentUserProvider.Returns(currentUser); 84 | 85 | var command = ReminderCommandFactory.CreateSetReminderCommand(); 86 | 87 | // Act 88 | var result = await _mediator.Send(command); 89 | 90 | // Assert 91 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Commands/SetReminder/SetReminder.ValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Commands.SetReminder; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class SetReminderValidationTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task SetReminder_WhenInvalidDateTime_ShouldReturnValidationError() 10 | { 11 | // Arrange 12 | var command = ReminderCommandFactory.CreateSetReminderCommand(dateTime: DateTime.UtcNow.AddDays(-1)); 13 | 14 | // Act 15 | var result = await _mediator.Send(command); 16 | 17 | // Assert 18 | result.IsError.Should().BeTrue(); 19 | result.FirstError.Type.Should().Be(ErrorType.Validation); 20 | } 21 | 22 | [Theory] 23 | [InlineData(1)] 24 | [InlineData(2)] 25 | [InlineData(10001)] 26 | public async Task SetReminder_WhenInvalidText_ShouldReturnValidationError(int textLength) 27 | { 28 | // Arrange 29 | var command = ReminderCommandFactory.CreateSetReminderCommand(text: new string('a', textLength)); 30 | 31 | // Act 32 | var result = await _mediator.Send(command); 33 | 34 | // Assert 35 | result.IsError.Should().BeTrue(); 36 | result.FirstError.Type.Should().Be(ErrorType.Validation); 37 | } 38 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Queries/GetReminder/GetReminder.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Queries.GetReminder; 5 | 6 | public class GetReminderAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public GetReminderAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task GetReminderForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var query = ReminderQueryFactory.CreateGetReminderQuery(); 29 | 30 | // Act 31 | var result = await _mediator.Send(query); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task GetReminderForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var query = ReminderQueryFactory.CreateGetReminderQuery(); 48 | 49 | // Act 50 | var result = await _mediator.Send(query); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task GetReminderForSelf_WhenHasRequiredPermission_ShouldAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | roles: [], 62 | permissions: [Permission.Reminder.Get]); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var query = ReminderQueryFactory.CreateGetReminderQuery(); 67 | 68 | // Act 69 | var result = await _mediator.Send(query); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task GetReminderForSelf_WhenDoesNotHaveRequiredPermission_ShouldNotAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | roles: [], 81 | permissions: []); 82 | 83 | _currentUserProvider.Returns(currentUser); 84 | 85 | var query = ReminderQueryFactory.CreateGetReminderQuery(); 86 | 87 | // Act 88 | var result = await _mediator.Send(query); 89 | 90 | // Assert 91 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Queries/GetReminder/GetReminderTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Queries.GetReminder; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class GetReminderTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task GetReminder_WhenValidQuery_ShouldReturnReminder() 10 | { 11 | // Arrange 12 | var subscription = await _mediator.CreateSubscriptionAsync(); 13 | var reminder = await _mediator.SetReminderAsync( 14 | ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 15 | 16 | var query = ReminderQueryFactory.CreateGetReminderQuery( 17 | subscriptionId: subscription.Id, 18 | reminderId: reminder.Id); 19 | 20 | // Act 21 | var result = await _mediator.Send(query); 22 | 23 | // Assert 24 | result.IsError.Should().BeFalse(); 25 | result.Value.Should().BeEquivalentTo(reminder); 26 | } 27 | 28 | [Fact] 29 | public async Task GetReminder_WhenNoSubscription_ShouldReturnNotFound() 30 | { 31 | // Arrange 32 | var query = ReminderQueryFactory.CreateGetReminderQuery(); 33 | 34 | // Act 35 | var result = await _mediator.Send(query); 36 | 37 | // Assert 38 | result.IsError.Should().BeTrue(); 39 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 40 | result.FirstError.Description.Should().Contain("Reminder"); 41 | } 42 | 43 | [Fact] 44 | public async Task GetReminder_WhenNoReminder_ShouldReturnNotFound() 45 | { 46 | // Arrange 47 | var subscription = await _mediator.CreateSubscriptionAsync(); 48 | 49 | var query = ReminderQueryFactory.CreateGetReminderQuery(subscriptionId: subscription.Id); 50 | 51 | // Act 52 | var result = await _mediator.Send(query); 53 | 54 | // Assert 55 | result.IsError.Should().BeTrue(); 56 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 57 | result.FirstError.Description.Should().Contain("Reminder"); 58 | } 59 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Queries/ListReminders/ListReminders.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Queries.ListReminders; 5 | 6 | public class ListRemindersAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public ListRemindersAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task ListRemindersForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var query = ReminderQueryFactory.CreateListRemindersQuery(); 29 | 30 | // Act 31 | var result = await _mediator.Send(query); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task ListRemindersForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var query = ReminderQueryFactory.CreateListRemindersQuery(); 48 | 49 | // Act 50 | var result = await _mediator.Send(query); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task ListRemindersForSelf_WhenHasRequiredPermission_ShouldAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | roles: [], 62 | permissions: [Permission.Reminder.Get]); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var query = ReminderQueryFactory.CreateListRemindersQuery(); 67 | 68 | // Act 69 | var result = await _mediator.Send(query); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task ListRemindersForSelf_WhenDoesNotHaveRequiredPermission_ShouldNotAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | roles: [], 81 | permissions: []); 82 | 83 | _currentUserProvider.Returns(currentUser); 84 | 85 | var query = ReminderQueryFactory.CreateListRemindersQuery(); 86 | 87 | // Act 88 | var result = await _mediator.Send(query); 89 | 90 | // Assert 91 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Reminders/Queries/ListReminders/ListRemindersTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Reminders.Queries.ListReminders; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class ListRemindersTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task ListReminders_WhenValidQuery_ShouldReturnReminder() 10 | { 11 | // Arrange 12 | var subscription = await _mediator.CreateSubscriptionAsync(); 13 | var reminder = await _mediator.SetReminderAsync( 14 | ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 15 | 16 | var query = ReminderQueryFactory.CreateListRemindersQuery( 17 | subscriptionId: subscription.Id); 18 | 19 | // Act 20 | var result = await _mediator.Send(query); 21 | 22 | // Assert 23 | result.IsError.Should().BeFalse(); 24 | result.Value.Should().ContainSingle().Which.Should().BeEquivalentTo(reminder); 25 | } 26 | 27 | [Fact] 28 | public async Task ListReminders_WhenNoSubscription_ShouldReturnEmptyList() 29 | { 30 | // Arrange 31 | var query = ReminderQueryFactory.CreateListRemindersQuery(); 32 | 33 | // Act 34 | var result = await _mediator.Send(query); 35 | 36 | // Assert 37 | result.IsError.Should().BeFalse(); 38 | result.Value.Should().BeEmpty(); 39 | } 40 | 41 | [Fact] 42 | public async Task ListReminders_WhenNoReminder_ShouldReturnEmptyList() 43 | { 44 | // Arrange 45 | var subscription = await _mediator.CreateSubscriptionAsync(); 46 | 47 | var query = ReminderQueryFactory.CreateListRemindersQuery(subscriptionId: subscription.Id); 48 | 49 | // Act 50 | var result = await _mediator.Send(query); 51 | 52 | // Assert 53 | result.IsError.Should().BeFalse(); 54 | result.Value.Should().BeEmpty(); 55 | } 56 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Commands/CancelSubscription/CancelSubscription.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Roles; 2 | 3 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Commands.CancelSubscription; 4 | 5 | public class CancelSubscriptionAuthorizationTests 6 | { 7 | private readonly IMediator _mediator; 8 | private readonly TestCurrentUserProvider _currentUserProvider; 9 | 10 | public CancelSubscriptionAuthorizationTests() 11 | { 12 | var webAppFactory = new WebAppFactory(); 13 | _mediator = webAppFactory.CreateMediator(); 14 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 15 | } 16 | 17 | [Fact] 18 | public async Task CancelSubscriptionForSelf_WhenIsAdmin_ShouldAuthorize() 19 | { 20 | // Arrange 21 | var currentUser = CurrentUserFactory.CreateCurrentUser( 22 | roles: [Role.Admin]); 23 | 24 | _currentUserProvider.Returns(currentUser); 25 | 26 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(); 27 | 28 | // Act 29 | var result = await _mediator.Send(command); 30 | 31 | // Assert 32 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 33 | } 34 | 35 | [Fact] 36 | public async Task CancelSubscriptionForSelf_WhenIsNotAdmin_ShouldNotAuthorize() 37 | { 38 | // Arrange 39 | var currentUser = CurrentUserFactory.CreateCurrentUser( 40 | roles: []); 41 | 42 | _currentUserProvider.Returns(currentUser); 43 | 44 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(); 45 | 46 | // Act 47 | var result = await _mediator.Send(command); 48 | 49 | // Assert 50 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 51 | } 52 | 53 | [Fact] 54 | public async Task CancelSubscriptionForDifferentUser_WhenIsAdminRole_ShouldAuthorize() 55 | { 56 | // Arrange 57 | var currentUser = CurrentUserFactory.CreateCurrentUser( 58 | id: Guid.NewGuid(), 59 | roles: [Role.Admin]); 60 | 61 | _currentUserProvider.Returns(currentUser); 62 | 63 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(); 64 | 65 | // Act 66 | var result = await _mediator.Send(command); 67 | 68 | // Assert 69 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 70 | } 71 | 72 | [Fact] 73 | public async Task CancelSubscriptionForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 74 | { 75 | // Arrange 76 | var currentUser = CurrentUserFactory.CreateCurrentUser( 77 | id: Guid.NewGuid(), 78 | roles: []); 79 | 80 | _currentUserProvider.Returns(currentUser); 81 | 82 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(); 83 | 84 | // Act 85 | var result = await _mediator.Send(command); 86 | 87 | // Assert 88 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 89 | } 90 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Commands/CancelSubscription/CancelSubscriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Commands.CancelSubscription; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class CancelSubscriptionTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task CancelSubscription_WhenSubscriptionExists_ShouldCancelSubscription() 10 | { 11 | // Arrange 12 | var subscription = await _mediator.CreateSubscriptionAsync(); 13 | 14 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(subscriptionId: subscription.Id); 15 | 16 | // Act 17 | var result = await _mediator.Send(command); 18 | 19 | // Assert 20 | result.IsError.Should().BeFalse(); 21 | result.Value.Should().Be(Result.Success); 22 | 23 | // Assert side effects took place 24 | var getSubscriptionResult = await _mediator.GetSubscriptionAsync(); 25 | 26 | getSubscriptionResult.IsError.Should().BeTrue(); 27 | getSubscriptionResult.FirstError.Type.Should().Be(ErrorType.NotFound); 28 | } 29 | 30 | [Fact] 31 | public async Task CancelSubscription_WhenSubscriptionDoesNotExists_ShouldReturnNotFound() 32 | { 33 | // Arrange 34 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(subscriptionId: Guid.NewGuid()); 35 | 36 | // Act 37 | var result = await _mediator.Send(command); 38 | 39 | // Assert 40 | result.IsError.Should().BeTrue(); 41 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 42 | } 43 | 44 | [Fact] 45 | public async Task CancelSubscription_WhenSubscriptionHasReminders_ShouldCancelSubscriptionAndDeleteReminders() 46 | { 47 | // Arrange 48 | var subscription = await _mediator.CreateSubscriptionAsync(); 49 | await _mediator.SetReminderAsync(ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 50 | await _mediator.SetReminderAsync(ReminderCommandFactory.CreateSetReminderCommand(subscriptionId: subscription.Id)); 51 | 52 | var command = SubscriptionCommandFactory.CreateCancelSubscriptionCommand(subscriptionId: subscription.Id); 53 | 54 | // Act 55 | var result = await _mediator.Send(command); 56 | 57 | // Assert 58 | result.IsError.Should().BeFalse(); 59 | result.Value.Should().Be(Result.Success); 60 | 61 | // Assert side effects took place 62 | var getSubscriptionResult = await _mediator.GetSubscriptionAsync(); 63 | 64 | getSubscriptionResult.IsError.Should().BeTrue(); 65 | getSubscriptionResult.FirstError.Type.Should().Be(ErrorType.NotFound); 66 | 67 | var listRemindersResult = await _mediator.ListRemindersAsync(); 68 | 69 | listRemindersResult.IsError.Should().BeFalse(); 70 | listRemindersResult.Value.Should().BeEmpty(); 71 | } 72 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Commands/CreateSubscription/CreateSubscription.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Commands.CreateSubscription; 5 | 6 | public class CreateSubscriptionAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public CreateSubscriptionAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task CreateSubscriptionForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 29 | 30 | // Act 31 | var result = await _mediator.Send(command); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task CreateSubscriptionForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 48 | 49 | // Act 50 | var result = await _mediator.Send(command); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task CreateSubscriptionForSelf_WhenHasRequiredPermission_ShouldAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | roles: [], 62 | permissions: [Permission.Subscription.Create]); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 67 | 68 | // Act 69 | var result = await _mediator.Send(command); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task CreateSubscriptionForSelf_WhenDoesNotHaveRequiredPermission_ShouldNotAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | roles: [], 81 | permissions: []); 82 | _currentUserProvider.Returns(currentUser); 83 | 84 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 85 | 86 | // Act 87 | var result = await _mediator.Send(command); 88 | 89 | // Assert 90 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 91 | } 92 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Commands/CreateSubscription/CreateSubscription.ValidationTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Commands.CreateSubscription; 2 | 3 | public class CreateSubscriptionValidationTests 4 | { 5 | private readonly IMediator _mediator; 6 | private readonly TestCurrentUserProvider _currentUserProvider; 7 | 8 | public CreateSubscriptionValidationTests() 9 | { 10 | var webAppFactory = new WebAppFactory(); 11 | _mediator = webAppFactory.CreateMediator(); 12 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 13 | } 14 | 15 | [Theory] 16 | [InlineData(1)] 17 | [InlineData(10001)] 18 | public async Task CreateSubscription_WhenInvalidFirstName_ShouldReturnValidationError(int nameLength) 19 | { 20 | // Arrange 21 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand( 22 | firstName: new('a', nameLength)); 23 | 24 | // Act 25 | var result = await _mediator.Send(command); 26 | 27 | // Assert 28 | result.IsError.Should().BeTrue(); 29 | result.FirstError.Type.Should().Be(ErrorType.Validation); 30 | } 31 | 32 | [Theory] 33 | [InlineData(1)] 34 | [InlineData(10001)] 35 | public async Task CreateSubscription_WhenInvalidLastName_ShouldReturnValidationError(int nameLength) 36 | { 37 | // Arrange 38 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand( 39 | lastName: new('a', nameLength)); 40 | 41 | // Act 42 | var result = await _mediator.Send(command); 43 | 44 | // Assert 45 | result.IsError.Should().BeTrue(); 46 | result.FirstError.Type.Should().Be(ErrorType.Validation); 47 | } 48 | 49 | [Theory] 50 | [InlineData("foo.com")] 51 | [InlineData("foo")] 52 | public async Task CreateSubscription_WhenInvalidEmailAddress_ShouldReturnValidationError(string email) 53 | { 54 | // Arrange 55 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(email: email); 56 | 57 | // Act 58 | var result = await _mediator.Send(command); 59 | 60 | // Assert 61 | result.IsError.Should().BeTrue(); 62 | result.FirstError.Type.Should().Be(ErrorType.Validation); 63 | } 64 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Commands/CreateSubscription/CreateSubscriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Commands.CreateSubscription; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class CreateSubscriptionTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task CreateSubscription_WhenNoSubscription_ShouldCreateSubscription() 10 | { 11 | // Arrange 12 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 13 | 14 | // Act 15 | var result = await _mediator.Send(command); 16 | 17 | // Assert 18 | result.IsError.Should().BeFalse(); 19 | result.Value.AssertCreatedFrom(command); 20 | 21 | var getSubscriptionResult = await _mediator.GetSubscriptionAsync(); 22 | getSubscriptionResult.IsError.Should().BeFalse(); 23 | getSubscriptionResult.Value.Should().BeEquivalentTo(result.Value); 24 | } 25 | 26 | [Fact] 27 | public async Task CreateSubscription_WhenSubscriptionAlreadyExists_ShouldReturnConflict() 28 | { 29 | // Arrange 30 | var command = SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 31 | 32 | // Act 33 | var firstResult = await _mediator.Send(command); 34 | var secondResult = await _mediator.Send(command); 35 | 36 | // Assert 37 | firstResult.IsError.Should().BeFalse(); 38 | firstResult.Value.AssertCreatedFrom(command); 39 | 40 | secondResult.IsError.Should().BeTrue(); 41 | secondResult.FirstError.Type.Should().Be(ErrorType.Conflict); 42 | } 43 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Queries/GetSubscription/GetSubscription.AuthorizationTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Queries.GetSubscription; 5 | 6 | public class GetSubscriptionAuthorizationTests 7 | { 8 | private readonly IMediator _mediator; 9 | private readonly TestCurrentUserProvider _currentUserProvider; 10 | 11 | public GetSubscriptionAuthorizationTests() 12 | { 13 | var webAppFactory = new WebAppFactory(); 14 | _mediator = webAppFactory.CreateMediator(); 15 | _currentUserProvider = webAppFactory.TestCurrentUserProvider; 16 | } 17 | 18 | [Fact] 19 | public async Task GetSubscriptionForDifferentUser_WhenIsAdmin_ShouldAuthorize() 20 | { 21 | // Arrange 22 | var currentUser = CurrentUserFactory.CreateCurrentUser( 23 | id: Guid.NewGuid(), 24 | roles: [Role.Admin]); 25 | 26 | _currentUserProvider.Returns(currentUser); 27 | 28 | var command = SubscriptionQueryFactory.CreateGetSubscriptionQuery(); 29 | 30 | // Act 31 | var result = await _mediator.Send(command); 32 | 33 | // Assert 34 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 35 | } 36 | 37 | [Fact] 38 | public async Task GetSubscriptionForDifferentUser_WhenIsNotAdmin_ShouldNotAuthorize() 39 | { 40 | // Arrange 41 | var currentUser = CurrentUserFactory.CreateCurrentUser( 42 | id: Guid.NewGuid(), 43 | roles: []); 44 | 45 | _currentUserProvider.Returns(currentUser); 46 | 47 | var command = SubscriptionQueryFactory.CreateGetSubscriptionQuery(); 48 | 49 | // Act 50 | var result = await _mediator.Send(command); 51 | 52 | // Assert 53 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 54 | } 55 | 56 | [Fact] 57 | public async Task GetSubscriptionForSelf_WhenDoesNotHaveRequiredPermissions_ShouldNotAuthorize() 58 | { 59 | // Arrange 60 | var currentUser = CurrentUserFactory.CreateCurrentUser( 61 | permissions: [], 62 | roles: []); 63 | 64 | _currentUserProvider.Returns(currentUser); 65 | 66 | var command = SubscriptionQueryFactory.CreateGetSubscriptionQuery(); 67 | 68 | // Act 69 | var result = await _mediator.Send(command); 70 | 71 | // Assert 72 | result.FirstError.Type.Should().Be(ErrorType.Unauthorized); 73 | } 74 | 75 | [Fact] 76 | public async Task GetSubscriptionForSelf_WhenHasRequiredPermissions_ShouldAuthorize() 77 | { 78 | // Arrange 79 | var currentUser = CurrentUserFactory.CreateCurrentUser( 80 | permissions: [Permission.Subscription.Get], 81 | roles: []); 82 | 83 | _currentUserProvider.Returns(currentUser); 84 | 85 | var command = SubscriptionQueryFactory.CreateGetSubscriptionQuery(); 86 | 87 | // Act 88 | var result = await _mediator.Send(command); 89 | 90 | // Assert 91 | result.FirstError.Type.Should().NotBe(ErrorType.Unauthorized); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.SubcutaneousTests/Subscriptions/Queries/GetSubscription/GetSubscriptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Application.SubcutaneousTests.Subscriptions.Queries.GetSubscription; 2 | 3 | [Collection(WebAppFactoryCollection.CollectionName)] 4 | public class GetSubscriptionTests(WebAppFactory webAppFactory) 5 | { 6 | private readonly IMediator _mediator = webAppFactory.CreateMediator(); 7 | 8 | [Fact] 9 | public async Task GetSubscription_WhenSubscriptionExists_ShouldReturnSubscription() 10 | { 11 | // Arrange 12 | var subscription = await _mediator.CreateSubscriptionAsync(); 13 | 14 | var query = SubscriptionQueryFactory.CreateGetSubscriptionQuery(); 15 | 16 | // Act 17 | var result = await _mediator.Send(query); 18 | 19 | // Assert 20 | result.IsError.Should().BeFalse(); 21 | result.Value.Should().BeEquivalentTo(subscription); 22 | } 23 | 24 | [Fact] 25 | public async Task GetSubscription_WhenNoSubscription_ShouldReturnNotFound() 26 | { 27 | // Arrange 28 | var query = SubscriptionQueryFactory.CreateGetSubscriptionQuery(); 29 | 30 | // Act 31 | var result = await _mediator.Send(query); 32 | 33 | // Assert 34 | result.IsError.Should().BeTrue(); 35 | result.FirstError.Type.Should().Be(ErrorType.NotFound); 36 | } 37 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.UnitTests/CleanArchitecture.Application.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.UnitTests/Common/Behaviors/ValidationBehaviorTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Behaviors; 2 | using CleanArchitecture.Application.Reminders.Commands.SetReminder; 3 | using CleanArchitecture.Domain.Reminders; 4 | 5 | using FluentValidation; 6 | using FluentValidation.Results; 7 | 8 | using MediatR; 9 | 10 | namespace CleanArchitecture.Application.UnitTests.Common.Behaviors; 11 | 12 | public class ValidationBehaviorTests 13 | { 14 | private readonly ValidationBehavior> _validationBehavior; 15 | private readonly IValidator _mockValidator; 16 | private readonly RequestHandlerDelegate> _mockNextBehavior; 17 | 18 | public ValidationBehaviorTests() 19 | { 20 | _mockNextBehavior = Substitute.For>>(); 21 | _mockValidator = Substitute.For>(); 22 | 23 | _validationBehavior = new(_mockValidator); 24 | } 25 | 26 | [Fact] 27 | public async Task InvokeValidationBehavior_WhenValidatorResultIsValid_ShouldInvokeNextBehavior() 28 | { 29 | // Arrange 30 | var setReminderCommand = ReminderCommandFactory.CreateSetReminderCommand(); 31 | var reminder = ReminderFactory.CreateReminder(); 32 | 33 | _mockValidator 34 | .ValidateAsync(setReminderCommand, Arg.Any()) 35 | .Returns(new ValidationResult()); 36 | 37 | _mockNextBehavior.Invoke().Returns(reminder); 38 | 39 | // Act 40 | var result = await _validationBehavior.Handle(setReminderCommand, _mockNextBehavior, default); 41 | 42 | // Assert 43 | result.IsError.Should().BeFalse(); 44 | result.Value.Should().BeEquivalentTo(reminder); 45 | } 46 | 47 | [Fact] 48 | public async Task InvokeValidationBehavior_WhenValidatorResultIsNotValid_ShouldReturnListOfErrors() 49 | { 50 | // Arrange 51 | var setReminderCommand = ReminderCommandFactory.CreateSetReminderCommand(); 52 | List validationFailures = [new(propertyName: "foo", errorMessage: "bad foo")]; 53 | 54 | _mockValidator 55 | .ValidateAsync(setReminderCommand, Arg.Any()) 56 | .Returns(new ValidationResult(validationFailures)); 57 | 58 | // Act 59 | var result = await _validationBehavior.Handle(setReminderCommand, _mockNextBehavior, default); 60 | 61 | // Assert 62 | result.IsError.Should().BeTrue(); 63 | result.FirstError.Code.Should().Be("foo"); 64 | result.FirstError.Description.Should().Be("bad foo"); 65 | } 66 | 67 | [Fact] 68 | public async Task InvokeValidationBehavior_WhenNoValidator_ShouldInvokeNextBehavior() 69 | { 70 | // Arrange 71 | var setReminderCommand = ReminderCommandFactory.CreateSetReminderCommand(); 72 | var validationBehavior = new ValidationBehavior>(); 73 | 74 | var reminder = ReminderFactory.CreateReminder(); 75 | _mockNextBehavior.Invoke().Returns(reminder); 76 | 77 | // Act 78 | var result = await validationBehavior.Handle(setReminderCommand, _mockNextBehavior, default); 79 | 80 | // Assert 81 | result.IsError.Should().BeFalse(); 82 | result.Value.Should().Be(reminder); 83 | } 84 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Application.UnitTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using ErrorOr; 2 | 3 | global using FluentAssertions; 4 | 5 | global using NSubstitute; 6 | 7 | global using TestCommon.Reminders; 8 | global using TestCommon.TestConstants; 9 | 10 | global using Xunit; -------------------------------------------------------------------------------- /tests/CleanArchitecture.Domain.UnitTests/CleanArchitecture.Domain.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.Domain.UnitTests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using ErrorOr; 2 | 3 | global using FluentAssertions; 4 | 5 | global using TestCommon.Reminders; 6 | global using TestCommon.Users; 7 | 8 | global using Xunit; -------------------------------------------------------------------------------- /tests/CleanArchitecture.Domain.UnitTests/Reminders/ReminderTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Domain.UnitTests.Reminders; 2 | 3 | public class ReminderTests 4 | { 5 | [Fact] 6 | public void CreateReminder_WhenConstructedSuccessfully_ShouldHaveIsDismissedFalse() 7 | { 8 | // Act 9 | var reminder = ReminderFactory.CreateReminder(); 10 | 11 | // Assert 12 | reminder.IsDismissed.Should().BeFalse(); 13 | } 14 | 15 | [Fact] 16 | public void DismissReminder_WhenReminderNotDismissed_ShouldDismissReminder() 17 | { 18 | // Arrange 19 | var reminder = ReminderFactory.CreateReminder(); 20 | 21 | // Act 22 | var dismissReminderResult = reminder.Dismiss(); 23 | 24 | // Assert 25 | dismissReminderResult.IsError.Should().BeFalse(); 26 | reminder.IsDismissed.Should().BeTrue(); 27 | } 28 | 29 | [Fact] 30 | public void DismissReminder_WhenReminderAlreadyDismissed_ShouldReturnConflict() 31 | { 32 | // Arrange 33 | var reminder = ReminderFactory.CreateReminder(); 34 | 35 | // Act 36 | var firstDismissReminderResult = reminder.Dismiss(); 37 | var secondDismissReminderResult = reminder.Dismiss(); 38 | 39 | // Assert 40 | firstDismissReminderResult.IsError.Should().BeFalse(); 41 | 42 | secondDismissReminderResult.IsError.Should().BeTrue(); 43 | secondDismissReminderResult.FirstError.Type.Should().Be(ErrorType.Conflict); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/CleanArchitecture.Domain.UnitTests/Users/UserTests.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | 3 | namespace CleanArchitecture.Domain.UnitTests.Users; 4 | 5 | public class UserTests 6 | { 7 | [Theory] 8 | [MemberData(nameof(ListSubscriptionsWithLimit))] 9 | public void SetReminder_WhenMoreThanSubscriptionAllows_ShouldFail(SubscriptionType subscriptionType) 10 | { 11 | // Arrange 12 | // Create user 13 | var subscription = SubscriptionFactory.CreateSubscription(subscriptionType: subscriptionType); 14 | var user = UserFactory.CreateUser(subscription: subscription); 15 | 16 | // Create max number of daily reminders + 1 17 | var reminders = Enumerable.Range(0, subscriptionType.GetMaxDailyReminders() + 1) 18 | .Select(_ => ReminderFactory.CreateReminder(id: Guid.NewGuid(), subscriptionId: subscription.Id)); 19 | 20 | // Act 21 | var setReminderResults = reminders.Select(user.SetReminder).ToList(); 22 | 23 | // Assert all reminders set successfully 24 | var allButLastSetReminderResults = setReminderResults[..^1]; 25 | 26 | allButLastSetReminderResults.Should().AllSatisfy( 27 | setReminderResult => setReminderResult.Value.Should().Be(Result.Success)); 28 | 29 | // Assert settings last reminder returned conflict 30 | var lastReminder = setReminderResults.Last(); 31 | 32 | lastReminder.IsError.Should().BeTrue(); 33 | lastReminder.FirstError.Should().Be(UserErrors.CannotCreateMoreRemindersThanSubscriptionAllows); 34 | } 35 | 36 | /// 37 | /// This is completely redundant as there is only one subscription with a limit. 38 | /// I added this here just so you have a copy-paste method for your own usage. 39 | /// 40 | public static TheoryData ListSubscriptionsWithLimit() 41 | { 42 | TheoryData theoryData = []; 43 | 44 | SubscriptionType.List.Except([SubscriptionType.Pro]).ToList() 45 | .ForEach(theoryData.Add); 46 | 47 | return theoryData; 48 | } 49 | } -------------------------------------------------------------------------------- /tests/TestCommon/Reminders/MediatorExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Reminders.Commands.SetReminder; 2 | using CleanArchitecture.Application.Reminders.Queries.GetReminder; 3 | using CleanArchitecture.Application.Reminders.Queries.ListReminders; 4 | using CleanArchitecture.Domain.Reminders; 5 | 6 | using ErrorOr; 7 | 8 | using FluentAssertions; 9 | 10 | using MediatR; 11 | 12 | namespace TestCommon.Reminders; 13 | 14 | public static class MediatorExtensions 15 | { 16 | public static async Task SetReminderAsync( 17 | this IMediator mediator, 18 | SetReminderCommand? command = null) 19 | { 20 | command ??= ReminderCommandFactory.CreateSetReminderCommand(); 21 | var result = await mediator.Send(command); 22 | 23 | result.IsError.Should().BeFalse(); 24 | result.Value.AssertCreatedFrom(command); 25 | 26 | return result.Value; 27 | } 28 | 29 | public static async Task>> ListRemindersAsync( 30 | this IMediator mediator, 31 | ListRemindersQuery? query = null) 32 | { 33 | return await mediator.Send(query ?? ReminderQueryFactory.CreateListRemindersQuery()); 34 | } 35 | 36 | public static async Task> GetReminderAsync( 37 | this IMediator mediator, 38 | GetReminderQuery? query = null) 39 | { 40 | query ??= ReminderQueryFactory.CreateGetReminderQuery(); 41 | return await mediator.Send(query); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/TestCommon/Reminders/ReminderCommandFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Reminders.Commands.DeleteReminder; 2 | using CleanArchitecture.Application.Reminders.Commands.DismissReminder; 3 | using CleanArchitecture.Application.Reminders.Commands.SetReminder; 4 | 5 | using TestCommon.TestConstants; 6 | 7 | namespace TestCommon.Reminders; 8 | 9 | public static class ReminderCommandFactory 10 | { 11 | public static SetReminderCommand CreateSetReminderCommand( 12 | Guid? userId = null, 13 | Guid? subscriptionId = null, 14 | string text = Constants.Reminder.Text, 15 | DateTime? dateTime = null) 16 | { 17 | return new SetReminderCommand( 18 | userId ?? Constants.User.Id, 19 | subscriptionId ?? Constants.Subscription.Id, 20 | text, 21 | dateTime ?? Constants.Reminder.DateTime); 22 | } 23 | 24 | public static DismissReminderCommand CreateDismissReminderCommand( 25 | Guid? userId = null, 26 | Guid? subscriptionId = null, 27 | Guid? reminderId = null) 28 | { 29 | return new DismissReminderCommand( 30 | userId ?? Constants.User.Id, 31 | subscriptionId ?? Constants.Subscription.Id, 32 | reminderId ?? Constants.Reminder.Id); 33 | } 34 | 35 | public static DeleteReminderCommand CreateDeleteReminderCommand( 36 | Guid? userId = null, 37 | Guid? subscriptionId = null, 38 | Guid? reminderId = null) 39 | { 40 | return new DeleteReminderCommand( 41 | userId ?? Constants.User.Id, 42 | subscriptionId ?? Constants.Subscription.Id, 43 | reminderId ?? Constants.Reminder.Id); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/TestCommon/Reminders/ReminderFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Reminders; 2 | 3 | using TestCommon.TestConstants; 4 | 5 | namespace TestCommon.Reminders; 6 | 7 | public static class ReminderFactory 8 | { 9 | public static Reminder CreateReminder( 10 | Guid? userId = null, 11 | Guid? subscriptionId = null, 12 | string text = Constants.Reminder.Text, 13 | DateTime? dateTime = null, 14 | Guid? id = null) 15 | { 16 | return new Reminder( 17 | userId ?? Constants.User.Id, 18 | subscriptionId ?? Constants.Subscription.Id, 19 | text ?? Constants.Reminder.Text, 20 | dateTime ?? Constants.Reminder.DateTime, 21 | id ?? Constants.Reminder.Id); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/TestCommon/Reminders/ReminderQueyFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Reminders.Queries.GetReminder; 2 | using CleanArchitecture.Application.Reminders.Queries.ListReminders; 3 | 4 | using TestCommon.TestConstants; 5 | 6 | namespace TestCommon.Reminders; 7 | 8 | public static class ReminderQueryFactory 9 | { 10 | public static ListRemindersQuery CreateListRemindersQuery( 11 | Guid? userId = null, 12 | Guid? subscriptionId = null) 13 | { 14 | return new ListRemindersQuery( 15 | userId ?? Constants.User.Id, 16 | subscriptionId ?? Constants.Subscription.Id); 17 | } 18 | 19 | public static GetReminderQuery CreateGetReminderQuery( 20 | Guid? userId = null, 21 | Guid? subscriptionId = null, 22 | Guid? reminderId = null) 23 | { 24 | return new GetReminderQuery( 25 | userId ?? Constants.User.Id, 26 | subscriptionId ?? Constants.Subscription.Id, 27 | reminderId ?? Constants.Reminder.Id); 28 | } 29 | } -------------------------------------------------------------------------------- /tests/TestCommon/Reminders/ReminderValidationsExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Reminders.Commands.SetReminder; 2 | using CleanArchitecture.Domain.Reminders; 3 | 4 | using FluentAssertions; 5 | 6 | namespace TestCommon.Reminders; 7 | 8 | public static class ReminderValidator 9 | { 10 | public static void AssertCreatedFrom(this Reminder reminder, SetReminderCommand command) 11 | { 12 | reminder.SubscriptionId.Should().Be(command.SubscriptionId); 13 | reminder.DateTime.Should().Be(command.DateTime); 14 | reminder.Text.Should().Be(command.Text); 15 | reminder.IsDismissed.Should().BeFalse(); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/TestCommon/Security/CurrentUserFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 2 | 3 | using TestCommon.TestConstants; 4 | 5 | namespace TestCommon.Security; 6 | 7 | public static class CurrentUserFactory 8 | { 9 | public static CurrentUser CreateCurrentUser( 10 | Guid? id = null, 11 | string firstName = Constants.User.FirstName, 12 | string lastName = Constants.User.LastName, 13 | string email = Constants.User.Email, 14 | IReadOnlyList? permissions = null, 15 | IReadOnlyList? roles = null) 16 | { 17 | return new CurrentUser( 18 | id ?? Constants.User.Id, 19 | firstName, 20 | lastName, 21 | email, 22 | permissions ?? Constants.User.Permissions, 23 | roles ?? Constants.User.Roles); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/TestCommon/Security/TestCurrentUserProvider.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Security.CurrentUserProvider; 2 | 3 | namespace TestCommon.Security; 4 | 5 | public class TestCurrentUserProvider : ICurrentUserProvider 6 | { 7 | private CurrentUser? _currentUser; 8 | 9 | public void Returns(CurrentUser currentUser) 10 | { 11 | _currentUser = currentUser; 12 | } 13 | 14 | public CurrentUser GetCurrentUser() => _currentUser ?? CurrentUserFactory.CreateCurrentUser(); 15 | } 16 | -------------------------------------------------------------------------------- /tests/TestCommon/Subscriptions/MediatorExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 2 | using CleanArchitecture.Application.Subscriptions.Common; 3 | using CleanArchitecture.Application.Subscriptions.Queries.GetSubscription; 4 | 5 | using ErrorOr; 6 | 7 | using FluentAssertions; 8 | 9 | using MediatR; 10 | 11 | namespace TestCommon.Subscriptions; 12 | 13 | public static class MediatorExtensions 14 | { 15 | public static async Task CreateSubscriptionAsync( 16 | this IMediator mediator, 17 | CreateSubscriptionCommand? command = null) 18 | { 19 | command ??= SubscriptionCommandFactory.CreateCreateSubscriptionCommand(); 20 | 21 | var result = await mediator.Send(command); 22 | 23 | result.IsError.Should().BeFalse(); 24 | result.Value.AssertCreatedFrom(command); 25 | 26 | return result.Value; 27 | } 28 | 29 | public static async Task> GetSubscriptionAsync( 30 | this IMediator mediator, 31 | GetSubscriptionQuery? query = null) 32 | { 33 | return await mediator.Send(query ?? SubscriptionQueryFactory.CreateGetSubscriptionQuery()); 34 | } 35 | } -------------------------------------------------------------------------------- /tests/TestCommon/Subscriptions/SubscriptionCommandFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Subscriptions.Commands.CancelSubscription; 2 | using CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 3 | using CleanArchitecture.Domain.Users; 4 | 5 | using TestCommon.TestConstants; 6 | 7 | namespace TestCommon.Subscriptions; 8 | 9 | public static class SubscriptionCommandFactory 10 | { 11 | public static CreateSubscriptionCommand CreateCreateSubscriptionCommand( 12 | Guid? userId = null, 13 | string firstName = Constants.User.FirstName, 14 | string lastName = Constants.User.LastName, 15 | string email = Constants.User.Email, 16 | SubscriptionType? subscriptionType = null) 17 | { 18 | return new CreateSubscriptionCommand( 19 | userId ?? Constants.User.Id, 20 | firstName, 21 | lastName, 22 | email, 23 | subscriptionType ?? Constants.Subscription.Type); 24 | } 25 | 26 | public static CancelSubscriptionCommand CreateCancelSubscriptionCommand( 27 | Guid? userId = null, 28 | Guid? subscriptionId = null) 29 | { 30 | return new CancelSubscriptionCommand( 31 | userId ?? Constants.User.Id, 32 | subscriptionId ?? Constants.Subscription.Id); 33 | } 34 | } -------------------------------------------------------------------------------- /tests/TestCommon/Subscriptions/SubscriptionFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Subscriptions; 2 | using CleanArchitecture.Domain.Users; 3 | 4 | using TestCommon.TestConstants; 5 | 6 | namespace TestCommon.Users; 7 | 8 | public static class SubscriptionFactory 9 | { 10 | public static Subscription CreateSubscription( 11 | SubscriptionType? subscriptionType = null, 12 | Guid? id = null) 13 | { 14 | return new Subscription( 15 | subscriptionType ?? Constants.Subscription.Type, 16 | id ?? Constants.Subscription.Id); 17 | } 18 | } -------------------------------------------------------------------------------- /tests/TestCommon/Subscriptions/SubscriptionQueryFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Subscriptions.Queries.GetSubscription; 2 | 3 | using TestCommon.TestConstants; 4 | 5 | namespace TestCommon.Subscriptions; 6 | 7 | public static class SubscriptionQueryFactory 8 | { 9 | public static GetSubscriptionQuery CreateGetSubscriptionQuery( 10 | Guid? userId = null) 11 | { 12 | return new GetSubscriptionQuery(userId ?? Constants.User.Id); 13 | } 14 | } -------------------------------------------------------------------------------- /tests/TestCommon/Subscriptions/SubscriptionValidationExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Subscriptions.Commands.CreateSubscription; 2 | using CleanArchitecture.Application.Subscriptions.Common; 3 | 4 | using FluentAssertions; 5 | 6 | namespace TestCommon.Subscriptions; 7 | 8 | public static class SubscriptionValidationExtensions 9 | { 10 | public static void AssertCreatedFrom(this SubscriptionResult subscriptionType, CreateSubscriptionCommand command) 11 | { 12 | subscriptionType.SubscriptionType.Should().Be(command.SubscriptionType); 13 | subscriptionType.UserId.Should().Be(command.UserId); 14 | } 15 | } -------------------------------------------------------------------------------- /tests/TestCommon/TestCommon.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | false 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/TestCommon/TestConstants/Constants.Reminder.cs: -------------------------------------------------------------------------------- 1 | namespace TestCommon.TestConstants; 2 | 3 | public static partial class Constants 4 | { 5 | public static class Reminder 6 | { 7 | public const string Text = "Remind to to dismiss this reminder"; 8 | public static readonly Guid Id = Guid.NewGuid(); 9 | public static readonly DateTime DateTime = DateTime.UtcNow 10 | .AddDays(1).Date 11 | .AddHours(8); // tomorrow 8 AM 12 | } 13 | } -------------------------------------------------------------------------------- /tests/TestCommon/TestConstants/Constants.Subscription.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Users; 2 | 3 | namespace TestCommon.TestConstants; 4 | 5 | public static partial class Constants 6 | { 7 | public static class Subscription 8 | { 9 | public static readonly Guid Id = Guid.NewGuid(); 10 | public static readonly SubscriptionType Type = SubscriptionType.Basic; 11 | } 12 | } -------------------------------------------------------------------------------- /tests/TestCommon/TestConstants/Constants.User.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Application.Common.Security.Permissions; 2 | using CleanArchitecture.Application.Common.Security.Roles; 3 | 4 | namespace TestCommon.TestConstants; 5 | 6 | public static partial class Constants 7 | { 8 | public static class User 9 | { 10 | public const string FirstName = "Amiko"; 11 | public const string LastName = "Mantinband"; 12 | public const string Email = "amiko@mantinband.com"; 13 | public static readonly Guid Id = Guid.NewGuid(); 14 | public static readonly List Permissions = 15 | [ 16 | Permission.Reminder.Get, 17 | Permission.Reminder.Set, 18 | Permission.Reminder.Delete, 19 | Permission.Reminder.Dismiss, 20 | Permission.Subscription.Create, 21 | Permission.Subscription.Delete, 22 | Permission.Subscription.Get, 23 | ]; 24 | 25 | public static readonly List Roles = 26 | [ 27 | Role.Admin 28 | ]; 29 | } 30 | } -------------------------------------------------------------------------------- /tests/TestCommon/TestUtilities/NSubstitute/Must.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | namespace NSubstitute; 4 | 5 | public static class Must 6 | { 7 | public static List BeEmptyList() => 8 | Arg.Do>(x => x.Should().BeEmpty()); 9 | 10 | public static List BeListWith(List value) => 11 | Arg.Do>(x => x.Should().BeEquivalentTo(value)); 12 | } -------------------------------------------------------------------------------- /tests/TestCommon/Users/UserFactory.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Domain.Subscriptions; 2 | using CleanArchitecture.Domain.Users; 3 | 4 | using TestCommon.TestConstants; 5 | 6 | namespace TestCommon.Users; 7 | 8 | public static class UserFactory 9 | { 10 | public static User CreateUser( 11 | Guid? id = null, 12 | string firstName = Constants.User.FirstName, 13 | string lastName = Constants.User.LastName, 14 | string emailName = Constants.User.Email, 15 | Subscription? subscription = null) 16 | { 17 | return new User( 18 | id ?? Constants.User.Id, 19 | firstName, 20 | lastName, 21 | emailName, 22 | subscription ?? SubscriptionFactory.CreateSubscription()); 23 | } 24 | } --------------------------------------------------------------------------------