summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-23 10:08:44 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-23 10:08:44 +0200
commite06248ef69b33d102a57aaa31fffcde3240dad9c (patch)
treed2b82eb5a5e2918336f89e241efdeed8bed5ca95 /tests
parent79b27457125dc2e259783359085021eea1d26e3a (diff)
downloadbokf-e06248ef69b33d102a57aaa31fffcde3240dad9c.tar.gz
bokf-e06248ef69b33d102a57aaa31fffcde3240dad9c.zip
user.set_password: change your own password; TUI "Byt lösenord"v0.1.68
Needs the current password (wrong ones rate limited like logins) and a password session, requires at least 10 characters, closes the user's other sessions and is audited without secrets. The TUI main menu gets "Byt lösenord" with masked prompts; ^R keeps working with the new password. Masked prompt buffers are wiped before they are freed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tests')
-rw-r--r--tests/test_core.c96
1 files changed, 96 insertions, 0 deletions
diff --git a/tests/test_core.c b/tests/test_core.c
index 071b5ce..6376517 100644
--- a/tests/test_core.c
+++ b/tests/test_core.c
@@ -1309,6 +1309,101 @@ static void test_tokens(struct tctx *t)
yyjson_doc_free(d);
}
+static void set_pw(const char *sess, const char *cur, const char *pw,
+ const char *want_code)
+{
+ yyjson_doc *d = call(reqf(
+ "{\"v\":1,\"id\":\"pw\",\"cmd\":\"user.set_password\","
+ "\"session\":\"%s\",\"args\":{\"current_password\":\"%s\","
+ "\"new_password\":\"%s\"}}",
+ sess, cur, pw));
+ if (want_code)
+ CHECK_STR(d, "error.code", want_code);
+ else
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+}
+
+static void test_set_password(struct tctx *t)
+{
+ yyjson_doc *d;
+ CHECK(login("admin", "secret123"));
+ d = call(reqf("{\"v\":1,\"id\":\"p1\",\"cmd\":\"user.create\","
+ "\"session\":\"%s\",\"args\":{\"username\":\"pwuser\","
+ "\"password\":\"oldpassword1\"}}",
+ g_session));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+
+ /* two sessions of the same user */
+ CHECK(login("pwuser", "oldpassword1"));
+ char other[128];
+ snprintf(other, sizeof other, "%s", g_session);
+ CHECK(login("pwuser", "oldpassword1"));
+ char mine[128];
+ snprintf(mine, sizeof mine, "%s", g_session);
+
+ set_pw(mine, "wrongpassword", "newpassword22", "AUTH_FAILED");
+ set_pw(mine, "oldpassword1", "short", "INVALID_ARGS");
+ set_pw(mine, "oldpassword1", "oldpassword1", "INVALID_ARGS");
+ d = call(reqf("{\"v\":1,\"id\":\"p2\",\"cmd\":\"user.set_password\","
+ "\"session\":\"%s\",\"args\":{\"new_password\":"
+ "\"newpassword22\"}}",
+ mine));
+ CHECK_STR(d, "error.code", "INVALID_ARGS");
+ yyjson_doc_free(d);
+ /* nothing changed yet: the other session and the old password work */
+ d = call(reqf("{\"v\":1,\"id\":\"p3\",\"cmd\":\"session.whoami\","
+ "\"session\":\"%s\"}", other));
+ CHECK_OK(d);
+ yyjson_doc_free(d);
+
+ d = call(reqf("{\"v\":1,\"id\":\"p4\",\"cmd\":\"user.set_password\","
+ "\"session\":\"%s\",\"args\":{\"current_password\":"
+ "\"oldpassword1\",\"new_password\":\"newpassword22\"}}",
+ mine));
+ CHECK_OK(d);
+ CHECK(jint(d, "result.sessions_closed") == 1);
+ yyjson_doc_free(d);
+ /* other sessions are closed, this one stays */
+ d = call(reqf("{\"v\":1,\"id\":\"p5\",\"cmd\":\"session.whoami\","
+ "\"session\":\"%s\"}", other));
+ CHECK_STR(d, "error.code", "SESSION_EXPIRED");
+ yyjson_doc_free(d);
+ d = call(reqf("{\"v\":1,\"id\":\"p6\",\"cmd\":\"session.whoami\","
+ "\"session\":\"%s\"}", mine));
+ CHECK_OK(d);
+ CHECK_STR(d, "result.user.username", "pwuser");
+ yyjson_doc_free(d);
+ CHECK(!login("pwuser", "oldpassword1"));
+ CHECK(login("pwuser", "newpassword22"));
+ snprintf(mine, sizeof mine, "%s", g_session);
+
+ /* wrong current passwords are rate limited like logins */
+ for (int i = 0; i < 5; i++)
+ set_pw(mine, "wrongpassword", "otherpassword3", "AUTH_FAILED");
+ set_pw(mine, "newpassword22", "otherpassword3", "RATE_LIMITED");
+
+ /* a token session cannot change the password */
+ CHECK(login("admin", "secret123"));
+ d = call(reqf("{\"v\":1,\"id\":\"p7\",\"cmd\":\"token.create\","
+ "\"session\":\"%s\",\"org\":%d,\"args\":"
+ "{\"label\":\"pw-test\",\"scopes\":[\"read\"]}}",
+ g_session, (int)t->org_id));
+ CHECK_OK(d);
+ char tok[128];
+ snprintf(tok, sizeof tok, "%s", jstr(d, "result.token"));
+ yyjson_doc_free(d);
+ d = call(reqf("{\"v\":1,\"id\":\"p8\",\"cmd\":\"session.open\",\"args\":"
+ "{\"method\":\"token\",\"token\":\"%s\"}}", tok));
+ CHECK_OK(d);
+ char tsess[128];
+ snprintf(tsess, sizeof tsess, "%s", jstr(d, "result.session"));
+ yyjson_doc_free(d);
+ set_pw(tsess, "secret123", "anotherpassword4", "FORBIDDEN");
+ CHECK(login("admin", "secret123"));
+}
+
static void test_accounts(struct tctx *t)
{
yyjson_doc *d;
@@ -5801,6 +5896,7 @@ static const struct ttest TESTS[] = {
{ "session_auth", test_session_auth, "" },
{ "org_members", test_org_members, "session_auth" },
{ "tokens", test_tokens, "org_members" },
+ { "set_password", test_set_password, "org_members" },
{ "accounts", test_accounts, "org_members" },
{ "fiscal_years", test_fiscal_years, "org_members" },
{ "vouchers", test_vouchers, "org_members" },