using System.Text.Json.Nodes; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Actions { // Settings as a persona: its profile saved (update_credentials) and read back from PrivaPub, a server blocked and // unblocked, the follows exported and servers imported in Mastodon's files, and a new persona made; what a test changes // is put back through the API afterwards, but for the persona (PrivaPub keeps every persona, so the test makes it under // the scenarios' own root, never a town one) public sealed class SettingsTests(Browser browser) { static async Task Open(IPage page, string section) { await page.GotoAsync($"{Pasture.Site}/settings"); var header = page.Locator(".collapse-title", new() { HasText = section }).First; await header.ClickAsync(); return header.Locator("xpath=.."); } static JsonObject Restore(JsonNode account) { var fields = new JsonObject(); var source = account["source"]!; var kept = source["fields"]!.AsArray(); for (var i = 0; i < 4; i++) fields[i.ToString()] = new JsonObject { ["name"] = i < kept.Count ? kept[i]!["name"]!.GetValue() : "", ["value"] = i < kept.Count ? kept[i]!["value"]!.GetValue() : "" }; return new JsonObject { ["display_name"] = account["display_name"]!.GetValue(), ["note"] = source["note"]!.GetValue(), ["locked"] = account["locked"]!.GetValue(), ["source"] = new JsonObject { ["privacy"] = source["privacy"]!.GetValue() }, ["fields_attributes"] = fields }; } [Fact] public async Task The_personas_profile_is_saved_from_settings() { var persona = Pasture.Personas().Last(); var original = (await PersonaApi.Call(persona, "/api/v1/accounts/verify_credentials"))!; var mark = Guid.NewGuid().ToString("N")[..6]; await using var session = await SignIn.As(browser, persona); var page = session.Page; try { await Results.Cell("client.action.profile-edit", "privapub", persona.UserName, async () => { var panel = await Open(page, "Profile"); var editor = panel.Locator("[data-profile-editor]"); await editor.WaitForAsync(new() { Timeout = 30000 }); await Assertions.Expect(editor.Locator("[data-profile-name]")).ToHaveValueAsync(original["display_name"]!.GetValue(), new() { Timeout = 10000 }); await editor.Locator("[data-profile-name]").FillAsync($"Edited {mark}"); await editor.Locator("[data-profile-note]").FillAsync($"A biography written in the e2e tests {mark}"); var row = editor.Locator("[data-profile-field='3'] input"); await row.Nth(0).FillAsync("e2e"); await row.Nth(1).FillAsync(mark); await editor.Locator("[data-profile-privacy]").SelectOptionAsync("Unlisted"); await editor.Locator("[data-profile-save]").ClickAsync(); await Assertions.Expect(page.GetByText("Profile saved.")).ToBeVisibleAsync(new() { Timeout = 15000 }); var saved = (await PersonaApi.Call(persona, "/api/v1/accounts/verify_credentials"))!; Assert.Equal($"Edited {mark}", saved["display_name"]!.GetValue()); Assert.Equal($"A biography written in the e2e tests {mark}", saved["source"]!["note"]!.GetValue()); Assert.Equal("unlisted", saved["source"]!["privacy"]!.GetValue()); Assert.Contains(saved["source"]!["fields"]!.AsArray(), f => f!["name"]!.GetValue() == "e2e" && f["value"]!.GetValue() == mark); // the persona chooser shows the new name at once await Assertions.Expect(page.GetByText($"Edited {mark}", new() { Exact = true }).First).ToBeVisibleAsync(new() { Timeout = 5000 }); await session.AssertGuards(); }, page); } finally { await PersonaApi.Call(persona, "/api/v1/accounts/update_credentials", HttpMethod.Patch, Restore(original)); } } [Fact] public async Task A_server_is_blocked_and_unblocked_from_settings() { var persona = Pasture.Personas().Last(); const string server = "blocked-by-e2e.example"; await using var session = await SignIn.As(browser, persona); var page = session.Page; try { await Results.Cell("client.action.domain-block", "privapub", persona.UserName, async () => { var panel = await Open(page, "Mutes/Blocks"); var servers = panel.Locator("[data-domain-blocks]"); await servers.WaitForAsync(new() { Timeout = 30000 }); await servers.Locator("[data-domain-new]").FillAsync($"https://{server.ToUpperInvariant()}/about"); await servers.GetByRole(AriaRole.Button, new() { Name = "Block server" }).ClickAsync(); await Assertions.Expect(servers.Locator($"[data-domain='{server}']")).ToBeVisibleAsync(new() { Timeout = 15000 }); Assert.Contains(server, (await PersonaApi.Call(persona, "/api/v1/domain_blocks"))!.AsArray().Select(d => d!.GetValue())); await servers.Locator($"[data-domain='{server}']").GetByRole(AriaRole.Button, new() { Name = "Unblock" }).ClickAsync(); await Assertions.Expect(servers.Locator($"[data-domain='{server}']")).ToHaveCountAsync(0, new() { Timeout = 15000 }); Assert.DoesNotContain(server, (await PersonaApi.Call(persona, "/api/v1/domain_blocks"))!.AsArray().Select(d => d!.GetValue())); await session.AssertGuards(); }, page); } finally { await PersonaApi.Call(persona, $"/api/v1/domain_blocks?domain={server}", HttpMethod.Delete); } } // the follows export is Mastodon's following_accounts.csv, naming every account in full [Fact] public async Task The_follows_are_exported_in_mastodons_format() { var persona = Pasture.Personas().First(); var following = (await PersonaApi.Call(persona, $"/api/v1/accounts/{persona.LocalId}/following?limit=80"))!.AsArray(); Assert.SkipWhen(following.Count == 0, $"{persona.UserName} follows nobody in this town"); await using var session = await SignIn.As(browser, persona); var page = session.Page; await Results.Cell("client.action.export", "privapub", persona.UserName, async () => { var panel = await Open(page, "Data import/export"); var button = panel.Locator("[data-export-kind='Follows']"); await button.WaitForAsync(new() { Timeout = 30000 }); var download = await page.RunAndWaitForDownloadAsync(() => button.ClickAsync(), new() { Timeout = 60000 }); Assert.Equal("following_accounts.csv", download.SuggestedFilename); var lines = (await File.ReadAllLinesAsync((await download.PathAsync())!)).ToList(); Assert.Equal("Account address,Show boosts,Notify on new posts,Languages", lines[0]); foreach (var account in following) { var acct = account!["acct"]!.GetValue(); var full = acct.Contains('@') ? acct : $"{acct}@privapub.test"; Assert.Contains(lines, l => l.StartsWith(full + ",", StringComparison.Ordinal)); } await session.AssertGuards(); }, page); } [Fact] public async Task Blocked_servers_are_imported_from_a_mastodon_file() { var persona = Pasture.Personas().Last(); string[] servers = ["imported-one.example", "imported-two.example"]; var csv = Path.Combine(Path.GetTempPath(), $"blocked_domains-{Guid.NewGuid():N}.csv"); await File.WriteAllTextAsync(csv, string.Join("\n", servers) + "\n"); await using var session = await SignIn.As(browser, persona); var page = session.Page; try { await Results.Cell("client.action.import", "privapub", persona.UserName, async () => { var panel = await Open(page, "Data import/export"); var import = panel.Locator("[data-import]"); await import.WaitForAsync(new() { Timeout = 30000 }); await import.Locator("[data-import-kind]").SelectOptionAsync("ServerBlocks"); await import.Locator("[data-import-file]").SetInputFilesAsync(csv); await import.Locator("[data-import-start]").ClickAsync(); await Assertions.Expect(import.Locator("[data-import-report]")).ToContainTextAsync("2 of 2 done, 0 could not be.", new() { Timeout = 30000 }); var blocked = (await PersonaApi.Call(persona, "/api/v1/domain_blocks"))!.AsArray().Select(d => d!.GetValue()).ToList(); Assert.All(servers, s => Assert.Contains(s, blocked)); await session.AssertGuards(); }, page); } finally { foreach (var server in servers) await PersonaApi.Call(persona, $"/api/v1/domain_blocks?domain={server}", HttpMethod.Delete); File.Delete(csv); } } [Fact] public async Task A_new_persona_is_made_and_used() { var name = $"e2e_{Guid.NewGuid():N}"[..12]; await using var session = await SignIn.As(browser, new Pasture.Persona("alice", "alice", "pastureroot", "Pasture-Pass-1!", null, null, null)); var page = session.Page; await Results.Cell("client.action.persona-create", "privapub", "pastureroot", async () => { var panel = await Open(page, "Personas"); var form = panel.Locator("[data-persona-new]"); await form.WaitForAsync(new() { Timeout = 30000 }); await form.Locator("[data-persona-username]").FillAsync(name); await form.Locator("[data-persona-name]").FillAsync($"Made by e2e {name}"); await form.Locator("[data-persona-biography]").FillAsync("A persona the end-to-end tests made."); await form.Locator("[data-persona-create]").ClickAsync(); var made = panel.Locator($"[data-persona='{name}']"); await made.WaitForAsync(new() { Timeout = 15000 }); await made.GetByRole(AriaRole.Button, new() { Name = "Use" }).ClickAsync(); await Assertions.Expect(made).ToContainTextAsync("in use", new() { Timeout = 15000 }); await Assertions.Expect(page.GetByText($"Made by e2e {name}", new() { Exact = true }).First).ToBeVisibleAsync(); using var http = Pasture.Http(); var webfinger = await http.GetAsync($"{Pasture.Api}/.well-known/webfinger?resource=acct:{name}@privapub.test"); Assert.True(webfinger.IsSuccessStatusCode, $"{name} is not found by WebFinger"); await session.AssertGuards(); }, page); } } }