2026-10-03 10:59:55 +02:00
|
|
|
using MongoDB.Bson;
|
|
|
|
|
using MongoDB.Driver;
|
|
|
|
|
using MongoDB.Entities;
|
|
|
|
|
|
|
|
|
|
using PrivaPub.Infrastructure.Statistics;
|
|
|
|
|
using PrivaPub.Models.Statistics;
|
|
|
|
|
using PrivaPub.Tests.Support;
|
|
|
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
|
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Text.Json.Nodes;
|
|
|
|
|
|
|
|
|
|
namespace PrivaPub.Tests.Statistics
|
|
|
|
|
{
|
|
|
|
|
[Trait("Category", "Integration")]
|
|
|
|
|
public sealed class LedgerOverHttpTests : IAsyncLifetime
|
|
|
|
|
{
|
|
|
|
|
PrivaPubHost _host;
|
|
|
|
|
Peer _peer;
|
|
|
|
|
|
|
|
|
|
public async ValueTask InitializeAsync()
|
|
|
|
|
{
|
|
|
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
|
|
|
_host = await PrivaPubHost.Shared();
|
|
|
|
|
_peer = await Peer.Start();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async ValueTask DisposeAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_peer != default)
|
|
|
|
|
await _peer.DisposeAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
JsonObject DirectNote(RemoteActor sender, Persona persona, string text)
|
|
|
|
|
{
|
|
|
|
|
var noteId = $"{_peer.A}/notes/{Guid.NewGuid():N}";
|
|
|
|
|
var to = new JsonArray($"{PrivaPubHost.Base}/peasants/{persona.UserName}");
|
|
|
|
|
return new JsonObject
|
|
|
|
|
{
|
|
|
|
|
["id"] = noteId + "/activity",
|
|
|
|
|
["type"] = "Create",
|
|
|
|
|
["actor"] = sender.Id,
|
|
|
|
|
["to"] = to.DeepClone(),
|
|
|
|
|
["object"] = new JsonObject { ["id"] = noteId, ["type"] = "Note", ["attributedTo"] = sender.Id, ["to"] = to.DeepClone(), ["content"] = text }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task<List<BsonDocument>> StoredSince(DateTime since, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
await _host.Get<InteractionLedger>().Flush(token);
|
|
|
|
|
return await DB.Default.Database().GetCollection<BsonDocument>(nameof(InteractionEvent))
|
|
|
|
|
.Find(Builders<BsonDocument>.Filter.Gte(nameof(InteractionEvent.At), since) & Builders<BsonDocument>.Filter.In(nameof(InteractionEvent.Host), new[] { "127.0.0.1", Interactions.Unknown }))
|
|
|
|
|
.ToListAsync(token);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task A_delivery_and_its_refusals_are_recorded_naming_only_the_server()
|
|
|
|
|
{
|
|
|
|
|
var token = TestContext.Current.CancellationToken;
|
|
|
|
|
var since = DateTime.UtcNow.AddSeconds(-1);
|
|
|
|
|
var persona = await _host.Persona(await _host.SignUp(), "ledger");
|
|
|
|
|
var bob = new RemoteActor(_peer, "bob");
|
|
|
|
|
using var client = _host.Client();
|
|
|
|
|
|
|
|
|
|
Assert.Equal(HttpStatusCode.Accepted, (await client.SendAsync(bob.SignedPost($"/peasants/{persona.UserName}/mouth", DirectNote(bob, persona, "psst")), token)).StatusCode);
|
|
|
|
|
var forged = bob.SignedPost("/human-centipede", DirectNote(bob, persona, "forged"));
|
|
|
|
|
forged.Headers.Remove("Signature");
|
|
|
|
|
forged.Headers.TryAddWithoutValidation("Signature", bob.SignedPost("/elsewhere", new JsonObject()).Headers.GetValues("Signature").Single());
|
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await client.SendAsync(forged, token)).StatusCode);
|
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, (await client.SendAsync(bob.SignedPost($"/peasants/nobody{Guid.NewGuid():N}"[..20] + "/mouth", DirectNote(bob, persona, "lost")), token)).StatusCode);
|
|
|
|
|
|
|
|
|
|
var stored = await StoredSince(since, token);
|
|
|
|
|
var reasons = stored.Select(e => e.GetValue(nameof(InteractionEvent.Reason), BsonNull.Value).ToString()).ToList();
|
|
|
|
|
Assert.Contains("queued", reasons);
|
|
|
|
|
Assert.Contains("signature-invalid", reasons);
|
|
|
|
|
Assert.Contains("unknown-recipient", reasons);
|
|
|
|
|
var everything = string.Join("\n", stored.Select(e => e.ToJson()));
|
|
|
|
|
Assert.DoesNotContain(persona.UserName, everything);
|
|
|
|
|
Assert.DoesNotContain(persona.Id, everything);
|
|
|
|
|
Assert.DoesNotContain(persona.Root.Id, everything);
|
|
|
|
|
Assert.DoesNotContain(bob.Name, everything);
|
|
|
|
|
Assert.DoesNotContain("/notes/", everything);
|
|
|
|
|
}
|
2026-10-03 11:10:42 +02:00
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Client_and_served_traffic_is_counted_per_day_never_per_server()
|
|
|
|
|
{
|
|
|
|
|
var token = TestContext.Current.CancellationToken;
|
|
|
|
|
var persona = await _host.Persona(await _host.SignUp(), "served");
|
|
|
|
|
var reader = new RemoteActor(_peer, "reader");
|
|
|
|
|
var day = DateTime.UtcNow.Date;
|
|
|
|
|
await _host.Get<InteractionLedger>().Flush(token);
|
|
|
|
|
var before = await DB.Default.Find<ServerDay>().Match(d => d.Day == day).ExecuteFirstAsync(token) ?? new ServerDay();
|
|
|
|
|
using var client = _host.Client();
|
|
|
|
|
|
|
|
|
|
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/v1/instance", token)).StatusCode);
|
|
|
|
|
using (var actorRequest = new HttpRequestMessage(HttpMethod.Get, $"/peasants/{persona.UserName}"))
|
|
|
|
|
{
|
|
|
|
|
actorRequest.Headers.Accept.ParseAdd("application/activity+json");
|
|
|
|
|
Assert.Equal(HttpStatusCode.OK, (await client.SendAsync(actorRequest, token)).StatusCode);
|
|
|
|
|
}
|
|
|
|
|
Assert.Equal(HttpStatusCode.OK, (await client.SendAsync(reader.SignedGet($"/peasants/{persona.UserName}/anus"), token)).StatusCode);
|
|
|
|
|
await _host.Get<InteractionLedger>().Flush(token);
|
|
|
|
|
|
|
|
|
|
var after = await DB.Default.Find<ServerDay>().Match(d => d.Day == day).ExecuteFirstAsync(token);
|
|
|
|
|
long Grew(Dictionary<string, long> now, Dictionary<string, long> then, string key) => now.GetValueOrDefault(key) - then.GetValueOrDefault(key);
|
|
|
|
|
Assert.True(Grew(after.Client, before.Client, "api/v1/instance:GET:2xx") >= 1);
|
|
|
|
|
Assert.True(Grew(after.Served, before.Served, "actor:200:unsigned") >= 1);
|
|
|
|
|
Assert.True(Grew(after.Served, before.Served, "outbox:200:signed") >= 1);
|
|
|
|
|
Assert.DoesNotContain(after.Served.Keys, k => k.Contains(persona.UserName));
|
|
|
|
|
}
|
2026-10-03 10:59:55 +02:00
|
|
|
}
|
|
|
|
|
}
|