using MongoDB.Entities; using PrivaPub.Api.Mastodon.Mappers; using PrivaPub.Models.Social; using PrivaPub.Models.User; using PrivaPub.Tests.Support; using System.Text.Json.Nodes; using static PrivaPub.Tests.Support.FederatedSeeds; namespace PrivaPub.Tests.Federation { // An account moving to another (Mastodon's Move), believed as Mastodon believes it: sent by the moving account about // itself, and the new account naming it among its alsoKnownAs [Trait("Category", "Integration")] public sealed class MoveTests : 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(); } // the new account, serving a document that names the old one (or not) RemoteActor Target(string name, RemoteActor aliasOf) { var target = new RemoteActor(_harness.Peer, name); var document = target.Document(); if (aliasOf != default) document["alsoKnownAs"] = new JsonArray(aliasOf.Id); _harness.Peer.Serve(new Uri(target.Id).AbsolutePath, document.ToJsonString()); return target; } Task Move(RemoteActor sender, RemoteActor moved, RemoteActor target) => _harness.Deliver(sender, "/human-centipede", new JsonObject { ["id"] = NewId(sender, "moves"), ["type"] = "Move", ["actor"] = sender.Id, ["object"] = moved.Id, ["target"] = target.Id }); static Task Stored(RemoteActor actor) => DB.Default.Find().Match(a => a.ActorURI == actor.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken); // as Mastodon does it (owner decision 2026-10-05): the follow moves to the new account, in the same lists, and a mute // or a block of the old account carries over [Fact] public async Task A_move_the_new_account_confirms_moves_the_personas_follows_lists_mutes_and_blocks() { var token = TestContext.Current.CancellationToken; var (_, alice) = await _harness.Persona("alice"); var (_, bob) = await _harness.Persona("bob"); var old = new RemoteActor(_harness.Peer, "old"); await _harness.Remote.GetActor(old.Id, refresh: true, token); await Follows(alice.Id, old); var oldAccount = await Stored(old); var list = new PersonaList { AvatarId = alice.Id, Title = "friends" }; await DB.Default.SaveAsync(list, token); await DB.Default.SaveAsync(new PersonaListMember { ListId = list.ID, AvatarId = alice.Id, AccountId = oldAccount.ID }, token); await _harness.Relationships.Mute(alice, old.Id, oldAccount.ID, hideNotifications: true, duration: default, token); await _harness.Relationships.Block(bob, old.Id, oldAccount.ID, token); var target = Target("new", aliasOf: old); await Move(old, old, target); var stored = await Stored(old); Assert.Equal(target.Id, stored.MovedToURL); var account = await new MastodonMapper(_harness.Db, _harness.Local).Account(stored.ID, token); Assert.Equal(target.Name, account.Moved?.Username); Assert.Contains(_harness.Ledger.Of("in"), e => e.Activity == "Move" && e.Reason == "moved"); var newAccount = await Stored(target); Assert.True(await DB.Default.Find().Match(f => f.AvatarId == alice.Id && f.TargetActorURI == target.Id).ExecuteAnyAsync(token)); Assert.False(await DB.Default.Find().Match(f => f.AvatarId == alice.Id && f.TargetActorURI == old.Id).ExecuteAnyAsync(token)); Assert.Contains(await _harness.Outgoing(target.Id + "/inbox"), a => a["type"]!.GetValue() == "Follow"); Assert.Contains(await _harness.Outgoing(old.Id + "/inbox"), a => a["type"]!.GetValue() == "Undo"); Assert.True(await DB.Default.Find().Match(m => m.ListId == list.ID && m.AccountId == newAccount.ID).ExecuteAnyAsync(token)); Assert.True(await DB.Default.Find().Match(m => m.AvatarId == alice.Id && m.TargetActorURI == target.Id).ExecuteAnyAsync(token)); Assert.True(await DB.Default.Find().Match(b => b.AvatarId == bob.Id && b.TargetActorURI == target.Id).ExecuteAnyAsync(token)); } [Fact] public async Task A_move_the_new_account_does_not_confirm_or_someone_else_sends_changes_nothing() { var (_, alice) = await _harness.Persona("alice"); var old = new RemoteActor(_harness.Peer, "old"); var stranger = new RemoteActor(_harness.Peer, "stranger"); await Follows(alice.Id, old); var unconfirmed = Target("new", aliasOf: default); var hijack = Target("hijack", aliasOf: stranger); await Move(old, old, unconfirmed); await Move(stranger, old, hijack); Assert.Null((await Stored(old)).MovedToURL); Assert.Equal(new[] { "not-aliased", "not-self" }, _harness.Ledger.Of("in").Where(e => e.Activity == "Move").Select(e => e.Reason)); } } }