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
97 lines
5.1 KiB
C#
97 lines
5.1 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|
|
}
|