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 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() == false) { persona = candidate; break; } } Assert.SkipWhen(persona is null, "every persona of the town is locked"); var actorUri = me["url"]!.GetValue().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(); Assert.True(await Pasture.Eventually(async () => (await PersonaApi.Call(persona, $"/api/v1/accounts/relationships?id[]={smuser}"))![0]!["followed_by"]!.GetValue(), 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().Contains(text))) is not null, 60), "the post on the wall never reached the persona"); Assert.Equal(me["id"]!.GetValue(), status!["privapub"]?["wall"]?["id"]?.GetValue()); 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()}']").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() is { Length: > 0 } name ? name : me["username"]!.GetValue()); 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()}") 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)); } } } }