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(); } [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); } } }