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 Personas() { var sessions = JsonNode.Parse(File.ReadAllText(Path.Combine(Root, ".state", "town", "sessions.json")))!.AsObject(); var personas = new List(); 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() is not { } root) continue; var account = value["account"]!; personas.Add(new Persona(account["username"]!.GetValue(), account["name"]?.GetValue(), root, account["password"]!.GetValue(), value["token"]!.GetValue(), value["local_id"]!.GetValue(), value["actor_uri"]!.GetValue())); } return personas; } // tools/pasture/town.sh , its standard output parsed as JSON public static async Task 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())!; } // what a server holds of each object, from its database (town.sh stored) public static async Task Stored(string platform, params string[] uris) => (await Town(["stored", platform, .. uris])).AsObject(); public static async Task Eventually(Func> 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://.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 } }); } }