2026-10-06 08:38:49 +02:00
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 ];
2026-10-06 13:03:19 +02:00
// (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" );
2026-10-06 08:38:49 +02:00
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 ();
2026-10-06 23:28:22 +02:00
await session . Page . Locator ( "[data-confirm-yes]" ). ClickAsync ();
2026-10-06 08:38:49 +02:00
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 ));
}
}
}
}