2026-10-05 01:23:18 +02:00
|
|
|
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())!;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 16:47:51 +02:00
|
|
|
// 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}");
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 01:23:18 +02:00
|
|
|
// 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 }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|