Settings: change the login's password, or delete the login

A Login section in Settings changes the root's password (the current one
given) and deletes the whole login, every persona with it, after its
password and an explicit "I understand"; the client then signs out. Both
use PrivaPub's existing /clientapi/user routes.

The e2e signs up a login of its own, changes its password and deletes it,
checking each against PrivaPub's login. Signing in now waits for either
the feed or an enabled persona chooser, so a login with one persona (which
goes on by itself) and a new persona's empty feed both sign in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 18:32:47 +02:00
1 parent 0bbab720a5
commit 228708c328
11 files changed
+258 -6

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
using System.Net;
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.Actions
{
// A login of its own, made for the test: its password changed in Settings, then the whole of it deleted there, each
// checked against PrivaPub's own login
public sealed class AccountTests(Browser browser)
{
const string FirstPassword = "Pasture-Pass-1!";
const string SecondPassword = "Pasture-Pass-2!";
static async Task<HttpStatusCode> Login(string root, string password)
{
using var http = Pasture.Http();
using var response = await http.PostAsJsonAsync(Pasture.Api + "/clientapi/user/login", new { userName = root, password });
return response.StatusCode;
}
[Fact]
public async Task A_logins_password_is_changed_and_the_login_deleted_from_settings()
{
var stamp = DateTime.UtcNow.ToString("MMddHHmmss");
var root = $"e2eroot{stamp}";
using (var http = Pasture.Http())
{
using var signup = await http.PostAsJsonAsync(Pasture.Api + "/clientapi/user/signup", new { userName = root, password = FirstPassword });
Assert.SkipWhen(!signup.IsSuccessStatusCode, $"PrivaPub refuses sign-ups ({(int)signup.StatusCode})");
var jwt = JsonNode.Parse(await signup.Content.ReadAsStringAsync())!["token"]!.GetValue<string>();
using var insert = new HttpRequestMessage(HttpMethod.Post, Pasture.Api + "/clientapi/avatar/private/insert")
{
Content = JsonContent.Create(new { userName = $"e2eacct{stamp}", name = "an e2e persona", biography = "for the account test" })
};
insert.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
using var inserted = await http.SendAsync(insert);
Assert.True(inserted.IsSuccessStatusCode, $"the persona could not be made ({(int)inserted.StatusCode})");
}
var persona = new Pasture.Persona($"e2eacct{stamp}", "an e2e persona", root, FirstPassword, null, null, null);
await using var session = await SignIn.As(browser, persona, fresh: true);
var page = session.Page;
await Results.Cell("client.action.account", "privapub", root, async () =>
{
await page.GotoAsync($"{Pasture.Site}/settings");
await page.Locator("[data-settings-account]").ClickAsync();
await page.Locator("input[data-account-current]").FillAsync(FirstPassword);
await page.Locator("input[data-account-new]").FillAsync(SecondPassword);
await page.Locator("input[data-account-repeat]").FillAsync(SecondPassword);
await page.Locator("[data-account-password-save]").ClickAsync();
Assert.True(await Pasture.Eventually(async () => await Login(root, SecondPassword) == HttpStatusCode.OK, 15), "the new password never worked");
Assert.NotEqual(HttpStatusCode.OK, await Login(root, FirstPassword));
await page.Locator("input[data-account-delete-password]").FillAsync(SecondPassword);
await page.Locator("input[data-account-delete-understood]").CheckAsync();
await page.Locator("[data-account-delete-confirm]").ClickAsync();
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/login", new() { Timeout = 30000 });
Assert.True(await Pasture.Eventually(async () => await Login(root, SecondPassword) != HttpStatusCode.OK, 15), "the deleted login still signs in");
await session.AssertGuards();
}, page);
}
}
}
+18 -5
View File
@@ -52,12 +52,25 @@ namespace decePubClient.E2E.Support
await page.Locator("input[autocomplete=username]").FillAsync(persona.Root);
await page.Locator("input[autocomplete=current-password]").FillAsync(persona.Password);
await page.Locator("button[type=submit]").ClickAsync();
// a root with several personas picks one; with one it goes straight on
var chooser = page.Locator("section button").Filter(new() { HasText = persona.UserName + "@privapub.test" });
await chooser.Or(page.Locator("article[data-uri]")).First.WaitForAsync(new() { Timeout = 30000 });
if (await chooser.CountAsync() > 0)
await chooser.First.ClickAsync();
// a root with several personas picks one; one with a single persona goes on by itself, its button disabled while it
// does. Only the login page's chooser is clicked: the feed's persona switcher names the persona too
var chooser = page.Locator("section button:enabled").Filter(new() { HasText = persona.UserName + "@privapub.test" });
var until = DateTime.UtcNow.AddSeconds(30);
while (new Uri(page.Url).AbsolutePath != "/" && DateTime.UtcNow < until)
{
if (await chooser.CountAsync() > 0)
try
{
await chooser.First.ClickAsync(new() { Timeout = 2000 });
}
catch (TimeoutException)
{
}
await page.WaitForTimeoutAsync(250);
}
await page.WaitForURLAsync(url => new Uri(url).AbsolutePath == "/", new() { Timeout = 30000 });
// (a new persona's feed is empty)
await page.Locator("article[data-uri], [data-feed-empty]").First.WaitForAsync(new() { Timeout = 30000 });
}
}
}