├── .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 | }
--------------------------------------------------------------------------------