Add integration and unit tests for Notes API with PostgreSQL-based test hosting configuration
This commit is contained in:
70
Freman.Sample.Web.IntegrationTests/Api/NotesApiTests.cs
Normal file
70
Freman.Sample.Web.IntegrationTests/Api/NotesApiTests.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Freman.Sample.Web.Contracts;
|
||||
using Shouldly;
|
||||
using Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
namespace Freman.Sample.Web.IntegrationTests.Api;
|
||||
|
||||
[Collection(IntegrationTestCollection.Name)]
|
||||
public class NotesApiTests(IntegrationTestFixture fixture) : IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client = fixture.Factory.CreateClient();
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await fixture.ResetDatabaseAsync();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task GetNotes_InitiallyEmpty_Returns200AndEmptyList()
|
||||
{
|
||||
var resp = await _client.GetAsync("/api/notes", TestContext.Current.CancellationToken);
|
||||
resp.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var notes = await resp.Content.ReadFromJsonAsync<List<NoteDto>>(cancellationToken: TestContext.Current.CancellationToken);
|
||||
notes.ShouldNotBeNull();
|
||||
notes!.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostNote_ThenGetNotes_ReturnsTheNewNote()
|
||||
{
|
||||
var uniqueText = $"test note {Guid.NewGuid():N}";
|
||||
|
||||
var post = await _client.PostAsJsonAsync("/api/notes",
|
||||
new CreateNoteRequest { Text = uniqueText },
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
|
||||
post.StatusCode.ShouldBe(HttpStatusCode.Created);
|
||||
|
||||
var notes = await _client.GetFromJsonAsync<List<NoteDto>>("/api/notes", cancellationToken: TestContext.Current.CancellationToken);
|
||||
notes.ShouldNotBeNull();
|
||||
notes!.Any(n => n.Text == uniqueText).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteNote_RemovesIt()
|
||||
{
|
||||
var post = await _client.PostAsJsonAsync("/api/notes", new CreateNoteRequest { Text = "delete me" },
|
||||
cancellationToken: TestContext.Current.CancellationToken);
|
||||
var created = await post.Content.ReadFromJsonAsync<NoteDto>(cancellationToken: TestContext.Current.CancellationToken);
|
||||
created.ShouldNotBeNull();
|
||||
|
||||
var del = await _client.DeleteAsync($"/api/notes/{created!.Id}", TestContext.Current.CancellationToken);
|
||||
del.StatusCode.ShouldBe(HttpStatusCode.NoContent);
|
||||
|
||||
var notes = await _client.GetFromJsonAsync<List<NoteDto>>("/api/notes", cancellationToken: TestContext.Current.CancellationToken);
|
||||
notes.ShouldNotBeNull();
|
||||
notes!.Any(n => n.Id == created.Id).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteMissingNote_Returns404()
|
||||
{
|
||||
var del = await _client.DeleteAsync("/api/notes/123456", TestContext.Current.CancellationToken);
|
||||
del.StatusCode.ShouldBe(HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3"/>
|
||||
<PackageReference Include="Testcontainers.PostgreSql" Version="4.10.0"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="xunit.v3" Version="3.2.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Freman.Sample.Web.Contracts\Freman.Sample.Web.Contracts.csproj"/>
|
||||
<ProjectReference Include="..\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||
public sealed class IntegrationTestCollection : ICollectionFixture<IntegrationTestFixture>
|
||||
{
|
||||
public const string Name = "Integration tests";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Freman.Sample.Web.Data;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Testcontainers.PostgreSql;
|
||||
|
||||
namespace Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
public sealed class IntegrationTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
private readonly PostgreSqlContainer _pg = new PostgreSqlBuilder("postgres:16")
|
||||
.WithDatabase("testdb")
|
||||
.WithUsername("appuser")
|
||||
.WithPassword("testpassword")
|
||||
.Build();
|
||||
|
||||
public SampleWebFactory Factory { get; private set; } = null!;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _pg.StartAsync();
|
||||
|
||||
Factory = new SampleWebFactory(_pg.GetConnectionString());
|
||||
|
||||
// Apply migrations once
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
public new async ValueTask DisposeAsync()
|
||||
{
|
||||
await Factory.DisposeAsync();
|
||||
await _pg.DisposeAsync();
|
||||
}
|
||||
|
||||
public async Task ResetDatabaseAsync()
|
||||
{
|
||||
using var scope = Factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.ExecuteSqlRawAsync(@"TRUNCATE TABLE notes RESTART IDENTITY;");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Freman.Sample.Web.IntegrationTests.TestHost;
|
||||
|
||||
public sealed class SampleWebFactory(string connectionString) : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
builder.ConfigureAppConfiguration((_, config) =>
|
||||
{
|
||||
var overrides = new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:AppDb"] = connectionString,
|
||||
["ApplyMigrationsOnStartup"] = "false"
|
||||
};
|
||||
|
||||
config.AddInMemoryCollection(overrides);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Freman.Sample.Web.Contracts;
|
||||
using Shouldly;
|
||||
|
||||
namespace Freman.Sample.Web.UnitTests.Contracts;
|
||||
|
||||
public class CreateNoteRequestTests
|
||||
{
|
||||
[Fact]
|
||||
public void CreateNoteRequest_EmptyText_IsInvalid()
|
||||
{
|
||||
var model = new CreateNoteRequest { Text = "" };
|
||||
|
||||
var results = Validate(model);
|
||||
|
||||
results.Count.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateNoteRequest_TooLong_IsInvalid()
|
||||
{
|
||||
var model = new CreateNoteRequest { Text = new string('a', 501) };
|
||||
|
||||
var results = Validate(model);
|
||||
|
||||
results.Count.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateNoteRequest_NormalText_IsValid()
|
||||
{
|
||||
var model = new CreateNoteRequest { Text = "hello" };
|
||||
|
||||
var results = Validate(model);
|
||||
|
||||
results.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
private static List<ValidationResult> Validate(object model)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
var ctx = new ValidationContext(model);
|
||||
Validator.TryValidateObject(model, ctx, results, validateAllProperties: true);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Freman.Sample.Web.UnitTests</AssemblyName>
|
||||
<RootNamespace>Freman.Sample.Web.UnitTests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0"/>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0"/>
|
||||
<PackageReference Include="xunit.v3" Version="3.2.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Freman.Sample.Web.Contracts\Freman.Sample.Web.Contracts.csproj"/>
|
||||
<ProjectReference Include="..\Freman.Sample.Web\Freman.Sample.Web\Freman.Sample.Web.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,23 +11,82 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Freman.Sample.Web.Contracts", "Freman.Sample.Web.Contracts\Freman.Sample.Web.Contracts.csproj", "{E92F3324-DD83-4B66-BF80-B287C6C5597D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Freman.Sample.Web.UnitTests", "Freman.Sample.Web.UnitTests\Freman.Sample.Web.UnitTests.csproj", "{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Freman.Sample.Web.IntegrationTests", "Freman.Sample.Web.IntegrationTests\Freman.Sample.Web.IntegrationTests.csproj", "{265A416F-B516-43EA-9407-A6B05FABFD4D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FF628414-CE48-41D6-A139-3835F423F9C3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7FCB89A6-518D-404C-B782-9A5A6F86FEC5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E92F3324-DD83-4B66-BF80-B287C6C5597D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DA0BF730-6BC5-47E0-8A31-F97A494B7ABB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{265A416F-B516-43EA-9407-A6B05FABFD4D}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
14
Freman.Sample.Web.sln.DotSettings.user
Normal file
14
Freman.Sample.Web.sln.DotSettings.user
Normal file
@@ -0,0 +1,14 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAsyncDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Faa1b45f3f86166f2831a18e0a327c822ba284c2e2367db0ac9752edf3e867d_003FIAsyncDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAsyncLifetime_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9240e1f8cbc4e1b776695ecbb3f17cc2ed23f3d32e6fd9b9335df88691beba86_003FIAsyncLifetime_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANotFound_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9fc45acb8dd787f63c351275caf64d0e279ce2bcc2e143a21216bf1dd60fac5_003FNotFound_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARequestDelegateFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc122dc6196c2679e6afe8e125bcd1ff18735d2b014810e358113f75cbc554a8_003FRequestDelegateFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AResultsCache_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3fca946e2973ada7faf7b24bdfae1ab2ae2fe8917b3ac422855a5d4b954101e_003FResultsCache_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATypedResults_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F69af2765fdfcf5d7815ef1e4c2bcdfe25f15b9ac164e532b89617588ebe280_003FTypedResults_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AWebApplicationFactory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb1dcf3158124f84888ef6e971e8838c373a93c4ba6786df5dcf4c08228c736_003FWebApplicationFactory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7b49acf8_002D6310_002D4073_002Db94a_002Dfef27f743ad7/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;Freman.Sample.Web.IntegrationTests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<Or>
|
||||
<Project Location="C:\Users\smitt\RiderProjects\Freman.Sample.Web\Freman.Sample.Web.IntegrationTests" Presentation="&lt;Freman.Sample.Web.IntegrationTests&gt;" />
|
||||
<Project Location="C:\Users\smitt\RiderProjects\Freman.Sample.Web\Freman.Sample.Web.UnitTests" Presentation="&lt;Freman.Sample.Web.UnitTests&gt;" />
|
||||
</Or>
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
@@ -10,10 +10,11 @@ public static class NotesEndpoints
|
||||
{
|
||||
var notesApi = app.MapGroup("/api/notes");
|
||||
|
||||
notesApi.MapGet("/", async (AppDbContext db, CancellationToken ct) =>
|
||||
notesApi.MapGet("", async (AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var notes = await db.Notes
|
||||
.OrderByDescending(n => n.CreatedUtc)
|
||||
.ThenByDescending(n => n.Id)
|
||||
.Take(50)
|
||||
.Select(n => new NoteDto(n.Id, n.Text, n.CreatedUtc))
|
||||
.ToListAsync(ct);
|
||||
@@ -21,14 +22,16 @@ public static class NotesEndpoints
|
||||
return Results.Ok(notes);
|
||||
});
|
||||
|
||||
notesApi.MapPost("/", async (CreateNoteRequest request, AppDbContext db, CancellationToken ct) =>
|
||||
notesApi.MapPost("", async (CreateNoteRequest request, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var text = (request.Text ?? "").Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return Results.BadRequest("Text is required.");
|
||||
|
||||
if (text.Length > 500)
|
||||
{
|
||||
return Results.BadRequest("Text must be 500 characters or less.");
|
||||
}
|
||||
|
||||
var note = new NoteEntity
|
||||
{
|
||||
@@ -39,14 +42,16 @@ public static class NotesEndpoints
|
||||
db.Notes.Add(note);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Results.Created($"/api/notes/{note.Id}", new NoteDto(note.Id, note.Text, note.CreatedUtc));
|
||||
return Results.Created($"{note.Id}", new NoteDto(note.Id, note.Text, note.CreatedUtc));
|
||||
});
|
||||
|
||||
notesApi.MapDelete("/{id:int}", async (int id, AppDbContext db, CancellationToken ct) =>
|
||||
notesApi.MapDelete("{id:int}", async (int id, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var note = await db.Notes.FirstOrDefaultAsync(n => n.Id == id, ct);
|
||||
if (note is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
db.Notes.Remove(note);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
@@ -34,7 +34,12 @@ else
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
|
||||
// Only rewrite status codes for non-API routes (UI). APIs should return their real status codes.
|
||||
app.UseWhen(
|
||||
context => !context.Request.Path.StartsWithSegments("/api"),
|
||||
uiApp => uiApp.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true));
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAntiforgery();
|
||||
@@ -49,3 +54,5 @@ app.MapRazorComponents<App>()
|
||||
.AddAdditionalAssemblies(typeof(Freman.Sample.Web.Client._Imports).Assembly);
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program;
|
||||
Reference in New Issue
Block a user