Settings has a Notifications section, also at /notifications/filtered: what notifications from people the persona does not follow, people who do not follow it, new accounts, private mentions it did not ask for and limited accounts become (accept, filter, drop), and the filtered ones, one request per account, let in (with that account from then on) or dismissed. The Notifications tab says when requests wait. e2e: a stranger's like waits in a request reached from the tab and comes back once let in; the policy and the stranger's permission are put back afterwards (Pasture.Mongo). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
106 lines
4.5 KiB
C#
106 lines
4.5 KiB
C#
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
|
|
namespace decePubClient.E2E.Support
|
|
{
|
|
// The pasture and its town as the tests see them: the sites, the accounts the town made (read from the town's own
|
|
// state), and the town's command line for what each server holds, so a test checks the other side of the wire.
|
|
public static class Pasture
|
|
{
|
|
public const string Site = "https://decepub.test";
|
|
public const string Api = "https://privapub.test";
|
|
|
|
public static readonly string Root = Path.GetFullPath(Path.Combine(RepoRoot(), "..", "SocialPub", "tools", "pasture"));
|
|
|
|
static string RepoRoot()
|
|
{
|
|
var dir = AppContext.BaseDirectory;
|
|
while (dir is not null && !File.Exists(Path.Combine(dir, "decePubClient.csproj")))
|
|
dir = Path.GetDirectoryName(dir);
|
|
return dir ?? throw new InvalidOperationException("decePubClient.csproj not found above the test output");
|
|
}
|
|
|
|
public sealed record Persona(string UserName, string Name, string Root, string Password, string Token, string LocalId, string ActorUri);
|
|
|
|
// every PrivaPub persona the town made, with its root's login
|
|
public static IReadOnlyList<Persona> Personas()
|
|
{
|
|
var sessions = JsonNode.Parse(File.ReadAllText(Path.Combine(Root, ".state", "town", "sessions.json")))!.AsObject();
|
|
var personas = new List<Persona>();
|
|
foreach (var (key, value) in sessions)
|
|
{
|
|
// st_* are the drivers' selftest accounts, not the town's
|
|
if (!key.StartsWith("privapub/") || key.StartsWith("privapub/st_") || value?["account"]?["root"]?.GetValue<string>() is not { } root)
|
|
continue;
|
|
var account = value["account"]!;
|
|
personas.Add(new Persona(account["username"]!.GetValue<string>(), account["name"]?.GetValue<string>(), root,
|
|
account["password"]!.GetValue<string>(),
|
|
value["token"]!.GetValue<string>(), value["local_id"]!.GetValue<string>(), value["actor_uri"]!.GetValue<string>()));
|
|
}
|
|
return personas;
|
|
}
|
|
|
|
// tools/pasture/town.sh <args>, its standard output parsed as JSON
|
|
public static async Task<JsonNode> Town(params string[] args)
|
|
{
|
|
var start = new ProcessStartInfo("python3") { RedirectStandardOutput = true, RedirectStandardError = true };
|
|
start.ArgumentList.Add(Path.Combine(Root, "town", "town.py"));
|
|
foreach (var arg in args)
|
|
start.ArgumentList.Add(arg);
|
|
using var process = Process.Start(start)!;
|
|
var output = await process.StandardOutput.ReadToEndAsync();
|
|
var error = await process.StandardError.ReadToEndAsync();
|
|
await process.WaitForExitAsync();
|
|
if (process.ExitCode != 0)
|
|
throw new InvalidOperationException($"town.sh {string.Join(' ', args)} failed: {error}");
|
|
return JsonNode.Parse(output.Trim().Split('\n').Last())!;
|
|
}
|
|
|
|
// a script on PrivaPub's own database in the pasture, to undo what no API undoes
|
|
public static async Task Mongo(string script)
|
|
{
|
|
var start = new ProcessStartInfo("podman") { RedirectStandardOutput = true, RedirectStandardError = true };
|
|
foreach (var arg in new[] { "exec", "pasture-mongo", "mongosh", "--quiet", "PrivaPub", "--eval", script })
|
|
start.ArgumentList.Add(arg);
|
|
using var process = Process.Start(start)!;
|
|
var error = await process.StandardError.ReadToEndAsync();
|
|
await process.StandardOutput.ReadToEndAsync();
|
|
await process.WaitForExitAsync();
|
|
if (process.ExitCode != 0)
|
|
throw new InvalidOperationException($"mongosh failed: {error}");
|
|
}
|
|
|
|
// what a server holds of each object, from its database (town.sh stored)
|
|
public static async Task<JsonObject> Stored(string platform, params string[] uris) =>
|
|
(await Town(["stored", platform, .. uris])).AsObject();
|
|
|
|
public static async Task<bool> Eventually(Func<Task<bool>> condition, int seconds = 30)
|
|
{
|
|
var until = DateTime.UtcNow.AddSeconds(seconds);
|
|
while (true)
|
|
{
|
|
if (await condition())
|
|
return true;
|
|
if (DateTime.UtcNow > until)
|
|
return false;
|
|
await Task.Delay(1000);
|
|
}
|
|
}
|
|
|
|
public static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
|
|
|
|
// an HttpClient that reaches https://<name>.test through Caddy on 127.0.0.1:6443, as the browser does
|
|
public static HttpClient Http() => new(new SocketsHttpHandler
|
|
{
|
|
ConnectCallback = async (context, token) =>
|
|
{
|
|
var socket = new System.Net.Sockets.Socket(System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp);
|
|
await socket.ConnectAsync("127.0.0.1", 6443, token);
|
|
return new System.Net.Sockets.NetworkStream(socket, ownsSocket: true);
|
|
},
|
|
SslOptions = { RemoteCertificateValidationCallback = (_, _, _, _) => true }
|
|
});
|
|
}
|
|
}
|