On opening, the feed showed "Empty" until the session had been read and a persona chosen, then loaded: a flash of a wrong answer on every start (and the end-to-end sign-in took it for a loaded feed). CascadingState says once the session has been read (IsSessionLoaded), and until then a feed with no persona yet is loading; a login without any persona still gets its empty feed. The persona-switch test waits for the sibling's home read with another token than the first persona's, instead of any home read after the switch was asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
70 lines
3.2 KiB
C#
70 lines
3.2 KiB
C#
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();
|
|
|
|
// the sibling's home, read with another token than the first persona's (whose own home may still be loading when the
|
|
// switch is asked for)
|
|
string FirstToken() => session.ApiCalls.FirstOrDefault(c => new Uri(c.Url).AbsolutePath.StartsWith("/api/") && c.Authorization is not null).Authorization;
|
|
Assert.True(await Pasture.Eventually(() => Task.FromResult(session.ApiCalls.Skip(before)
|
|
.Any(c => new Uri(c.Url).AbsolutePath == "/api/v1/timelines/home" && c.Authorization is not null && c.Authorization != FirstToken())), 20),
|
|
"the sibling's home was never read with its own 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"));
|
|
}
|
|
}
|
|
}
|