A post on a wall says whose, and the persona takes posts off its own

PrivaPub hosts what a persona's followers write on its wall (Smithereen's, FEP-400e) and shows posts written on walls
elsewhere to their owners' followers. A card names the wall (privapub.wall, data-wall, linking the profile), and one
written on the persona's own wall is taken off from the card's menu, which PrivaPub tells as a Remove and Smithereen
applies as a deletion. WallTests checks it against Smithereen.

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 08:38:49 +02:00
1 parent 23dbc68fb4
commit 03824babbc
6 files changed
+155

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
using System.Net.Http.Headers;
using System.Text.Json.Nodes;
using decePubClient.E2E.Support;
using Microsoft.Playwright;
namespace decePubClient.E2E.Actions
{
// A Smithereen account that follows the persona writes on its wall (FEP-400e): the card says so, and the persona takes
// it off from the card's menu, which deletes it on Smithereen too. SocialPub's tools/pasture/scenarios/smithereen.sh
// checks the rest of the wire.
public sealed class WallTests(Browser browser)
{
// Smithereen's VKontakte-like API as smuser (the token SocialPub's peers/smithereen.sh keeps)
static async Task<JsonNode> Smithereen(string token, string method, params (string Name, string Value)[] args)
{
using var http = Pasture.Http();
using var request = new HttpRequestMessage(HttpMethod.Post, $"https://smithereen.test/api/method/{method}")
{
Content = new FormUrlEncodedContent(args.Select(a => KeyValuePair.Create(a.Name, a.Value)).Append(KeyValuePair.Create("v", "1.0")))
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var response = await http.SendAsync(request);
return JsonNode.Parse(await response.Content.ReadAsStringAsync());
}
[Fact]
public async Task A_post_on_the_personas_wall_says_so_and_the_persona_takes_it_off()
{
var tokenFile = Path.Combine(Pasture.Root, ".state", "smithereen", "smuser.token");
Assert.SkipUnless(File.Exists(tokenFile), "no Smithereen: run tools/pasture/run.sh add smithereen");
var token = File.ReadAllText(tokenFile).Trim();
// (one whose follows need no approval)
var persona = default(Pasture.Persona);
var me = default(JsonNode);
foreach (var candidate in Pasture.Personas())
{
me = await PersonaApi.Call(candidate, "/api/v1/accounts/verify_credentials");
if (me?["locked"]?.GetValue<bool>() == false)
{
persona = candidate;
break;
}
}
Assert.SkipWhen(persona is null, "every persona of the town is locked");
var actorUri = me["url"]!.GetValue<string>().Replace("/@", "/peasants/");
// (its profile told again, so Smithereen reads its wall; smuser follows it, as its followers may write there)
await PersonaApi.Call(persona, "/api/v1/accounts/update_credentials", HttpMethod.Patch, new JsonObject());
var personaOnSm = (await Smithereen(token, "utils.loadRemoteObject", ("q", actorUri)))?["response"]?["id"]?.ToString();
Assert.SkipWhen(string.IsNullOrEmpty(personaOnSm), "Smithereen cannot resolve the persona");
await Smithereen(token, "friends.add", ("user_id", personaOnSm));
try
{
var smuser = (await PersonaApi.Call(persona, "/api/v2/search?q=smuser%40smithereen.test&resolve=true&type=accounts"))!["accounts"]![0]!["id"]!.GetValue<string>();
Assert.True(await Pasture.Eventually(async () =>
(await PersonaApi.Call(persona, $"/api/v1/accounts/relationships?id[]={smuser}"))![0]!["followed_by"]!.GetValue<bool>(), 45), "smuser never followed the persona");
var text = $"a word on the wall {Guid.NewGuid():N}"[..32];
var written = (await Smithereen(token, "wall.post", ("owner_id", personaOnSm), ("message", text)))?["response"]?.ToString();
Assert.False(string.IsNullOrEmpty(written), "Smithereen refused smuser's post on the persona's wall");
JsonNode status = null;
Assert.True(await Pasture.Eventually(async () =>
(status = (await PersonaApi.Call(persona, "/api/v1/timelines/home?limit=40"))!.AsArray()
.FirstOrDefault(s => s!["content"]!.GetValue<string>().Contains(text))) is not null, 60), "the post on the wall never reached the persona");
Assert.Equal(me["id"]!.GetValue<string>(), status!["privapub"]?["wall"]?["id"]?.GetValue<string>());
await using var session = await SignIn.As(browser, persona);
var page = session.Page;
await Results.Cell("client.action.wall", "smithereen", persona.UserName, async () =>
{
await page.GotoAsync($"{Pasture.Site}/");
await page.EvaluateAsync("() => localStorage.removeItem('FeedFilter')");
await page.GotoAsync($"{Pasture.Site}/");
var card = page.Locator($"article[data-uri='{status["uri"]!.GetValue<string>()}']").First;
await card.WaitForAsync(new() { Timeout = 30000 });
await Assertions.Expect(card.Locator("[data-wall]")).ToBeVisibleAsync();
await Assertions.Expect(card.Locator("[data-wall]")).ToContainTextAsync(me["display_name"]!.GetValue<string>() is { Length: > 0 } name ? name : me["username"]!.GetValue<string>());
await card.Locator("footer [title='Other'] > [role=button]").ClickAsync();
await card.Locator("button[data-take-off-wall]").ClickAsync();
Assert.True(await Pasture.Eventually(async () => await PersonaApi.Call(persona, $"/api/v1/statuses/{status["id"]!.GetValue<string>()}") is null, 15),
"the post is still on PrivaPub");
Assert.True(await Pasture.Eventually(async () =>
(await Smithereen(token, "wall.getById", ("posts", written)))?["response"] is not JsonArray { Count: > 0 }, 30), "the post is still on Smithereen");
await session.AssertGuards();
}, page);
}
finally
{
// (the town as it was: smuser no longer follows the persona)
await Smithereen(token, "friends.delete", ("user_id", personaOnSm));
}
}
}
}