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
110 lines
4.3 KiB
C#
110 lines
4.3 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
|
|
using PrivaPub.Federation.Signing;
|
|
|
|
using System.Globalization;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace PrivaPub.Tests.Support
|
|
{
|
|
public sealed class RemoteActor
|
|
{
|
|
readonly RSA _key = RSA.Create(2048);
|
|
|
|
public RemoteActor(Peer peer, string name, string origin = default, string type = "Person")
|
|
{
|
|
Name = $"{name}{Guid.NewGuid():N}"[..20];
|
|
Id = $"{origin ?? peer.A}/users/{Name}";
|
|
Type = type;
|
|
peer.Serve($"/users/{Name}", Document().ToJsonString());
|
|
}
|
|
|
|
public string Type { get; }
|
|
|
|
public string Name { get; }
|
|
public string Id { get; }
|
|
public string KeyId => Id + "#main-key";
|
|
public string SharedInbox => Id.Split("/users/")[0] + "/inbox";
|
|
|
|
public JsonObject Document() => new()
|
|
{
|
|
["id"] = Id,
|
|
["type"] = Type,
|
|
["preferredUsername"] = Name,
|
|
["inbox"] = Id + "/inbox",
|
|
["followers"] = Id + "/followers",
|
|
["publicKey"] = new JsonObject
|
|
{
|
|
["id"] = KeyId,
|
|
["owner"] = Id,
|
|
["publicKeyPem"] = _key.ExportSubjectPublicKeyInfoPem()
|
|
}
|
|
};
|
|
|
|
public HttpRequestMessage SignedGet(string path, string host = "privapub.test")
|
|
{
|
|
var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
|
var request = new HttpRequestMessage(HttpMethod.Get, path);
|
|
request.Headers.TryAddWithoutValidation("Date", date);
|
|
request.Headers.TryAddWithoutValidation("Accept", "application/activity+json");
|
|
request.Headers.TryAddWithoutValidation("Signature", Signature($"(request-target): get {path}\nhost: {host}\ndate: {date}", "(request-target) host date"));
|
|
return request;
|
|
}
|
|
|
|
public HttpRequestMessage SignedPost(string path, JsonNode activity, string host = "privapub.test", string date = default)
|
|
{
|
|
var body = Encoding.UTF8.GetBytes(activity.ToJsonString());
|
|
date ??= DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
|
var digest = HttpSignatures.Digest(body);
|
|
var request = new HttpRequestMessage(HttpMethod.Post, path) { Content = new ByteArrayContent(body) };
|
|
request.Content.Headers.TryAddWithoutValidation("Content-Type", "application/activity+json");
|
|
request.Headers.TryAddWithoutValidation("Date", date);
|
|
request.Headers.TryAddWithoutValidation("Digest", digest);
|
|
request.Headers.TryAddWithoutValidation("Signature",
|
|
Signature($"(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}", "(request-target) host date digest"));
|
|
return request;
|
|
}
|
|
|
|
string Signature(string signingString, string headers)
|
|
{
|
|
var signature = Convert.ToBase64String(_key.SignData(Encoding.UTF8.GetBytes(signingString), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
|
|
return $"keyId=\"{KeyId}\",algorithm=\"rsa-sha256\",headers=\"{headers}\",signature=\"{signature}\"";
|
|
}
|
|
|
|
public HttpRequest Get(string host, string path)
|
|
{
|
|
var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
|
var context = new DefaultHttpContext();
|
|
context.Request.Method = "GET";
|
|
context.Request.Host = new HostString(host);
|
|
context.Request.Path = path;
|
|
context.Features.Get<IHttpRequestFeature>().RawTarget = path;
|
|
context.Request.Headers["Date"] = date;
|
|
context.Request.Headers["Signature"] = Signature($"(request-target): get {path}\nhost: {host}\ndate: {date}", "(request-target) host date");
|
|
return context.Request;
|
|
}
|
|
|
|
public HttpRequest Post(string host, string path, JsonNode activity)
|
|
{
|
|
var body = Encoding.UTF8.GetBytes(activity.ToJsonString());
|
|
var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
|
var digest = HttpSignatures.Digest(body);
|
|
var context = new DefaultHttpContext();
|
|
context.Request.Method = "POST";
|
|
context.Request.Host = new HostString(host);
|
|
context.Request.Path = path;
|
|
context.Features.Get<IHttpRequestFeature>().RawTarget = path;
|
|
context.Request.Headers["Date"] = date;
|
|
context.Request.Headers["Digest"] = digest;
|
|
context.Request.Headers["Content-Type"] = "application/activity+json";
|
|
context.Request.Headers["Signature"] = Signature($"(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}", "(request-target) host date digest");
|
|
context.Request.Body = new MemoryStream(body);
|
|
context.Request.ContentLength = body.Length;
|
|
return context.Request;
|
|
}
|
|
}
|
|
}
|