2026-10-05 01:23:18 +02:00
using System.Text.Json.Nodes ;
using decePubClient.E2E.Support ;
using Microsoft.Playwright ;
namespace decePubClient.E2E.Actions
{
// Accounts in decePub: found by searching their handle (PrivaPub resolves it), opened as a profile, followed and
// unfollowed there; the follow is checked on the account's own server, where the persona must appear as a follower.
public sealed class ProfileTests ( Browser browser )
{
static readonly string [] Platforms = [ "gts" , "mastodon" , "akkoma" , "misskey" , "sharkey" ];
public static TheoryData < string > Origins () => new ( Platforms );
// an account of the platform that the persona does not follow yet, from the town's own sessions
static ( string Platform , string UserName , string Token ) Stranger ( string platform , Pasture . Persona persona )
{
var sessions = JsonNode . Parse ( File . ReadAllText ( Path . Combine ( Pasture . Root , ".state" , "town" , "sessions.json" )))!. AsObject ();
var candidates = sessions . Where ( kv => kv . Key . StartsWith ( platform + "/" ) && ! kv . Key . Contains ( "/st_" ))
. Select ( kv => ( platform , kv . Value ![ "account" ]![ "username" ]!. GetValue < string >(), kv . Value ![ "token" ]!. GetValue < string >()))
. ToList ();
return candidates . Last ();
}
2026-10-05 18:39:56 +02:00
// the newest GoToSocial account that SocialPub's moves scenario moved (scenarios/moves.sh), as PrivaPub knows it
static async Task <( string Old , string New )> Moved ()
{
var start = new System . Diagnostics . ProcessStartInfo ( "podman" ) { RedirectStandardOutput = true , RedirectStandardError = true };
foreach ( var arg in new [] { "exec" , "pasture-mongo" , "mongosh" , "--quiet" , "PrivaPub" , "--eval" ,
"const a = db.ForeignAvatar.find({ActorURI: {$regex: 'gts.test.users.mvold'}, MovedToURL: {$ne: null}}).sort({_id: -1}).limit(1).toArray()[0];" +
"print(a ? a.ActorURI + ' ' + a.MovedToURL : '')" })
start . ArgumentList . Add ( arg );
using var process = System . Diagnostics . Process . Start ( start )!;
var output = ( await process . StandardOutput . ReadToEndAsync ()). Trim ();
await process . WaitForExitAsync ();
var parts = output . Split ( ' ' , StringSplitOptions . RemoveEmptyEntries );
return parts . Length == 2 ? ( parts [ 0 ], parts [ 1 ]) : default ;
}
[Fact]
public async Task A_moved_accounts_profile_says_where_it_went ()
{
var ( old , moved ) = await Moved ();
Assert . SkipWhen ( old is null , "no moved account: run SocialPub's tools/pasture/interop.sh moves" );
var persona = Pasture . Personas (). First ();
var handle = old . Split ( "/users/" )[ 1 ] + "@gts.test" ;
await using var session = await SignIn . As ( browser , persona );
await Results . Cell ( "client.render.moved" , "gts" , handle , async () =>
{
await session . Page . GotoAsync ( $"{Pasture.Site}/search?q={Uri.EscapeDataString("@" + handle)}" );
var banner = session . Page . Locator ( "[data-moved]" );
await banner . WaitForAsync ( new () { Timeout = 30000 });
await Assertions . Expect ( banner ). ToContainTextAsync ( "@" + moved . Split ( "/users/" )[ 1 ] + "@gts.test" );
await session . AssertGuards ();
}, session . Page );
}
2026-10-05 01:23:18 +02:00
[Theory]
[MemberData(nameof(Origins))]
public async Task A_remote_account_found_by_its_handle_is_followed_and_unfollowed_from_its_profile ( string platform )
{
var persona = Pasture . Personas (). First ();
var ( _ , userName , _ ) = Stranger ( platform , persona );
var handle = $"{userName}@{platform}.test" ;
await using var session = await SignIn . As ( browser , persona );
await Results . Cell ( "client.action.follow" , platform , handle , async () =>
{
await session . Page . GotoAsync ( $"{Pasture.Site}/search?q={Uri.EscapeDataString("@" + handle)}" );
var profile = session . Page . Locator ( "article[data-account-id]" ). First ;
await profile . WaitForAsync ( new () { Timeout = 30000 });
await Assertions . Expect ( profile ). ToContainTextAsync ( "@" + handle );
var follow = profile . GetByRole ( AriaRole . Button ). Filter ( new () { HasTextRegex = new System . Text . RegularExpressions . Regex ( "^(Follow|Unfollow|Cancel the request)$" ) }). First ;
if (( await follow . InnerTextAsync ()). Trim () != "Follow" )
{
await follow . ClickAsync ();
await Assertions . Expect ( follow ). ToHaveTextAsync ( "Follow" , new () { Timeout = 15000 });
}
await follow . ClickAsync ();
await Assertions . Expect ( follow ). Not . ToHaveTextAsync ( "Follow" , new () { Timeout = 15000 });
Assert . True ( await Pasture . Eventually ( async () => await FollowedOn ( platform , userName , persona . UserName ), 45 ),
$"{handle} never learnt that {persona.UserName} follows it" );
await follow . ClickAsync ();
await Assertions . Expect ( follow ). ToHaveTextAsync ( "Follow" , new () { Timeout = 15000 });
Assert . True ( await Pasture . Eventually ( async () => ! await FollowedOn ( platform , userName , persona . UserName ), 45 ),
$"{handle} still has {persona.UserName} as a follower after the unfollow" );
await session . AssertGuards ();
}, session . Page );
}
// whether the peer itself lists the persona as following (or asking to follow) its account
static async Task < bool > FollowedOn ( string platform , string userName , string persona )
{
var result = await Pasture . Town ( "drive" , platform , userName , "relationship" , $"{{\" acct \ ": \"{persona}@privapub.test\"}}" );
return result ?[ "followed_by" ]?. GetValue < bool >() == true || ( await Pasture . Town ( "drive" , platform , userName , "pending" , "{}" ))
. AsArray (). Any ( a => a ?. GetValue < string >()?. StartsWith ( persona + "@" ) == true );
}
[Fact]
public async Task The_personas_own_profile_lists_its_posts ()
{
var persona = Pasture . Personas (). First ();
await using var session = await SignIn . As ( browser , persona );
await Results . Cell ( "client.render.profile" , "privapub" , persona . UserName , async () =>
{
await session . Page . GotoAsync ( $"{Pasture.Site}/account/{persona.LocalId}" );
var profile = session . Page . Locator ( "article[data-account-id]" ). First ;
await profile . WaitForAsync ( new () { Timeout = 30000 });
await Assertions . Expect ( profile ). ToContainTextAsync ( "@" + persona . UserName );
await session . Page . Locator ( "article[data-uri], [data-feed-empty]" ). First . WaitForAsync ( new () { Timeout = 30000 });
await session . AssertGuards ();
}, session . Page );
}
}
}