using System.Text.Json.Nodes; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Actions { // Accounts in decePub: found by searching their handle (PrivaPub resolves it), opened as a profile, followed and // unfollowed there; the follow is checked on the account's own server, where the persona must appear as a follower. public sealed class ProfileTests(Browser browser) { static readonly string[] Platforms = ["gts", "mastodon", "akkoma", "misskey", "sharkey"]; public static TheoryData Origins() => new(Platforms); // an account of the platform that the persona does not follow yet, from the town's own sessions static (string Platform, string UserName, string Token) Stranger(string platform, Pasture.Persona persona) { var sessions = JsonNode.Parse(File.ReadAllText(Path.Combine(Pasture.Root, ".state", "town", "sessions.json")))!.AsObject(); var candidates = sessions.Where(kv => kv.Key.StartsWith(platform + "/") && !kv.Key.Contains("/st_")) .Select(kv => (platform, kv.Value!["account"]!["username"]!.GetValue(), kv.Value!["token"]!.GetValue())) .ToList(); return candidates.Last(); } // the newest GoToSocial account that SocialPub's moves scenario moved (scenarios/moves.sh), as PrivaPub knows it static async Task<(string Old, string New)> Moved() { var start = new System.Diagnostics.ProcessStartInfo("podman") { RedirectStandardOutput = true, RedirectStandardError = true }; foreach (var arg in new[] { "exec", "pasture-mongo", "mongosh", "--quiet", "PrivaPub", "--eval", "const a = db.ForeignAvatar.find({ActorURI: {$regex: 'gts.test.users.mvold'}, MovedToURL: {$ne: null}}).sort({_id: -1}).limit(1).toArray()[0];" + "print(a ? a.ActorURI + ' ' + a.MovedToURL : '')" }) start.ArgumentList.Add(arg); using var process = System.Diagnostics.Process.Start(start)!; var output = (await process.StandardOutput.ReadToEndAsync()).Trim(); await process.WaitForExitAsync(); var parts = output.Split(' ', StringSplitOptions.RemoveEmptyEntries); return parts.Length == 2 ? (parts[0], parts[1]) : default; } [Fact] public async Task A_moved_accounts_profile_says_where_it_went() { var (old, moved) = await Moved(); Assert.SkipWhen(old is null, "no moved account: run SocialPub's tools/pasture/interop.sh moves"); var persona = Pasture.Personas().First(); var handle = old.Split("/users/")[1] + "@gts.test"; await using var session = await SignIn.As(browser, persona); await Results.Cell("client.render.moved", "gts", handle, async () => { await session.Page.GotoAsync($"{Pasture.Site}/search?q={Uri.EscapeDataString("@" + handle)}"); var banner = session.Page.Locator("[data-moved]"); await banner.WaitForAsync(new() { Timeout = 30000 }); await Assertions.Expect(banner).ToContainTextAsync("@" + moved.Split("/users/")[1] + "@gts.test"); await session.AssertGuards(); }, session.Page); } [Theory] [MemberData(nameof(Origins))] public async Task A_remote_account_found_by_its_handle_is_followed_and_unfollowed_from_its_profile(string platform) { var persona = Pasture.Personas().First(); var (_, userName, _) = Stranger(platform, persona); var handle = $"{userName}@{platform}.test"; await using var session = await SignIn.As(browser, persona); await Results.Cell("client.action.follow", platform, handle, async () => { await session.Page.GotoAsync($"{Pasture.Site}/search?q={Uri.EscapeDataString("@" + handle)}"); var profile = session.Page.Locator("article[data-account-id]").First; await profile.WaitForAsync(new() { Timeout = 30000 }); await Assertions.Expect(profile).ToContainTextAsync("@" + handle); var follow = profile.GetByRole(AriaRole.Button).Filter(new() { HasTextRegex = new System.Text.RegularExpressions.Regex("^(Follow|Unfollow|Cancel the request)$") }).First; if ((await follow.InnerTextAsync()).Trim() != "Follow") { await follow.ClickAsync(); await Assertions.Expect(follow).ToHaveTextAsync("Follow", new() { Timeout = 15000 }); } await follow.ClickAsync(); await Assertions.Expect(follow).Not.ToHaveTextAsync("Follow", new() { Timeout = 15000 }); Assert.True(await Pasture.Eventually(async () => await FollowedOn(platform, userName, persona.UserName), 45), $"{handle} never learnt that {persona.UserName} follows it"); await follow.ClickAsync(); await Assertions.Expect(follow).ToHaveTextAsync("Follow", new() { Timeout = 15000 }); Assert.True(await Pasture.Eventually(async () => !await FollowedOn(platform, userName, persona.UserName), 45), $"{handle} still has {persona.UserName} as a follower after the unfollow"); await session.AssertGuards(); }, session.Page); } // whether the peer itself lists the persona as following (or asking to follow) its account static async Task FollowedOn(string platform, string userName, string persona) { var result = await Pasture.Town("drive", platform, userName, "relationship", $"{{\"acct\": \"{persona}@privapub.test\"}}"); return result?["followed_by"]?.GetValue() == true || (await Pasture.Town("drive", platform, userName, "pending", "{}")) .AsArray().Any(a => a?.GetValue()?.StartsWith(persona + "@") == true); } [Fact] public async Task The_personas_own_profile_lists_its_posts() { var persona = Pasture.Personas().First(); await using var session = await SignIn.As(browser, persona); await Results.Cell("client.render.profile", "privapub", persona.UserName, async () => { await session.Page.GotoAsync($"{Pasture.Site}/account/{persona.LocalId}"); var profile = session.Page.Locator("article[data-account-id]").First; await profile.WaitForAsync(new() { Timeout = 30000 }); await Assertions.Expect(profile).ToContainTextAsync("@" + persona.UserName); await session.Page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 }); await session.AssertGuards(); }, session.Page); } } }