(`api/weather-forecasts/${id}`);
26 | }
27 |
28 | public getTemperatureSummary(temperature: number): string {
29 | if (temperature > 40) {
30 | return "Scorching";
31 | }
32 | else if (temperature > 20) {
33 | return "Hot";
34 | }
35 | else if (temperature > 10) {
36 | return "Mild";
37 | }
38 | else if (temperature > 0) {
39 | return "Cold";
40 | }
41 | else if (temperature === null) {
42 | return "";
43 | }
44 | else {
45 | return "Freezing";
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | standalone: false
7 | })
8 | export class AppComponent {
9 | title = 'app';
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
5 | import { RouterModule } from '@angular/router';
6 |
7 | import { AppComponent } from './app.component';
8 | import { NavMenuComponent } from './nav-menu/nav-menu.component';
9 | import { HomeComponent } from './home/home.component';
10 | import { WeatherForecastsComponent } from './weather-forecasts/weather-forecasts.component';
11 |
12 | @NgModule({
13 | declarations: [
14 | AppComponent,
15 | NavMenuComponent,
16 | HomeComponent,
17 | WeatherForecastsComponent
18 | ],
19 | bootstrap: [
20 | AppComponent
21 | ],
22 | imports: [
23 | BrowserModule,
24 | FormsModule,
25 | RouterModule.forRoot([
26 | { path: '', component: HomeComponent, pathMatch: 'full' },
27 | { path: 'weather-forecast', component: WeatherForecastsComponent },
28 | ])
29 | ],
30 | providers: [
31 | provideHttpClient(withInterceptorsFromDi())
32 | ]
33 | })
34 | export class AppModule { }
35 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/app.server.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ServerModule } from '@angular/platform-server';
3 | import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
4 | import { AppComponent } from './app.component';
5 | import { AppModule } from './app.module';
6 |
7 | @NgModule({
8 | imports: [AppModule, ServerModule, ModuleMapLoaderModule],
9 | bootstrap: [AppComponent]
10 | })
11 | export class AppServerModule { }
12 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/home/home.component.html:
--------------------------------------------------------------------------------
1 | Hello, world!
2 | Welcome to your new single-page application, built with:
3 |
8 | To help you get started, we've also set up:
9 |
10 | Client-side navigation . For example, click Counter then Back to return here.
11 | Angular CLI integration . In development mode, there's no need to run ng serve
. It runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.
12 | Efficient production builds . In production mode, development-time features are disabled, and your dotnet publish
configuration automatically invokes ng build
to produce minified, ahead-of-time compiled JavaScript files.
13 |
14 | The ClientApp
subdirectory is a standard Angular CLI application. If you open a command prompt in that directory, you can run any ng
command (e.g., ng test
), or use npm
to install extra packages into it.
15 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/home/home.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-home',
5 | templateUrl: './home.component.html',
6 | standalone: false
7 | })
8 | export class HomeComponent {
9 | }
10 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/nav-menu/nav-menu.component.css:
--------------------------------------------------------------------------------
1 | a.navbar-brand {
2 | white-space: normal;
3 | text-align: center;
4 | word-break: break-all;
5 | }
6 |
7 | html {
8 | font-size: 14px;
9 | }
10 | @media (min-width: 768px) {
11 | html {
12 | font-size: 16px;
13 | }
14 | }
15 |
16 | .box-shadow {
17 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
18 | }
19 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/nav-menu/nav-menu.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/nav-menu/nav-menu.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-nav-menu',
5 | templateUrl: './nav-menu.component.html',
6 | styleUrls: ['./nav-menu.component.css'],
7 | standalone: false
8 | })
9 | export class NavMenuComponent {
10 | isExpanded = false;
11 |
12 | collapse() {
13 | this.isExpanded = false;
14 | }
15 |
16 | toggle() {
17 | this.isExpanded = !this.isExpanded;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/weather-forecasts/weather-forecasts.component.html:
--------------------------------------------------------------------------------
1 | Weather forecast
2 |
3 | This component demonstrates fetching data from the server.
4 |
5 | Please select a location to see the forecast.
6 |
7 |
8 |
9 |
10 |
11 |
13 | {{location.city}}
14 |
15 | Location
16 |
17 |
18 |
19 |
20 | Generate
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Use the Generate button to generate some weather forecasts.
31 |
32 |
33 |
34 |
35 | Date
36 | Temp. (C)
37 | Temp. (F)
38 | Summary
39 |
40 |
41 |
42 |
43 |
44 | {{ forecast.date }}
45 | {{ forecast.temperatureC }}
46 | {{ forecast.temperatureF }}
47 | {{ forecast.summary }}
48 | Delete
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/app/weather-forecasts/weather-forecasts.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { WeatherService } from '../_shared/services/weather.service';
3 | import { CreateWeatherForecast, WeatherForecast } from '../_shared/models/weather.model';
4 | import { LocationsService } from '../_shared/services/locations.service';
5 | import { WeatherLocation } from '../_shared/models/location.model';
6 |
7 | @Component({
8 | selector: 'app-weather-forecasts',
9 | templateUrl: './weather-forecasts.component.html',
10 | standalone: false
11 | })
12 | export class WeatherForecastsComponent implements OnInit {
13 |
14 | public locations: WeatherLocation[] = [];
15 | public forecasts: WeatherForecast[] = [];
16 | public selectedLocationId?: string;
17 |
18 | public constructor(private readonly _weatherService: WeatherService,
19 | private readonly _locationsService: LocationsService) {
20 |
21 | }
22 |
23 | public generate(): void {
24 | function getRandom(min: number, max: number) {
25 | const floatRandom = Math.random()
26 |
27 | const difference = max - min
28 |
29 | // random between 0 and the difference
30 | const random = Math.round(difference * floatRandom)
31 |
32 | const randomWithinRange = random + min
33 |
34 | return randomWithinRange
35 | }
36 | const temperature = getRandom(-50, 50);
37 | const forecast: CreateWeatherForecast = {
38 | date: new Date(),
39 | temperatureC: temperature,
40 | summary: this._weatherService.getTemperatureSummary(temperature),
41 | locationId: this.selectedLocationId!
42 | };
43 | this._weatherService.create(forecast)
44 | .subscribe(() => {
45 | this.loadForecasts();
46 | });
47 | }
48 |
49 | public delete(id: string): void {
50 | this._weatherService.delete(id)
51 | .subscribe(() => {
52 | this.loadForecasts();
53 | });
54 | }
55 |
56 | public loadForecasts(): void {
57 | if (this.selectedLocationId) {
58 | this._weatherService.get(this.selectedLocationId)
59 | .subscribe(forecasts => {
60 | this.forecasts = forecasts;
61 | });
62 | }
63 | }
64 |
65 | public ngOnInit(): void {
66 | this.loadLocations();
67 | }
68 |
69 | private loadLocations(): void {
70 | this._locationsService.get()
71 | .subscribe(locations => {
72 | this.locations = locations;
73 | });
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt-bentley/CleanArchitecture/e92a0bb774b33c9e581ac3168507c8d4090466e5/src/CleanArchitecture.Web/ClientApp/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CleanArchitecture.Web
6 |
7 |
8 |
9 |
10 |
11 |
12 | Loading...
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | export function getBaseUrl() {
8 | return document.getElementsByTagName('base')[0].href;
9 | }
10 |
11 | const providers = [
12 | { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] }
13 | ];
14 |
15 | if (environment.production) {
16 | enableProdMode();
17 | }
18 |
19 | platformBrowserDynamic(providers).bootstrapModule(AppModule)
20 | .catch(err => console.log(err));
21 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * IE11 requires the following for NgClass support on SVG elements
23 | */
24 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
25 |
26 | /**
27 | * Web Animations `@angular/platform-browser/animations`
28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
30 | */
31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
32 |
33 | /**
34 | * By default, zone.js will patch all possible macroTask and DomEvents
35 | * user can disable parts of macroTask/DomEvents patch by setting following flags
36 | * because those flags need to be set before `zone.js` being loaded, and webpack
37 | * will put import in the top of bundle, so user need to create a separate file
38 | * in this directory (for example: zone-flags.ts), and put the following flags
39 | * into that file, and then add the following code before importing zone.js.
40 | * import './zone-flags';
41 | *
42 | * The flags allowed in zone-flags.ts are listed here.
43 | *
44 | * The following flags will work for all browsers.
45 | *
46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
49 | *
50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
52 | *
53 | * (window as any).__Zone_enable_cross_context_check = true;
54 | *
55 | */
56 |
57 | /***************************************************************************************************
58 | * Zone JS is required by default for Angular itself.
59 | */
60 | import 'zone.js'; // Included with Angular CLI.
61 |
62 |
63 | /***************************************************************************************************
64 | * APPLICATION IMPORTS
65 | */
66 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | /* Provide sufficient contrast against white background */
4 | a {
5 | color: #0366d6;
6 | }
7 |
8 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
9 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
10 | }
11 |
12 | code {
13 | color: #e01a76;
14 | }
15 |
16 | .btn-primary {
17 | color: #fff;
18 | background-color: #1b6ec2;
19 | border-color: #1861ac;
20 | }
21 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | getTestBed().initTestEnvironment(
11 | BrowserDynamicTestingModule,
12 | platformBrowserDynamicTesting()
13 | );
14 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "forceConsistentCasingInFileNames": true,
7 | "esModuleInterop": true,
8 | "strict": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "sourceMap": true,
12 | "declaration": false,
13 | "experimentalDecorators": true,
14 | "moduleResolution": "node",
15 | "importHelpers": true,
16 | "target": "es2022",
17 | "module": "es2020",
18 | "lib": [
19 | "es2018",
20 | "dom"
21 | ],
22 | "useDefineForClassFields": false
23 | },
24 | "angularCompilerOptions": {
25 | "enableI18nLegacyMessageIdFormat": false,
26 | "strictInjectionParameters": true,
27 | "strictInputAccessModifiers": true,
28 | "strictTemplates": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/ClientApp/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "src/test.ts",
13 | "src/polyfills.ts"
14 | ],
15 | "include": [
16 | "src/**/*.spec.ts",
17 | "src/**/*.d.ts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled-extra AS base
2 |
3 | WORKDIR /app
4 | EXPOSE 8080
5 |
6 | ENV ASPNETCORE_URLS=http://+:8080;
7 |
8 | FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS source
9 |
10 | # Setup Node and NPM
11 | RUN apt-get update && \
12 | apt-get install -y curl gnupg2 && \
13 | mkdir -p /etc/apt/keyrings && \
14 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
15 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
16 | apt-get update && \
17 | apt-get install -y build-essential nodejs && \
18 | npm install -g npm@latest
19 |
20 | COPY ["src/", "/src/"]
21 |
22 | FROM source AS publish
23 | WORKDIR "/src/CleanArchitecture.Web"
24 | RUN dotnet restore "CleanArchitecture.Web.csproj" && \
25 | dotnet publish "CleanArchitecture.Web.csproj" --no-restore -c Release -o /app
26 |
27 | RUN chown -R 1000:1000 /app/wwwroot
28 |
29 | FROM base AS final
30 | COPY --from=publish /app .
31 | USER 1000
32 | ENTRYPOINT ["dotnet", "CleanArchitecture.Web.dll"]
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/Program.cs:
--------------------------------------------------------------------------------
1 | var builder = WebApplication.CreateBuilder(args);
2 |
3 | // Add services to the container.
4 | builder.Services.AddControllers();
5 |
6 | var app = builder.Build();
7 |
8 | // Configure the HTTP request pipeline.
9 | if (!app.Environment.IsDevelopment())
10 | {
11 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
12 | app.UseHsts();
13 | }
14 |
15 | app.UseHttpsRedirection();
16 | app.UseStaticFiles();
17 | app.UseRouting();
18 |
19 |
20 | app.MapControllerRoute(
21 | name: "default",
22 | pattern: "{controller}/{action=Index}/{id?}");
23 |
24 | app.MapFallbackToFile("index.html");
25 |
26 | app.Run();
27 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "CleanArchitecture.Web": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "applicationUrl": "https://localhost:7251;http://localhost:5141",
7 | "environmentVariables": {
8 | "ASPNETCORE_ENVIRONMENT": "Development",
9 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
10 | }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.AspNetCore.SpaProxy": "Information",
7 | "Microsoft.Hosting.Lifetime": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/src/CleanArchitecture.Web/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/matt-bentley/CleanArchitecture/e92a0bb774b33c9e581ac3168507c8d4090466e5/src/CleanArchitecture.Web/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/CleanArchitecture.AcceptanceTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | disable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | PreserveNewest
19 | true
20 | PreserveNewest
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | all
33 | runtime; build; native; contentfiles; analyzers; buildtransitive
34 |
35 |
36 | all
37 | runtime; build; native; contentfiles; analyzers; buildtransitive
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/playwright:v1.52.0-jammy
2 |
3 | RUN wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && \
4 | dpkg -i packages-microsoft-prod.deb && \
5 | rm packages-microsoft-prod.deb && \
6 | apt-get update && \
7 | apt-get install -y dotnet-sdk-8.0
8 |
9 | COPY ["src/", "/src/"]
10 | COPY ["tests/", "/tests/"]
11 |
12 | WORKDIR /tests/CleanArchitecture.AcceptanceTests
13 |
14 | RUN dotnet restore "CleanArchitecture.AcceptanceTests.csproj"
15 | RUN dotnet build "CleanArchitecture.AcceptanceTests.csproj" --no-restore -c Release
16 | ENTRYPOINT ["dotnet", "test", "-c", "Release"]
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Features/weather-forecast.feature:
--------------------------------------------------------------------------------
1 | @weather_cleanup
2 | Feature: Weather Forecast
3 |
4 | Weather Forecast page shows a table of weather forecasts
5 | and allows new forecasts to be generated.
6 |
7 | Scenario: 1 Navigate to Weather Forecast page
8 | Given a user is on the Home page
9 | When Weather Forecast page is opened
10 | Then Weather Forecast page is open
11 |
12 | Scenario: 2 Generate a Weather Forecasts
13 | Given a user is on the Weather Forecast page
14 | When 'London' location is selected
15 | And a weather forecast is generated
16 | And a weather forecast is generated
17 | Then '2' weather forecasts present
18 |
19 | Scenario: 3 Generate Weather Forecasts prompt shown
20 | Given a user is on the Weather Forecast page
21 | When 'Mumbai' location is selected
22 | Then Generate prompt is visible
23 | And '0' weather forecasts present
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Hooks/GlobalHooks.cs:
--------------------------------------------------------------------------------
1 | using SpecFlow.Autofac.SpecFlowPlugin;
2 | using SpecFlow.Autofac;
3 | using Microsoft.Extensions.Configuration;
4 | using CleanArchitecture.AcceptanceTests.Settings;
5 | using Autofac;
6 | using CleanArchitecture.Infrastructure.AutofacModules;
7 | using CleanArchitecture.AcceptanceTests.Pages;
8 |
9 | namespace CleanArchitecture.AcceptanceTests.Hooks
10 | {
11 | [Binding]
12 | public sealed class GlobalHooks
13 | {
14 | private static IConfiguration Configuration;
15 |
16 | [GlobalDependencies]
17 | public static void CreateGlobalContainer(ContainerBuilder container)
18 | {
19 | Configuration = new ConfigurationBuilder()
20 | .AddJsonFile("appsettings.json", true)
21 | .Build();
22 |
23 | var browserSettings = new BrowserSettings();
24 | Configuration.GetSection("Browser").Bind(browserSettings);
25 |
26 | container.RegisterInstance(new TestHostEnvironment())
27 | .AsImplementedInterfaces();
28 |
29 | var testHarness = new TestHarness(browserSettings);
30 |
31 | container.RegisterInstance(testHarness).AsSelf();
32 | container.RegisterInstance(browserSettings);
33 |
34 | RegisterApplicationServices(container);
35 | }
36 |
37 | [ScenarioDependencies]
38 | public static void CreateContainerBuilder(ContainerBuilder container)
39 | {
40 | container.AddSpecFlowBindings();
41 | RegisterApplicationServices(container);
42 | }
43 |
44 | private static void RegisterApplicationServices(ContainerBuilder container)
45 | {
46 | container.RegisterModule(new InfrastructureModule(Configuration));
47 | }
48 |
49 | [BeforeFeature]
50 | public static async Task BeforeFeatureAsync(TestHarness testHarness)
51 | {
52 | await testHarness.StartAsync();
53 | testHarness.CurrentPage = new HomePage(testHarness.Page);
54 | }
55 |
56 | [BeforeScenario]
57 | public static async Task BeforeScenarioAsync(FeatureContext featureContext, ScenarioContext scenarioContext, TestHarness testHarness)
58 | {
59 | await testHarness.StartScenarioAsync(featureContext.FeatureInfo.Title, scenarioContext.ScenarioInfo.Title);
60 | }
61 |
62 | [AfterScenario]
63 | public static async Task AfterScenarioAsync(ScenarioContext scenarioContext, TestHarness testHarness)
64 | {
65 | await testHarness.StopScenarioAsync(scenarioContext.ScenarioExecutionStatus.ToString());
66 | }
67 |
68 | [AfterFeature]
69 | public static async Task AfterFeature(TestHarness testHarness)
70 | {
71 | await testHarness.StopAsync();
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Hooks/WeatherForecastHooks.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Tests.Builders;
2 | using CleanArchitecture.Infrastructure;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace CleanArchitecture.AcceptanceTests.Hooks
6 | {
7 | [Binding]
8 | public class WeatherForecastHooks
9 | {
10 | [BeforeFeature("weather_cleanup")]
11 | public static async Task CleanupWeatherForecasts(WeatherContext context)
12 | {
13 | var forecasts = await context.WeatherForecasts.ToListAsync();
14 | context.RemoveRange(forecasts);
15 | await context.SaveChangesAsync();
16 | var location = await context.Locations.FirstOrDefaultAsync(e => e.City == "New York");
17 | var forecast = new WeatherForecastBuilder().WithLocation(location.Id).Build();
18 | context.Add(forecast);
19 | await context.SaveChangesAsync();
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Pages/Abstract/PageObject.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace CleanArchitecture.AcceptanceTests.Pages.Abstract
3 | {
4 | public abstract class PageObject
5 | {
6 | protected PageObject(IPage page)
7 | {
8 | Page = page;
9 | }
10 |
11 | public readonly IPage Page;
12 | public TPage As() where TPage : PageObject
13 | {
14 | return (TPage)this;
15 | }
16 |
17 | public async Task RefreshAsync()
18 | {
19 | await Page.ReloadAsync();
20 | }
21 |
22 | public async Task WaitForConditionAsync(Func> condition, bool waitForValue = true, int checkDelayMs = 100, int numberOfChecks = 300)
23 | {
24 | var value = !waitForValue;
25 | for (int i = 0; i < numberOfChecks; i++)
26 | {
27 | value = await condition();
28 | if (value == waitForValue)
29 | {
30 | break;
31 | }
32 |
33 | await Task.Delay(checkDelayMs);
34 | }
35 | return value;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Pages/HomePage.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.AcceptanceTests.Pages.Abstract;
2 |
3 | namespace CleanArchitecture.AcceptanceTests.Pages
4 | {
5 | public class HomePage : PageObject
6 | {
7 | public readonly NavBar NavBar;
8 |
9 | public HomePage(IPage page) : base(page)
10 | {
11 | NavBar = new NavBar(page);
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Pages/NavBar.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.AcceptanceTests.Pages.Abstract;
2 |
3 | namespace CleanArchitecture.AcceptanceTests.Pages
4 | {
5 | public class NavBar : PageObject
6 | {
7 | public NavBar(IPage page) : base(page)
8 | {
9 | }
10 |
11 | public ILocator Header => Page.Locator("app-nav-menu");
12 | public ILocator Home => Header.GetByText("Home");
13 | public ILocator WeatherForecast => Header.GetByText("Weather Forecast");
14 |
15 | public async Task OpenWeatherForecast()
16 | {
17 | await WeatherForecast.ClickAsync();
18 | return new WeatherForecastPage(Page);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Pages/WeatherForecastPage.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.AcceptanceTests.Pages.Abstract;
2 |
3 | namespace CleanArchitecture.AcceptanceTests.Pages
4 | {
5 | public class WeatherForecastPage : PageObject
6 | {
7 | public readonly NavBar NavBar;
8 |
9 | public WeatherForecastPage(IPage page) : base(page)
10 | {
11 | NavBar = new NavBar(page);
12 | }
13 |
14 | public ILocator Title => Page.Locator("h1").GetByText("Weather forecast");
15 | public ILocator LocationSelector => Page.GetByLabel("Location");
16 | public ILocator GenerateButton => Page.GetByRole(AriaRole.Button, new() { Name = "Generate" });
17 | public ILocator Forecasts => Page.Locator("#forecasts");
18 | public ILocator ForecastRows => Forecasts.Locator("tbody").Locator("tr");
19 | public ILocator GeneratePrompt => Page.Locator("#generate-prompt");
20 |
21 | public async Task SelectLocation(string location)
22 | {
23 | await LocationSelector.SelectOptionAsync(new[] { location });
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Settings/BrowserSettings.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace CleanArchitecture.AcceptanceTests.Settings
3 | {
4 | public class BrowserSettings
5 | {
6 | public bool Headless { get; set; }
7 | public int SlowMoMilliseconds { get; set; }
8 | public string BaseUrl { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Steps/Abstract/BaseSteps.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace CleanArchitecture.AcceptanceTests.Steps.Abstract
3 | {
4 | [Binding]
5 | public abstract class BaseSteps
6 | {
7 | protected readonly TestHarness TestHarness;
8 |
9 | protected BaseSteps(TestHarness testHarness)
10 | {
11 | TestHarness = testHarness;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Steps/HomeSteps.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.AcceptanceTests.Pages;
2 | using CleanArchitecture.AcceptanceTests.Steps.Abstract;
3 |
4 | namespace CleanArchitecture.AcceptanceTests.Steps
5 | {
6 | public class HomeSteps : BaseSteps
7 | {
8 | private HomePage _page;
9 |
10 | public HomeSteps(TestHarness testHarness) : base(testHarness)
11 | {
12 |
13 | }
14 |
15 | [Given(@"a user is on the Home page")]
16 | public async Task GivenUserOnHomePage()
17 | {
18 | _page = new HomePage(await TestHarness.GotoAsync("/"));
19 | TestHarness.CurrentPage = _page;
20 | }
21 |
22 | [When(@"Weather Forecast page is opened")]
23 | public async Task WhenWeatherForecastOpened()
24 | {
25 | TestHarness.CurrentPage = await _page.NavBar.OpenWeatherForecast();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Steps/WeatherForecastSteps.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.AcceptanceTests.Pages;
2 | using CleanArchitecture.AcceptanceTests.Steps.Abstract;
3 |
4 | namespace CleanArchitecture.AcceptanceTests.Steps
5 | {
6 | public class WeatherForecastSteps : BaseSteps
7 | {
8 | private WeatherForecastPage _page;
9 |
10 | public WeatherForecastSteps(TestHarness testHarness) : base(testHarness)
11 | {
12 |
13 | }
14 |
15 | [Given(@"a user is on the Weather Forecast page")]
16 | public async Task GivenUserOnHomePage()
17 | {
18 | _page = new WeatherForecastPage(await TestHarness.GotoAsync("/weather-forecast"));
19 | TestHarness.CurrentPage = _page;
20 | }
21 |
22 | [When(@"'(.*)' location is selected")]
23 | public async Task WhenSelectLocation(string location)
24 | {
25 | await _page.SelectLocation(location);
26 | }
27 |
28 | [When(@"a weather forecast is generated")]
29 | public async Task WhenWeatherForecastGenerated()
30 | {
31 | await _page.GenerateButton.ClickAsync();
32 | }
33 |
34 | [Then(@"Weather Forecast page is open")]
35 | public async Task ThenWeatherForecastOpen()
36 | {
37 | _page = TestHarness.CurrentPage as WeatherForecastPage;
38 | var isVisiable = await _page.Title.IsVisibleAsync();
39 | isVisiable.Should().BeTrue();
40 | }
41 |
42 | [Then(@"'(.*)' weather forecasts present")]
43 | public async Task ThenWeatherForecastsPresent(int count)
44 | {
45 | if (count == 0)
46 | {
47 | var isVisible = await _page.Forecasts.IsVisibleAsync();
48 | isVisible.Should().BeFalse();
49 | }
50 | else
51 | {
52 | var hasCount = await _page.WaitForConditionAsync(async () =>
53 | {
54 | var actualCount = await _page.ForecastRows.CountAsync();
55 | return actualCount == count;
56 | });
57 | hasCount.Should().BeTrue();
58 | }
59 | }
60 |
61 | [Then(@"Generate prompt is visible")]
62 | public async Task ThenGeneratePromptVisible()
63 | {
64 | var isVisiable = await _page.GeneratePrompt.IsVisibleAsync();
65 | isVisiable.Should().BeTrue();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/TestHostEnvironment.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.FileProviders;
2 | using Microsoft.Extensions.Hosting;
3 |
4 | namespace CleanArchitecture.AcceptanceTests
5 | {
6 | public class TestHostEnvironment : IHostEnvironment
7 | {
8 | public string EnvironmentName { get; set; } = Environments.Development;
9 | public string ApplicationName { get; set; } = typeof(TestHostEnvironment).Namespace;
10 | public string ContentRootPath { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
11 | public IFileProvider ContentRootFileProvider { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Microsoft.Playwright;
2 | global using TechTalk.SpecFlow;
3 | global using FluentAssertions;
--------------------------------------------------------------------------------
/tests/CleanArchitecture.AcceptanceTests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Browser": {
3 | "Headless": false,
4 | "SlowMoMilliseconds": 200,
5 | "BaseUrl": "https://localhost:44411"
6 | },
7 | "Database": {
8 | //#if( UseSqlServer )
9 | "SqlConnectionString": "Server=127.0.0.1, 1433; Database=Weather; Integrated Security=False; User Id = SA; Password=Admin1234!; MultipleActiveResultSets=False;TrustServerCertificate=True",
10 | //#else
11 | "PostgresConnectionString": "Host=127.0.0.1;Database=Weather;Username=postgres;Password=Admin1234!"
12 | //#endif
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/CleanArchitecture.Api.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | <_Parameter1>$(MSBuildProjectName).Tests
19 |
20 |
21 |
22 |
23 |
24 | PreserveNewest
25 | true
26 | PreserveNewest
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | runtime; build; native; contentfiles; analyzers; buildtransitive
36 | all
37 |
38 |
39 | runtime; build; native; contentfiles; analyzers; buildtransitive
40 | all
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/Controllers/ErrorsControllerTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Tests.Builders;
2 |
3 | namespace CleanArchitecture.Api.Tests.Controllers
4 | {
5 | public class ErrorsControllerTests
6 | {
7 | private const string BASE_URL = "api/weather-forecasts";
8 | private readonly TestWebApplication _application = new TestWebApplication();
9 |
10 | public ErrorsControllerTests()
11 | {
12 | _application.TestWeatherForecasts.Add(new WeatherForecastBuilder().Build());
13 | }
14 |
15 | [Fact]
16 | public async Task GivenController_WhenUnhandledError_ThenInternalServerError()
17 | {
18 | using var client = _application.CreateClient();
19 | _application.WeatherForecastsRepository.Setup(e => e.GetByIdAsync(It.IsAny())).Throws(new Exception("There was an error"));
20 |
21 | var response = await client.GetAsync($"{BASE_URL}/{_application.TestWeatherForecasts.First().Id}");
22 |
23 | await response.ReadAndAssertError(HttpStatusCode.InternalServerError);
24 | }
25 |
26 | [Fact]
27 | public async Task GivenController_WhenUnauthorizedAccessException_ThenForbidden()
28 | {
29 | using var client = _application.CreateClient();
30 | _application.WeatherForecastsRepository.Setup(e => e.GetByIdAsync(It.IsAny())).Throws(new UnauthorizedAccessException("Unauthorized"));
31 |
32 | var response = await client.GetAsync($"{BASE_URL}/{_application.TestWeatherForecasts.First().Id}");
33 |
34 | await response.ReadAndAssertError(HttpStatusCode.Forbidden);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/Controllers/HealthControllerTests.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace CleanArchitecture.Api.Tests.Controllers
3 | {
4 | public class HealthControllerTests
5 | {
6 | private readonly TestWebApplication _application = new TestWebApplication();
7 |
8 | [Fact]
9 | public async Task GivenHealthEndpoint_WhenHealthy_ThenOk()
10 | {
11 | using var client = _application.CreateClient();
12 | var response = await client.GetAsync("healthz");
13 | Assert.True(response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.ServiceUnavailable);
14 | }
15 |
16 | [Fact]
17 | public async Task GivenLivenessEndpoint_WhenHealthy_ThenOk()
18 | {
19 | using var client = _application.CreateClient();
20 | var response = await client.GetAsync("liveness");
21 |
22 | response.IsSuccessStatusCode.Should().BeTrue();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/Controllers/LocationsControllerTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Locations.Models;
2 |
3 | namespace CleanArchitecture.Api.Tests.Controllers
4 | {
5 | public class LocationsControllerTests
6 | {
7 | private const string BASE_URL = "api/locations";
8 | private readonly TestWebApplication _application = new TestWebApplication();
9 |
10 | [Fact]
11 | public async Task GivenLocationsController_WhenGet_ThenOk()
12 | {
13 | using var client = _application.CreateClient();
14 | var response = await client.GetAsync(BASE_URL);
15 |
16 | var locations = await response.ReadAndAssertSuccessAsync>();
17 |
18 | locations.Should().HaveCount(_application.TestLocations.Count);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/Extensions/HttpResponseMessageExtensions.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Api.Infrastructure.ActionResults;
2 | using Newtonsoft.Json;
3 |
4 | namespace CleanArchitecture.Api.Tests
5 | {
6 | internal static class HttpResponseMessageExtensions
7 | {
8 | public static async Task ReadAndAssertSuccessAsync(this HttpResponseMessage response) where T : class
9 | {
10 | response.IsSuccessStatusCode.Should().BeTrue();
11 | var json = await response.Content.ReadAsStringAsync();
12 | if (typeof(T) == typeof(string))
13 | {
14 | return json as T;
15 | }
16 | else
17 | {
18 | return JsonConvert.DeserializeObject(json);
19 | }
20 | }
21 |
22 | public static async Task ReadAndAssertError(this HttpResponseMessage response, HttpStatusCode statusCode)
23 | {
24 | response.StatusCode.Should().Be(statusCode);
25 | var json = await response.Content.ReadAsStringAsync();
26 | return JsonConvert.DeserializeObject(json)!;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
2 | global using FluentAssertions;
3 | global using Moq;
4 | global using System.Net;
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Api.Tests/appsettings.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "Serilog": {
3 | }
4 | }
5 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Application.Tests/CleanArchitecture.Application.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Application.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
2 | global using FluentAssertions;
3 | global using Moq;
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Application.Tests/Weather/DomainEventHandlers/WeatherForecastCreatedDomainEventHandlerTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Weather.DomainEventHandlers;
2 | using CleanArchitecture.Application.Weather.IntegrationEvents;
3 | using CleanArchitecture.Core.Weather.DomainEvents;
4 | using Microsoft.Extensions.Logging;
5 | using MiniTransit;
6 |
7 | namespace CleanArchitecture.Application.Tests.Weather.DomainEventHandlers
8 | {
9 | public class WeatherForecastCreatedDomainEventHandlerTests
10 | {
11 | private readonly WeatherForecastCreatedDomainEventHandler _handler;
12 | private readonly Mock _eventBus = new Mock();
13 |
14 | public WeatherForecastCreatedDomainEventHandlerTests()
15 | {
16 | _handler = new WeatherForecastCreatedDomainEventHandler(Mock.Of>(), _eventBus.Object);
17 | }
18 |
19 | [Fact]
20 | public async Task GivenWeatherForecastCreatedDomainEvent_WhenHandle_ThenPublishIntegrationEvent()
21 | {
22 | var @event = new WeatherForecastCreatedDomainEvent(Guid.NewGuid(), 25, "Sunny", DateTime.UtcNow);
23 | Func action = () => _handler.Handle(@event, default);
24 | await action.Should().NotThrowAsync();
25 | _eventBus.Verify(e => e.PublishAsync(It.IsAny()), Times.Once);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Application.Tests/Weather/IntegrationEvents/WeatherForecastCreatedEventTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Weather.IntegrationEvents;
2 | using CleanArchitecture.Core.Abstractions.Services;
3 | using Microsoft.Extensions.Logging;
4 | using MiniTransit;
5 | using MiniTransit.Subscriptions;
6 |
7 | namespace CleanArchitecture.Application.Tests.Weather.IntegrationEvents
8 | {
9 | public class WeatherForecastCreatedEventTests
10 | {
11 | private readonly WeatherForecastCreatedEventHandler _handler;
12 | private readonly Mock _notificationsService = new Mock();
13 | private readonly string _correlationId = Guid.NewGuid().ToString();
14 |
15 | public WeatherForecastCreatedEventTests()
16 | {
17 | _handler = new WeatherForecastCreatedEventHandler(_notificationsService.Object, Mock.Of>());
18 | }
19 |
20 | [Fact]
21 | public async Task GivenWeatherForecastCreatedDomainEvent_WhenHandleHotTemperature_ThenSendAlert()
22 | {
23 | var context = GenerateContext(new WeatherForecastCreatedEvent(Guid.NewGuid(), 50, "Hot", DateTime.UtcNow, _correlationId));
24 | Func action = () => _handler.ConsumeAsync(context);
25 | await action.Should().NotThrowAsync();
26 | _notificationsService.Verify(e => e.WeatherAlertAsync("Hot", 50, It.IsAny()), Times.Once);
27 | }
28 |
29 | [Fact]
30 | public async Task GivenWeatherForecastCreatedDomainEvent_WhenHandleColdTemperature_ThenSendAlert()
31 | {
32 | var context = GenerateContext(new WeatherForecastCreatedEvent(Guid.NewGuid(), -1, "Cold", DateTime.UtcNow, _correlationId));
33 | Func action = () => _handler.ConsumeAsync(context);
34 | await action.Should().NotThrowAsync();
35 | _notificationsService.Verify(e => e.WeatherAlertAsync("Cold", -1, It.IsAny()), Times.Once);
36 | }
37 |
38 | [Fact]
39 | public async Task GivenWeatherForecastCreatedDomainEvent_WhenHandleNormalTemperature_ThenDontSendAlert()
40 | {
41 | var context = GenerateContext(new WeatherForecastCreatedEvent(Guid.NewGuid(), 20, "Mild", DateTime.UtcNow, _correlationId));
42 | Func action = () => _handler.ConsumeAsync(context);
43 | await action.Should().NotThrowAsync();
44 | _notificationsService.Verify(e => e.WeatherAlertAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
45 | }
46 |
47 | private ConsumeContext GenerateContext(WeatherForecastCreatedEvent @event)
48 | {
49 | var subscriptionContext = new SubscriptionContext("events", "test", @event.GetType().Name, _handler.GetType().Name, 0);
50 | return new ConsumeContext(@event, subscriptionContext, Mock.Of(), default);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/ApiLayerTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace CleanArchitecture.Arch.Tests
4 | {
5 | [Collection("Sequential")]
6 | public class ApiLayerTests : BaseTests
7 | {
8 | [Fact]
9 | public void Api_Controllers_ShouldOnlyResideInApi()
10 | {
11 | AllTypes.That().Inherit(typeof(ControllerBase))
12 | .Should().ResideInNamespaceStartingWith("CleanArchitecture.Api")
13 | .AssertIsSuccessful();
14 | }
15 |
16 | [Fact]
17 | public void Api_Controllers_ShouldInheritFromControllerBase()
18 | {
19 | Types.InAssembly(ApiAssembly)
20 | .That().HaveNameEndingWith("Controller")
21 | .Should().Inherit(typeof(ControllerBase))
22 | .AssertIsSuccessful();
23 | }
24 |
25 | [Fact]
26 | public void Api_Controllers_ShouldEndWithController()
27 | {
28 | AllTypes.That().Inherit(typeof(ControllerBase))
29 | .Should().HaveNameEndingWith("Controller")
30 | .AssertIsSuccessful();
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/ApplicationLayerTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Abstractions.Commands;
2 | using CleanArchitecture.Application.Abstractions.Queries;
3 | using AutoMapper;
4 |
5 | namespace CleanArchitecture.Arch.Tests
6 | {
7 | [Collection("Sequential")]
8 | public class ApplicationLayerTests : BaseTests
9 | {
10 | [Fact]
11 | public void ApplicationLayer_Cqrs_QueriesEndWithQuery()
12 | {
13 | AllTypes.That().Inherit(typeof(Query<>))
14 | .Should().HaveNameEndingWith("Query")
15 | .AssertIsSuccessful();
16 | }
17 |
18 | [Fact]
19 | public void ApplicationLayer_Cqrs_ContainsAllQueries()
20 | {
21 | AllTypes.That().HaveNameEndingWith("Query")
22 | .Should().ResideInNamespace("CleanArchitecture.Application")
23 | .AssertIsSuccessful();
24 | }
25 |
26 | [Fact]
27 | public void ApplicationLayer_Cqrs_CommandsEndWithCommand()
28 | {
29 | AllTypes.That().Inherit(typeof(CommandBase<>))
30 | .Should().HaveNameEndingWith("Command")
31 | .AssertIsSuccessful();
32 |
33 | AllTypes.That().Inherit(typeof(CreateCommand))
34 | .Should().HaveNameEndingWith("Command")
35 | .AssertIsSuccessful();
36 | }
37 |
38 | [Fact]
39 | public void ApplicationLayer_Cqrs_ContainsAllCommands()
40 | {
41 | AllTypes.That().HaveNameEndingWith("Command")
42 | .Should().ResideInNamespace("CleanArchitecture.Application")
43 | .AssertIsSuccessful();
44 | }
45 |
46 | [Fact]
47 | public void ApplicationLayer_Cqrs_QueryHandlersEndWithQueryHandler()
48 | {
49 | AllTypes.That().Inherit(typeof(QueryHandler<,>))
50 | .Should().HaveNameEndingWith("QueryHandler")
51 | .AssertIsSuccessful();
52 | }
53 |
54 | [Fact]
55 | public void ApplicationLayer_Cqrs_ContainsAllQueryHandlers()
56 | {
57 | AllTypes.That().HaveNameEndingWith("QueryHandler")
58 | .Should().ResideInNamespace("CleanArchitecture.Application")
59 | .AssertIsSuccessful();
60 | }
61 |
62 | [Fact]
63 | public void ApplicationLayer_Cqrs_CommandHandlersEndWithCommandHandler()
64 | {
65 | AllTypes.That().Inherit(typeof(CommandHandler<>))
66 | .Should().HaveNameEndingWith("CommandHandler")
67 | .AssertIsSuccessful();
68 |
69 | AllTypes.That().Inherit(typeof(CreateCommandHandler<>))
70 | .Should().HaveNameEndingWith("CommandHandler")
71 | .AssertIsSuccessful();
72 | }
73 |
74 | [Fact]
75 | public void ApplicationLayer_Cqrs_ContainsAllCommandHandlers()
76 | {
77 | AllTypes.That().HaveNameEndingWith("CommandHandler")
78 | .Should().ResideInNamespace("CleanArchitecture.Application")
79 | .AssertIsSuccessful();
80 | }
81 |
82 | [Fact]
83 | public void ApplicationLayer_Dtos_ShouldBeMutable()
84 | {
85 | AllTypes.That().HaveNameEndingWith("Dto")
86 | .And().DoNotHaveName("IntegrationSupportGroupUserDto")
87 | .Should().BeMutable()
88 | .AssertIsSuccessful();
89 | }
90 |
91 | [Fact]
92 | public void ApplicationLayer_MappingProfiles_ShouldOnlyResideInApplication()
93 | {
94 | AllTypes.That().Inherit(typeof(Profile))
95 | .Should().ResideInNamespaceStartingWith("CleanArchitecture.Application")
96 | .AssertIsSuccessful();
97 | }
98 |
99 | [Fact]
100 | public void ApplicationLayer_MappingProfiles_ShouldEndWithProfile()
101 | {
102 | AllTypes.That().Inherit(typeof(Profile))
103 | .Should().HaveNameEndingWith("Profile")
104 | .AssertIsSuccessful();
105 | }
106 | }
107 | }
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/BaseTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.AutofacModules;
2 | using CleanArchitecture.Core.Abstractions.Entities;
3 | using CleanArchitecture.Infrastructure.AutofacModules;
4 | using System.Reflection;
5 |
6 | namespace CleanArchitecture.Arch.Tests
7 | {
8 | public abstract class BaseTests
9 | {
10 | protected static Assembly ApiAssembly = typeof(Api.Controllers.WeatherForecastsController).Assembly;
11 | protected static Assembly ApplicationAssembly = typeof(ApplicationModule).Assembly;
12 | protected static Assembly InfrastuctureAssembly = typeof(InfrastructureModule).Assembly;
13 | protected static Assembly CoreAssembly = typeof(EntityBase).Assembly;
14 | protected static Types AllTypes = Types.InAssemblies(new List { ApiAssembly, ApplicationAssembly, InfrastuctureAssembly, CoreAssembly });
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/CleanArchitecture.Arch.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 | runtime; build; native; contentfiles; analyzers; buildtransitive
22 | all
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/CleanArchitectureTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Application.Abstractions.Repositories;
2 |
3 | namespace CleanArchitecture.Arch.Tests
4 | {
5 | [Collection("Sequential")]
6 | public class CleanArchitectureTests : BaseTests
7 | {
8 | [Fact]
9 | public void CleanArchitecture_Layers_ApplicationDoesNotReferenceInfrastructure()
10 | {
11 | AllTypes.That().ResideInNamespace("CleanArchitecture.Application")
12 | .ShouldNot().HaveDependencyOn("CleanArchitecture.Infrastructure")
13 | .AssertIsSuccessful();
14 | }
15 |
16 | [Fact]
17 | public void CleanArchitecture_Layers_CoreDoesNotReferenceOuter()
18 | {
19 | var coreTypes = AllTypes.That().ResideInNamespace("CleanArchitecture.Core");
20 |
21 | coreTypes.ShouldNot().HaveDependencyOn("CleanArchitecture.Infrastructure")
22 | .AssertIsSuccessful();
23 |
24 | coreTypes.ShouldNot().HaveDependencyOn("CleanArchitecture.Application")
25 | .AssertIsSuccessful();
26 | }
27 |
28 | [Fact]
29 | public void CleanArchitecture_Repositories_OnlyInInfrastructure()
30 | {
31 | AllTypes.That().HaveNameEndingWith("Repository")
32 | .Should().ResideInNamespaceStartingWith("CleanArchitecture.Infrastructure")
33 | .AssertIsSuccessful();
34 |
35 | AllTypes.That().HaveNameEndingWith("Repository")
36 | .And().AreClasses()
37 | .Should().ImplementInterface(typeof(IRepository<>))
38 | .AssertIsSuccessful();
39 | }
40 |
41 | [Fact]
42 | public void CleanArchitecture_Repositories_ShouldEndWithRepository()
43 | {
44 | AllTypes.That().Inherit(typeof(IRepository<>))
45 | .Should().HaveNameEndingWith("Repository")
46 | .AssertIsSuccessful();
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/DomainDrivenDesignTests.cs:
--------------------------------------------------------------------------------
1 | using CSharpFunctionalExtensions;
2 | using CleanArchitecture.Application.Abstractions.DomainEventHandlers;
3 | using CleanArchitecture.Core.Abstractions.DomainEvents;
4 | using CleanArchitecture.Core.Abstractions.Entities;
5 |
6 | namespace CleanArchitecture.Arch.Tests
7 | {
8 | [Collection("Sequential")]
9 | public class DomainDrivenDesignTests : BaseTests
10 | {
11 | [Fact]
12 | public void DomainDrivenDesign_ValueObjects_ShouldBeImmutable()
13 | {
14 | Types.InAssembly(CoreAssembly)
15 | .That().Inherit(typeof(ValueObject))
16 | .Should().BeImmutable()
17 | .AssertIsSuccessful();
18 | }
19 |
20 | [Fact]
21 | public void DomainDrivenDesign_Aggregates_ShouldBeHavePrivateSettings()
22 | {
23 | Types.InAssembly(CoreAssembly)
24 | .That().Inherit(typeof(AggregateRoot))
25 | .Should().BeImmutable()
26 | .AssertIsSuccessful();
27 | }
28 |
29 | [Fact]
30 | public void DomainDrivenDesign_Entities_ShouldBeHavePrivateSettings()
31 | {
32 | Types.InAssembly(CoreAssembly).That().Inherit(typeof(EntityBase))
33 | .Should().BeImmutable()
34 | .AssertIsSuccessful();
35 | }
36 |
37 | [Fact]
38 | public void DomainDrivenDesign_Aggregates_ShouldOnlyResideInCore()
39 | {
40 | AllTypes.That().Inherit(typeof(AggregateRoot))
41 | .Should().ResideInNamespaceStartingWith("CleanArchitecture.Core")
42 | .AssertIsSuccessful();
43 | }
44 |
45 | [Fact]
46 | public void DomainDrivenDesign_DomainEvents_ShouldOnlyResideInCore()
47 | {
48 | AllTypes.That().Inherit(typeof(DomainEvent))
49 | .Should().ResideInNamespaceStartingWith("CleanArchitecture.Core")
50 | .AssertIsSuccessful();
51 | }
52 |
53 | [Fact]
54 | public void DomainDrivenDesign_DomainEvents_ShouldEndWithDomainEvent()
55 | {
56 | AllTypes.That().Inherit(typeof(DomainEvent))
57 | .Should().HaveNameEndingWith("DomainEvent")
58 | .AssertIsSuccessful();
59 | }
60 |
61 | [Fact]
62 | public void DomainDrivenDesign_DomainEventHandlers_ShouldOnlyResideInApplication()
63 | {
64 | AllTypes.That().Inherit(typeof(DomainEventHandler<>))
65 | .Should().ResideInNamespaceStartingWith("CleanArchitecture.Application")
66 | .AssertIsSuccessful();
67 | }
68 |
69 | [Fact]
70 | public void DomainDrivenDesign_DomainEventHandlers_ShouldEndWithDomainEventHandler()
71 | {
72 | AllTypes.That().Inherit(typeof(DomainEventHandler<>))
73 | .Should().HaveNameEndingWith("DomainEventHandler")
74 | .AssertIsSuccessful();
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/Extensions/ConditionListExtensions.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace NetArchTest.Rules
3 | {
4 | internal static class ConditionListExtensions
5 | {
6 | internal static void AssertIsSuccessful(this ConditionList conditionList)
7 | {
8 | var result = conditionList.GetResult();
9 | (result.FailingTypeNames ?? Array.Empty()).Should().HaveCount(0);
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Arch.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
2 | global using NetArchTest.Rules;
3 | global using FluentAssertions;
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/Builders/LocationBuilder.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Locations.Entities;
2 | using CleanArchitecture.Core.Locations.ValueObjects;
3 |
4 | namespace CleanArchitecture.Core.Tests.Builders
5 | {
6 | public class LocationBuilder
7 | {
8 | private string _country = "United Kingdom";
9 | private string _city = "London";
10 | private decimal _latitude = 51.51m;
11 | private decimal _longitude = -0.13m;
12 |
13 | public Location Build()
14 | {
15 | return Location.Create(_country, _city, Coordinates.Create(_latitude, _longitude));
16 | }
17 |
18 | public LocationBuilder WithCity(string city)
19 | {
20 | _city = city;
21 | return this;
22 | }
23 |
24 | public LocationBuilder WithLatitude(decimal latitude)
25 | {
26 | _latitude = latitude;
27 | return this;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/Builders/WeatherForecastBuilder.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Weather.Entities;
2 | using CleanArchitecture.Core.Weather.ValueObjects;
3 |
4 | namespace CleanArchitecture.Core.Tests.Builders
5 | {
6 | public class WeatherForecastBuilder
7 | {
8 | private DateTime _date = DateTime.UtcNow;
9 | private int _temperature = 8;
10 | private string? _summary = "Mild";
11 | private Guid _location = new Guid("B0C91847-8931-4C45-9FD5-018A3A3398CF");
12 |
13 | public WeatherForecast Build()
14 | {
15 | return WeatherForecast.Create(_date, Temperature.FromCelcius(_temperature), _summary, _location);
16 | }
17 |
18 | public WeatherForecastBuilder WithTemperature(int temperature)
19 | {
20 | _temperature = temperature;
21 | return this;
22 | }
23 |
24 | public WeatherForecastBuilder WithSummary(string? summary)
25 | {
26 | _summary = summary;
27 | return this;
28 | }
29 |
30 | public WeatherForecastBuilder WithDate(DateTime date)
31 | {
32 | _date = date;
33 | return this;
34 | }
35 |
36 | public WeatherForecastBuilder WithLocation(Guid locationId)
37 | {
38 | _location = locationId;
39 | return this;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/CleanArchitecture.Core.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 | all
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/Factories/MockRepositoryFactory.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Abstractions.Entities;
2 | using CleanArchitecture.Application.Abstractions.Repositories;
3 | using MockQueryable.Moq;
4 |
5 | namespace CleanArchitecture.Core.Tests.Factories
6 | {
7 | public static class MockRepositoryFactory
8 | {
9 | public static Mock Create(IEnumerable? items = null)
10 | where T : AggregateRoot
11 | where TRepository : class, IRepository
12 | {
13 | var repository = new Mock();
14 | return Setup(repository, items);
15 | }
16 |
17 | public static Mock> Create(IEnumerable? items = null)
18 | where T : AggregateRoot
19 | {
20 | var repository = new Mock>();
21 | return Setup(repository, items);
22 | }
23 |
24 | public static Mock Setup(Mock repository, IEnumerable? items = null)
25 | where T : AggregateRoot
26 | where TRepository : class, IRepository
27 | {
28 | if (items == null)
29 | {
30 | items = new List();
31 | }
32 | repository.Setup(e => e.GetByIdAsync(It.IsAny())).Returns((id) => Task.FromResult(items.FirstOrDefault(e => e.Id == id)));
33 | repository.Setup(e => e.GetAll(It.IsAny())).Returns(() => items.AsQueryable().BuildMockDbSet().Object);
34 | return repository;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/Locations/Entities/LocationTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Abstractions.Exceptions;
2 | using CleanArchitecture.Core.Tests.Builders;
3 |
4 | namespace CleanArchitecture.Core.Tests.Locations.Entities
5 | {
6 | public class LocationTests
7 | {
8 | [Fact]
9 | public void GivenLocation_WhenCreateValid_ThenCreate()
10 | {
11 | var location = new LocationBuilder().Build();
12 | location.City.Should().NotBeNullOrWhiteSpace();
13 | }
14 |
15 | [Fact]
16 | public void GivenLocation_WhenCreateEmptyCity_ThenError()
17 | {
18 | Action action = () => new LocationBuilder().WithCity("").Build();
19 | action.Should().Throw().WithMessage("Required input 'City' is missing.");
20 | }
21 |
22 | [Fact]
23 | public void GivenLocation_WhenLatitudeOver90_ThenError()
24 | {
25 | Action action = () => new LocationBuilder().WithLatitude(91).Build();
26 | action.Should().Throw().WithMessage("'Latitude' must be between -90° and 90°.");
27 | }
28 |
29 | [Fact]
30 | public void GivenLocation_WhenLatitudeUnder90_ThenError()
31 | {
32 | Action action = () => new LocationBuilder().WithLatitude(-91).Build();
33 | action.Should().Throw().WithMessage("'Latitude' must be between -90° and 90°.");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
2 | global using FluentAssertions;
3 | global using Moq;
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Core.Tests/Weather/Entities/WeatherForecastTests.cs:
--------------------------------------------------------------------------------
1 | using CleanArchitecture.Core.Abstractions.Exceptions;
2 | using CleanArchitecture.Core.Tests.Builders;
3 | using CleanArchitecture.Core.Weather.DomainEvents;
4 | using CleanArchitecture.Core.Weather.ValueObjects;
5 |
6 | namespace CleanArchitecture.Core.Tests.Weather.Entities
7 | {
8 | public class WeatherForecastTests
9 | {
10 | [Fact]
11 | public void GivenWeatherForecast_WhenCreate_ThenCreate()
12 | {
13 | var forecast = new WeatherForecastBuilder().Build();
14 | forecast.Summary.Should().NotBeNullOrWhiteSpace();
15 | forecast.DomainEvents.Where(e => e is WeatherForecastCreatedDomainEvent).Should().HaveCount(1);
16 | }
17 |
18 | [Fact]
19 | public void GivenWeatherForecast_WhenTemperature10C_ThenCalculateTemperature50F()
20 | {
21 | var forecast = new WeatherForecastBuilder().WithTemperature(10).Build();
22 | var farenheit = forecast.Temperature.Farenheit;
23 | farenheit.Should().Be(50);
24 | }
25 |
26 | [Fact]
27 | public void GivenWeatherForecast_WhenTemperatureBelowAbsoluteZero_ThenError()
28 | {
29 | var forecastBuilder = new WeatherForecastBuilder().WithTemperature(-300);
30 | Action action = () => forecastBuilder.Build();
31 | action.Should().Throw().WithMessage("Temperature cannot be below Absolute Zero");
32 | }
33 |
34 | [Fact]
35 | public void GivenWeatherForecast_WhenSummaryEmpty_ThenError()
36 | {
37 | var forecastBuilder = new WeatherForecastBuilder().WithSummary(null);
38 | Action action = () => forecastBuilder.Build();
39 | action.Should().Throw().WithMessage("Required input 'Summary' is missing.");
40 | }
41 |
42 | [Fact]
43 | public void GivenWeatherForecast_WhenUpdate_ThenUpdate()
44 | {
45 | var forecast = new WeatherForecastBuilder().Build();
46 | forecast.Update(Temperature.FromCelcius(21), "Hot");
47 | forecast.Summary.Should().Be("Hot");
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Infrastructure.Tests/CleanArchitecture.Infrastructure.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 | all
20 |
21 |
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 | all
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Infrastructure.Tests/Repositories/Abstract/BaseRepositoryTests.cs:
--------------------------------------------------------------------------------
1 | using MediatR;
2 | using Autofac;
3 | using CleanArchitecture.Core.Abstractions.Entities;
4 | using CleanArchitecture.Application.Abstractions.Repositories;
5 | using CleanArchitecture.Infrastructure.AutofacModules;
6 | using Microsoft.Data.Sqlite;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.Configuration;
9 | using Microsoft.Extensions.Hosting;
10 | using CleanArchitecture.Core.Tests.Builders;
11 | using CleanArchitecture.Core.Locations.Entities;
12 |
13 | namespace CleanArchitecture.Infrastructure.Tests.Repositories.Abstract
14 | {
15 | public abstract class BaseRepositoryTests : IAsyncLifetime
16 | {
17 | private const string InMemoryConnectionString = "DataSource=:memory:";
18 | private readonly SqliteConnection _connection;
19 | protected readonly WeatherContext Database;
20 | private readonly IContainer _container;
21 | protected readonly Location Location = new LocationBuilder().Build();
22 |
23 | public BaseRepositoryTests()
24 | {
25 | _connection = new SqliteConnection(InMemoryConnectionString);
26 | _connection.Open();
27 | var options = new DbContextOptionsBuilder()
28 | .UseSqlite(_connection)
29 | .Options;
30 |
31 | var configuration = new ConfigurationBuilder().Build();
32 | var containerBuilder = new ContainerBuilder();
33 |
34 | var env = Mock.Of();
35 | containerBuilder.RegisterInstance(env);
36 | containerBuilder.RegisterInstance(Mock.Of());
37 | Database = new WeatherContext(options, env);
38 | Database.Database.EnsureCreated();
39 |
40 | containerBuilder.RegisterModule(new InfrastructureModule(options, configuration));
41 | _container = containerBuilder.Build();
42 | }
43 |
44 | public async Task InitializeAsync()
45 | {
46 | var locationsRepository = GetRepository();
47 | locationsRepository.Insert(Location);
48 | await GetUnitOfWork().CommitAsync();
49 | }
50 |
51 | public Task DisposeAsync()
52 | {
53 | Database.Dispose();
54 | _connection.Close();
55 | _connection.Dispose();
56 | return Task.CompletedTask;
57 | }
58 |
59 | protected IRepository GetRepository()
60 | where T : AggregateRoot
61 | {
62 | return _container.Resolve>();
63 | }
64 |
65 | protected IUnitOfWork GetUnitOfWork()
66 | {
67 | return _container.Resolve();
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/tests/CleanArchitecture.Infrastructure.Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
2 | global using FluentAssertions;
3 | global using Moq;
--------------------------------------------------------------------------------