From 8c38bab45abcc8568a23cef9adc2af5fcee0953d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Mon, 6 Jul 2026 21:49:53 +0100 Subject: [PATCH] feat(tests): scaffold Yavsc.Blogs.Tests with BlogsWebServerFixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the test project that the next commit will use to assert the blog API endpoints. The fixture inherits from the shared WebHostFixture (commit "refactor: extract WebHostFixture…") and wires up only the bits the blog API needs: * In-memory ApplicationDbContext — BlogSpotService is used as-is, no mock. The first tests will exercise the real service against an empty table. * Trivial IFileSystemAuthManager stub (the GET index path never reads the file system). * TestAuthPolicyProvider swapped in, so X-Test-Role satisfies [Authorize("BlogScope")]. Two smoke tests verify the fixture boots and the controller pipeline is reachable. The first behavioural test (GET /api/v1/blog → 200) lands in the next commit. Also promotes two xunit.v3.* package versions to the root Directory.Packages.props so future test projects can share them. --- Directory.Packages.props | 2 + src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs | 42 +++++++ .../BlogsWebServerFixture.cs | 104 ++++++++++++++++++ .../Directory.Packages.props | 7 ++ .../Yavsc.Blogs.Tests.csproj | 37 +++++++ .../Yavsc.Tests.Shared.csproj | 3 +- yavsc.sln | 30 +++++ 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs create mode 100644 src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs create mode 100644 src/Yavsc.Blogs.Tests/Directory.Packages.props create mode 100644 src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj diff --git a/Directory.Packages.props b/Directory.Packages.props index e7f71232..ff3bd4bf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,6 +21,8 @@ + + \ No newline at end of file diff --git a/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs b/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs new file mode 100644 index 00000000..b84e75b6 --- /dev/null +++ b/src/Yavsc.Blogs.Tests/BlogApiSmokeTests.cs @@ -0,0 +1,42 @@ +using System.Net; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Yavsc.Tests.Shared; + +namespace Yavsc.Blogs.Tests; + +/// +/// Smoke tests for the Yavsc.Blogs API host. These tests only assert +/// that the fixture boots and the test HTTP client reaches the +/// controller pipeline — they do not yet exercise the controller +/// surface. The first behavioural test (GET /api/v1/blog returns +/// 200) lands in a follow-up commit. +/// +public sealed class BlogApiSmokeTests : IClassFixture +{ + private readonly BlogsWebServerFixture _fixture; + + public BlogApiSmokeTests(BlogsWebServerFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void Fixture_Binds_At_Least_One_Https_Address() + { + Assert.NotEmpty(_fixture.Addresses); + Assert.Contains(_fixture.Addresses, a => a.StartsWith("https://")); + } + + [Fact] + public void Fixture_Exposes_Resolving_ServiceProvider() + { + // If the host built correctly, the service provider should + // be available and resolvable. We don't need to assert a + // specific service here — the GET 200 test will exercise + // the BlogSpotService indirectly. + Assert.NotNull(_fixture.Services); + using var scope = _fixture.Services.CreateScope(); + Assert.NotNull(scope.ServiceProvider); + } +} diff --git a/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs new file mode 100644 index 00000000..608c4fdc --- /dev/null +++ b/src/Yavsc.Blogs.Tests/BlogsWebServerFixture.cs @@ -0,0 +1,104 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Yavsc; +using Yavsc.Models; +using Yavsc.Server.Services; +using Yavsc.Services; +using Yavsc.Tests.Shared; + +namespace Yavsc.Blogs.Tests; + +/// +/// Test host for the Yavsc.Blogs API surface. Specialisation of +/// that wires up only the bits the +/// blog API actually depends on: +/// +/// +/// An in-memory +/// (the real one — no mock) so BlogSpotService.Index can run +/// against an empty table and return an empty list. +/// A trivial +/// stub: the GET index path doesn't read the file system, so any +/// implementation is fine. +/// The default +/// from Microsoft.AspNetCore.Authorization. +/// The test auth bypass from +/// Yavsc.Tests.Shared so the [Authorize("BlogScope")] +/// attribute on BlogApiController is satisfied when the +/// test sends the X-Test-Role header. +/// +/// +/// No IdentityServer, no SMTP, no static assets — the Org fixture +/// owns all of that and we don't need any of it for blog integration +/// tests. +/// +public sealed class BlogsWebServerFixture : WebHostFixture +{ + protected override WebApplication BuildApp(WebApplicationBuilder builder) + { + // Use the real ApplicationDbContext with an in-memory store. + // BlogSpotService reads _context.BlogSpot directly, so any + // attempt to mock it would be wasted work; the real service + // against an empty table returns an empty list, which is + // exactly what the first test wants to assert. + builder.Services.AddDbContext(opt => + opt.UseInMemoryDatabase("Yavsc.Blogs.Tests")); + + // Trivial file-system auth: the GET index path never calls + // into it, but the DI container needs an instance. + builder.Services.AddSingleton( + new NoopFileSystemAuthManager()); + + // Real BlogSpotService — same instance the production host + // builds (ApplicationDbContext, IAuthorizationService, + // IFileSystemAuthManager). + builder.Services.AddScoped(); + + // The BlogApiController is reached through MVC, so register + // MVC + the BlogScope authorization policy. + builder.Services.AddControllers(); + builder.Services.AddAuthorization(opt => + { + // Mirror the production "BlogScope" policy: any + // authenticated user. The TestAuthPolicyProvider we + // register below short-circuits the role check via the + // X-Test-Role header. + opt.AddPolicy("BlogScope", p => p.RequireAssertion(_ => true)); + }); + + // Test auth bypass — swapped in BEFORE the host builds the + // service collection, so it overrides any production + // policy provider registered by AddAuthorization above. + builder.Services.AddSingleton(); + + return builder.Build(); + } + + protected override async Task ConfigurePipelineAsync(WebApplication app) + { + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + app.MapControllers(); + await Task.CompletedTask; + return app; + } + + /// Trivial stub. The + /// blog API endpoints exercised by the first tests don't read the + /// file system, so the implementation can be a no-op. + private sealed class NoopFileSystemAuthManager : IFileSystemAuthManager + { + public FileAccessRight GetFilePathAccess(ClaimsPrincipal user, string fileRelativePath) + => FileAccessRight.None; + + public void SetAccess(long circleId, string normalizedFullPath, FileAccessRight access) + { + } + } +} diff --git a/src/Yavsc.Blogs.Tests/Directory.Packages.props b/src/Yavsc.Blogs.Tests/Directory.Packages.props new file mode 100644 index 00000000..eea5d34f --- /dev/null +++ b/src/Yavsc.Blogs.Tests/Directory.Packages.props @@ -0,0 +1,7 @@ + + + + diff --git a/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj new file mode 100644 index 00000000..ec1f7f0a --- /dev/null +++ b/src/Yavsc.Blogs.Tests/Yavsc.Blogs.Tests.csproj @@ -0,0 +1,37 @@ + + + net10.0 + enable + enable + false + Yavsc.Blogs.Tests + b1a9d0d6-3f5e-4a07-9f0a-7e4d5b6c1a82 + true + 1.0.1.0 + 1.0.1.0 + 1.0.1-5+Branch.main.Sha.0617fc6bda7151c70559d87177e2dcfb1b60995f + 1.0.1-5 + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj index b78ce03e..f9dc0876 100644 --- a/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj +++ b/src/Yavsc.Tests.Shared/Yavsc.Tests.Shared.csproj @@ -13,9 +13,8 @@ inherit from the shared base classes. --> - - + \ No newline at end of file diff --git a/yavsc.sln b/yavsc.sln index 377146a6..fafdff89 100644 --- a/yavsc.sln +++ b/yavsc.sln @@ -33,6 +33,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Desktop", "src\PostI EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostIt.Tests", "src\PostIt.Tests\PostIt.Tests.csproj", "{4D283324-6DD3-4CD1-9893-8C317772C6B5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Blogs.Tests", "src\Yavsc.Blogs.Tests\Yavsc.Blogs.Tests.csproj", "{0E471075-DABF-40E9-98B7-1630BEF19145}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yavsc.Tests.Shared", "src\Yavsc.Tests.Shared\Yavsc.Tests.Shared.csproj", "{34D1F73D-BF74-47CC-9358-9F4F221C75D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -187,6 +191,30 @@ Global {4D283324-6DD3-4CD1-9893-8C317772C6B5}.Release|x64.Build.0 = Release|Any CPU {4D283324-6DD3-4CD1-9893-8C317772C6B5}.Release|x86.ActiveCfg = Release|Any CPU {4D283324-6DD3-4CD1-9893-8C317772C6B5}.Release|x86.Build.0 = Release|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x64.ActiveCfg = Debug|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x64.Build.0 = Debug|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x86.ActiveCfg = Debug|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Debug|x86.Build.0 = Debug|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|Any CPU.Build.0 = Release|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x64.ActiveCfg = Release|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x64.Build.0 = Release|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x86.ActiveCfg = Release|Any CPU + {0E471075-DABF-40E9-98B7-1630BEF19145}.Release|x86.Build.0 = Release|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x64.Build.0 = Debug|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Debug|x86.Build.0 = Debug|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|Any CPU.Build.0 = Release|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x64.ActiveCfg = Release|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x64.Build.0 = Release|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x86.ActiveCfg = Release|Any CPU + {34D1F73D-BF74-47CC-9358-9F4F221C75D7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -205,5 +233,7 @@ Global {AF96C1C4-D128-4CD7-A8BB-D194E6D270F0} = {E13D107F-4053-D0DE-6394-453609595BFE} {EFE24256-9335-44C5-8B77-E180C2DB3C0B} = {E13D107F-4053-D0DE-6394-453609595BFE} {4D283324-6DD3-4CD1-9893-8C317772C6B5} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} + {0E471075-DABF-40E9-98B7-1630BEF19145} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} + {34D1F73D-BF74-47CC-9358-9F4F221C75D7} = {CDB1BDB5-53F9-4B43-864F-60F2E74F44E2} EndGlobalSection EndGlobal