using Microsoft.Extensions.Logging.Abstractions; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Entities; using PrivaPub.Infrastructure.Statistics; using PrivaPub.Models.Jobs; using PrivaPub.Models.Statistics; using PrivaPub.Tests.Support; namespace PrivaPub.Tests.Statistics { [Trait("Category", "Integration")] public sealed class LedgerStoreTests : IAsyncLifetime { readonly string _known = $"known{Guid.NewGuid():N}.example"; readonly string _stranger = $"stranger{Guid.NewGuid():N}.example"; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); await DB.Default.SaveAsync(new RemoteInstance { Host = _known }); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; static InteractionLedger Ledger() => new(new InteractionSalts(), new StaticOptions(new StatisticsOptions()), NullLogger.Instance); [Fact] public async Task Events_are_stored_with_a_hash_and_never_the_actor() { var token = TestContext.Current.CancellationToken; var ledger = Ledger(); var actor = $"https://{_known}/users/someone{Guid.NewGuid():N}"; ledger.Record(new InteractionEvent { Host = _known, Channel = Interactions.In, Activity = "Create", Object = "Note", Outcome = Interactions.Accepted }, actor); ledger.Record(new InteractionEvent { Host = _stranger, Channel = Interactions.Receive, Status = 401, Outcome = Interactions.Refused, Reason = "no-signature" }, hostClaimed: true); ledger.Record(new InteractionEvent { Host = _known, Channel = Interactions.Receive, Status = 401, Outcome = Interactions.Refused, Reason = "signature-invalid" }, hostClaimed: true); await ledger.Flush(token); var stored = await DB.Default.Find().Match(e => e.Host == _known).ExecuteAsync(token); Assert.Equal(2, stored.Count); var accepted = Assert.Single(stored, e => e.Channel == Interactions.In); Assert.NotNull(accepted.ActorHash); Assert.Equal("signature-invalid", Assert.Single(stored, e => e.Channel == Interactions.Receive).Reason); Assert.False(await DB.Default.Find().Match(e => e.Host == _stranger).ExecuteAnyAsync(token)); var raw = await DB.Default.Database().GetCollection(nameof(InteractionEvent)).Find(new BsonDocument("_id", ObjectId.Parse(accepted.ID))).FirstAsync(token); Assert.DoesNotContain("someone", raw.ToJson()); Assert.DoesNotContain("/users/", raw.ToJson()); } [Fact] public async Task Two_processes_agree_on_a_days_salt_and_no_salt_is_made_for_the_past() { var token = TestContext.Current.CancellationToken; var one = await new InteractionSalts().For(DateTime.UtcNow, token); var two = await new InteractionSalts().For(DateTime.UtcNow, token); Assert.Equal(Convert.ToBase64String(one), Convert.ToBase64String(two)); Assert.Null(await new InteractionSalts().For(new DateTime(2001, 1, 1, 0, 0, 0, DateTimeKind.Utc), token)); } [Fact] public async Task Counters_add_up_per_day() { var token = TestContext.Current.CancellationToken; var ledger = Ledger(); ledger.Count(_known, "media:hit", 1000); ledger.Count(_known, "media:hit", 500); ledger.Count(_known, "media.miss"); ledger.CountServer(ServerSections.Client, $"test{_known}:GET:2xx", 120); ledger.CountServer(ServerSections.Served, $"test{_known}:200:signed"); await ledger.Flush(token); var day = await DB.Default.Find().Match(d => d.Host == _known && d.Day == DateTime.UtcNow.Date).ExecuteFirstAsync(token); Assert.Equal(2, day.Reads["media:hit"]); Assert.Equal(1500, day.Reads["media:hit:bytes"]); Assert.Equal(1, day.Reads["media_miss"]); var server = await DB.Default.Find().Match(d => d.Day == DateTime.UtcNow.Date).ExecuteFirstAsync(token); Assert.Equal(1, server.Client[$"test{_known.Replace('.', '_')}:GET:2xx"]); Assert.Equal(1, server.Served[$"test{_known.Replace('.', '_')}:200:signed"]); Assert.True(server.ClientLatency["le250ms"] >= 1); } } }