T3: the whole server under test
PrivaPubHost is a WebApplicationFactory<Program> on the fixture's database, configured only through UseSetting (visible before Build, unlike ConfigureAppConfiguration). It drops the background workers so tests run the jobs they queue (Jobs.Run, RunInbox), gives each client its own address for the rate limiter, and has a SecureMode variant. Accounts signs up roots, adds personas and gets Mastodon tokens through the real /oauth code flow. RemoteActor signs HttpRequestMessages for the real /peasants routes; Peer records bodies and headers and serves files with ranges and text pages. Program registers the Guid serializer with TryRegisterSerializer, so a second host in one process starts; the fixture runs the migrations in production's order before any test. HostBootTests: the host shares the fixture database; the harness handles exactly the activities the server registers; every job kind has one handler; every controller and page model can be made; the service graph validates with ValidateOnBuild and ValidateScopes; Swagger is 404 outside Development; a persona's token never names its root; a signed DM through the real /mouth route is queued, processed and stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
1 parent
85fdff606d
commit
5e34517e73
11 files changed
+546
-18
No files matched your search
@@ -0,0 +1,146 @@
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Inbox;
|
||||
using PrivaPub.Infrastructure.Jobs;
|
||||
using PrivaPub.Models;
|
||||
using PrivaPub.Models.Jobs;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Tests.Support;
|
||||
using PrivaPub.Tests.Support.Host;
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Tests.Http
|
||||
{
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class HostBootTests : IAsyncLifetime
|
||||
{
|
||||
PrivaPubHost _host;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
||||
_host = await PrivaPubHost.Shared();
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task The_host_shares_the_fixture_database()
|
||||
{
|
||||
Assert.True(await DB.Default.Find<AppConfiguration>().ExecuteAnyAsync(TestContext.Current.CancellationToken));
|
||||
using var client = _host.Client();
|
||||
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/build.json", TestContext.Current.CancellationToken)).StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_harness_handles_the_same_activities_as_the_server()
|
||||
{
|
||||
await using var harness = await Harness.Start();
|
||||
var registered = _host.Services.GetServices<IActivityHandler>().Select(h => h.GetType()).OrderBy(t => t.Name);
|
||||
var harnessed = harness.Handlers.Select(h => h.GetType()).OrderBy(t => t.Name);
|
||||
|
||||
Assert.Equal(registered, harnessed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_job_kind_has_exactly_one_handler()
|
||||
{
|
||||
var handlers = _host.Services.GetServices<IJobHandler>().GroupBy(h => h.Kind).ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
foreach (var kind in Enum.GetValues<JobKind>())
|
||||
Assert.True(handlers.GetValueOrDefault(kind) == 1, $"{kind} has {handlers.GetValueOrDefault(kind)} handlers");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_controller_and_page_model_can_be_made()
|
||||
{
|
||||
using var scope = _host.Services.CreateScope();
|
||||
var controllers = _host.Get<IActionDescriptorCollectionProvider>().ActionDescriptors.Items
|
||||
.OfType<ControllerActionDescriptor>()
|
||||
.Select(d => d.ControllerTypeInfo.AsType())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var pages = typeof(Program).Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(PageModel)) && !t.IsAbstract).ToList();
|
||||
|
||||
Assert.NotEmpty(controllers);
|
||||
Assert.NotEmpty(pages);
|
||||
foreach (var type in controllers.Concat(pages))
|
||||
Assert.NotNull(ActivatorUtilities.CreateInstance(scope.ServiceProvider, type));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_service_graph_is_valid()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
foreach (var descriptor in _host.Registered)
|
||||
((ICollection<ServiceDescriptor>)services).Add(descriptor);
|
||||
|
||||
using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Swagger_is_not_served_outside_development()
|
||||
{
|
||||
using var client = _host.Client();
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync("/swagger/index.html", TestContext.Current.CancellationToken)).StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_persona_gets_a_mastodon_token_through_the_real_code_flow()
|
||||
{
|
||||
var root = await _host.SignUp();
|
||||
var persona = await _host.Persona(root, "boot");
|
||||
|
||||
var token = await _host.MastodonToken(persona);
|
||||
|
||||
using var client = _host.As(token);
|
||||
var account = await client.GetStringAsync("/api/v1/accounts/verify_credentials", TestContext.Current.CancellationToken);
|
||||
Assert.Contains($"\"id\":\"{persona.Id}\"", account);
|
||||
Assert.DoesNotContain(root.Id, account);
|
||||
Assert.DoesNotContain(root.UserName, account);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_signed_delivery_through_the_real_route_is_queued_and_stored()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
await using var peer = await Peer.Start();
|
||||
var persona = await _host.Persona(await _host.SignUp(), "mouth");
|
||||
var bob = new RemoteActor(peer, "bob");
|
||||
var noteId = $"{peer.A}/notes/{Guid.NewGuid():N}";
|
||||
var activity = new JsonObject
|
||||
{
|
||||
["id"] = noteId + "/activity",
|
||||
["type"] = "Create",
|
||||
["actor"] = bob.Id,
|
||||
["to"] = new JsonArray($"{PrivaPubHost.Base}/peasants/{persona.UserName}"),
|
||||
["object"] = new JsonObject
|
||||
{
|
||||
["id"] = noteId,
|
||||
["type"] = "Note",
|
||||
["attributedTo"] = bob.Id,
|
||||
["to"] = new JsonArray($"{PrivaPubHost.Base}/peasants/{persona.UserName}"),
|
||||
["content"] = "<p>hello through the front door</p>",
|
||||
["published"] = DateTime.UtcNow.ToString("O")
|
||||
}
|
||||
};
|
||||
using var client = _host.Client();
|
||||
|
||||
var response = await client.SendAsync(bob.SignedPost($"/peasants/{persona.UserName}/mouth", activity), token);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
Assert.Equal(1, await _host.RunInbox(noteId + "/activity", token));
|
||||
var stored = await DB.Default.Find<Post>().Match(p => p.ObjectURI == noteId).ExecuteFirstAsync(token);
|
||||
Assert.Equal(PostVisibility.Direct, stored.Visibility);
|
||||
Assert.Contains("front door", stored.ContentHtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user