Test web dev.
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

Chris Dill (Jun 6, 2026, 10:47 AM +0100) 1199bef0

+1200
+51
.editorconfig
··· 1 + # EditorConfig is awesome: http://EditorConfig.org 2 + 3 + root = true 4 + 5 + [*] 6 + indent_style = space 7 + 8 + # General 9 + [*.{cs,csx,vb,vbx}] 10 + indent_size = 4 11 + insert_final_newline = true 12 + charset = utf-8 13 + trim_trailing_whitespace = true 14 + 15 + # C# styles 16 + [*.cs] 17 + # Indentation preferences 18 + csharp_indent_block_contents = true 19 + csharp_indent_braces = false 20 + csharp_indent_case_contents = true 21 + csharp_indent_case_contents_when_block = true 22 + csharp_indent_switch_labels = true 23 + csharp_indent_labels = flush_left 24 + csharp_style_namespace_declarations = file_scoped:warning 25 + 26 + # Newline settings 27 + csharp_new_line_before_open_brace = all 28 + csharp_new_line_before_else = true 29 + csharp_new_line_before_catch = true 30 + csharp_new_line_before_finally = true 31 + csharp_new_line_before_members_in_object_initializers = true 32 + csharp_new_line_before_members_in_anonymous_types = true 33 + csharp_new_line_between_query_expression_clauses = true 34 + 35 + # Spacing 36 + csharp_space_after_cast = false 37 + csharp_space_after_colon_in_inheritance_clause = true 38 + csharp_space_after_keywords_in_control_flow_statements = true 39 + csharp_space_around_binary_operators = before_and_after 40 + csharp_space_before_colon_in_inheritance_clause = true 41 + csharp_space_between_method_call_empty_parameter_list_parentheses = false 42 + csharp_space_between_method_call_name_and_opening_parenthesis = false 43 + csharp_space_between_method_call_parameter_list_parentheses = false 44 + csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 45 + csharp_space_between_method_declaration_parameter_list_parentheses = false 46 + csharp_space_between_parentheses = false 47 + 48 + # Blocks are allowed 49 + csharp_prefer_braces = true:error 50 + csharp_preserve_single_line_blocks = false 51 + csharp_preserve_single_line_statements = false
+29
.forgejo/workflows/build.yml
··· 1 + name: Build 2 + on: 3 + push: 4 + branches: 5 + - main 6 + pull_request: 7 + branches: 8 + - main 9 + 10 + jobs: 11 + build: 12 + runs-on: ubuntu-latest 13 + env: 14 + DOTNET_NOLOGO: true 15 + DOTNET_CLI_TELEMETRY_OPTOUT: true 16 + steps: 17 + - name: Checkout repository 18 + uses: actions/checkout@v6 19 + 20 + - name: Set up dotnet 21 + uses: https://github.com/actions/setup-dotnet@v5 22 + with: 23 + dotnet-version: 10.0.x 24 + 25 + - name: Build project 26 + run: dotnet build -c Release 27 + 28 + - name: Test project 29 + run: dotnet test -c Release
+2
.gitattributes
··· 1 + # Auto detect text files and perform LF normalization 2 + * text=auto
+6
.gitignore
··· 1 + .nuget/ 2 + bin/ 3 + obj/ 4 + app.db 5 + app.db-shm 6 + app.db-wal
+7
README.md
··· 1 + # test-web 2 + 3 + Test web dev. 4 + 5 + ```shell 6 + dotnet run --project src 7 + ```
+8
TestWeb.slnx
··· 1 + <Solution> 2 + <Folder Name="/src/"> 3 + <Project Path="src/TestWeb.csproj" /> 4 + </Folder> 5 + <Folder Name="/tests/"> 6 + <Project Path="tests/TestWeb.Tests.csproj" /> 7 + </Folder> 8 + </Solution>
+40
src/Core/Jobs/CreateJobEndpoint.cs
··· 1 + namespace TestWeb.Core.Jobs; 2 + 3 + using Infra; 4 + 5 + public static class CreateJobEndpoint 6 + { 7 + public record Request( 8 + string ClientName, 9 + string Pickup, 10 + string Dropoff 11 + ); 12 + 13 + public record Response(int Id); 14 + 15 + public class RequestValidator : AbstractValidator<Request> 16 + { 17 + public RequestValidator() 18 + { 19 + RuleFor(x => x.ClientName).NotEmpty().MaximumLength(250); 20 + RuleFor(x => x.Pickup).NotEmpty().MaximumLength(120); 21 + RuleFor(x => x.Dropoff).NotEmpty().MaximumLength(120); 22 + } 23 + } 24 + 25 + public static async Task<Ok<Response>> Handle(Request request, AppDbContext db, CancellationToken ct) 26 + { 27 + var row = new Job 28 + { 29 + ClientName = request.ClientName, 30 + Pickup = request.Pickup, 31 + Dropoff = request.Dropoff, 32 + }; 33 + 34 + await db.Jobs.AddAsync(row, ct); 35 + await db.SaveChangesAsync(ct); 36 + 37 + var response = new Response(row.Id); 38 + return TypedResults.Ok(response); 39 + } 40 + }
+20
src/Core/Jobs/DeleteJobEndpoint.cs
··· 1 + namespace TestWeb.Core.Jobs; 2 + 3 + using Infra; 4 + 5 + public static class DeleteJobEndpoint 6 + { 7 + public static async Task<Results<Ok, NotFound>> Handle( 8 + int id, 9 + AppDbContext db, 10 + CancellationToken ct) 11 + { 12 + var rowsDeleted = await db.Jobs 13 + .Where(x => x.Id == id) 14 + .ExecuteDeleteAsync(ct); 15 + 16 + return rowsDeleted == 1 17 + ? TypedResults.Ok() 18 + : TypedResults.NotFound(); 19 + } 20 + }
+20
src/Core/Jobs/GetJobEndpoint.cs
··· 1 + namespace TestWeb.Core.Jobs; 2 + 3 + using Infra; 4 + 5 + public static class GetJobEndpoint 6 + { 7 + public static async Task<Results<Ok<Job>, NotFound>> Handle( 8 + int id, 9 + AppDbContext db, 10 + CancellationToken ct) 11 + { 12 + var result = await db.Jobs 13 + .Where(x => x.Id == id) 14 + .SingleOrDefaultAsync(ct); 15 + 16 + return result is null 17 + ? TypedResults.NotFound() 18 + : TypedResults.Ok(result); 19 + } 20 + }
+12
src/Core/Jobs/GetJobsEndpoint.cs
··· 1 + namespace TestWeb.Core.Jobs; 2 + 3 + using Infra; 4 + 5 + public static class GetJobsEndpoint 6 + { 7 + public static async Task<Ok<Job[]>> Handle(AppDbContext db, CancellationToken ct) 8 + { 9 + var results = await db.Jobs.ToArrayAsync(ct); 10 + return TypedResults.Ok(results); 11 + } 12 + }
+26
src/Core/Jobs/JobsModule.cs
··· 1 + namespace TestWeb.Core.Jobs; 2 + 3 + using Utils; 4 + 5 + public static class JobsModule 6 + { 7 + public const string Prefix = "/api/jobs"; 8 + 9 + public static void MapEndpoints(IEndpointRouteBuilder endpoints) 10 + { 11 + var group = endpoints.MapGroup(Prefix) 12 + .WithTags("Jobs"); 13 + group.MapPost("/", CreateJobEndpoint.Handle) 14 + .WithSummary("Create a job") 15 + .WithRequestValidation<CreateJobEndpoint.Request>(); 16 + group.MapDelete("/{id:int}", DeleteJobEndpoint.Handle) 17 + .WithSummary("Delete job by id"); 18 + group.MapGet("/{id:int}", GetJobEndpoint.Handle) 19 + .WithSummary("Get job by id"); 20 + group.MapGet("/", GetJobsEndpoint.Handle) 21 + .WithSummary("Get job list"); 22 + group.MapPut("/{id:int}", UpdateJobEndpoint.Handle) 23 + .WithSummary("Update job by id") 24 + .WithRequestValidation<UpdateJobEndpoint.Request>(); 25 + } 26 + }
+47
src/Core/Jobs/UpdateJobEndpoint.cs
··· 1 + namespace TestWeb.Core.Jobs; 2 + 3 + using Infra; 4 + 5 + public static class UpdateJobEndpoint 6 + { 7 + public record Request( 8 + string ClientName, 9 + string Pickup, 10 + string Dropoff 11 + ); 12 + 13 + public class RequestValidator : AbstractValidator<Request> 14 + { 15 + public RequestValidator() 16 + { 17 + RuleFor(x => x.ClientName).NotEmpty().MaximumLength(250); 18 + RuleFor(x => x.Pickup).NotEmpty().MaximumLength(120); 19 + RuleFor(x => x.Dropoff).NotEmpty().MaximumLength(120); 20 + } 21 + } 22 + 23 + public static async Task<Results<Ok, NotFound>> Handle( 24 + int id, 25 + Request request, 26 + AppDbContext db, 27 + CancellationToken ct) 28 + { 29 + var result = await db.Jobs 30 + .Where(x => x.Id == id) 31 + .SingleOrDefaultAsync(ct); 32 + 33 + if (result == null) 34 + { 35 + return TypedResults.NotFound(); 36 + } 37 + 38 + result.ClientName = request.ClientName; 39 + result.Pickup = request.Pickup; 40 + result.Dropoff = request.Dropoff; 41 + result.LastUpdated = DateTime.UtcNow; 42 + 43 + await db.SaveChangesAsync(ct); 44 + 45 + return TypedResults.Ok(); 46 + } 47 + }
+34
src/Core/Users/CreateUserEndpoint.cs
··· 1 + namespace TestWeb.Core.Users; 2 + 3 + using Infra; 4 + 5 + public static class CreateUserEndpoint 6 + { 7 + public record Request(string UserName); 8 + 9 + public class RequestValidator : AbstractValidator<Request> 10 + { 11 + public RequestValidator() 12 + { 13 + RuleFor(x => x.UserName).NotEmpty().MaximumLength(250); 14 + } 15 + } 16 + 17 + public record Response(int Id); 18 + 19 + public static async Task<Ok<Response>> Handle(Request request, AppDbContext db, CancellationToken ct) 20 + { 21 + var row = new User 22 + { 23 + Username = request.UserName, 24 + DisplayName = request.UserName, 25 + Password = string.Empty 26 + }; 27 + 28 + await db.Users.AddAsync(row, ct); 29 + await db.SaveChangesAsync(ct); 30 + 31 + var response = new Response(row.Id); 32 + return TypedResults.Ok(response); 33 + } 34 + }
+21
src/Core/Users/DeleteUserEndpoint.cs
··· 1 + using TestWeb.Infra; 2 + using TestWeb.Utils; 3 + 4 + namespace TestWeb.Core.Users; 5 + 6 + public static class DeleteUserEndpoint 7 + { 8 + public static async Task<Results<Ok, NotFound>> Handle( 9 + int id, 10 + AppDbContext db, 11 + CancellationToken ct) 12 + { 13 + var rowsDeleted = await db.Users 14 + .Where(x => x.Id == id) 15 + .ExecuteDeleteAsync(ct); 16 + 17 + return rowsDeleted == 1 18 + ? TypedResults.Ok() 19 + : TypedResults.NotFound(); 20 + } 21 + }
+20
src/Core/Users/GetUserEndpoint.cs
··· 1 + namespace TestWeb.Core.Users; 2 + 3 + using Infra; 4 + 5 + public static class GetUserEndpoint 6 + { 7 + public static async Task<Results<Ok<User>, NotFound>> Handle( 8 + int id, 9 + AppDbContext db, 10 + CancellationToken ct) 11 + { 12 + var result = await db.Users 13 + .Where(x => x.Id == id) 14 + .SingleOrDefaultAsync(ct); 15 + 16 + return result is null 17 + ? TypedResults.NotFound() 18 + : TypedResults.Ok(result); 19 + } 20 + }
+12
src/Core/Users/GetUsersEndpoint.cs
··· 1 + namespace TestWeb.Core.Users; 2 + 3 + using Infra; 4 + 5 + public static class GetUsersEndpoint 6 + { 7 + public static async Task<Ok<User[]>> Handle(AppDbContext db, CancellationToken ct) 8 + { 9 + var results = await db.Users.ToArrayAsync(ct); 10 + return TypedResults.Ok(results); 11 + } 12 + }
+39
src/Core/Users/UpdateUserEndpoint.cs
··· 1 + namespace TestWeb.Core.Users; 2 + 3 + using Infra; 4 + 5 + public static class UpdateUserEndpoint 6 + { 7 + public record Request(string DisplayName); 8 + 9 + public class RequestValidator : AbstractValidator<Request> 10 + { 11 + public RequestValidator() 12 + { 13 + RuleFor(x => x.DisplayName).NotEmpty().MaximumLength(250); 14 + } 15 + } 16 + 17 + public static async Task<Results<Ok, NotFound>> Handle( 18 + int id, 19 + Request request, 20 + AppDbContext db, 21 + CancellationToken ct) 22 + { 23 + var result = await db.Users 24 + .Where(x => x.Id == id) 25 + .SingleOrDefaultAsync(ct); 26 + 27 + if (result == null) 28 + { 29 + return TypedResults.NotFound(); 30 + } 31 + 32 + result.DisplayName = request.DisplayName; 33 + result.LastUpdated = DateTime.UtcNow; 34 + 35 + await db.SaveChangesAsync(ct); 36 + 37 + return TypedResults.Ok(); 38 + } 39 + }
+26
src/Core/Users/UsersModule.cs
··· 1 + namespace TestWeb.Core.Users; 2 + 3 + using Utils; 4 + 5 + public static class UsersModule 6 + { 7 + public const string Prefix = "/api/users"; 8 + 9 + public static void MapEndpoints(IEndpointRouteBuilder endpoints) 10 + { 11 + var group = endpoints.MapGroup(Prefix) 12 + .WithTags("Users"); 13 + group.MapPost("/", CreateUserEndpoint.Handle) 14 + .WithSummary("Create a user") 15 + .WithRequestValidation<CreateUserEndpoint.Request>(); 16 + group.MapDelete("/{id}", DeleteUserEndpoint.Handle) 17 + .WithSummary("Delete user by id"); 18 + group.MapGet("/{id}", GetUserEndpoint.Handle) 19 + .WithSummary("Get user by id"); 20 + group.MapGet("/", GetUsersEndpoint.Handle) 21 + .WithSummary("Get user list"); 22 + group.MapPut("/{id}", UpdateUserEndpoint.Handle) 23 + .WithSummary("Update user by id") 24 + .WithRequestValidation<UpdateUserEndpoint.Request>(); 25 + } 26 + }
+35
src/Core/Vehicles/CreateVehicleEndpoint.cs
··· 1 + namespace TestWeb.Core.Vehicles; 2 + 3 + using Infra; 4 + 5 + public static class CreateVehicleEndpoint 6 + { 7 + public record Request(string Make, string Model); 8 + 9 + public record Response(int Id); 10 + 11 + public class RequestValidator : AbstractValidator<Request> 12 + { 13 + public RequestValidator() 14 + { 15 + RuleFor(x => x.Make).NotEmpty().MaximumLength(250); 16 + RuleFor(x => x.Model).NotEmpty().MaximumLength(250); 17 + } 18 + } 19 + 20 + public static async Task<Ok<Response>> Handle(Request request, AppDbContext db, CancellationToken ct) 21 + { 22 + var row = new Vehicle 23 + { 24 + Make = request.Make, 25 + Model = request.Model, 26 + Year = DateTime.Now.Year 27 + }; 28 + 29 + await db.Vehicles.AddAsync(row, ct); 30 + await db.SaveChangesAsync(ct); 31 + 32 + var response = new Response(row.Id); 33 + return TypedResults.Ok(response); 34 + } 35 + }
+20
src/Core/Vehicles/DeleteVehicleEndpoint.cs
··· 1 + namespace TestWeb.Core.Vehicles; 2 + 3 + using Infra; 4 + 5 + public static class DeleteVehicleEndpoint 6 + { 7 + public static async Task<Results<Ok, NotFound>> Handle( 8 + int id, 9 + AppDbContext db, 10 + CancellationToken ct) 11 + { 12 + var rowsDeleted = await db.Vehicles 13 + .Where(x => x.Id == id) 14 + .ExecuteDeleteAsync(ct); 15 + 16 + return rowsDeleted == 1 17 + ? TypedResults.Ok() 18 + : TypedResults.NotFound(); 19 + } 20 + }
+20
src/Core/Vehicles/GetVehicleEndpoint.cs
··· 1 + namespace TestWeb.Core.Vehicles; 2 + 3 + using Infra; 4 + 5 + public static class GetVehicleEndpoint 6 + { 7 + public static async Task<Results<Ok<Vehicle>, NotFound>> Handle( 8 + int id, 9 + AppDbContext db, 10 + CancellationToken ct) 11 + { 12 + var result = await db.Vehicles 13 + .Where(x => x.Id == id) 14 + .SingleOrDefaultAsync(ct); 15 + 16 + return result is null 17 + ? TypedResults.NotFound() 18 + : TypedResults.Ok(result); 19 + } 20 + }
+12
src/Core/Vehicles/GetVehiclesEndpoint.cs
··· 1 + namespace TestWeb.Core.Vehicles; 2 + 3 + using Infra; 4 + 5 + public static class GetVehiclesEndpoint 6 + { 7 + public static async Task<Ok<Vehicle[]>> Handle(AppDbContext db, CancellationToken ct) 8 + { 9 + var results = await db.Vehicles.ToArrayAsync(ct); 10 + return TypedResults.Ok(results); 11 + } 12 + }
+41
src/Core/Vehicles/UpdateVehicleEndpoint.cs
··· 1 + namespace TestWeb.Core.Vehicles; 2 + 3 + using Infra; 4 + 5 + public static class UpdateVehicleEndpoint 6 + { 7 + public record Request(string Make, string Model); 8 + 9 + public class RequestValidator : AbstractValidator<Request> 10 + { 11 + public RequestValidator() 12 + { 13 + RuleFor(x => x.Make).NotEmpty().MaximumLength(250); 14 + RuleFor(x => x.Model).NotEmpty().MaximumLength(250); 15 + } 16 + } 17 + 18 + public static async Task<Results<Ok, NotFound>> Handle( 19 + int id, 20 + Request request, 21 + AppDbContext db, 22 + CancellationToken ct) 23 + { 24 + var result = await db.Vehicles 25 + .Where(x => x.Id == id) 26 + .SingleOrDefaultAsync(ct); 27 + 28 + if (result == null) 29 + { 30 + return TypedResults.NotFound(); 31 + } 32 + 33 + result.Make = request.Make; 34 + result.Model = request.Model; 35 + result.LastUpdated = DateTime.UtcNow; 36 + 37 + await db.SaveChangesAsync(ct); 38 + 39 + return TypedResults.Ok(); 40 + } 41 + }
+26
src/Core/Vehicles/VehiclesModule.cs
··· 1 + namespace TestWeb.Core.Vehicles; 2 + 3 + using Utils; 4 + 5 + public static class VehiclesModule 6 + { 7 + public const string Prefix = "/api/vehicles"; 8 + 9 + public static void MapEndpoints(IEndpointRouteBuilder endpoints) 10 + { 11 + var group = endpoints.MapGroup(Prefix) 12 + .WithTags("Vehicles"); 13 + group.MapPost("/", CreateVehicleEndpoint.Handle) 14 + .WithSummary("Create a vehicle") 15 + .WithRequestValidation<CreateVehicleEndpoint.Request>(); 16 + group.MapDelete("/{id}", DeleteVehicleEndpoint.Handle) 17 + .WithSummary("Delete vehicle by id"); 18 + group.MapGet("/{id}", GetVehicleEndpoint.Handle) 19 + .WithSummary("Get vehicle by id"); 20 + group.MapGet("/", GetVehiclesEndpoint.Handle) 21 + .WithSummary("Get vehicle list"); 22 + group.MapPut("/{id}", UpdateVehicleEndpoint.Handle) 23 + .WithSummary("Update vehicle by id") 24 + .WithRequestValidation<UpdateVehicleEndpoint.Request>(); 25 + } 26 + }
+25
src/Core/WeatherForecasts/GetWeatherForecastsEndpoint.cs
··· 1 + namespace TestWeb.Core.WeatherForecasts; 2 + 3 + public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) 4 + { 5 + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 6 + } 7 + 8 + public static class GetWeatherForecastsEndpoint 9 + { 10 + private static readonly string[] Summaries = new[] 11 + { 12 + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 13 + }; 14 + 15 + public static WeatherForecast[] Handle() 16 + { 17 + var random = new Random(); 18 + return Enumerable.Range(1, 5).Select(index => new WeatherForecast 19 + ( 20 + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 21 + random.Next(-20, 55), 22 + Summaries[random.Next(Summaries.Length)] 23 + )).ToArray(); 24 + } 25 + }
+15
src/Core/WeatherForecasts/WeatherForecastsModule.cs
··· 1 + namespace TestWeb.Core.WeatherForecasts; 2 + 3 + public static class WeatherForecastsModule 4 + { 5 + public const string Prefix = "/api/weatherforecasts"; 6 + 7 + public static void MapEndpoints(IEndpointRouteBuilder endpoints) 8 + { 9 + var group = endpoints.MapGroup("/api/weatherforecasts") 10 + .WithTags("WeatherForecasts") 11 + .AllowAnonymous(); 12 + group.MapGet("/", GetWeatherForecastsEndpoint.Handle) 13 + .WithSummary("Get weather forecast list"); 14 + } 15 + }
+4
src/Global.Usings.cs
··· 1 + global using FluentValidation; 2 + global using Microsoft.AspNetCore.Authentication.JwtBearer; 3 + global using Microsoft.AspNetCore.Http.HttpResults; 4 + global using Microsoft.EntityFrameworkCore;
+17
src/Infra/AppDbContext.cs
··· 1 + namespace TestWeb.Infra; 2 + 3 + public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) 4 + { 5 + public DbSet<Job> Jobs 6 + { 7 + get; set; 8 + } 9 + public DbSet<User> Users 10 + { 11 + get; set; 12 + } 13 + public DbSet<Vehicle> Vehicles 14 + { 15 + get; set; 16 + } 17 + }
+33
src/Infra/Job.cs
··· 1 + using System.ComponentModel.DataAnnotations; 2 + 3 + namespace TestWeb.Infra; 4 + 5 + public class Job 6 + { 7 + [Key] 8 + public int Id 9 + { 10 + get; init; 11 + } 12 + public required string ClientName 13 + { 14 + get; set; 15 + } 16 + public required string Pickup 17 + { 18 + get; set; 19 + } 20 + public string? Dropoff 21 + { 22 + get; set; 23 + } 24 + public int VehicleId 25 + { 26 + get; set; 27 + } 28 + public DateTime DateCreated { get; private init; } = DateTime.UtcNow; 29 + public DateTime? LastUpdated 30 + { 31 + get; set; 32 + } 33 + }
+29
src/Infra/User.cs
··· 1 + using System.ComponentModel.DataAnnotations; 2 + 3 + namespace TestWeb.Infra; 4 + 5 + public class User 6 + { 7 + [Key] 8 + public int Id 9 + { 10 + get; init; 11 + } 12 + public required string Username 13 + { 14 + get; set; 15 + } 16 + public required string Password 17 + { 18 + get; set; 19 + } 20 + public required string DisplayName 21 + { 22 + get; set; 23 + } 24 + public DateTime DateCreated { get; private init; } = DateTime.UtcNow; 25 + public DateTime? LastUpdated 26 + { 27 + get; set; 28 + } 29 + }
+29
src/Infra/Vehicle.cs
··· 1 + using System.ComponentModel.DataAnnotations; 2 + 3 + namespace TestWeb.Infra; 4 + 5 + public class Vehicle 6 + { 7 + [Key] 8 + public int Id 9 + { 10 + get; init; 11 + } 12 + public required string Make 13 + { 14 + get; set; 15 + } 16 + public required string Model 17 + { 18 + get; set; 19 + } 20 + public required int Year 21 + { 22 + get; set; 23 + } 24 + public DateTime DateCreated { get; private init; } = DateTime.UtcNow; 25 + public DateTime? LastUpdated 26 + { 27 + get; set; 28 + } 29 + }
+86
src/Program.cs
··· 1 + using Microsoft.OpenApi.Models; 2 + using TestWeb.Core.Jobs; 3 + using TestWeb.Core.Users; 4 + using TestWeb.Core.Vehicles; 5 + using TestWeb.Core.WeatherForecasts; 6 + using TestWeb.Infra; 7 + using TestWeb.Utils; 8 + 9 + namespace TestWeb; 10 + 11 + public class Program 12 + { 13 + public static void Main(string[] args) 14 + { 15 + var builder = WebApplication.CreateBuilder(args); 16 + AddServices(builder); 17 + 18 + var app = builder.Build(); 19 + Configure(app); 20 + 21 + app.Run(); 22 + } 23 + 24 + public static void AddServices(WebApplicationBuilder builder) 25 + { 26 + builder.Services.AddDbContext<AppDbContext>( 27 + options => options.UseSqlite("DataSource=app.db")); 28 + 29 + builder.Services.AddOpenApi(); 30 + builder.Services.AddEndpointsApiExplorer(); 31 + builder.Services.AddSwaggerGen(); 32 + 33 + builder.Services.AddCors(options => 34 + { 35 + options.AddPolicy( 36 + "CorsPolicy", 37 + builder => builder 38 + .AllowAnyOrigin() 39 + .AllowAnyHeader() 40 + .AllowAnyMethod() 41 + ); 42 + }); 43 + 44 + builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly); 45 + } 46 + 47 + public static void Configure(WebApplication app) 48 + { 49 + app.MapOpenApi(); 50 + 51 + app.UseSwagger(); 52 + app.UseSwaggerUI(); 53 + app.UseDeveloperExceptionPage(); 54 + app.UseCors("CorsPolicy"); 55 + app.UseHttpsRedirection(); 56 + app.UseRouting(); 57 + 58 + var endpoints = app.MapGroup("") 59 + .AddEndpointFilter<RequestLoggingFilter>(); 60 + 61 + var securityScheme = new OpenApiSecurityScheme() 62 + { 63 + Type = SecuritySchemeType.Http, 64 + Name = JwtBearerDefaults.AuthenticationScheme, 65 + Scheme = JwtBearerDefaults.AuthenticationScheme, 66 + Reference = new() 67 + { 68 + Type = ReferenceType.SecurityScheme, 69 + Id = JwtBearerDefaults.AuthenticationScheme 70 + } 71 + }; 72 + endpoints.WithOpenApi(x => new() 73 + { 74 + Security = [new() { [securityScheme] = [] }] 75 + }); 76 + 77 + JobsModule.MapEndpoints(endpoints); 78 + UsersModule.MapEndpoints(endpoints); 79 + VehiclesModule.MapEndpoints(endpoints); 80 + WeatherForecastsModule.MapEndpoints(endpoints); 81 + 82 + using var scope = app.Services.CreateScope(); 83 + var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); 84 + db.Database.EnsureCreated(); 85 + } 86 + }
+16
src/TestWeb.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk.Web"> 2 + <PropertyGroup> 3 + <TargetFramework>net10.0</TargetFramework> 4 + <Nullable>enable</Nullable> 5 + <ImplicitUsings>enable</ImplicitUsings> 6 + </PropertyGroup> 7 + 8 + <ItemGroup> 9 + <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" /> 10 + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" /> 11 + <PackageReference Include="FluentValidation" Version="12.0.0" /> 12 + <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" /> 13 + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> 14 + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" /> 15 + </ItemGroup> 16 + </Project>
+15
src/Utils/RequestLoggingFilter.cs
··· 1 + namespace TestWeb.Utils; 2 + 3 + public class RequestLoggingFilter(ILogger<RequestLoggingFilter> logger) : IEndpointFilter 4 + { 5 + public async ValueTask<object?> InvokeAsync( 6 + EndpointFilterInvocationContext context, 7 + EndpointFilterDelegate next) 8 + { 9 + logger.LogInformation( 10 + "HTTP {Method} {Path} recieved", 11 + context.HttpContext.Request.Method, 12 + context.HttpContext.Request.Path); 13 + return await next(context); 14 + } 15 + }
+34
src/Utils/RequestValidationFilter.cs
··· 1 + namespace TestWeb.Utils; 2 + 3 + public class RequestValidationFilter<TRequest>( 4 + ILogger<RequestValidationFilter<TRequest>> logger, 5 + IValidator<TRequest>? validator = null) : IEndpointFilter 6 + { 7 + public async ValueTask<object?> InvokeAsync( 8 + EndpointFilterInvocationContext context, 9 + EndpointFilterDelegate next) 10 + { 11 + var requestName = typeof(TRequest).FullName; 12 + 13 + if (validator is null) 14 + { 15 + logger.LogInformation("{Request}: No validator configured.", requestName); 16 + return await next(context); 17 + } 18 + 19 + logger.LogInformation("{Request}: Validating...", requestName); 20 + var request = context.Arguments.OfType<TRequest>().First(); 21 + var validationResult = await validator.ValidateAsync( 22 + request, 23 + context.HttpContext.RequestAborted 24 + ); 25 + if (!validationResult.IsValid) 26 + { 27 + logger.LogWarning("{Request}: Validation failed.", requestName); 28 + return TypedResults.ValidationProblem(validationResult.ToDictionary()); 29 + } 30 + 31 + logger.LogInformation("{Request}: Validation succeeded.", requestName); 32 + return await next(context); 33 + } 34 + }
+12
src/Utils/ValidationExtensions.cs
··· 1 + namespace TestWeb.Utils; 2 + 3 + public static class ValidationExtensions 4 + { 5 + public static RouteHandlerBuilder WithRequestValidation<TRequest>( 6 + this RouteHandlerBuilder builder) 7 + { 8 + return builder 9 + .AddEndpointFilter<RequestValidationFilter<TRequest>>() 10 + .ProducesValidationProblem(); 11 + } 12 + }
+19
src/appsettings.json
··· 1 + { 2 + "Logging": { 3 + "LogLevel": { 4 + "Default": "Information", 5 + "System": "Information", 6 + "Microsoft": "Information" 7 + }, 8 + "Console": { 9 + "IncludeScopes": true 10 + } 11 + }, 12 + "AllowedHosts": "*", 13 + "ConnectionStrings": { 14 + "Default": "Server=.;Database=TestWeb;Trusted_Connection=True;Encrypt=False;" 15 + }, 16 + "Jwt": { 17 + "Key": "superdupersecretkeythatsreallyreallylong" 18 + } 19 + }
+1
tests/Global.Usings.cs
··· 1 + global using Xunit;
+74
tests/JobsModuleTests.cs
··· 1 + using System.Net; 2 + using System.Net.Http.Json; 3 + 4 + namespace TestWeb.Tests; 5 + 6 + using Core.Jobs; 7 + 8 + public class JobsModuleTests(TestFixture fixture) : IClassFixture<TestFixture> 9 + { 10 + private readonly HttpClient client = fixture.Client; 11 + 12 + [Fact] 13 + public async Task Base() 14 + { 15 + // Create 16 + var createRequest = new CreateJobEndpoint.Request( 17 + "Client name", "Pickup", "Dropoff" 18 + ); 19 + var createCall = await client.PostAsJsonAsync( 20 + JobsModule.Prefix, 21 + createRequest, 22 + cancellationToken: TestContext.Current.CancellationToken 23 + ); 24 + Assert.Equal(HttpStatusCode.OK, createCall.StatusCode); 25 + 26 + var createResponse = await createCall.Content 27 + .ReadFromJsonAsync<CreateJobEndpoint.Response>( 28 + cancellationToken: TestContext.Current.CancellationToken 29 + ); 30 + Assert.NotNull(createResponse); 31 + 32 + // Get 33 + var getListCall = await client.GetAsync( 34 + JobsModule.Prefix, 35 + TestContext.Current.CancellationToken 36 + ); 37 + Assert.Equal(HttpStatusCode.OK, getListCall.StatusCode); 38 + 39 + var getCall = await client.GetAsync( 40 + $"{JobsModule.Prefix}/{createResponse.Id}", 41 + TestContext.Current.CancellationToken 42 + ); 43 + Assert.Equal(HttpStatusCode.OK, getCall.StatusCode); 44 + 45 + // Update 46 + var updateRequest = new UpdateJobEndpoint.Request( 47 + "Client name", "Pickup update", "Dropoff update"); 48 + var updateCall = await client.PutAsJsonAsync( 49 + $"{JobsModule.Prefix}/{createResponse.Id}", updateRequest, 50 + cancellationToken: TestContext.Current.CancellationToken 51 + ); 52 + Assert.Equal(HttpStatusCode.OK, updateCall.StatusCode); 53 + 54 + // Delete 55 + var deleteCall = await client.DeleteAsync( 56 + $"{JobsModule.Prefix}/{createResponse.Id}", 57 + TestContext.Current.CancellationToken 58 + ); 59 + Assert.Equal(HttpStatusCode.OK, deleteCall.StatusCode); 60 + 61 + // Not found 62 + var deleteCallNotFound = await client.DeleteAsync( 63 + $"{JobsModule.Prefix}/{createResponse.Id}", 64 + TestContext.Current.CancellationToken 65 + ); 66 + Assert.Equal(HttpStatusCode.NotFound, deleteCallNotFound.StatusCode); 67 + 68 + var getCallNotFound = await client.GetAsync( 69 + $"{JobsModule.Prefix}/{createResponse.Id}", 70 + TestContext.Current.CancellationToken 71 + ); 72 + Assert.Equal(HttpStatusCode.NotFound, getCallNotFound.StatusCode); 73 + } 74 + }
+28
tests/TestFixture.cs
··· 1 + using Microsoft.AspNetCore.Mvc.Testing; 2 + 3 + [assembly: AssemblyFixture(typeof(TestWeb.Tests.TestFixture))] 4 + 5 + namespace TestWeb.Tests; 6 + 7 + public class TestFixture : IAsyncLifetime 8 + { 9 + public HttpClient Client { get; private set; } = null!; 10 + private readonly WebApplicationFactory<Program> factory; 11 + 12 + public TestFixture() 13 + { 14 + factory = new WebApplicationFactory<Program>(); 15 + } 16 + 17 + public ValueTask InitializeAsync() 18 + { 19 + Client = factory.CreateClient(); 20 + return ValueTask.CompletedTask; 21 + } 22 + 23 + public ValueTask DisposeAsync() 24 + { 25 + Client.Dispose(); 26 + return ValueTask.CompletedTask; 27 + } 28 + }
+23
tests/TestWeb.Tests.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk"> 2 + <PropertyGroup> 3 + <TargetFramework>net10.0</TargetFramework> 4 + <OutputType>Exe</OutputType> 5 + <ImplicitUsings>enable</ImplicitUsings> 6 + <Nullable>enable</Nullable> 7 + </PropertyGroup> 8 + 9 + <ItemGroup> 10 + <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> 11 + </ItemGroup> 12 + 13 + <ItemGroup> 14 + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> 15 + <PackageReference Include="xunit.v3" Version="3.2.1" /> 16 + <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" /> 17 + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.9" /> 18 + </ItemGroup> 19 + 20 + <ItemGroup> 21 + <ProjectReference Include="..\src\TestWeb.csproj" /> 22 + </ItemGroup> 23 + </Project>
+70
tests/UsersModuleTests.cs
··· 1 + using System.Net; 2 + using System.Net.Http.Json; 3 + 4 + namespace TestWeb.Tests; 5 + 6 + using Core.Users; 7 + 8 + public class UserModuleTests(TestFixture fixture) : IClassFixture<TestFixture> 9 + { 10 + private readonly HttpClient client = fixture.Client; 11 + 12 + [Fact] 13 + public async Task Base() 14 + { 15 + // Create 16 + var createRequest = new CreateUserEndpoint.Request("Test"); 17 + var createCall = await client.PostAsJsonAsync( 18 + UsersModule.Prefix, 19 + createRequest, 20 + cancellationToken: TestContext.Current.CancellationToken 21 + ); 22 + Assert.Equal(HttpStatusCode.OK, createCall.StatusCode); 23 + 24 + var createResponse = await createCall.Content 25 + .ReadFromJsonAsync<CreateUserEndpoint.Response>(cancellationToken: TestContext.Current.CancellationToken); 26 + Assert.NotNull(createResponse); 27 + 28 + // Get 29 + var getListCall = await client.GetAsync( 30 + UsersModule.Prefix, 31 + TestContext.Current.CancellationToken 32 + ); 33 + Assert.Equal(HttpStatusCode.OK, getListCall.StatusCode); 34 + 35 + var getCall = await client.GetAsync( 36 + $"{UsersModule.Prefix}/{createResponse.Id}", 37 + TestContext.Current.CancellationToken 38 + ); 39 + Assert.Equal(HttpStatusCode.OK, getCall.StatusCode); 40 + 41 + // Update 42 + var updateRequest = new UpdateUserEndpoint.Request("Test"); 43 + var updateCall = await client.PutAsJsonAsync( 44 + $"{UsersModule.Prefix}/{createResponse.Id}", 45 + updateRequest, 46 + cancellationToken: TestContext.Current.CancellationToken 47 + ); 48 + Assert.Equal(HttpStatusCode.OK, updateCall.StatusCode); 49 + 50 + // Delete 51 + var deleteCall = await client.DeleteAsync( 52 + $"{UsersModule.Prefix}/{createResponse.Id}", 53 + TestContext.Current.CancellationToken 54 + ); 55 + Assert.Equal(HttpStatusCode.OK, deleteCall.StatusCode); 56 + 57 + // Not found 58 + var deleteCallNotFound = await client.DeleteAsync( 59 + $"{UsersModule.Prefix}/{createResponse.Id}", 60 + TestContext.Current.CancellationToken 61 + ); 62 + Assert.Equal(HttpStatusCode.NotFound, deleteCallNotFound.StatusCode); 63 + 64 + var getCallNotFound = await client.GetAsync( 65 + $"{UsersModule.Prefix}/{createResponse.Id}", 66 + TestContext.Current.CancellationToken 67 + ); 68 + Assert.Equal(HttpStatusCode.NotFound, getCallNotFound.StatusCode); 69 + } 70 + }
+72
tests/VehiclesModuleTests.cs
··· 1 + using System.Net; 2 + using System.Net.Http.Json; 3 + 4 + namespace TestWeb.Tests; 5 + 6 + using Core.Vehicles; 7 + 8 + public class VehiclesModuleTests(TestFixture fixture) : IClassFixture<TestFixture> 9 + { 10 + private readonly HttpClient client = fixture.Client; 11 + 12 + [Fact] 13 + public async Task Base() 14 + { 15 + // Create 16 + var createRequest = new CreateVehicleEndpoint.Request("Test make", "Test model"); 17 + var createCall = await client.PostAsJsonAsync( 18 + VehiclesModule.Prefix, 19 + createRequest, 20 + cancellationToken: TestContext.Current.CancellationToken 21 + ); 22 + Assert.Equal(HttpStatusCode.OK, createCall.StatusCode); 23 + 24 + var createResponse = await createCall.Content 25 + .ReadFromJsonAsync<CreateVehicleEndpoint.Response>( 26 + cancellationToken: TestContext.Current.CancellationToken 27 + ); 28 + Assert.NotNull(createResponse); 29 + 30 + // Get 31 + var getListCall = await client.GetAsync( 32 + VehiclesModule.Prefix, 33 + TestContext.Current.CancellationToken 34 + ); 35 + Assert.Equal(HttpStatusCode.OK, getListCall.StatusCode); 36 + 37 + var getCall = await client.GetAsync( 38 + $"{VehiclesModule.Prefix}/{createResponse.Id}", 39 + TestContext.Current.CancellationToken 40 + ); 41 + Assert.Equal(HttpStatusCode.OK, getCall.StatusCode); 42 + 43 + // Update 44 + var updateRequest = new UpdateVehicleEndpoint.Request("Test make update", "Test model update"); 45 + var updateCall = await client.PutAsJsonAsync( 46 + $"{VehiclesModule.Prefix}/{createResponse.Id}", 47 + updateRequest, 48 + cancellationToken: TestContext.Current.CancellationToken 49 + ); 50 + Assert.Equal(HttpStatusCode.OK, updateCall.StatusCode); 51 + 52 + // Delete 53 + var deleteCall = await client.DeleteAsync( 54 + $"{VehiclesModule.Prefix}/{createResponse.Id}", 55 + TestContext.Current.CancellationToken 56 + ); 57 + Assert.Equal(HttpStatusCode.OK, deleteCall.StatusCode); 58 + 59 + // Not found 60 + var deleteCallNotFound = await client.DeleteAsync( 61 + $"{VehiclesModule.Prefix}/{createResponse.Id}", 62 + TestContext.Current.CancellationToken 63 + ); 64 + Assert.Equal(HttpStatusCode.NotFound, deleteCallNotFound.StatusCode); 65 + 66 + var getCallNotFound = await client.GetAsync( 67 + $"{VehiclesModule.Prefix}/{createResponse.Id}", 68 + TestContext.Current.CancellationToken 69 + ); 70 + Assert.Equal(HttpStatusCode.NotFound, getCallNotFound.StatusCode); 71 + } 72 + }
+21
tests/WeatherForecastsModuleTests.cs
··· 1 + using System.Net; 2 + 3 + namespace TestWeb.Tests; 4 + 5 + using Core.WeatherForecasts; 6 + 7 + public class WeatherForecastsModuleTests(TestFixture fixture) : IClassFixture<TestFixture> 8 + { 9 + private readonly HttpClient client = fixture.Client; 10 + 11 + [Fact] 12 + public async Task Base() 13 + { 14 + // Get 15 + var getCall = await client.GetAsync( 16 + WeatherForecastsModule.Prefix, 17 + TestContext.Current.CancellationToken 18 + ); 19 + Assert.Equal(HttpStatusCode.OK, getCall.StatusCode); 20 + } 21 + }
+3
tests/xunit.runner.json
··· 1 + { 2 + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" 3 + }