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