view mrjunejune/test/admin_api_test.c @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents
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;
}