using MongoDB.Entities; using PrivaPub.ClientModels.Social; using PrivaPub.Models.Federation; using PrivaPub.Models.Social; using PrivaPub.Tests.Support; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Domain { [Trait("Category", "Integration")] public sealed class FollowTests : 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(); } [Fact] public async Task A_local_follow_is_accepted_at_once_and_notified() { var token = TestContext.Current.CancellationToken; var (root, alice) = await _harness.Persona("alice"); var (_, bob) = await _harness.Persona("bob"); var result = await _harness.Follows.Follow(root, new FollowForm { AvatarId = alice.Id, Target = "@" + bob.UserName }, token); Assert.True(result.IsValid); var following = await DB.Default.Find().Match(f => f.AvatarId == alice.Id).ExecuteSingleAsync(token); Assert.Equal(FollowState.Accepted, following.State); Assert.True(following.TargetIsLocal); Assert.True((await DB.Default.Find().Match(f => f.LocalActorId == bob.Id && f.ActorURI == alice.Uri).ExecuteSingleAsync(token)).IsAccepted); Assert.Equal(NotificationType.Follow, (await DB.Default.Find().Match(n => n.AvatarId == bob.Id).ExecuteSingleAsync(token)).Type); Assert.Empty(await _harness.Outgoing(bob.Inbox)); } [Fact] public async Task Someone_elses_persona_cannot_be_made_to_follow() { var (_, alice) = await _harness.Persona("alice"); var (_, bob) = await _harness.Persona("bob"); var result = await _harness.Follows.Follow("someone-else", new FollowForm { AvatarId = alice.Id, Target = bob.UserName }, TestContext.Current.CancellationToken); Assert.False(result.IsValid); Assert.Equal(404, result.StatusCode); } [Fact] public async Task A_remote_follow_is_requested_then_accepted() { var token = TestContext.Current.CancellationToken; var (root, alice) = await _harness.Persona("alice"); var carol = new RemoteActor(_harness.Peer, "carol"); await _harness.Follows.Follow(root, new FollowForm { AvatarId = alice.Id, Target = carol.Id }, token); var following = await DB.Default.Find().Match(f => f.AvatarId == alice.Id).ExecuteSingleAsync(token); Assert.Equal(FollowState.Requested, following.State); var follow = Assert.Single(await _harness.Outgoing(carol.Id + "/inbox")); Assert.Equal("Follow", follow["type"]!.GetValue()); Assert.Equal(alice.Uri, follow["actor"]!.GetValue()); Assert.StartsWith(alice.Uri + "/grunts/follow-", follow["id"]!.GetValue()); var accept = new JsonObject { ["id"] = $"{carol.Id}#accepts/{Guid.NewGuid():N}", ["type"] = "Accept", ["actor"] = carol.Id, ["object"] = follow.DeepClone() }; Assert.Equal(202, (await _harness.Deliver(carol, $"/peasants/{alice.UserName}/mouth", accept)).StatusCode); Assert.Equal(FollowState.Accepted, (await DB.Default.Find().OneAsync(following.ID, token)).State); } [Fact] public async Task An_accept_from_someone_else_changes_nothing_and_a_reject_ends_the_request() { var token = TestContext.Current.CancellationToken; var (root, alice) = await _harness.Persona("alice"); var carol = new RemoteActor(_harness.Peer, "carol"); var mallory = new RemoteActor(_harness.Peer, "mallory"); await _harness.Follows.Follow(root, new FollowForm { AvatarId = alice.Id, Target = carol.Id }, token); var following = await DB.Default.Find().Match(f => f.AvatarId == alice.Id).ExecuteSingleAsync(token); await _harness.Deliver(mallory, "/human-centipede", new JsonObject { ["id"] = $"{mallory.Id}#accepts/{Guid.NewGuid():N}", ["type"] = "Accept", ["actor"] = mallory.Id, ["object"] = following.FollowActivityURI }); Assert.Equal(FollowState.Requested, (await DB.Default.Find().OneAsync(following.ID, token)).State); await _harness.Deliver(carol, "/human-centipede", new JsonObject { ["id"] = $"{carol.Id}#rejects/{Guid.NewGuid():N}", ["type"] = "Reject", ["actor"] = carol.Id, ["object"] = following.FollowActivityURI }); Assert.Null(await DB.Default.Find().OneAsync(following.ID, token)); } [Fact] public async Task Unfollowing_a_remote_account_sends_an_undo() { var token = TestContext.Current.CancellationToken; var (root, alice) = await _harness.Persona("alice"); var carol = new RemoteActor(_harness.Peer, "carol"); await _harness.Follows.Follow(root, new FollowForm { AvatarId = alice.Id, Target = carol.Id }, token); await _harness.Follows.Unfollow(root, new FollowForm { AvatarId = alice.Id, Target = carol.Id }, token); Assert.False(await DB.Default.Find().Match(f => f.AvatarId == alice.Id).ExecuteAnyAsync(token)); var undo = (await _harness.Outgoing(carol.Id + "/inbox")).Single(a => a["type"]!.GetValue() == "Undo"); Assert.Equal("Follow", undo["object"]!["type"]!.GetValue()); } } }