view mrjunejune/test/admin_api_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

/*
 * admin_api_test.c — unit tests for the admin API handlers.
 *
 * Tests run in-process against real store + crypto (no network).
 * A fresh SQLite database is created per group.
 */

#include "mrjunejune/admin_api.h"
#include "mrjunejune/auth_api.h"

#include "auth/auth_crypto.h"
#include "auth/auth_store.h"
#include "deita/deita.h"
#include "dowa/dowa.h"
#include "seobeo/seobeo.h"

#include <assert.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <openssl/crypto.h>

/* ------------------------------------------------------------------ */
/* Utilities                                                            */
/* ------------------------------------------------------------------ */

#define ASSERT(cond) \
  do { \
    if (!(cond)) { \
      fprintf(stderr, "FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
      abort(); \
    } \
  } while (0)

#define TEST(name) \
  do { fprintf(stdout, "  %-60s", name); fflush(stdout); } while (0)

#define PASS() \
  do { fprintf(stdout, "PASS\n"); } while (0)

static const uint8 k_secret[64] = {
  0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
  0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
  0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
  0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
  0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
  0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
  0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
  0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40,
};

static void make_temp_db(char *out, size_t capacity)
{
  snprintf(out, capacity, "/tmp/admin_api_test_XXXXXX");
  int fd = mkstemp(out);
  ASSERT(fd >= 0);
  close(fd);
}

static void init_auth(const char *db)
{
  boolean ok = Auth_API_Init(
      db, k_secret, sizeof(k_secret),
      NULL, NULL, NULL,
      AUTH_API_SESSION_IDLE_TTL_DEFAULT,
      AUTH_API_SESSION_ABS_TTL_DEFAULT,
      AUTH_API_GUEST_TTL_DEFAULT,
      TRUE);  /* dev_insecure_cookie */
  ASSERT(ok);
}

static Seobeo_Request_Entry *make_request(Dowa_Arena *arena, ...)
{
  Seobeo_Request_Entry *req = NULL;
  va_list ap;
  va_start(ap, arena);
  const char *key;
  while ((key = va_arg(ap, const char *)) != NULL)
  {
    char *k = (char *)key;
    const char *val = va_arg(ap, const char *);
    char *stored = (char *)val;
    if (strcmp(key, "Body") == 0)
      stored = Dowa_Arena_Copy(arena, val, strlen(val) + 1);
    Dowa_HashMap_Push_Arena(req, k, stored, arena);
  }
  va_end(ap);
  return req;
}

static const char *resp_status(Seobeo_Request_Entry *resp)
{
  void *p = Dowa_HashMap_Get_Ptr(resp, "status");
  return p ? ((Seobeo_Request_Entry *)p)->value : NULL;
}

static const char *resp_body(Seobeo_Request_Entry *resp)
{
  void *p = Dowa_HashMap_Get_Ptr(resp, "body");
  return p ? ((Seobeo_Request_Entry *)p)->value : NULL;
}

/*
 * Create a user and return the new user_id.
 * Hash a real password so the store accepts it.
 */
static boolean create_test_user(
    Auth_Store  *store,
    const char  *username,
    const char  *password,
    const char  *role,
    boolean      must_change,
    char         id_out[37])
{
  char hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  if (Auth_Crypto_Password_Hash(password, hash, sizeof(hash)) != AUTH_CRYPTO_OK)
    return FALSE;
  Auth_Store_Result r = Auth_Store_Create_User(
      store, username, hash, role, must_change, id_out);
  OPENSSL_cleanse(hash, sizeof(hash));
  return r == AUTH_STORE_OK;
}

/*
 * Login as a user and return the session cookie value + CSRF token.
 * Returns TRUE on success.
 */
static boolean login_user(
    Dowa_Arena *arena,
    const char *username,
    const char *password,
    char        session_cookie_out[],
    size_t      cookie_cap,
    char        csrf_out[],
    size_t      csrf_cap)
{
  /* 1. Get a guest session to obtain a CSRF token + guest cookie. */
  Seobeo_Request_Entry *sess_req = make_request(
      arena,
      "Host",        "localhost",
      "Remote-Addr", "127.0.0.1",
      NULL, NULL);
  Seobeo_Request_Entry *sess_resp =
      Auth_API_Test_Session_Handler(sess_req, arena);
  const char *sess_body = resp_body(sess_resp);
  ASSERT(sess_body);

  /* Extract csrfToken from session response. */
  const char *csrf_start = strstr(sess_body, "\"csrfToken\":\"");
  ASSERT(csrf_start);
  csrf_start += strlen("\"csrfToken\":\"");
  const char *csrf_end = strchr(csrf_start, '"');
  ASSERT(csrf_end);
  size_t csrf_len = (size_t)(csrf_end - csrf_start);
  ASSERT(csrf_len < csrf_cap);
  memcpy(csrf_out, csrf_start, csrf_len);
  csrf_out[csrf_len] = '\0';

  /* Extract the guest cookie from Set-Cookie to send with login. */
  char guest_cookie_val[512] = {0};
  void *sc_kv = Dowa_HashMap_Get_Ptr(sess_resp, "Set-Cookie");
  if (sc_kv)
  {
    const char *sc_hdr = ((Seobeo_Request_Entry *)sc_kv)->value;
    const char *gc_start = strstr(sc_hdr, "mjj_guest=");
    if (gc_start)
    {
      gc_start += strlen("mjj_guest=");
      const char *gc_end = strchr(gc_start, ';');
      size_t gclen = gc_end
          ? (size_t)(gc_end - gc_start)
          : strlen(gc_start);
      if (gclen < sizeof(guest_cookie_val))
      {
        memcpy(guest_cookie_val, gc_start, gclen);
        guest_cookie_val[gclen] = '\0';
      }
    }
  }

  /* 2. Call login with the guest cookie so Resolve_Principal finds the same guest. */
  char login_body[512];
  snprintf(login_body, sizeof(login_body),
           "{\"username\":\"%s\",\"password\":\"%s\",\"csrfToken\":\"%s\"}",
           username, password, csrf_out);

  char cookie_hdr[512] = {0};
  if (guest_cookie_val[0] != '\0')
    snprintf(cookie_hdr, sizeof(cookie_hdr),
             "mjj_guest=%s", guest_cookie_val);

  Seobeo_Request_Entry *login_req;
  if (cookie_hdr[0] != '\0')
  {
    login_req = make_request(
        arena,
        "Host",        "localhost",
        "Remote-Addr", "127.0.0.1",
        "Origin",      "http://localhost",
        "Cookie",      cookie_hdr,
        "Body",        login_body,
        NULL, NULL);
  }
  else
  {
    login_req = make_request(
        arena,
        "Host",        "localhost",
        "Remote-Addr", "127.0.0.1",
        "Origin",      "http://localhost",
        "Body",        login_body,
        NULL, NULL);
  }

  Seobeo_Request_Entry *login_resp =
      Auth_API_Test_Login_Handler(login_req, arena);
  const char *st = resp_status(login_resp);
  if (!st || strcmp(st, "200") != 0)
    return FALSE;

  /* 3. Extract session cookie value. */
  void *lsc_kv = Dowa_HashMap_Get_Ptr(login_resp, "Set-Cookie");
  if (!lsc_kv) return FALSE;
  const char *lsc_hdr = ((Seobeo_Request_Entry *)lsc_kv)->value;
  const char *cookie_start = strstr(lsc_hdr, "mjj_session=");
  if (!cookie_start) return FALSE;
  cookie_start += strlen("mjj_session=");
  const char *cookie_end = strchr(cookie_start, ';');
  size_t clen = cookie_end
      ? (size_t)(cookie_end - cookie_start)
      : strlen(cookie_start);
  ASSERT(clen < cookie_cap);
  memcpy(session_cookie_out, cookie_start, clen);
  session_cookie_out[clen] = '\0';

  /* 4. Fetch a fresh CSRF from the authenticated session. */
  char auth_cookie_hdr[512];
  snprintf(auth_cookie_hdr, sizeof(auth_cookie_hdr),
           "mjj_session=%s", session_cookie_out);
  Seobeo_Request_Entry *csrfsess_req = make_request(
      arena,
      "Host",        "localhost",
      "Remote-Addr", "127.0.0.1",
      "Cookie",      auth_cookie_hdr,
      NULL, NULL);
  Seobeo_Request_Entry *csrfsess_resp =
      Auth_API_Test_Session_Handler(csrfsess_req, arena);
  const char *csrfsess_body = resp_body(csrfsess_resp);
  ASSERT(csrfsess_body);
  const char *cs2 = strstr(csrfsess_body, "\"csrfToken\":\"");
  ASSERT(cs2);
  cs2 += strlen("\"csrfToken\":\"");
  const char *ce2 = strchr(cs2, '"');
  ASSERT(ce2);
  size_t cl2 = (size_t)(ce2 - cs2);
  ASSERT(cl2 < csrf_cap);
  memcpy(csrf_out, cs2, cl2);
  csrf_out[cl2] = '\0';

  return TRUE;
}

/* Build a request with session cookie + CSRF header + body. */
static Seobeo_Request_Entry *make_admin_req(
    Dowa_Arena  *arena,
    const char  *method,
    const char  *session_cookie,
    const char  *csrf_token,
    const char  *body,
    const char  *id_param)
{
  /* Allocate cookie_hdr from arena so the pointer stays valid after return. */
  char *cookie_hdr = Dowa_Arena_Allocate(arena, 528);
  ASSERT(cookie_hdr);
  snprintf(cookie_hdr, 528, "mjj_session=%s", session_cookie);

  Seobeo_Request_Entry *req = make_request(
      arena,
      "Host",          "localhost",
      "Remote-Addr",   "127.0.0.1",
      "Origin",        "http://localhost",
      "Cookie",        cookie_hdr,
      "X-CSRF-Token",  (char *)csrf_token,
      "Body",          body ? (char *)body : "",
      NULL, NULL);
  if (id_param)
    Dowa_HashMap_Push_Arena(req, ":id", (char *)id_param, arena);
  return req;
  (void)method;
}

/* ------------------------------------------------------------------ */
/* Test group: non-admin denial                                         */
/* ------------------------------------------------------------------ */

static void test_non_admin_denial(void)
{
  printf("\n[non-admin denial]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char member_id[37];
  ASSERT(create_test_user(store, "member1", "password123456", "member", FALSE, member_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);

  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "member1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("member cannot list users (403)");
  {
    Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  TEST("member cannot create users (403)");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"hack\",\"temporaryPassword\":\"password123456\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  TEST("unauthenticated list → 401");
  {
    Seobeo_Request_Entry *req = make_request(
        arena,
        "Host",        "localhost",
        "Remote-Addr", "127.0.0.1",
        NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "401") == 0);
    /* No page content must leak */
    const char *body = resp_body(resp);
    ASSERT(!body || strstr(body, "password") == NULL);
  }
  PASS();

  TEST("unauthenticated page → redirect (not content)");
  {
    Seobeo_Request_Entry *req = make_request(
        arena,
        "Host",        "localhost",
        "Remote-Addr", "127.0.0.1",
        NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Page_Handler(req, arena);
    const char *st = resp_status(resp);
    /* Must be 302 redirect; body must be empty/no admin content */
    ASSERT(st && (strcmp(st, "302") == 0 || strcmp(st, "401") == 0));
    const char *body = resp_body(resp);
    ASSERT(!body || strstr(body, "<table") == NULL);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: forced-password denial                                   */
/* ------------------------------------------------------------------ */

static void test_forced_password_denial(void)
{
  printf("\n[forced-password denial]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37];
  ASSERT(create_test_user(store, "fadmin", "password123456", "admin", TRUE, admin_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);

  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "fadmin", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("admin with must_change_password blocked from list (403)");
  {
    Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  TEST("admin with must_change_password blocked from create (403)");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"newu\",\"temporaryPassword\":\"password123456\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: create user                                              */
/* ------------------------------------------------------------------ */

static void test_create_user(void)
{
  printf("\n[create user]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("create user succeeds → 201, mustChangePassword=true");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"newuser\",\"temporaryPassword\":\"temppass123456\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "201") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "\"mustChangePassword\":true") != NULL);
    /* No hash/digest in response */
    ASSERT(strstr(body, "hash") == NULL);
    ASSERT(strstr(body, "password_hash") == NULL);
    ASSERT(strstr(body, "digest") == NULL);
    ASSERT(strstr(body, "session") == NULL);
  }
  PASS();

  TEST("duplicate username → 409");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"newuser\",\"temporaryPassword\":\"temppass123456\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "409") == 0);
  }
  PASS();

  TEST("short password → 400 policy error");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"shortpw\",\"temporaryPassword\":\"tooshort\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "400") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "password_policy") != NULL);
  }
  PASS();

  TEST("invalid role → 400");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"badrole\",\"temporaryPassword\":\"temppass123456\",\"role\":\"superuser\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "400") == 0);
  }
  PASS();

  TEST("CSRF rejection → 403");
  {
    char bad_cookie_hdr[512];
    snprintf(bad_cookie_hdr, sizeof(bad_cookie_hdr), "mjj_session=%s", scookie);
    Seobeo_Request_Entry *req = make_request(
        arena,
        "Host",          "localhost",
        "Remote-Addr",   "127.0.0.1",
        "Origin",        "http://localhost",
        "Cookie",        bad_cookie_hdr,
        "X-CSRF-Token",  "BADCSRF",
        "Body",          "{\"username\":\"x\",\"temporaryPassword\":\"temppass123456\"}",
        NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  TEST("origin rejection → 403");
  {
    char cookie_hdr[512];
    snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", scookie);
    Seobeo_Request_Entry *req = make_request(
        arena,
        "Host",          "localhost",
        "Remote-Addr",   "127.0.0.1",
        "Origin",        "http://evil.com",
        "Cookie",        cookie_hdr,
        "X-CSRF-Token",  csrf,
        "Body",          "{\"username\":\"y\",\"temporaryPassword\":\"temppass123456\"}",
        NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  TEST("audit insertion failure rolls back user creation");
  {
    Deita_Connection *connection = Deita_Connection_Create(
        DEITA_DATABASE_TYPE_SQLITE3, db);
    ASSERT(connection);
    ASSERT(Deita_Query_Execute_Update(
        connection,
        "CREATE TRIGGER fail_admin_api_audit"
        " BEFORE INSERT ON admin_audit_log"
        " BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END") >= 0);
    Deita_Connection_Close(connection);

    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"auditfail\","
        "\"temporaryPassword\":\"temppass123456\"}",
        NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "500") == 0);

    Auth_User_Auth_Record record;
    memset(&record, 0, sizeof(record));
    ASSERT(Auth_Store_Find_User_By_Username(
        store, "auditfail", &record) == AUTH_STORE_NOT_FOUND);

    connection = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, db);
    ASSERT(connection);
    ASSERT(Deita_Query_Execute_Update(
        connection, "DROP TRIGGER fail_admin_api_audit") >= 0);
    Deita_Connection_Close(connection);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: enable/disable                                           */
/* ------------------------------------------------------------------ */

static void test_enable_disable(void)
{
  printf("\n[enable/disable]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37], user_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));
  ASSERT(create_test_user(store, "user1",  "password123456", "member", FALSE, user_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  const char *user_token_digest =
      "1111111111111111111111111111111111111111111111111111111111111111";
  const char *user_csrf_digest =
      "2222222222222222222222222222222222222222222222222222222222222222";
  int64 session_now = (int64)time(NULL);
  Auth_Session_Record user_session;
  ASSERT(Auth_Store_Create_Session(
      store, user_id, user_token_digest, user_csrf_digest,
      3600, 86400, session_now, &user_session) == AUTH_STORE_OK);

  TEST("disable user → 200, status=disabled");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf, "{\"op\":\"disable\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "\"status\":\"disabled\"") != NULL);
    Auth_Session_Record found_session;
    Auth_User_Record found_user;
    ASSERT(Auth_Store_Find_Session(
        store, user_token_digest, session_now + 1,
        &found_session, &found_user) == AUTH_STORE_REVOKED);
  }
  PASS();

  TEST("enable user → 200, status=active");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf, "{\"op\":\"enable\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "\"status\":\"active\"") != NULL);
    Auth_Session_Record found_session;
    Auth_User_Record found_user;
    ASSERT(Auth_Store_Find_Session(
        store, user_token_digest, session_now + 2,
        &found_session, &found_user) == AUTH_STORE_REVOKED);
  }
  PASS();

  TEST("disable last active admin → 409 last_admin");
  {
    /* admin1 is the only admin */
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf, "{\"op\":\"disable\"}", admin_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "409") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "last_admin") != NULL);
  }
  PASS();

  TEST("response body contains no secrets");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf, "{\"op\":\"enable\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "hash") == NULL);
    ASSERT(body && strstr(body, "digest") == NULL);
    ASSERT(body && strstr(body, "session") == NULL);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: role update                                              */
/* ------------------------------------------------------------------ */

static void test_role_update(void)
{
  printf("\n[role update]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37], user_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));
  ASSERT(create_test_user(store, "user1",  "password123456", "member", FALSE, user_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("promote member to admin → 200");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf,
        "{\"op\":\"set_role\",\"role\":\"admin\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    ASSERT(strstr(resp_body(resp), "\"role\":\"admin\"") != NULL);
  }
  PASS();

  TEST("demote admin to member → 200 (2 admins → safe)");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf,
        "{\"op\":\"set_role\",\"role\":\"member\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
  }
  PASS();

  TEST("demote last admin → 409");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf,
        "{\"op\":\"set_role\",\"role\":\"member\"}", admin_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "409") == 0);
  }
  PASS();

  TEST("invalid role value → 400");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf,
        "{\"op\":\"set_role\",\"role\":\"superuser\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "400") == 0);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: temp password reset                                      */
/* ------------------------------------------------------------------ */

static void test_temp_reset(void)
{
  printf("\n[temp password reset]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37], user_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));
  ASSERT(create_test_user(store, "user1",  "password123456", "member", FALSE, user_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("temp reset sets mustChangePassword=true in response");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf,
        "{\"op\":\"temp_reset\",\"temporaryPassword\":\"newtemp123456\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "\"mustChangePassword\":true") != NULL);
    /* No raw password, hash, or digest in response. */
    ASSERT(strstr(body, "hash")             == NULL);
    ASSERT(strstr(body, "digest")           == NULL);
    ASSERT(strstr(body, "newtemp123456")    == NULL);
  }
  PASS();

  TEST("temp reset short password → 400");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "PATCH", scookie, csrf,
        "{\"op\":\"temp_reset\",\"temporaryPassword\":\"short\"}", user_id);
    Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "400") == 0);
  }
  PASS();

  TEST("user can login after reset with new temp password");
  {
    /* Login as user1 with the new temp password. */
    char u_cookie[512], u_csrf[512];
    boolean ok = login_user(arena, "user1", "newtemp123456",
                            u_cookie, sizeof(u_cookie),
                            u_csrf, sizeof(u_csrf));
    ASSERT(ok);
    /* Confirm must_change_password is set. */
    char cookie_hdr[512];
    snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", u_cookie);
    Seobeo_Request_Entry *sreq = make_request(
        arena,
        "Host",        "localhost",
        "Remote-Addr", "127.0.0.1",
        "Cookie",      cookie_hdr,
        NULL, NULL);
    Seobeo_Request_Entry *sresp = Auth_API_Test_Session_Handler(sreq, arena);
    const char *sbody = resp_body(sresp);
    ASSERT(sbody && strstr(sbody, "\"mustChangePassword\":true") != NULL);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: session revocation                                       */
/* ------------------------------------------------------------------ */

static void test_session_revocation(void)
{
  printf("\n[session revocation]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37], user_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));
  ASSERT(create_test_user(store, "user1",  "password123456", "member", FALSE, user_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char admin_cookie[512], admin_csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    admin_cookie, sizeof(admin_cookie),
                    admin_csrf, sizeof(admin_csrf)));

  /* Login user1 to create a session. */
  char u_cookie[512], u_csrf[512];
  ASSERT(login_user(arena, "user1", "password123456",
                    u_cookie, sizeof(u_cookie), u_csrf, sizeof(u_csrf)));

  TEST("DELETE /api/admin/users/:id/sessions → 200");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "DELETE", admin_cookie, admin_csrf, NULL, user_id);
    Seobeo_Request_Entry *resp =
        Admin_API_Test_Revoke_Sessions_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
  }
  PASS();

  TEST("user session is invalid after revocation");
  {
    char cookie_hdr[512];
    snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", u_cookie);
    Seobeo_Request_Entry *sreq = make_request(
        arena,
        "Host",        "localhost",
        "Remote-Addr", "127.0.0.1",
        "Cookie",      cookie_hdr,
        NULL, NULL);
    Seobeo_Request_Entry *sresp = Auth_API_Test_Session_Handler(sreq, arena);
    const char *sbody = resp_body(sresp);
    /* Should fall back to guest after session revoked */
    ASSERT(sbody && strstr(sbody, "\"kind\":\"user\"") == NULL);
  }
  PASS();

  TEST("revoke sessions for non-existent user → 404");
  {
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "DELETE", admin_cookie, admin_csrf, NULL,
        "00000000-0000-0000-0000-000000000000");
    Seobeo_Request_Entry *resp =
        Admin_API_Test_Revoke_Sessions_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "404") == 0);
  }
  PASS();

  TEST("CSRF required for DELETE → 403 without CSRF");
  {
    char cookie_hdr[512];
    snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", admin_cookie);
    Seobeo_Request_Entry *req = make_request(
        arena,
        "Host",          "localhost",
        "Remote-Addr",   "127.0.0.1",
        "Origin",        "http://localhost",
        "Cookie",        cookie_hdr,
        ":id",           user_id,
        NULL, NULL);
    Seobeo_Request_Entry *resp =
        Admin_API_Test_Revoke_Sessions_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "403") == 0);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: no secrets in responses                                  */
/* ------------------------------------------------------------------ */

static void test_no_secret_fields(void)
{
  printf("\n[no secret fields in JSON/page]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));
  ASSERT(create_test_user(store, "user2", "password123456", "member", FALSE, admin_id));

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("list response has no password_hash, session, digest, or guest fields");
  {
    Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    const char *body = resp_body(resp);
    ASSERT(body);
    ASSERT(strstr(body, "password_hash")   == NULL);
    ASSERT(strstr(body, "passwordHash")    == NULL);
    ASSERT(strstr(body, "token_digest")    == NULL);
    ASSERT(strstr(body, "csrf_digest")     == NULL);
    ASSERT(strstr(body, "guest_id")        == NULL);
    ASSERT(strstr(body, "ip_binding")      == NULL);
    ASSERT(strstr(body, "session")         == NULL);
  }
  PASS();

  TEST("audit log for create contains no credentials");
  {
    /* Create a user, then verify audit log doesn't have hash/password. */
    Seobeo_Request_Entry *req = make_admin_req(
        arena, "POST", scookie, csrf,
        "{\"username\":\"audituser\",\"temporaryPassword\":\"auditpass123456\"}", NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "201") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "auditpass123456") == NULL);
    ASSERT(body && strstr(body, "hash") == NULL);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* Test group: list pagination                                          */
/* ------------------------------------------------------------------ */

static void test_list_pagination(void)
{
  printf("\n[list pagination]\n");

  char db[256];
  make_temp_db(db, sizeof(db));
  init_auth(db);
  Auth_Store *store = Auth_API_Get_Store();

  char admin_id[37];
  ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id));
  /* Create 5 more users. */
  for (int i = 0; i < 5; i++)
  {
    char uname[32], uid[37];
    snprintf(uname, sizeof(uname), "user%d", i);
    ASSERT(create_test_user(store, uname, "password123456", "member", FALSE, uid));
  }

  Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024);
  char scookie[512], csrf[512];
  ASSERT(login_user(arena, "admin1", "password123456",
                    scookie, sizeof(scookie), csrf, sizeof(csrf)));

  TEST("list all users includes total count");
  {
    Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL);
    Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "\"total\":6") != NULL);
    ASSERT(body && strstr(body, "\"users\":[") != NULL);
  }
  PASS();

  TEST("huge page number returns an empty page without overflow");
  {
    Seobeo_Request_Entry *req =
        make_admin_req(arena, "GET", scookie, csrf, NULL, NULL);
    Dowa_HashMap_Push_Arena(
        req, "Query-page", "9223372036854775807", arena);
    Dowa_HashMap_Push_Arena(req, "Query-limit", "100", arena);
    Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena);
    ASSERT(strcmp(resp_status(resp), "200") == 0);
    const char *body = resp_body(resp);
    ASSERT(body && strstr(body, "\"users\":[]") != NULL);
  }
  PASS();

  Dowa_Arena_Free(arena);
  Auth_API_Destroy();
  unlink(db);
}

/* ------------------------------------------------------------------ */
/* main                                                                 */
/* ------------------------------------------------------------------ */

int main(void)
{
  printf("=== admin_api_test ===\n");

  test_non_admin_denial();
  test_forced_password_denial();
  test_create_user();
  test_enable_disable();
  test_role_update();
  test_temp_reset();
  test_session_revocation();
  test_no_secret_fields();
  test_list_pagination();

  printf("\n=== ALL TESTS PASSED ===\n");
  return 0;
}