208 lines
9.8 KiB
C#
208 lines
9.8 KiB
C#
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<ILocator> 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<string>() : "",
|
||
|
|
["value"] = i < kept.Count ? kept[i]!["value"]!.GetValue<string>() : ""
|
||
|
|
};
|
||
|
|
return new JsonObject
|
||
|
|
{
|
||
|
|
["display_name"] = account["display_name"]!.GetValue<string>(),
|
||
|
|
["note"] = source["note"]!.GetValue<string>(),
|
||
|
|
["locked"] = account["locked"]!.GetValue<bool>(),
|
||
|
|
["source"] = new JsonObject { ["privacy"] = source["privacy"]!.GetValue<string>() },
|
||
|
|
["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<string>(),
|
||
|
|
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<string>());
|
||
|
|
Assert.Equal($"A biography written in the e2e tests {mark}", saved["source"]!["note"]!.GetValue<string>());
|
||
|
|
Assert.Equal("unlisted", saved["source"]!["privacy"]!.GetValue<string>());
|
||
|
|
Assert.Contains(saved["source"]!["fields"]!.AsArray(), f => f!["name"]!.GetValue<string>() == "e2e" && f["value"]!.GetValue<string>() == 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<string>()));
|
||
|
|
|
||
|
|
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<string>()));
|
||
|
|
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<string>();
|
||
|
|
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<string>()).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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|