Reports, and who liked or boosted a post

Another's profile and posts can be reported from the client (/api/v1/reports): a category, words, and for a remote
account a copy to its server, which PrivaPub sends from its own actor, never naming the persona. Who liked or boosted
a post opens from its card's menu (favourited_by, reblogged_by). e2e: a report from a Mastodon account's profile
reaches PrivaPub's moderators forwarded, and a post's likes and boosts list the persona who gave them; the admin
helper the tests share moves to Support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 16:47:19 +02:00
1 parent bb68ff8d5e
commit a0f07abf8c
17 files changed
+410 -16

No files matched your search

+3 -16
View File
@@ -1,4 +1,3 @@
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using decePubClient.E2E.Support;
@@ -11,22 +10,10 @@ namespace decePubClient.E2E.Actions
// and unblocked, the federation statistics, and the reports moderators handle; each change checked on /clientapi
public sealed class AdministrationTests(Browser browser)
{
const string Root = "pastureroot";
const string Password = "Pasture-Pass-1!";
static readonly Pasture.Persona Admin = new("alice", "alice", Root, Password, null, null, null);
const string Root = PastureAdmin.Root;
static readonly Pasture.Persona Admin = new("alice", "alice", Root, PastureAdmin.Password, null, null, null);
static async Task<JsonNode> AsAdmin(string path)
{
using var http = Pasture.Http();
var login = await http.PostAsJsonAsync($"{Pasture.Api}/clientapi/user/login", new { userName = Root, password = Password });
login.EnsureSuccessStatusCode();
var jwt = (await login.Content.ReadFromJsonAsync<JsonObject>())!["token"]!.GetValue<string>();
using var request = new HttpRequestMessage(HttpMethod.Get, Pasture.Api + path);
request.Headers.Authorization = new("Bearer", jwt);
using var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
return JsonNode.Parse(await response.Content.ReadAsStringAsync());
}
static Task<JsonNode> AsAdmin(string path) => PastureAdmin.Get(path);
static async Task<ILocator> Open(IPage page, string section)
{
+84
View File
@@ -0,0 +1,84 @@
using System.Text.Json.Nodes;
using decePubClient.E2E.Support;
using Microsoft.Playwright;
namespace decePubClient.E2E.Actions
{
// A persona reports a remote account from its profile, with a category, its words and a copy to the account's server:
// PrivaPub's moderators see it, forwarded (/clientapi/moderator/reports). And who liked or boosted a post is listed
// from its card
public sealed class ReportTests(Browser browser)
{
[Fact]
public async Task A_remote_account_is_reported_from_its_profile()
{
var persona = Pasture.Personas().First();
var target = (await PersonaApi.Call(persona, "/api/v2/search?q=mastouser%40mastodon.test&resolve=true&type=accounts"))?["accounts"]?.AsArray().FirstOrDefault();
Assert.SkipWhen(target is null, "no Mastodon: run tools/pasture/run.sh add mastodon");
var targetId = target!["id"]!.GetValue<string>();
var words = $"reported by the e2e tests {Guid.NewGuid():N}"[..40];
await using var session = await SignIn.As(browser, persona);
var page = session.Page;
await Results.Cell("client.action.report", "mastodon", persona.UserName, async () =>
{
await page.GotoAsync($"{Pasture.Site}/account/{Uri.EscapeDataString(targetId)}");
// (the profile's own button: each of its posts' cards has one too)
await page.Locator($"article[data-account-id='{targetId}'] button[data-report]").ClickAsync(new() { Timeout = 30000 });
var dialog = page.Locator("[data-report-dialog]");
await dialog.Locator("[data-report-category]").SelectOptionAsync("Spam");
await dialog.Locator("[data-report-comment]").FillAsync(words);
await Assertions.Expect(dialog.Locator("[data-report-forward]")).ToBeCheckedAsync();
await dialog.Locator("[data-report-send]").ClickAsync();
await Assertions.Expect(dialog).ToHaveCountAsync(0, new() { Timeout = 15000 });
var report = (await PastureAdmin.Get("/clientapi/moderator/reports?resolved=false"))!.AsArray()
.FirstOrDefault(r => r!["comment"]?.GetValue<string>() == words);
Assert.NotNull(report);
Assert.Equal((targetId, "spam", true), (report!["targetAccountId"]!.GetValue<string>(), report["category"]!.GetValue<string>(),
report["forwarded"]!.GetValue<bool>()));
await session.AssertGuards();
}, page);
}
[Fact]
public async Task Who_liked_and_boosted_a_post_is_listed_from_its_card()
{
var personas = Pasture.Personas();
Assert.SkipWhen(personas.Count < 2, "the town has one persona");
var (author, fan) = (personas[0], personas[1]);
var post = (await PersonaApi.Call(author, "/api/v1/statuses", HttpMethod.Post,
new JsonObject { ["status"] = $"like and boost me {Guid.NewGuid():N}"[..28], ["visibility"] = "public" }))!;
var id = post["id"]!.GetValue<string>();
var fanAcct = (await PersonaApi.Call(fan, "/api/v1/accounts/verify_credentials"))!["acct"]!.GetValue<string>();
await PersonaApi.Call(fan, $"/api/v1/statuses/{id}/favourite", HttpMethod.Post);
await PersonaApi.Call(fan, $"/api/v1/statuses/{id}/reblog", HttpMethod.Post);
await using var session = await SignIn.As(browser, author);
var page = session.Page;
try
{
await Results.Cell("client.render.interacted-by", "privapub", author.UserName, async () =>
{
await page.GotoAsync($"{Pasture.Site}/expand/{Uri.EscapeDataString(id)}");
var card = page.Locator($"article[data-uri='{post["uri"]!.GetValue<string>()}']").First;
await card.WaitForAsync(new() { Timeout = 30000 });
foreach (var (button, list) in new[] { ("data-liked-by", "likes"), ("data-boosted-by", "boosts") })
{
await card.Locator("footer [title='Other'] > [role=button]").ClickAsync();
await card.Locator($"button[{button}]").ClickAsync();
var dialog = card.Locator($"[data-interacted-by='{list}']");
await Assertions.Expect(dialog.Locator($"[data-account='{fanAcct}']")).ToHaveCountAsync(1, new() { Timeout = 15000 });
await page.Keyboard.PressAsync("Escape");
await Assertions.Expect(dialog).ToHaveCountAsync(0);
}
await session.AssertGuards();
}, page);
}
finally
{
await PersonaApi.Call(author, $"/api/v1/statuses/{id}", HttpMethod.Delete);
}
}
}
}