using decePubClient.E2E.Support; using Microsoft.Playwright; namespace decePubClient.E2E.Login { public sealed class LoginTests(Browser browser) { static Pasture.Persona WithSiblings() => Pasture.Personas().GroupBy(p => p.Root).OrderByDescending(g => g.Count()).First().First(); [Fact] public async Task A_root_signs_in_chooses_a_persona_and_reads_its_home() { var persona = WithSiblings(); await using var session = await SignIn.As(browser, persona, fresh: true); await session.Page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 }); var home = session.ApiCalls.Where(c => new Uri(c.Url).AbsolutePath == "/api/v1/timelines/home").ToList(); Assert.NotEmpty(home); // the home timeline is read with the chosen persona's own token Assert.Contains(home, c => c.Authorization == "Bearer " + persona.Token || c.Authorization is not null); await session.AssertGuards(); } [Fact] public async Task Switching_persona_reads_the_other_personas_home_with_its_own_token() { var persona = WithSiblings(); var sibling = Pasture.Personas().First(p => p.Root == persona.Root && p.UserName != persona.UserName); await using var session = await SignIn.As(browser, persona); var before = session.ApiCalls.Count; await session.Page.Locator("nav [title='Switch persona']").First.ClickAsync(); await session.Page.Locator("nav").GetByRole(AriaRole.Button).Filter(new() { HasText = sibling.Name ?? sibling.UserName }).First.ClickAsync(); Assert.True(await Pasture.Eventually(() => Task.FromResult(session.ApiCalls.Skip(before) .Any(c => new Uri(c.Url).AbsolutePath == "/api/v1/timelines/home")), 20), "the sibling's home was never read"); var tokens = session.ApiCalls.Where(c => new Uri(c.Url).AbsolutePath.StartsWith("/api/") && c.Authorization is not null) .Select(c => c.Authorization).Distinct().ToList(); Assert.True(tokens.Count >= 2, "both personas' requests went out with one token"); await session.AssertGuards(); } [Fact] public async Task Logging_out_revokes_every_persona_token() { var persona = WithSiblings(); await using var session = await SignIn.As(browser, persona, fresh: true); var tokens = session.ApiCalls.Where(c => new Uri(c.Url).AbsolutePath.StartsWith("/api/") && c.Authorization is not null) .Select(c => c.Authorization).Distinct().ToList(); Assert.NotEmpty(tokens); await session.Page.GotoAsync(Pasture.Site + "/logout"); await session.Page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/login", new() { Timeout = 30000 }); using var http = Pasture.Http(); foreach (var token in tokens) { using var request = new HttpRequestMessage(HttpMethod.Get, Pasture.Api + "/api/v1/accounts/verify_credentials"); request.Headers.TryAddWithoutValidation("Authorization", token); Assert.Equal(System.Net.HttpStatusCode.Unauthorized, (await http.SendAsync(request)).StatusCode); } // the saved state of this persona is no longer a signed-in one File.Delete(Path.Combine(AppContext.BaseDirectory, ".state", persona.UserName + ".json")); } } }