using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json.Nodes; using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Login { // The two ways in now that sign-up is closed: the server's first login, made from the client while it has no // administrator, and a login made through a group's invitation public sealed class SignUpTests(Browser browser) { const string Password = "Pasture-Pass-1!"; static async Task SetupPending() { using var http = Pasture.Http(); return JsonNode.Parse(await http.GetStringAsync(Pasture.Api + "/clientapi/user/setup"))!["isPending"]!.GetValue(); } static async Task Jwt(string root, string password) { using var http = Pasture.Http(); using var login = await http.PostAsJsonAsync(Pasture.Api + "/clientapi/user/login", new { userName = root, password }); login.EnsureSuccessStatusCode(); return (await login.Content.ReadFromJsonAsync())!["token"]!.GetValue(); } static async Task Send(HttpMethod method, string path, string jwt, object body = null) { using var http = Pasture.Http(); using var request = new HttpRequestMessage(method, Pasture.Api + path) { Content = body is null ? null : JsonContent.Create(body) }; request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt); using var response = await http.SendAsync(request); response.EnsureSuccessStatusCode(); return JsonNode.Parse(await response.Content.ReadAsStringAsync()); } [Fact] public async Task The_first_login_sets_the_server_up_and_becomes_its_administrator() { var stamp = DateTime.UtcNow.ToString("MMddHHmmss"); var root = $"e2eadmin{stamp}"; var persona = $"e2efounder{stamp}"; // the pasture's administrator closed setup: it is opened for this test, and closed again whatever happens await Pasture.Mongo("db.ServerSetup.deleteOne({_id: 'setup'})"); try { Assert.True(await SetupPending()); await using var session = await browser.Open(); var page = session.Page; await Results.Cell("client.action.setup", "privapub", root, async () => { await page.GotoAsync($"{Pasture.Site}/login"); await page.Locator("[data-setup-offer]").ClickAsync(); await page.Locator("input[data-setup-username]").FillAsync(root); await page.Locator("input[data-setup-password]").FillAsync(Password); await page.Locator("input[data-setup-repeat]").FillAsync(Password); await page.Locator("input[data-setup-persona-username]").FillAsync(persona); await page.Locator("input[data-setup-persona-name]").FillAsync("The e2e founder"); await page.Locator("[data-setup-submit]").ClickAsync(); await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/", new() { Timeout = 30000 }); await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 }); await page.Locator("nav a[href='administration']").First.WaitForAsync(new() { Timeout = 15000 }); Assert.False(await SetupPending()); var jwt = await Jwt(root, Password); var personas = (await Send(HttpMethod.Get, "/clientapi/avatar/private/list", jwt))!.AsArray(); Assert.Equal(persona, Assert.Single(personas)!["userName"]!.GetValue()); await session.AssertGuards(); // set up: a reader without a login is offered sign-in and invitations only await using var stranger = await browser.Open(); await stranger.Page.GotoAsync($"{Pasture.Site}/setup"); await stranger.Page.Locator("[data-setup-done]").WaitForAsync(new() { Timeout = 15000 }); await stranger.Page.GotoAsync($"{Pasture.Site}/login"); await stranger.Page.Locator("[data-join-offer]").WaitForAsync(new() { Timeout = 15000 }); Assert.Equal(0, await stranger.Page.Locator("[data-setup-offer]").CountAsync()); }, page); } finally { // the pasture's administrator stays pastureroot: the test's is demoted, and setup stays closed await Pasture.Mongo($"db.RootUser.updateOne({{UserName: '{root}'}}, {{$set: {{Policies: ['isUser']}}}}); " + "if (!db.ServerSetup.findOne({_id: 'setup'})) db.ServerSetup.insertOne({_id: 'setup', AdminId: 'e2e', SealedAt: new Date()})"); } } [Fact] public async Task An_invitation_link_makes_a_login_whose_persona_joins_the_group() { var stamp = DateTime.UtcNow.ToString("MMddHHmmss"); // the circle's owner is a login of the test's own (the pasture's sign-up is open), so the town's sessions don't matter var ownerJwt = default(string); using (var http = Pasture.Http()) { using var signup = await http.PostAsJsonAsync(Pasture.Api + "/clientapi/user/signup", new { userName = $"e2eowner{stamp}", password = Password }); Assert.SkipWhen(!signup.IsSuccessStatusCode, $"PrivaPub refuses sign-ups ({(int)signup.StatusCode})"); ownerJwt = (await signup.Content.ReadFromJsonAsync())!["token"]!.GetValue(); } var ownerId = (await Send(HttpMethod.Post, "/clientapi/avatar/private/insert", ownerJwt, new { userName = $"e2ehost{stamp}", name = "An e2e host", biography = "for the invitation test" }))!["id"]!.GetValue(); var group = await Send(HttpMethod.Post, "/clientapi/group/insert", ownerJwt, new { avatarId = ownerId, userName = $"e2ecircle{stamp}", name = "An e2e circle", isCommunity = false }); var code = group!["invitationCode"]!.GetValue(); var root = $"e2ejoin{stamp}"; var persona = $"e2eguest{stamp}"; await using var session = await browser.Open(); var page = session.Page; await Results.Cell("client.action.invitation", "privapub", root, async () => { await page.GotoAsync($"{Pasture.Site}/join?code={Uri.EscapeDataString(code)}"); Assert.Equal(code, await page.Locator("input[data-join-code]").InputValueAsync()); await page.Locator("input[data-join-username]").FillAsync(root); await page.Locator("input[data-join-password]").FillAsync(Password); await page.Locator("input[data-join-repeat]").FillAsync(Password); await page.Locator("input[data-join-persona-username]").FillAsync(persona); await page.Locator("[data-join-submit]").ClickAsync(); await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/", new() { Timeout = 30000 }); await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 }); var members = (await Send(HttpMethod.Get, $"/clientapi/group/members?avatarId={Uri.EscapeDataString(ownerId)}&groupId={Uri.EscapeDataString(group["id"]!.GetValue())}", ownerJwt))!.AsArray(); Assert.Contains(members, m => (m!["handle"]?.GetValue() ?? "").Contains(persona) || m["name"]?.GetValue() == persona); await session.AssertGuards(); }, page); } } }