using MongoDB.Entities; using PrivaPub.Domain.Statuses; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Inbox; using PrivaPub.Federation.Objects; using PrivaPub.Models.Federation; using PrivaPub.Models.Group; using PrivaPub.Models.Post; using PrivaPub.Models.Statistics; using PrivaPub.Tests.Support; using System.Text.Json.Nodes; using GroupEntity = PrivaPub.Models.Group.Group; namespace PrivaPub.Tests.Statistics { public class ArrivalVerdictTests { [Fact] public void The_first_decision_wins_and_a_circle_has_no_kind() { var verdict = new ArrivalVerdict(); verdict.Drop("not-addressed"); verdict.Accept("stored"); verdict.About("Note", PostVisibility.Circle, DateTime.UtcNow.AddMinutes(-2), new LocalActor { Kind = LocalActorKind.Group, IsCircle = true }); Assert.Equal(("dropped", "not-addressed"), (verdict.Outcome, verdict.Reason)); Assert.Equal("private", verdict.Audience); Assert.Null(verdict.LocalKind); Assert.InRange(verdict.AgeSeconds!.Value, 110, 130); } [Theory] [InlineData(PostVisibility.Public, "public")] [InlineData(PostVisibility.Unlisted, "unlisted")] [InlineData(PostVisibility.FollowersOnly, "private")] [InlineData(PostVisibility.Direct, "private")] [InlineData(PostVisibility.Circle, "private")] [InlineData(PostVisibility.LocalGeo, "private")] public void Every_visibility_but_public_and_unlisted_is_private(PostVisibility visibility, string audience) => Assert.Equal(audience, ArrivalVerdict.AudienceOf(visibility)); [Fact] public void Outside_an_arrival_nothing_happens() { Arrival.Current = default; Arrival.Drop("x"); Arrival.About("Note"); } } [Trait("Category", "Integration")] public sealed class HandlerVerdictTests : IAsyncLifetime { Harness _harness; public async ValueTask InitializeAsync() { Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip); _harness = await Harness.Start(); } public async ValueTask DisposeAsync() { if (_harness != default) await _harness.DisposeAsync(); } InteractionEvent Processed(string activityType) => _harness.Ledger.Of("in").Last(e => e.Activity == activityType); JsonObject Create(RemoteActor author, IEnumerable to, JsonObject extra = default) { var origin = new Uri(author.Id).GetLeftPart(UriPartial.Authority); var note = new JsonObject { ["id"] = $"{origin}/notes/{Guid.NewGuid():N}", ["type"] = "Note", ["attributedTo"] = author.Id, ["content"] = "

hello

", ["to"] = new JsonArray(to.Select(t => (JsonNode)t).ToArray()) }; foreach (var (key, value) in extra ?? new JsonObject()) note[key] = value?.DeepClone(); return new JsonObject { ["id"] = $"{origin}/activities/{Guid.NewGuid():N}", ["type"] = "Create", ["actor"] = author.Id, ["object"] = note }; } [Fact] public async Task A_direct_message_is_stored_private_and_without_a_local_kind() { var (_, alice) = await _harness.Persona("alice"); var bob = new RemoteActor(_harness.Peer, "bob"); await _harness.Deliver(bob, "/human-centipede", Create(bob, new[] { alice.Uri })); var e = Processed("Create"); Assert.Equal(("accepted", "stored", "private"), (e.Outcome, e.Reason, e.Audience)); Assert.Equal("Note", e.Object); Assert.Null(e.LocalKind); Assert.Equal("127.0.0.1", e.Host); Assert.NotNull(e.Attempt); Assert.NotNull(e.WaitMs); Assert.Equal(bob.Id, _harness.Ledger.Events.Last(x => x.Event == e).ActorUri); } [Fact] public async Task A_public_reply_to_a_persona_names_its_kind_and_its_features() { var (_, alice) = await _harness.Persona("alice"); var bob = new RemoteActor(_harness.Peer, "bob"); var parent = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "first" }, TestContext.Current.CancellationToken); await _harness.Deliver(bob, "/human-centipede", Create(bob, new[] { Addressing.Public, alice.Uri }, new JsonObject { ["inReplyTo"] = parent.Post.ObjectURI, ["interactionPolicy"] = new JsonObject(), ["quoteUrl"] = "https://elsewhere.example/1" })); var e = Processed("Create"); Assert.Equal(("accepted", "public", "person"), (e.Outcome, e.Audience, e.LocalKind)); Assert.Contains("interaction-policy", e.Features); Assert.Contains("legacy-quote", e.Features); } [Fact] public async Task Unwanted_things_are_dropped_with_a_reason() { var stranger = new RemoteActor(_harness.Peer, "stranger"); await _harness.Deliver(stranger, "/human-centipede", Create(stranger, new[] { Addressing.Public })); var unaddressed = Processed("Create"); await _harness.Deliver(stranger, "/human-centipede", new JsonObject { ["id"] = new Uri(stranger.Id).GetLeftPart(UriPartial.Authority) + "/likes/" + Guid.NewGuid().ToString("N"), ["type"] = "Like", ["actor"] = stranger.Id, ["object"] = $"{Harness.Base}/peasants/x/scribbles/{Guid.NewGuid():N}" }); var like = Processed("Like"); await _harness.Deliver(stranger, "/human-centipede", new JsonObject { ["id"] = new Uri(stranger.Id).GetLeftPart(UriPartial.Authority) + "/x/" + Guid.NewGuid().ToString("N"), ["type"] = "Bite", ["actor"] = stranger.Id, ["object"] = stranger.Id }); var bite = Processed("Bite"); await _harness.Deliver(stranger, "/human-centipede", new JsonObject { ["id"] = new Uri(stranger.Id).GetLeftPart(UriPartial.Authority) + "/undo/" + Guid.NewGuid().ToString("N"), ["type"] = "Undo", ["actor"] = stranger.Id, ["object"] = new JsonObject { ["id"] = stranger.Id + "/likes/never", ["type"] = "Like", ["actor"] = stranger.Id, ["object"] = "https://nowhere.example/1" } }); var undo = Processed("Undo"); Assert.Equal(("dropped", "not-addressed", "public"), (unaddressed.Outcome, unaddressed.Reason, unaddressed.Audience)); Assert.Equal(("dropped", "unknown-object"), (like.Outcome, like.Reason)); Assert.Equal(("dropped", "unknown-type"), (bite.Outcome, bite.Reason)); Assert.Equal(("dropped", "unknown-object", "Like"), (undo.Outcome, undo.Reason, undo.Object)); } [Fact] public async Task A_stranger_posting_into_a_circle_never_reveals_the_circle() { var (_, owner) = await _harness.Persona("owner"); var (privateKey, publicKey) = Keys.NewKeyPair(); var entity = new GroupEntity { UserName = $"circle{Guid.NewGuid():N}"[..20], Kind = GroupKind.Circle, PostingPolicy = PostingPolicy.Followers, PrivateKey = privateKey, PublicKey = publicKey, Members = new() { new GroupMember { AvatarId = owner.Id, Role = GroupRole.Owner } } }; await DB.Default.SaveAsync(entity); var circle = _harness.Local.FromGroup(entity); var outsider = new RemoteActor(_harness.Peer, "outsider"); await _harness.Deliver(outsider, "/human-centipede", Create(outsider, new[] { circle.Uri, circle.Flock })); var e = Processed("Create"); Assert.Equal(("dropped", "not-addressed", "private"), (e.Outcome, e.Reason, e.Audience)); Assert.Null(e.LocalKind); Assert.DoesNotContain(_harness.Ledger.Events, x => System.Text.Json.JsonSerializer.Serialize(x.Event).Contains("circle", StringComparison.OrdinalIgnoreCase)); } [Fact] public async Task A_follow_is_accepted_and_its_undo_is_recorded() { var (_, alice) = await _harness.Persona("alice"); var bob = new RemoteActor(_harness.Peer, "bob"); var follow = new JsonObject { ["id"] = bob.Id + "/follows/" + Guid.NewGuid().ToString("N"), ["type"] = "Follow", ["actor"] = bob.Id, ["object"] = alice.Uri }; await _harness.Deliver(bob, "/human-centipede", follow); var followed = Processed("Follow"); await _harness.Deliver(bob, "/human-centipede", new JsonObject { ["id"] = bob.Id + "/undo/" + Guid.NewGuid().ToString("N"), ["type"] = "Undo", ["actor"] = bob.Id, ["object"] = follow.DeepClone() }); var undone = Processed("Undo"); Assert.Equal(("accepted", "auto-accepted", "none"), (followed.Outcome, followed.Reason, followed.Audience)); Assert.Equal(("accepted", "undone", "Follow"), (undone.Outcome, undone.Reason, undone.Object)); } } }