The card marks an edited post with a button that opens its versions, newest first (/statuses/:id/history), each with its time, warning and words; PrivaPub now has a remote post's earlier versions from Pleroma's formerRepresentations too. The composer's e2e test opens the history after its edit. The wall test retries smuser's post while Smithereen has not yet had the persona's Accept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
101 lines
5.3 KiB
C#
101 lines
5.3 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];
|
|
// (Smithereen lets its followers write there once the persona's Accept has reached it, which may be after
|
|
// PrivaPub counts the follow)
|
|
var written = default(string);
|
|
Assert.True(await Pasture.Eventually(async () =>
|
|
!string.IsNullOrEmpty(written = (await Smithereen(token, "wall.post", ("owner_id", personaOnSm), ("message", text)))?["response"]?.ToString()), 30),
|
|
"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));
|
|
}
|
|
}
|
|
}
|
|
}
|