Mercurial
view auth/test/auth_store_test.c @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
#include "auth/auth_store.h" #include "auth/auth_crypto.h" #include "deita/deita.h" #include <assert.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <time.h> #include <unistd.h> /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static char g_hash_buf[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; static const char *get_test_hash(void) { if (g_hash_buf[0] == '\0') { Auth_Crypto_Result r = Auth_Crypto_Password_Hash( "hunter2", g_hash_buf, sizeof(g_hash_buf)); assert(r == AUTH_CRYPTO_OK); } return g_hash_buf; } static void make_test_hash( const char *password, char output[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]) { assert(Auth_Crypto_Password_Hash( password, output, AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE) == AUTH_CRYPTO_OK); } /* Simple UUID generator for the test, matching the pattern in conversation_store.c (no dependency on auth_store internals). */ static boolean test__make_uuid(char output[37]) { uint8 bytes[16]; int fd = open("/dev/urandom", O_RDONLY); size_t offset = 0; if (fd < 0) return FALSE; while (offset < sizeof(bytes)) { ssize_t n = read(fd, bytes + offset, sizeof(bytes) - offset); if (n <= 0) { close(fd); return FALSE; } offset += (size_t)n; } close(fd); bytes[6] = (uint8)((bytes[6] & 0x0f) | 0x40); bytes[8] = (uint8)((bytes[8] & 0x3f) | 0x80); snprintf(output, 37, "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", bytes[0],bytes[1],bytes[2],bytes[3],bytes[4],bytes[5], bytes[6],bytes[7],bytes[8],bytes[9],bytes[10],bytes[11], bytes[12],bytes[13],bytes[14],bytes[15]); return TRUE; } /* ------------------------------------------------------------------ */ /* 1. Username normalization and rejection */ /* ------------------------------------------------------------------ */ static void test_username_normalization(void) { char out[AUTH_STORE_USERNAME_MAX + 1]; assert(Auth_Store_Normalize_Username(" JohnDoe ", out, sizeof(out))); assert(strcmp(out, "johndoe") == 0); assert(Auth_Store_Normalize_Username("Alice", out, sizeof(out))); assert(strcmp(out, "alice") == 0); assert(Auth_Store_Normalize_Username("june_bot-2.0", out, sizeof(out))); assert(strcmp(out, "june_bot-2.0") == 0); assert(Auth_Store_Normalize_Username("abc", out, sizeof(out))); /* Exactly 32 chars */ assert(Auth_Store_Normalize_Username( "abcdefghijklmnopqrstuvwxyz123456", out, sizeof(out))); /* Too short after trimming */ assert(!Auth_Store_Normalize_Username("ab", out, sizeof(out))); assert(!Auth_Store_Normalize_Username(" z ", out, sizeof(out))); /* Too long (33 chars) */ assert(!Auth_Store_Normalize_Username( "abcdefghijklmnopqrstuvwxyz1234567", out, sizeof(out))); /* Invalid characters */ assert(!Auth_Store_Normalize_Username("hello world", out, sizeof(out))); assert(!Auth_Store_Normalize_Username("invalid!", out, sizeof(out))); assert(!Auth_Store_Normalize_Username("utf8\xc3\xa9", out, sizeof(out))); /* Buffer too small */ char tiny[3]; assert(!Auth_Store_Normalize_Username("abc", tiny, sizeof(tiny))); /* Validate pre-normalized */ assert( Auth_Store_Validate_Username("johndoe")); assert( Auth_Store_Validate_Username("abc")); assert( Auth_Store_Validate_Username("june_bot-2.0")); assert(!Auth_Store_Validate_Username("ab")); assert(!Auth_Store_Validate_Username("Hello")); /* uppercase */ assert(!Auth_Store_Validate_Username("bad char!")); assert(!Auth_Store_Validate_Username(NULL)); puts("test_username_normalization: PASS"); } /* ------------------------------------------------------------------ */ /* 2. Migration idempotency / reopen */ /* ------------------------------------------------------------------ */ static void test_migrations_idempotent(const char *db_path) { Auth_Store *p = Auth_Store_Create(db_path); assert(p); Auth_Store_Destroy(p); /* Reopen: migrations must be no-ops. */ p = Auth_Store_Create(db_path); assert(p); Auth_Store_Destroy(p); /* Verify all expected tables exist via a second connection. */ Dowa_Arena *p_arena = Dowa_Arena_Create(4096); assert(p_arena); Deita_Connection *p_conn = Deita_Connection_Create( DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_conn); static const char *expected[] = { "admin_audit_log", "auth_schema_migrations", "auth_sessions", "guest_identities", "guest_usage", "guest_usage_reservations", "users", }; size_t found = 0; Deita_Result_Set *p_result = Deita_Query_Execute( p_conn, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name", p_arena); assert(p_result); while (Deita_Result_Set_Next(p_result)) { const char *name = Deita_Result_Set_Get_Text(p_result, 0); for (size_t i = 0; i < sizeof(expected)/sizeof(expected[0]); i++) if (name && strcmp(name, expected[i]) == 0) { found++; break; } } Deita_Result_Set_Free(p_result); Deita_Connection_Close(p_conn); Dowa_Arena_Free(p_arena); assert(found == sizeof(expected)/sizeof(expected[0])); puts("test_migrations_idempotent: PASS"); } /* ------------------------------------------------------------------ */ /* 3. Bootstrap admin idempotency */ /* ------------------------------------------------------------------ */ static void test_bootstrap_admin(Auth_Store *p_store) { char id1[37], id2[37]; Auth_Store_Bootstrap_Result br; assert(Auth_Store_Bootstrap_Admin( p_store, "Admin", get_test_hash(), &br, id1) == AUTH_STORE_OK); assert(br == AUTH_STORE_BOOTSTRAP_CREATED); assert(strlen(id1) == 36); /* Second call with a different username: must not overwrite existing admin. */ assert(Auth_Store_Bootstrap_Admin( p_store, "OtherAdmin", get_test_hash(), &br, id2) == AUTH_STORE_OK); assert(br == AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT); assert(strcmp(id1, id2) == 0); /* Exactly one admin must exist. */ Auth_User_Record *records = NULL; Dowa_Arena *p_arena = Dowa_Arena_Create(64 * 1024); assert(p_arena); assert(Auth_Store_List_Users(p_store, &records, p_arena) == AUTH_STORE_OK); size_t admin_count = 0; for (size_t i = 0; i < Dowa_Array_Length(records); i++) if (strcmp(records[i].role, "admin") == 0) admin_count++; assert(admin_count == 1); Dowa_Arena_Free(p_arena); puts("test_bootstrap_admin: PASS"); } /* ------------------------------------------------------------------ */ /* 4. Last-admin protection (run while only one admin exists) */ /* ------------------------------------------------------------------ */ static void test_last_admin_protection(Auth_Store *p_store) { /* At this point only the bootstrap admin ("Admin"/"admin") exists. */ Auth_User_Auth_Record ar; assert(Auth_Store_Find_User_By_Username( p_store, "admin", &ar) == AUTH_STORE_OK); const char *admin_id = ar.user.id; /* Cannot disable the last active admin. */ assert(Auth_Store_Update_User_Status( p_store, admin_id, "disabled", NULL) == AUTH_STORE_LAST_ADMIN); /* Cannot demote the last active admin. */ assert(Auth_Store_Update_User_Role( p_store, admin_id, "member", NULL) == AUTH_STORE_LAST_ADMIN); assert(Auth_Store_Disable_User_And_Revoke_Sessions( p_store, admin_id, NULL) == AUTH_STORE_LAST_ADMIN); assert(Auth_Store_Update_Role_And_Revoke_Sessions( p_store, admin_id, "member", NULL) == AUTH_STORE_LAST_ADMIN); /* Add a second active admin — operations on the first should now succeed. */ char id2[37]; assert(Auth_Store_Create_User( p_store, "SecondAdmin", get_test_hash(), "admin", FALSE, id2) == AUTH_STORE_OK); /* Now demotion of the first admin is allowed (two active admins). */ assert(Auth_Store_Update_User_Role( p_store, admin_id, "member", id2) == AUTH_STORE_OK); /* Re-promote so remaining tests can rely on admin being an admin. */ assert(Auth_Store_Update_User_Role( p_store, admin_id, "admin", id2) == AUTH_STORE_OK); puts("test_last_admin_protection: PASS"); } /* ------------------------------------------------------------------ */ /* 5. Username uniqueness */ /* ------------------------------------------------------------------ */ static void test_username_uniqueness(Auth_Store *p_store) { char id[37]; assert(Auth_Store_Create_User( p_store, "UniqueUser", get_test_hash(), "member", FALSE, id) == AUTH_STORE_OK); char id2[37]; assert(Auth_Store_Create_User( p_store, "uniqueuser", get_test_hash(), "member", FALSE, id2) == AUTH_STORE_CONFLICT); puts("test_username_uniqueness: PASS"); } /* ------------------------------------------------------------------ */ /* 6. User lookup */ /* ------------------------------------------------------------------ */ static void test_user_lookup(Auth_Store *p_store) { char id[37]; assert(Auth_Store_Create_User( p_store, "LookupUser", get_test_hash(), "member", FALSE, id) == AUTH_STORE_OK); Auth_User_Record r; assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK); assert(strcmp(r.id, id) == 0); assert(strcmp(r.username, "LookupUser") == 0); assert(strcmp(r.normalized_username, "lookupuser") == 0); assert(strcmp(r.role, "member") == 0); assert(strcmp(r.status, "active") == 0); assert(r.must_change_password == FALSE); /* Not found */ assert(Auth_Store_Get_User( p_store, "00000000-0000-0000-0000-000000000000", &r) == AUTH_STORE_NOT_FOUND); /* Case-insensitive find by username */ Auth_User_Auth_Record auth_r; assert(Auth_Store_Find_User_By_Username( p_store, " LOOKUPUSER ", &auth_r) == AUTH_STORE_OK); assert(strcmp(auth_r.user.id, id) == 0); assert(strncmp(auth_r.password_hash, "zenbu-scrypt$", 13) == 0); assert(Auth_Store_Find_User_By_Username( p_store, "nobody", &auth_r) == AUTH_STORE_NOT_FOUND); puts("test_user_lookup: PASS"); } /* ------------------------------------------------------------------ */ /* 7. Forced password flag */ /* ------------------------------------------------------------------ */ static void test_forced_password_flag(Auth_Store *p_store) { char id[37]; assert(Auth_Store_Create_User( p_store, "ForcedPwUser", get_test_hash(), "member", TRUE, id) == AUTH_STORE_OK); Auth_User_Record r; assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK); assert(r.must_change_password == TRUE); assert(Auth_Store_Set_Must_Change_Password( p_store, id, FALSE, NULL) == AUTH_STORE_OK); assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK); assert(r.must_change_password == FALSE); assert(Auth_Store_Set_Must_Change_Password( p_store, id, TRUE, NULL) == AUTH_STORE_OK); assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK); assert(r.must_change_password == TRUE); /* Updating password clears the flag. */ assert(Auth_Store_Update_Password( p_store, id, get_test_hash(), FALSE, NULL) == AUTH_STORE_OK); assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK); assert(r.must_change_password == FALSE); puts("test_forced_password_flag: PASS"); } /* ------------------------------------------------------------------ */ /* 8. Session create / resolve / touch / revoke */ /* ------------------------------------------------------------------ */ static void test_session_lifecycle(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "SessUser", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); const char *tok = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const char *csrf = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; int64 now = 1700000000LL; int64 idle_ttl = 3600; int64 abs_ttl = 86400; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, tok, csrf, idle_ttl, abs_ttl, now, &sess) == AUTH_STORE_OK); assert(strcmp(sess.user_id, user_id) == 0); assert(sess.created_at == now); assert(sess.idle_expires_at == now + idle_ttl); assert(sess.absolute_expires_at == now + abs_ttl); assert(sess.password_changed_at_snapshot == 0); Auth_Session_Record fs; Auth_User_Record fu; int64 check = now + 60; assert(Auth_Store_Find_Session( p_store, tok, check, &fs, &fu) == AUTH_STORE_OK); assert(strcmp(fu.id, user_id) == 0); /* Touch extends idle expiry. */ assert(Auth_Store_Touch_Session( p_store, tok, check, idle_ttl) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, tok, check, &fs, &fu) == AUTH_STORE_OK); assert(fs.idle_expires_at == check + idle_ttl); /* Revoke. */ assert(Auth_Store_Revoke_Session(p_store, tok) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, tok, check, &fs, &fu) == AUTH_STORE_REVOKED); /* Unknown digest. */ const char *unk = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; assert(Auth_Store_Find_Session( p_store, unk, check, &fs, &fu) == AUTH_STORE_NOT_FOUND); puts("test_session_lifecycle: PASS"); } /* ------------------------------------------------------------------ */ /* 9. Stale password snapshot */ /* ------------------------------------------------------------------ */ static void test_stale_password_snapshot(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "StaleUser", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); const char *tok = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; const char *csrf = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; int64 now = 1700100000LL; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, tok, csrf, 3600, 86400, now, &sess) == AUTH_STORE_OK); assert(sess.password_changed_at_snapshot == 0); Auth_Session_Record fs; Auth_User_Record fu; assert(Auth_Store_Find_Session( p_store, tok, now + 10, &fs, &fu) == AUTH_STORE_OK); assert(Auth_Store_Update_Password( p_store, user_id, get_test_hash(), FALSE, NULL) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, tok, now + 20, &fs, &fu) == AUTH_STORE_STALE_PASSWORD); puts("test_stale_password_snapshot: PASS"); } /* ------------------------------------------------------------------ */ /* 10. Disabled user blocks session resolution and creation */ /* ------------------------------------------------------------------ */ static void test_disabled_user(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "DisabledUser", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); const char *tok = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; const char *csrf = "1111111111111111111111111111111111111111111111111111111111111111"; int64 now = 1700200000LL; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, tok, csrf, 3600, 86400, now, &sess) == AUTH_STORE_OK); Auth_Session_Record fs; Auth_User_Record fu; assert(Auth_Store_Find_Session( p_store, tok, now + 10, &fs, &fu) == AUTH_STORE_OK); /* Disable the member user (no last-admin protection applies). */ assert(Auth_Store_Update_User_Status( p_store, user_id, "disabled", NULL) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, tok, now + 20, &fs, &fu) == AUTH_STORE_USER_DISABLED); /* New session for a disabled user must fail. */ const char *tok2 = "2222222222222222222222222222222222222222222222222222222222222222"; const char *csrf2 = "3333333333333333333333333333333333333333333333333333333333333333"; assert(Auth_Store_Create_Session( p_store, user_id, tok2, csrf2, 3600, 86400, now + 30, &sess) == AUTH_STORE_USER_DISABLED); /* Re-enable. */ assert(Auth_Store_Update_User_Status( p_store, user_id, "active", NULL) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, tok, now + 30, &fs, &fu) == AUTH_STORE_OK); puts("test_disabled_user: PASS"); } /* ------------------------------------------------------------------ */ /* 11. Password update revokes other sessions atomically */ /* ------------------------------------------------------------------ */ static void test_password_update_revokes_others(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "RevokeOthers", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); int64 now = 1700300000LL; const char *tok_keep = "4444444444444444444444444444444444444444444444444444444444444444"; const char *tok_rev1 = "5555555555555555555555555555555555555555555555555555555555555555"; const char *tok_rev2 = "6666666666666666666666666666666666666666666666666666666666666666"; const char *csrf_k = "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111"; const char *csrf_1 = "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222"; const char *csrf_2 = "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333"; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, tok_keep, csrf_k, 3600, 86400, now, &sess) == AUTH_STORE_OK); assert(Auth_Store_Create_Session( p_store, user_id, tok_rev1, csrf_1, 3600, 86400, now, &sess) == AUTH_STORE_OK); assert(Auth_Store_Create_Session( p_store, user_id, tok_rev2, csrf_2, 3600, 86400, now, &sess) == AUTH_STORE_OK); /* Update password: keep tok_keep, revoke all others. */ assert(Auth_Store_Update_Password( p_store, user_id, get_test_hash(), TRUE, tok_keep) == AUTH_STORE_OK); int64 check = now + 60; Auth_Session_Record fs; Auth_User_Record fu; /* tok_keep is not revoked but snapshot is stale because password changed. */ assert(Auth_Store_Find_Session( p_store, tok_keep, check, &fs, &fu) == AUTH_STORE_STALE_PASSWORD); assert(Auth_Store_Find_Session( p_store, tok_rev1, check, &fs, &fu) == AUTH_STORE_REVOKED); assert(Auth_Store_Find_Session( p_store, tok_rev2, check, &fs, &fu) == AUTH_STORE_REVOKED); puts("test_password_update_revokes_others: PASS"); } /* ------------------------------------------------------------------ */ /* 12. Revoke_All_Sessions */ /* ------------------------------------------------------------------ */ static void test_revoke_all_sessions(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "RevokeAll", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); int64 now = 1700400000LL; const char *t1 = "7777777777777777777777777777777777777777777777777777777777777777"; const char *t2 = "8888888888888888888888888888888888888888888888888888888888888888"; const char *t3 = "9999999999999999999999999999999999999999999999999999999999999999"; const char *c1 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; const char *c2 = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3"; const char *c3 = "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, t1, c1, 3600, 86400, now, &sess) == AUTH_STORE_OK); assert(Auth_Store_Create_Session( p_store, user_id, t2, c2, 3600, 86400, now, &sess) == AUTH_STORE_OK); assert(Auth_Store_Create_Session( p_store, user_id, t3, c3, 3600, 86400, now, &sess) == AUTH_STORE_OK); /* Revoke all except t2. */ assert(Auth_Store_Revoke_All_Sessions(p_store, user_id, t2) == AUTH_STORE_OK); int64 check = now + 60; Auth_Session_Record fs; Auth_User_Record fu; assert(Auth_Store_Find_Session(p_store, t1, check, &fs, &fu) == AUTH_STORE_REVOKED); assert(Auth_Store_Find_Session(p_store, t2, check, &fs, &fu) == AUTH_STORE_OK); assert(Auth_Store_Find_Session(p_store, t3, check, &fs, &fu) == AUTH_STORE_REVOKED); puts("test_revoke_all_sessions: PASS"); } /* ------------------------------------------------------------------ */ /* 13. Expired session */ /* ------------------------------------------------------------------ */ static void test_expired_session(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "ExpiredUser", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); const char *tok = "abababababababababababababababababababababababababababababababab"; const char *csrf = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"; int64 now = 1700500000LL; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, tok, csrf, 100, 200, now, &sess) == AUTH_STORE_OK); Auth_Session_Record fs; Auth_User_Record fu; assert(Auth_Store_Find_Session( p_store, tok, now + 50, &fs, &fu) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, tok, now + 110, &fs, &fu) == AUTH_STORE_EXPIRED); assert(Auth_Store_Find_Session( p_store, tok, now + 201, &fs, &fu) == AUTH_STORE_EXPIRED); puts("test_expired_session: PASS"); } /* ------------------------------------------------------------------ */ /* 14. Guest identity persistence and no raw IP storage */ /* ------------------------------------------------------------------ */ static void test_guest_identity(Auth_Store *p_store, const char *db_path) { uint8 secret[AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES]; memset(secret, 0xab, sizeof(secret)); const char *raw_ip = "203.0.113.42"; /* TEST-NET-3, never stored */ char ip_digest[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE]; assert(Auth_Crypto_IP_Binding_Digest( secret, sizeof(secret), raw_ip, ip_digest, sizeof(ip_digest)) == AUTH_CRYPTO_OK); char guest_id[37]; assert(test__make_uuid(guest_id)); int64 now = 1700600000LL; int64 exp = now + 86400; Auth_Guest_Identity_Record g; assert(Auth_Store_Upsert_Guest_Identity( p_store, guest_id, ip_digest, exp, &g) == AUTH_STORE_OK); assert(strcmp(g.id, guest_id) == 0); assert(g.expires_at == exp); /* Find within expiry. */ Auth_Guest_Identity_Record g2; assert(Auth_Store_Find_Guest_Identity( p_store, guest_id, now + 60, &g2) == AUTH_STORE_OK); assert(strcmp(g2.id, guest_id) == 0); /* Find after expiry. */ assert(Auth_Store_Find_Guest_Identity( p_store, guest_id, exp + 1, &g2) == AUTH_STORE_EXPIRED); /* Upsert refreshes expiry without losing the identity row. */ int64 new_exp = exp + 86400; assert(Auth_Store_Upsert_Guest_Identity( p_store, guest_id, ip_digest, new_exp, &g) == AUTH_STORE_OK); assert(g.expires_at == new_exp); assert(Auth_Store_Find_Guest_Identity( p_store, guest_id, exp + 1, &g2) == AUTH_STORE_OK); /* Unknown guest. */ assert(Auth_Store_Find_Guest_Identity( p_store, "00000000-0000-4000-8000-000000000000", now, &g2) == AUTH_STORE_NOT_FOUND); /* * Verify raw IP is NOT stored in the database: open a second connection * and inspect the ip_binding_digest column directly. */ Dowa_Arena *p_arena = Dowa_Arena_Create(4096); assert(p_arena); Deita_Connection *p_conn = Deita_Connection_Create( DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_conn); const char *sel_params[] = {guest_id}; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_conn, "SELECT ip_binding_digest FROM guest_identities WHERE id = ?", 1, sel_params, p_arena); assert(p_result); assert(Deita_Result_Set_Next(p_result)); const char *stored = Deita_Result_Set_Get_Text(p_result, 0); assert(stored != NULL); assert(strcmp(stored, ip_digest) == 0); /* HMAC digest is stored */ assert(strstr(stored, raw_ip) == NULL); /* raw IP is NOT stored */ Deita_Result_Set_Free(p_result); Deita_Connection_Close(p_conn); Dowa_Arena_Free(p_arena); puts("test_guest_identity: PASS"); } /* ------------------------------------------------------------------ */ /* 16. Rotate_Session */ /* ------------------------------------------------------------------ */ static void test_rotate_session(Auth_Store *p_store) { char user_id[37]; assert(Auth_Store_Create_User( p_store, "RotateUser", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); int64 now = 1700400000LL; const char *old_tok = "e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1"; const char *old_csrf = "f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2"; const char *new_tok = "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3"; const char *new_csrf = "b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4"; Auth_Session_Record sess; assert(Auth_Store_Create_Session( p_store, user_id, old_tok, old_csrf, 3600, 86400, now, &sess) == AUTH_STORE_OK); /* Rotate: new session created, old revoked atomically */ Auth_Session_Record new_sess; assert(Auth_Store_Rotate_Session( p_store, user_id, old_tok, new_tok, new_csrf, 3600, 86400, now + 10, &new_sess) == AUTH_STORE_OK); int64 check = now + 60; Auth_Session_Record fs; Auth_User_Record fu; /* Old session must be revoked */ assert(Auth_Store_Find_Session(p_store, old_tok, check, &fs, &fu) == AUTH_STORE_REVOKED); /* New session must be valid */ assert(Auth_Store_Find_Session(p_store, new_tok, check, &fs, &fu) == AUTH_STORE_OK); assert(strcmp(fs.user_id, user_id) == 0); /* Rotate again with already-revoked old token must still succeed * (idempotent revocation) */ const char *new_tok2 = "c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5"; const char *new_csrf2 = "d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6"; Auth_Session_Record sess2; assert(Auth_Store_Rotate_Session( p_store, user_id, old_tok, new_tok2, new_csrf2, 3600, 86400, now + 20, &sess2) == AUTH_STORE_OK); assert(Auth_Store_Find_Session(p_store, new_tok2, check, &fs, &fu) == AUTH_STORE_OK); puts("test_rotate_session: PASS"); } /* ------------------------------------------------------------------ */ /* 17. Compare-and-swap session creation */ /* ------------------------------------------------------------------ */ static void test_create_session_cas(Auth_Store *p_store) { char first_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; char second_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; char reset_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; make_test_hash("cas-first-password", first_hash); make_test_hash("cas-second-password", second_hash); make_test_hash("cas-reset-password", reset_hash); char user_id[37]; assert(Auth_Store_Create_User( p_store, "CasSessionUser", first_hash, "member", FALSE, user_id) == AUTH_STORE_OK); const char *token1 = "1010101010101010101010101010101010101010101010101010101010101010"; const char *csrf1 = "2020202020202020202020202020202020202020202020202020202020202020"; const char *token2 = "3030303030303030303030303030303030303030303030303030303030303030"; const char *csrf2 = "4040404040404040404040404040404040404040404040404040404040404040"; const char *token3 = "5050505050505050505050505050505050505050505050505050505050505050"; const char *csrf3 = "6060606060606060606060606060606060606060606060606060606060606060"; const char *token4 = "7070707070707070707070707070707070707070707070707070707070707070"; const char *csrf4 = "8080808080808080808080808080808080808080808080808080808080808080"; const char *disabled_token = "9090909090909090909090909090909090909090909090909090909090909090"; const char *disabled_csrf = "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"; int64 now = 1800000000LL; Auth_Session_Record session; assert(Auth_Store_Create_Session_CAS( p_store, user_id, first_hash, token1, csrf1, 3600, 86400, now, &session) == AUTH_STORE_OK); assert(Auth_Store_Update_User_Status( p_store, user_id, "disabled", NULL) == AUTH_STORE_OK); assert(Auth_Store_Create_Session_CAS( p_store, user_id, first_hash, disabled_token, disabled_csrf, 3600, 86400, now + 1, &session) == AUTH_STORE_USER_DISABLED); assert(Auth_Store_Update_User_Status( p_store, user_id, "active", NULL) == AUTH_STORE_OK); Auth_Session_Record found_session; Auth_User_Record found_user; assert(Auth_Store_Find_Session( p_store, disabled_token, now + 2, &found_session, &found_user) == AUTH_STORE_NOT_FOUND); assert(Auth_Store_Update_Password( p_store, user_id, second_hash, FALSE, NULL) == AUTH_STORE_OK); assert(Auth_Store_Create_Session_CAS( p_store, user_id, first_hash, token2, csrf2, 3600, 86400, now + 10, &session) == AUTH_STORE_STALE_PASSWORD); assert(Auth_Store_Find_Session( p_store, token2, now + 20, &found_session, &found_user) == AUTH_STORE_NOT_FOUND); assert(Auth_Store_Admin_Reset_Password( p_store, user_id, reset_hash, NULL) == AUTH_STORE_OK); assert(Auth_Store_Create_Session_CAS( p_store, user_id, second_hash, token3, csrf3, 3600, 86400, now + 20, &session) == AUTH_STORE_STALE_PASSWORD); assert(Auth_Store_Find_Session( p_store, token3, now + 30, &found_session, &found_user) == AUTH_STORE_NOT_FOUND); assert(Auth_Store_Create_Session_CAS( p_store, user_id, reset_hash, token4, csrf4, 3600, 86400, now + 30, &session) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, token4, now + 40, &found_session, &found_user) == AUTH_STORE_OK); memset(first_hash, 0, sizeof(first_hash)); memset(second_hash, 0, sizeof(second_hash)); memset(reset_hash, 0, sizeof(reset_hash)); puts("test_create_session_cas: PASS"); } /* ------------------------------------------------------------------ */ /* 18. Atomic self-service password change */ /* ------------------------------------------------------------------ */ static void test_self_change_password(Auth_Store *p_store) { char old_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; char new_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; char unused_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; make_test_hash("self-change-old", old_hash); make_test_hash("self-change-new", new_hash); make_test_hash("self-change-unused", unused_hash); char user_id[37]; assert(Auth_Store_Create_User( p_store, "SelfChangeUser", old_hash, "member", TRUE, user_id) == AUTH_STORE_OK); const char *old_token1 = "1111222211112222111122221111222211112222111122221111222211112222"; const char *old_csrf1 = "2222333322223333222233332222333322223333222233332222333322223333"; const char *old_token2 = "3333444433334444333344443333444433334444333344443333444433334444"; const char *old_csrf2 = "4444555544445555444455554444555544445555444455554444555544445555"; const char *new_token = "5555666655556666555566665555666655556666555566665555666655556666"; const char *new_csrf = "6666777766667777666677776666777766667777666677776666777766667777"; const char *failed_token = "7777888877778888777788887777888877778888777788887777888877778888"; const char *failed_csrf = "8888999988889999888899998888999988889999888899998888999988889999"; int64 now = 1810000000LL; Auth_Session_Record session; assert(Auth_Store_Create_Session_CAS( p_store, user_id, old_hash, old_token1, old_csrf1, 3600, 86400, now, &session) == AUTH_STORE_OK); assert(Auth_Store_Create_Session_CAS( p_store, user_id, old_hash, old_token2, old_csrf2, 3600, 86400, now, &session) == AUTH_STORE_OK); assert(Auth_Store_Self_Change_Password( p_store, user_id, old_hash, new_hash, new_token, new_csrf, 3600, 86400, now + 100, &session) == AUTH_STORE_OK); assert(session.password_changed_at_snapshot == now + 100); Auth_Session_Record found_session; Auth_User_Record found_user; assert(Auth_Store_Find_Session( p_store, old_token1, now + 101, &found_session, &found_user) == AUTH_STORE_REVOKED); assert(Auth_Store_Find_Session( p_store, old_token2, now + 101, &found_session, &found_user) == AUTH_STORE_REVOKED); assert(Auth_Store_Find_Session( p_store, new_token, now + 101, &found_session, &found_user) == AUTH_STORE_OK); assert(found_user.must_change_password == FALSE); assert(found_user.password_changed_at == now + 100); Auth_User_Auth_Record auth_record; assert(Auth_Store_Find_User_By_Username( p_store, "SelfChangeUser", &auth_record) == AUTH_STORE_OK); assert(strcmp(auth_record.password_hash, new_hash) == 0); memset(&auth_record, 0, sizeof(auth_record)); assert(Auth_Store_Self_Change_Password( p_store, user_id, old_hash, unused_hash, failed_token, failed_csrf, 3600, 86400, now + 200, &session) == AUTH_STORE_STALE_PASSWORD); assert(Auth_Store_Find_Session( p_store, failed_token, now + 201, &found_session, &found_user) == AUTH_STORE_NOT_FOUND); assert(Auth_Store_Find_Session( p_store, new_token, now + 201, &found_session, &found_user) == AUTH_STORE_OK); assert(Auth_Store_Find_User_By_Username( p_store, "SelfChangeUser", &auth_record) == AUTH_STORE_OK); assert(strcmp(auth_record.password_hash, new_hash) == 0); memset(&auth_record, 0, sizeof(auth_record)); memset(old_hash, 0, sizeof(old_hash)); memset(new_hash, 0, sizeof(new_hash)); memset(unused_hash, 0, sizeof(unused_hash)); puts("test_self_change_password: PASS"); } /* ------------------------------------------------------------------ */ /* 19. Audited and session-revoking admin operations */ /* ------------------------------------------------------------------ */ static void test_audited_admin_operations( Auth_Store *p_store, const char *db_path) { char user_id[37]; const char *actor_id = "00000000-0000-4000-8000-000000000019"; assert(Auth_Store_Create_User_Audited( p_store, "AuditedUser", get_test_hash(), "member", FALSE, actor_id, user_id) == AUTH_STORE_OK); Dowa_Arena *p_arena = Dowa_Arena_Create(2048); assert(p_arena); Deita_Connection *p_conn = Deita_Connection_Create( DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_conn); const char *audit_params[] = {user_id}; Deita_Result_Set *p_result = Deita_Query_Execute_Prepared( p_conn, "SELECT actor_user_id, action, detail FROM admin_audit_log" " WHERE target_user_id = ? ORDER BY id DESC LIMIT 1", 1, audit_params, p_arena); assert(p_result); assert(Deita_Result_Set_Next(p_result)); assert(strcmp(Deita_Result_Set_Get_Text(p_result, 0), actor_id) == 0); assert(strcmp(Deita_Result_Set_Get_Text(p_result, 1), "admin_user_created") == 0); assert(strcmp(Deita_Result_Set_Get_Text(p_result, 2), "member") == 0); assert(strstr(Deita_Result_Set_Get_Text(p_result, 2), "zenbu-scrypt") == NULL); Deita_Result_Set_Free(p_result); Deita_Connection_Close(p_conn); Dowa_Arena_Free(p_arena); const char *token1 = "9191919191919191919191919191919191919191919191919191919191919191"; const char *csrf1 = "9292929292929292929292929292929292929292929292929292929292929292"; const char *token2 = "9393939393939393939393939393939393939393939393939393939393939393"; const char *csrf2 = "9494949494949494949494949494949494949494949494949494949494949494"; const char *token3 = "9595959595959595959595959595959595959595959595959595959595959595"; const char *csrf3 = "9696969696969696969696969696969696969696969696969696969696969696"; int64 now = 1820000000LL; Auth_Session_Record session; Auth_Session_Record found_session; Auth_User_Record found_user; assert(Auth_Store_Create_Session( p_store, user_id, token1, csrf1, 3600, 86400, now, &session) == AUTH_STORE_OK); assert(Auth_Store_Enable_User(p_store, user_id, actor_id) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, token1, now + 1, &found_session, &found_user) == AUTH_STORE_OK); assert(Auth_Store_Disable_User_And_Revoke_Sessions( p_store, user_id, actor_id) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, token1, now + 2, &found_session, &found_user) == AUTH_STORE_REVOKED); assert(Auth_Store_Enable_User(p_store, user_id, actor_id) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, token1, now + 3, &found_session, &found_user) == AUTH_STORE_REVOKED); assert(Auth_Store_Create_Session( p_store, user_id, token2, csrf2, 3600, 86400, now + 4, &session) == AUTH_STORE_OK); assert(Auth_Store_Update_Role_And_Revoke_Sessions( p_store, user_id, "admin", actor_id) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, token2, now + 5, &found_session, &found_user) == AUTH_STORE_REVOKED); assert(Auth_Store_Get_User(p_store, user_id, &found_user) == AUTH_STORE_OK); assert(strcmp(found_user.role, "admin") == 0); assert(Auth_Store_Create_Session( p_store, user_id, token3, csrf3, 3600, 86400, now + 6, &session) == AUTH_STORE_OK); assert(Auth_Store_Revoke_All_Sessions_Audited( p_store, user_id, NULL, actor_id) == AUTH_STORE_OK); assert(Auth_Store_Find_Session( p_store, token3, now + 7, &found_session, &found_user) == AUTH_STORE_REVOKED); assert(Auth_Store_Enable_User( p_store, "00000000-0000-4000-8000-000000000000", actor_id) == AUTH_STORE_NOT_FOUND); puts("test_audited_admin_operations: PASS"); } /* ------------------------------------------------------------------ */ /* 20. Audit failures roll back transactional mutations */ /* ------------------------------------------------------------------ */ static void test_audit_failure_rollback( Auth_Store *p_store, const char *db_path) { char reset_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; make_test_hash("audit-reset-password", reset_hash); char user_id[37]; assert(Auth_Store_Create_User( p_store, "AuditRollbackUser", get_test_hash(), "member", FALSE, user_id) == AUTH_STORE_OK); const char *token = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"; const char *csrf = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"; int64 now = 1830000000LL; Auth_Session_Record session; assert(Auth_Store_Create_Session( p_store, user_id, token, csrf, 3600, 86400, now, &session) == AUTH_STORE_OK); Deita_Connection *p_conn = Deita_Connection_Create( DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_conn); assert(Deita_Query_Execute_Update( p_conn, "UPDATE users SET role = 'member' WHERE role = 'admin'") >= 0); assert(Deita_Query_Execute_Update( p_conn, "CREATE TRIGGER fail_auth_audit BEFORE INSERT ON admin_audit_log" " BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END") >= 0); Deita_Connection_Close(p_conn); assert(Auth_Store_Set_Must_Change_Password( p_store, user_id, TRUE, NULL) == AUTH_STORE_ERROR); Auth_User_Record user; assert(Auth_Store_Get_User(p_store, user_id, &user) == AUTH_STORE_OK); assert(user.must_change_password == FALSE); Auth_Session_Record found_session; Auth_User_Record found_user; assert(Auth_Store_Update_User_Status( p_store, user_id, "disabled", NULL) == AUTH_STORE_ERROR); assert(Auth_Store_Get_User(p_store, user_id, &user) == AUTH_STORE_OK); assert(strcmp(user.status, "active") == 0); assert(Auth_Store_Find_Session( p_store, token, now + 1, &found_session, &found_user) == AUTH_STORE_OK); assert(Auth_Store_Update_User_Role( p_store, user_id, "admin", NULL) == AUTH_STORE_ERROR); assert(Auth_Store_Get_User(p_store, user_id, &user) == AUTH_STORE_OK); assert(strcmp(user.role, "member") == 0); assert(Auth_Store_Disable_User_And_Revoke_Sessions( p_store, user_id, NULL) == AUTH_STORE_ERROR); assert(Auth_Store_Update_Role_And_Revoke_Sessions( p_store, user_id, "admin", NULL) == AUTH_STORE_ERROR); assert(Auth_Store_Revoke_All_Sessions_Audited( p_store, user_id, NULL, NULL) == AUTH_STORE_ERROR); assert(Auth_Store_Find_Session( p_store, token, now + 2, &found_session, &found_user) == AUTH_STORE_OK); assert(Auth_Store_Admin_Reset_Password( p_store, user_id, reset_hash, NULL) == AUTH_STORE_ERROR); Auth_User_Auth_Record auth_record; assert(Auth_Store_Find_User_By_Username( p_store, "AuditRollbackUser", &auth_record) == AUTH_STORE_OK); assert(strcmp(auth_record.password_hash, get_test_hash()) == 0); assert(auth_record.user.must_change_password == FALSE); memset(&auth_record, 0, sizeof(auth_record)); assert(Auth_Store_Find_Session( p_store, token, now + 3, &found_session, &found_user) == AUTH_STORE_OK); Auth_Store_Bootstrap_Result bootstrap_result; char bootstrap_id[37]; assert(Auth_Store_Bootstrap_Admin( p_store, "AuditBootstrapRollback", get_test_hash(), &bootstrap_result, bootstrap_id) == AUTH_STORE_ERROR); assert(Auth_Store_Find_User_By_Username( p_store, "AuditBootstrapRollback", &auth_record) == AUTH_STORE_NOT_FOUND); assert(Auth_Store_Insert_Audit_Log( p_store, NULL, "forced_failure", user_id, NULL) == AUTH_STORE_ERROR); char failed_id[37]; assert(Auth_Store_Create_User_Audited( p_store, "AuditCreateRollback", get_test_hash(), "member", FALSE, NULL, failed_id) == AUTH_STORE_ERROR); assert(Auth_Store_Find_User_By_Username( p_store, "AuditCreateRollback", &auth_record) == AUTH_STORE_NOT_FOUND); p_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, db_path); assert(p_conn); assert(Deita_Query_Execute_Update( p_conn, "DROP TRIGGER fail_auth_audit") >= 0); Deita_Connection_Close(p_conn); memset(reset_hash, 0, sizeof(reset_hash)); puts("test_audit_failure_rollback: PASS"); } /* ------------------------------------------------------------------ */ /* Guest quota tests */ /* ------------------------------------------------------------------ */ /* Create a guest identity row (required as FK parent). */ static void make_guest(Auth_Store *p_store, const char *guest_id) { uint8 secret[AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES]; memset(secret, 0xab, sizeof(secret)); char ip_digest[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE]; assert(Auth_Crypto_IP_Binding_Digest( secret, sizeof(secret), "10.0.0.1", ip_digest, sizeof(ip_digest)) == AUTH_CRYPTO_OK); Auth_Guest_Identity_Record g; assert(Auth_Store_Upsert_Guest_Identity( p_store, guest_id, ip_digest, 2000000000LL, &g) == AUTH_STORE_OK); } static void test_guest_quota(Auth_Store *p_store) { char guest_id[37]; assert(test__make_uuid(guest_id)); make_guest(p_store, guest_id); int64 window_start = 1700524800LL; /* UTC midnight 2023-11-21, used as window ID */ int64 expires = (int64)time(NULL) + 7200LL; /* 2 h from now — always future */ const int64 turns_limit = 3; const int64 tokens_limit = 1000; const int64 req_tokens = 200; /* 1. Initial usage is zero. */ { Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage( p_store, guest_id, window_start, &u) == AUTH_STORE_OK); assert(u.turns_used == 0); assert(u.output_tokens_used == 0); assert(u.output_tokens_reserved == 0); } puts(" guest_quota/initial_usage: PASS"); /* 2. Reserve 3 turns; fourth must be TURNS_EXHAUSTED. */ { char req1[37], req2[37], req3[37]; assert(test__make_uuid(req1)); assert(test__make_uuid(req2)); assert(test__make_uuid(req3)); assert(Auth_Store_Guest_Reserve( p_store, guest_id, req1, window_start, req_tokens, turns_limit, tokens_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, guest_id, req2, window_start, req_tokens, turns_limit, tokens_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, guest_id, req3, window_start, req_tokens, turns_limit, tokens_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK); /* Check state: 3 turns used, 600 tokens reserved. */ Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage( p_store, guest_id, window_start, &u) == AUTH_STORE_OK); assert(u.turns_used == 3); assert(u.output_tokens_used == 0); assert(u.output_tokens_reserved == req_tokens * 3); /* Fourth reservation: turns exhausted. */ char req4[37]; assert(test__make_uuid(req4)); assert(Auth_Store_Guest_Reserve( p_store, guest_id, req4, window_start, req_tokens, turns_limit, tokens_limit, expires) == AUTH_STORE_GUEST_QUOTA_TURNS_EXHAUSTED); puts(" guest_quota/turn_exhaustion: PASS"); /* Release all 3 reservations (keeps turn charge). */ assert(Auth_Store_Guest_Release(p_store, req1) == AUTH_STORE_OK); assert(Auth_Store_Guest_Release(p_store, req2) == AUTH_STORE_OK); assert(Auth_Store_Guest_Release(p_store, req3) == AUTH_STORE_OK); /* After release: turns still charged, tokens freed. */ assert(Auth_Store_Guest_Get_Usage( p_store, guest_id, window_start, &u) == AUTH_STORE_OK); assert(u.turns_used == 3); assert(u.output_tokens_reserved == 0); puts(" guest_quota/release_retains_turns: PASS"); } /* 3. Token exhaustion on a fresh window. */ { char guest2[37]; assert(test__make_uuid(guest2)); make_guest(p_store, guest2); int64 win2 = window_start + 86400LL; const int64 small_limit = 250; char r1[37], r2[37]; assert(test__make_uuid(r1)); assert(test__make_uuid(r2)); /* Reserve 200; only 250 total → second 200 would overflow. */ assert(Auth_Store_Guest_Reserve( p_store, guest2, r1, win2, 200, 10, small_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, guest2, r2, win2, 200, 10, small_limit, expires) == AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED); puts(" guest_quota/token_exhaustion: PASS"); /* Reconcile r1 with 150 actual (< 200 reserved). */ assert(Auth_Store_Guest_Reconcile(p_store, r1, 150) == AUTH_STORE_OK); Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage( p_store, guest2, win2, &u) == AUTH_STORE_OK); assert(u.output_tokens_used == 150); assert(u.output_tokens_reserved == 0); puts(" guest_quota/reconcile_actual_lt_reserved: PASS"); /* Reconcile with actual > reserved and charge the provider's actual use. */ char r3[37]; assert(test__make_uuid(r3)); assert(Auth_Store_Guest_Reserve( p_store, guest2, r3, win2, 50, 10, small_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reconcile(p_store, r3, 9999) == AUTH_STORE_OK); assert(Auth_Store_Guest_Get_Usage( p_store, guest2, win2, &u) == AUTH_STORE_OK); assert(u.output_tokens_used == 150 + 9999); puts(" guest_quota/reconcile_oversize_actual: PASS"); } /* 4. Idempotent reconcile/release. */ { char guest3[37]; assert(test__make_uuid(guest3)); make_guest(p_store, guest3); int64 win3 = window_start + 2 * 86400LL; char rid[37]; assert(test__make_uuid(rid)); assert(Auth_Store_Guest_Reserve( p_store, guest3, rid, win3, 100, 5, 500, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reconcile(p_store, rid, 80) == AUTH_STORE_OK); /* Second reconcile on same request_id: idempotent OK. */ assert(Auth_Store_Guest_Reconcile(p_store, rid, 80) == AUTH_STORE_OK); /* Release after reconcile: idempotent OK. */ assert(Auth_Store_Guest_Release(p_store, rid) == AUTH_STORE_OK); puts(" guest_quota/idempotent_reconcile_release: PASS"); } /* 5. Concurrent reservation invariant: two reservations, check totals. */ { char guest4[37]; assert(test__make_uuid(guest4)); make_guest(p_store, guest4); int64 win4 = window_start + 3 * 86400LL; char ra[37], rb[37]; assert(test__make_uuid(ra)); assert(test__make_uuid(rb)); assert(Auth_Store_Guest_Reserve( p_store, guest4, ra, win4, 300, 5, 500, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, guest4, rb, win4, 300, 5, 500, expires) == AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED); /* 300 used, 300 reserved → 600 total, limit 500 → second fails. */ Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage( p_store, guest4, win4, &u) == AUTH_STORE_OK); /* invariant: used + reserved <= limit */ assert(u.output_tokens_used + u.output_tokens_reserved <= 500); puts(" guest_quota/concurrent_invariant: PASS"); assert(Auth_Store_Guest_Release(p_store, ra) == AUTH_STORE_OK); } /* 6. Clear all reservations (login transfer). */ { char guest5[37]; assert(test__make_uuid(guest5)); make_guest(p_store, guest5); int64 win5 = window_start + 4 * 86400LL; char rc[37], rd[37]; assert(test__make_uuid(rc)); assert(test__make_uuid(rd)); assert(Auth_Store_Guest_Reserve( p_store, guest5, rc, win5, 100, 5, 500, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, guest5, rd, win5, 100, 5, 500, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Clear_Reservations( p_store, guest5) == AUTH_STORE_OK); Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage( p_store, guest5, win5, &u) == AUTH_STORE_OK); assert(u.output_tokens_reserved == 0); assert(u.turns_used == 2); /* turns kept */ puts(" guest_quota/clear_reservations: PASS"); } /* 7. UTC rollover: windows are independent. */ { char guest6[37]; assert(test__make_uuid(guest6)); make_guest(p_store, guest6); int64 winA = window_start + 5 * 86400LL; int64 winB = winA + 86400LL; char re[37], rf[37]; assert(test__make_uuid(re)); assert(test__make_uuid(rf)); assert(Auth_Store_Guest_Reserve( p_store, guest6, re, winA, 100, 2, 200, expires) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, guest6, rf, winA, 100, 2, 200, expires) == AUTH_STORE_GUEST_QUOTA_OK); /* winA full; winB is a fresh window. */ char rg[37]; assert(test__make_uuid(rg)); assert(Auth_Store_Guest_Reserve( p_store, guest6, rg, winB, 100, 2, 200, expires) == AUTH_STORE_GUEST_QUOTA_OK); Auth_Store_Guest_Usage uB; assert(Auth_Store_Guest_Get_Usage( p_store, guest6, winB, &uB) == AUTH_STORE_OK); assert(uB.turns_used == 1); puts(" guest_quota/utc_rollover: PASS"); assert(Auth_Store_Guest_Release(p_store, re) == AUTH_STORE_OK); assert(Auth_Store_Guest_Release(p_store, rf) == AUTH_STORE_OK); assert(Auth_Store_Guest_Release(p_store, rg) == AUTH_STORE_OK); } puts("test_guest_quota: PASS"); }/* ------------------------------------------------------------------ */ /* 19. Migration v2 preserves legacy guest_usage.count in turns_used */ /* ------------------------------------------------------------------ */ /* * Build a genuine v1-only database using a raw Deita connection (no * Auth_Store_Create) so that the v2 migration has not yet run. Insert * a legacy guest_usage row with count=7 and no turns_used column, then * open the database through Auth_Store_Create — which applies v2 — * and verify that turns_used is backfilled from count. */ static void test_migration_v2_legacy_backfill(const char *db_path) { (void)db_path; /* we use our own temp file */ char legacy_db[] = "/tmp/zenbu-auth-legacy-XXXXXX"; int fd = mkstemp(legacy_db); assert(fd >= 0); close(fd); /* Build the v1-only schema directly. */ Deita_Connection *p_conn = Deita_Connection_Create( DEITA_DATABASE_TYPE_SQLITE3, legacy_db); assert(p_conn); Deita_Query_Execute_Update(p_conn, "PRAGMA foreign_keys = OFF;" "PRAGMA journal_mode = WAL;"); /* auth_schema_migrations ledger */ Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS auth_schema_migrations (" " version INTEGER PRIMARY KEY," " applied_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" ");"); /* v1 tables (condensed: only what the migration test needs) */ Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS guest_identities (" " id TEXT PRIMARY KEY," " ip_binding_digest TEXT NOT NULL," " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " expires_at INTEGER NOT NULL" ");"); Deita_Query_Execute_Update(p_conn, /* v1 guest_usage: count only, no turns_used/token columns */ "CREATE TABLE IF NOT EXISTS guest_usage (" " guest_id TEXT NOT NULL" " REFERENCES guest_identities(id) ON DELETE CASCADE," " window_start INTEGER NOT NULL," " count INTEGER NOT NULL DEFAULT 0," " PRIMARY KEY (guest_id, window_start)" ");"); Deita_Query_Execute_Update(p_conn, /* v1 reservations: no request_id, no token count */ "CREATE TABLE IF NOT EXISTS guest_usage_reservations (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " guest_id TEXT NOT NULL" " REFERENCES guest_identities(id) ON DELETE CASCADE," " reserved_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " expires_at INTEGER NOT NULL" ");"); /* Stub other tables so FK checks don't break */ Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS users (" " id TEXT PRIMARY KEY," " username TEXT NOT NULL," " normalized_username TEXT NOT NULL UNIQUE," " password_hash TEXT NOT NULL," " role TEXT NOT NULL," " status TEXT NOT NULL DEFAULT 'active'," " must_change_password INTEGER NOT NULL DEFAULT 0," " password_changed_at INTEGER NOT NULL DEFAULT 0," " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" ");"); Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS auth_sessions (" " token_digest TEXT PRIMARY KEY," " csrf_digest TEXT NOT NULL," " user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE," " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))," " idle_expires_at INTEGER NOT NULL," " absolute_expires_at INTEGER NOT NULL," " password_changed_at_snapshot INTEGER NOT NULL DEFAULT 0," " revoked_at INTEGER" ");"); Deita_Query_Execute_Update(p_conn, "CREATE TABLE IF NOT EXISTS admin_audit_log (" " id INTEGER PRIMARY KEY AUTOINCREMENT," " actor_user_id TEXT," " action TEXT NOT NULL," " target_user_id TEXT," " detail TEXT," " created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))" ");"); /* Mark v1 as applied; v2 is intentionally absent. */ Deita_Query_Execute_Update(p_conn, "INSERT OR IGNORE INTO auth_schema_migrations (version) VALUES (1);"); /* Insert a guest identity and a legacy usage row with count=7. */ char g_id[37]; assert(test__make_uuid(g_id)); const char *gid_p[] = {g_id}; Deita_Query_Execute_Update_Prepared(p_conn, "INSERT INTO guest_identities (id, ip_binding_digest, expires_at)" " VALUES (?, 'test-digest', 9999999999)", 1, gid_p); int64 win = 1700524800LL; char win_str[32]; snprintf(win_str, sizeof(win_str), "%lld", (long long)win); const char *gu_p[] = {g_id, win_str}; /* count = 7 in the legacy column */ Deita_Query_Execute_Update_Prepared(p_conn, "INSERT INTO guest_usage (guest_id, window_start, count) VALUES (?, ?, 7)", 2, gu_p); Deita_Connection_Close(p_conn); /* Open through Auth_Store_Create: should apply v2 (backfill turns_used). */ Auth_Store *p_store = Auth_Store_Create(legacy_db); assert(p_store); Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage( p_store, g_id, win, &u) == AUTH_STORE_OK); assert(u.turns_used == 7); /* backfilled from count */ puts("test_migration_v2_legacy_backfill: PASS"); Auth_Store_Destroy(p_store); unlink(legacy_db); } /* ------------------------------------------------------------------ */ /* 20. Expired reservation reaping */ /* ------------------------------------------------------------------ */ static void test_reap_expired_reservations(Auth_Store *p_store) { char gid[37]; assert(test__make_uuid(gid)); make_guest(p_store, gid); int64 now = (int64)time(NULL); /* Use a window far in the future so it can never collide with clock-based * reaping inside Auth_Store_Guest_Reserve. */ int64 win = now + 86400LL; /* tomorrow's window */ /* "past" and "future" are relative to now_sim (= now + 3600), but both * must be > now so the internal Reserve reap does not touch them. */ int64 past = now + 1800LL; /* expires in 30 min: past from now_sim */ int64 future = now + 7200LL; /* expires in 2 h: future from now_sim */ int64 now_sim = now + 3600LL; /* simulated "now": 1 h from now */ char ra[37], rb[37]; assert(test__make_uuid(ra)); assert(test__make_uuid(rb)); /* Reserve two turns: ra expires before now_sim, rb expires after. */ assert(Auth_Store_Guest_Reserve( p_store, gid, ra, win, 100, 10, 1000, past) == AUTH_STORE_GUEST_QUOTA_OK); assert(Auth_Store_Guest_Reserve( p_store, gid, rb, win, 200, 10, 1000, future) == AUTH_STORE_GUEST_QUOTA_OK); Auth_Store_Guest_Usage u; assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK); assert(u.turns_used == 2); assert(u.output_tokens_reserved == 300); /* 100 + 200 */ /* Reap with now_sim > past but < future: only ra should be reaped. */ assert(Auth_Store_Guest_Reap_Expired(p_store, now_sim) == AUTH_STORE_OK); assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK); assert(u.turns_used == 2); /* turns retained */ assert(u.output_tokens_reserved == 200); /* only ra's 100 removed */ puts(" reap/partial: PASS"); /* Idempotent: reaping again changes nothing. */ assert(Auth_Store_Guest_Reap_Expired(p_store, now_sim) == AUTH_STORE_OK); assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK); assert(u.output_tokens_reserved == 200); puts(" reap/idempotent: PASS"); /* Reap with future time: rb is now expired too. */ assert(Auth_Store_Guest_Reap_Expired(p_store, future + 1) == AUTH_STORE_OK); assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK); assert(u.output_tokens_reserved == 0); assert(u.turns_used == 2); puts(" reap/all_expired: PASS"); /* Release on an already-reaped reservation: idempotent OK. */ assert(Auth_Store_Guest_Release(p_store, ra) == AUTH_STORE_OK); assert(Auth_Store_Guest_Release(p_store, rb) == AUTH_STORE_OK); puts(" reap/release_after_reap: PASS"); puts("test_reap_expired_reservations: PASS"); } /* ------------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------------ */ int main(void) { char db_path[] = "/tmp/zenbu-auth-XXXXXX"; int fd = mkstemp(db_path); assert(fd >= 0); close(fd); test_migrations_idempotent(db_path); test_migration_v2_legacy_backfill(db_path); Auth_Store *p_store = Auth_Store_Create(db_path); assert(p_store); test_username_normalization(); /* Bootstrap and last-admin tests must run first (only one admin). */ test_bootstrap_admin(p_store); test_last_admin_protection(p_store); test_username_uniqueness(p_store); test_user_lookup(p_store); test_forced_password_flag(p_store); test_session_lifecycle(p_store); test_stale_password_snapshot(p_store); test_disabled_user(p_store); test_password_update_revokes_others(p_store); test_revoke_all_sessions(p_store); test_expired_session(p_store); test_guest_identity(p_store, db_path); test_rotate_session(p_store); test_create_session_cas(p_store); test_self_change_password(p_store); test_audited_admin_operations(p_store, db_path); test_audit_failure_rollback(p_store, db_path); test_guest_quota(p_store); test_reap_expired_reservations(p_store); Auth_Store_Destroy(p_store); unlink(db_path); puts("auth_store_test: ALL PASS"); return 0; }