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> StoredSince(DateTime since, CancellationToken token) { await _host.Get().Flush(token); return await DB.Default.Database().GetCollection(nameof(InteractionEvent)) .Find(Builders.Filter.Gte(nameof(InteractionEvent.At), since) & Builders.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); } [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().Flush(token); var before = await DB.Default.Find().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().Flush(token); var after = await DB.Default.Find().Match(d => d.Day == day).ExecuteFirstAsync(token); long Grew(Dictionary now, Dictionary 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)); } } }