Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 263:ee04e4e69fed | 264:04fee26ecce0 |
|---|---|
| 1 /* | |
| 2 * admin_api_test.c — unit tests for the admin API handlers. | |
| 3 * | |
| 4 * Tests run in-process against real store + crypto (no network). | |
| 5 * A fresh SQLite database is created per group. | |
| 6 */ | |
| 7 | |
| 8 #include "mrjunejune/admin_api.h" | |
| 9 #include "mrjunejune/auth_api.h" | |
| 10 | |
| 11 #include "auth/auth_crypto.h" | |
| 12 #include "auth/auth_store.h" | |
| 13 #include "deita/deita.h" | |
| 14 #include "dowa/dowa.h" | |
| 15 #include "seobeo/seobeo.h" | |
| 16 | |
| 17 #include <assert.h> | |
| 18 #include <stdarg.h> | |
| 19 #include <stdio.h> | |
| 20 #include <string.h> | |
| 21 #include <stdlib.h> | |
| 22 #include <unistd.h> | |
| 23 | |
| 24 #include <openssl/crypto.h> | |
| 25 | |
| 26 /* ------------------------------------------------------------------ */ | |
| 27 /* Utilities */ | |
| 28 /* ------------------------------------------------------------------ */ | |
| 29 | |
| 30 #define ASSERT(cond) \ | |
| 31 do { \ | |
| 32 if (!(cond)) { \ | |
| 33 fprintf(stderr, "FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \ | |
| 34 abort(); \ | |
| 35 } \ | |
| 36 } while (0) | |
| 37 | |
| 38 #define TEST(name) \ | |
| 39 do { fprintf(stdout, " %-60s", name); fflush(stdout); } while (0) | |
| 40 | |
| 41 #define PASS() \ | |
| 42 do { fprintf(stdout, "PASS\n"); } while (0) | |
| 43 | |
| 44 static const uint8 k_secret[64] = { | |
| 45 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, | |
| 46 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, | |
| 47 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, | |
| 48 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, | |
| 49 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, | |
| 50 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, | |
| 51 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, | |
| 52 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, | |
| 53 }; | |
| 54 | |
| 55 static void make_temp_db(char *out, size_t capacity) | |
| 56 { | |
| 57 snprintf(out, capacity, "/tmp/admin_api_test_XXXXXX"); | |
| 58 int fd = mkstemp(out); | |
| 59 ASSERT(fd >= 0); | |
| 60 close(fd); | |
| 61 } | |
| 62 | |
| 63 static void init_auth(const char *db) | |
| 64 { | |
| 65 boolean ok = Auth_API_Init( | |
| 66 db, k_secret, sizeof(k_secret), | |
| 67 NULL, NULL, NULL, | |
| 68 AUTH_API_SESSION_IDLE_TTL_DEFAULT, | |
| 69 AUTH_API_SESSION_ABS_TTL_DEFAULT, | |
| 70 AUTH_API_GUEST_TTL_DEFAULT, | |
| 71 TRUE); /* dev_insecure_cookie */ | |
| 72 ASSERT(ok); | |
| 73 } | |
| 74 | |
| 75 static Seobeo_Request_Entry *make_request(Dowa_Arena *arena, ...) | |
| 76 { | |
| 77 Seobeo_Request_Entry *req = NULL; | |
| 78 va_list ap; | |
| 79 va_start(ap, arena); | |
| 80 const char *key; | |
| 81 while ((key = va_arg(ap, const char *)) != NULL) | |
| 82 { | |
| 83 char *k = (char *)key; | |
| 84 const char *val = va_arg(ap, const char *); | |
| 85 char *stored = (char *)val; | |
| 86 if (strcmp(key, "Body") == 0) | |
| 87 stored = Dowa_Arena_Copy(arena, val, strlen(val) + 1); | |
| 88 Dowa_HashMap_Push_Arena(req, k, stored, arena); | |
| 89 } | |
| 90 va_end(ap); | |
| 91 return req; | |
| 92 } | |
| 93 | |
| 94 static const char *resp_status(Seobeo_Request_Entry *resp) | |
| 95 { | |
| 96 void *p = Dowa_HashMap_Get_Ptr(resp, "status"); | |
| 97 return p ? ((Seobeo_Request_Entry *)p)->value : NULL; | |
| 98 } | |
| 99 | |
| 100 static const char *resp_body(Seobeo_Request_Entry *resp) | |
| 101 { | |
| 102 void *p = Dowa_HashMap_Get_Ptr(resp, "body"); | |
| 103 return p ? ((Seobeo_Request_Entry *)p)->value : NULL; | |
| 104 } | |
| 105 | |
| 106 /* | |
| 107 * Create a user and return the new user_id. | |
| 108 * Hash a real password so the store accepts it. | |
| 109 */ | |
| 110 static boolean create_test_user( | |
| 111 Auth_Store *store, | |
| 112 const char *username, | |
| 113 const char *password, | |
| 114 const char *role, | |
| 115 boolean must_change, | |
| 116 char id_out[37]) | |
| 117 { | |
| 118 char hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; | |
| 119 if (Auth_Crypto_Password_Hash(password, hash, sizeof(hash)) != AUTH_CRYPTO_OK) | |
| 120 return FALSE; | |
| 121 Auth_Store_Result r = Auth_Store_Create_User( | |
| 122 store, username, hash, role, must_change, id_out); | |
| 123 OPENSSL_cleanse(hash, sizeof(hash)); | |
| 124 return r == AUTH_STORE_OK; | |
| 125 } | |
| 126 | |
| 127 /* | |
| 128 * Login as a user and return the session cookie value + CSRF token. | |
| 129 * Returns TRUE on success. | |
| 130 */ | |
| 131 static boolean login_user( | |
| 132 Dowa_Arena *arena, | |
| 133 const char *username, | |
| 134 const char *password, | |
| 135 char session_cookie_out[], | |
| 136 size_t cookie_cap, | |
| 137 char csrf_out[], | |
| 138 size_t csrf_cap) | |
| 139 { | |
| 140 /* 1. Get a guest session to obtain a CSRF token + guest cookie. */ | |
| 141 Seobeo_Request_Entry *sess_req = make_request( | |
| 142 arena, | |
| 143 "Host", "localhost", | |
| 144 "Remote-Addr", "127.0.0.1", | |
| 145 NULL, NULL); | |
| 146 Seobeo_Request_Entry *sess_resp = | |
| 147 Auth_API_Test_Session_Handler(sess_req, arena); | |
| 148 const char *sess_body = resp_body(sess_resp); | |
| 149 ASSERT(sess_body); | |
| 150 | |
| 151 /* Extract csrfToken from session response. */ | |
| 152 const char *csrf_start = strstr(sess_body, "\"csrfToken\":\""); | |
| 153 ASSERT(csrf_start); | |
| 154 csrf_start += strlen("\"csrfToken\":\""); | |
| 155 const char *csrf_end = strchr(csrf_start, '"'); | |
| 156 ASSERT(csrf_end); | |
| 157 size_t csrf_len = (size_t)(csrf_end - csrf_start); | |
| 158 ASSERT(csrf_len < csrf_cap); | |
| 159 memcpy(csrf_out, csrf_start, csrf_len); | |
| 160 csrf_out[csrf_len] = '\0'; | |
| 161 | |
| 162 /* Extract the guest cookie from Set-Cookie to send with login. */ | |
| 163 char guest_cookie_val[512] = {0}; | |
| 164 void *sc_kv = Dowa_HashMap_Get_Ptr(sess_resp, "Set-Cookie"); | |
| 165 if (sc_kv) | |
| 166 { | |
| 167 const char *sc_hdr = ((Seobeo_Request_Entry *)sc_kv)->value; | |
| 168 const char *gc_start = strstr(sc_hdr, "mjj_guest="); | |
| 169 if (gc_start) | |
| 170 { | |
| 171 gc_start += strlen("mjj_guest="); | |
| 172 const char *gc_end = strchr(gc_start, ';'); | |
| 173 size_t gclen = gc_end | |
| 174 ? (size_t)(gc_end - gc_start) | |
| 175 : strlen(gc_start); | |
| 176 if (gclen < sizeof(guest_cookie_val)) | |
| 177 { | |
| 178 memcpy(guest_cookie_val, gc_start, gclen); | |
| 179 guest_cookie_val[gclen] = '\0'; | |
| 180 } | |
| 181 } | |
| 182 } | |
| 183 | |
| 184 /* 2. Call login with the guest cookie so Resolve_Principal finds the same guest. */ | |
| 185 char login_body[512]; | |
| 186 snprintf(login_body, sizeof(login_body), | |
| 187 "{\"username\":\"%s\",\"password\":\"%s\",\"csrfToken\":\"%s\"}", | |
| 188 username, password, csrf_out); | |
| 189 | |
| 190 char cookie_hdr[512] = {0}; | |
| 191 if (guest_cookie_val[0] != '\0') | |
| 192 snprintf(cookie_hdr, sizeof(cookie_hdr), | |
| 193 "mjj_guest=%s", guest_cookie_val); | |
| 194 | |
| 195 Seobeo_Request_Entry *login_req; | |
| 196 if (cookie_hdr[0] != '\0') | |
| 197 { | |
| 198 login_req = make_request( | |
| 199 arena, | |
| 200 "Host", "localhost", | |
| 201 "Remote-Addr", "127.0.0.1", | |
| 202 "Origin", "http://localhost", | |
| 203 "Cookie", cookie_hdr, | |
| 204 "Body", login_body, | |
| 205 NULL, NULL); | |
| 206 } | |
| 207 else | |
| 208 { | |
| 209 login_req = make_request( | |
| 210 arena, | |
| 211 "Host", "localhost", | |
| 212 "Remote-Addr", "127.0.0.1", | |
| 213 "Origin", "http://localhost", | |
| 214 "Body", login_body, | |
| 215 NULL, NULL); | |
| 216 } | |
| 217 | |
| 218 Seobeo_Request_Entry *login_resp = | |
| 219 Auth_API_Test_Login_Handler(login_req, arena); | |
| 220 const char *st = resp_status(login_resp); | |
| 221 if (!st || strcmp(st, "200") != 0) | |
| 222 return FALSE; | |
| 223 | |
| 224 /* 3. Extract session cookie value. */ | |
| 225 void *lsc_kv = Dowa_HashMap_Get_Ptr(login_resp, "Set-Cookie"); | |
| 226 if (!lsc_kv) return FALSE; | |
| 227 const char *lsc_hdr = ((Seobeo_Request_Entry *)lsc_kv)->value; | |
| 228 const char *cookie_start = strstr(lsc_hdr, "mjj_session="); | |
| 229 if (!cookie_start) return FALSE; | |
| 230 cookie_start += strlen("mjj_session="); | |
| 231 const char *cookie_end = strchr(cookie_start, ';'); | |
| 232 size_t clen = cookie_end | |
| 233 ? (size_t)(cookie_end - cookie_start) | |
| 234 : strlen(cookie_start); | |
| 235 ASSERT(clen < cookie_cap); | |
| 236 memcpy(session_cookie_out, cookie_start, clen); | |
| 237 session_cookie_out[clen] = '\0'; | |
| 238 | |
| 239 /* 4. Fetch a fresh CSRF from the authenticated session. */ | |
| 240 char auth_cookie_hdr[512]; | |
| 241 snprintf(auth_cookie_hdr, sizeof(auth_cookie_hdr), | |
| 242 "mjj_session=%s", session_cookie_out); | |
| 243 Seobeo_Request_Entry *csrfsess_req = make_request( | |
| 244 arena, | |
| 245 "Host", "localhost", | |
| 246 "Remote-Addr", "127.0.0.1", | |
| 247 "Cookie", auth_cookie_hdr, | |
| 248 NULL, NULL); | |
| 249 Seobeo_Request_Entry *csrfsess_resp = | |
| 250 Auth_API_Test_Session_Handler(csrfsess_req, arena); | |
| 251 const char *csrfsess_body = resp_body(csrfsess_resp); | |
| 252 ASSERT(csrfsess_body); | |
| 253 const char *cs2 = strstr(csrfsess_body, "\"csrfToken\":\""); | |
| 254 ASSERT(cs2); | |
| 255 cs2 += strlen("\"csrfToken\":\""); | |
| 256 const char *ce2 = strchr(cs2, '"'); | |
| 257 ASSERT(ce2); | |
| 258 size_t cl2 = (size_t)(ce2 - cs2); | |
| 259 ASSERT(cl2 < csrf_cap); | |
| 260 memcpy(csrf_out, cs2, cl2); | |
| 261 csrf_out[cl2] = '\0'; | |
| 262 | |
| 263 return TRUE; | |
| 264 } | |
| 265 | |
| 266 /* Build a request with session cookie + CSRF header + body. */ | |
| 267 static Seobeo_Request_Entry *make_admin_req( | |
| 268 Dowa_Arena *arena, | |
| 269 const char *method, | |
| 270 const char *session_cookie, | |
| 271 const char *csrf_token, | |
| 272 const char *body, | |
| 273 const char *id_param) | |
| 274 { | |
| 275 /* Allocate cookie_hdr from arena so the pointer stays valid after return. */ | |
| 276 char *cookie_hdr = Dowa_Arena_Allocate(arena, 528); | |
| 277 ASSERT(cookie_hdr); | |
| 278 snprintf(cookie_hdr, 528, "mjj_session=%s", session_cookie); | |
| 279 | |
| 280 Seobeo_Request_Entry *req = make_request( | |
| 281 arena, | |
| 282 "Host", "localhost", | |
| 283 "Remote-Addr", "127.0.0.1", | |
| 284 "Origin", "http://localhost", | |
| 285 "Cookie", cookie_hdr, | |
| 286 "X-CSRF-Token", (char *)csrf_token, | |
| 287 "Body", body ? (char *)body : "", | |
| 288 NULL, NULL); | |
| 289 if (id_param) | |
| 290 Dowa_HashMap_Push_Arena(req, ":id", (char *)id_param, arena); | |
| 291 return req; | |
| 292 (void)method; | |
| 293 } | |
| 294 | |
| 295 /* ------------------------------------------------------------------ */ | |
| 296 /* Test group: non-admin denial */ | |
| 297 /* ------------------------------------------------------------------ */ | |
| 298 | |
| 299 static void test_non_admin_denial(void) | |
| 300 { | |
| 301 printf("\n[non-admin denial]\n"); | |
| 302 | |
| 303 char db[256]; | |
| 304 make_temp_db(db, sizeof(db)); | |
| 305 init_auth(db); | |
| 306 Auth_Store *store = Auth_API_Get_Store(); | |
| 307 | |
| 308 char member_id[37]; | |
| 309 ASSERT(create_test_user(store, "member1", "password123456", "member", FALSE, member_id)); | |
| 310 | |
| 311 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 312 | |
| 313 char scookie[512], csrf[512]; | |
| 314 ASSERT(login_user(arena, "member1", "password123456", | |
| 315 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 316 | |
| 317 TEST("member cannot list users (403)"); | |
| 318 { | |
| 319 Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL); | |
| 320 Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena); | |
| 321 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 322 } | |
| 323 PASS(); | |
| 324 | |
| 325 TEST("member cannot create users (403)"); | |
| 326 { | |
| 327 Seobeo_Request_Entry *req = make_admin_req( | |
| 328 arena, "POST", scookie, csrf, | |
| 329 "{\"username\":\"hack\",\"temporaryPassword\":\"password123456\"}", NULL); | |
| 330 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 331 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 332 } | |
| 333 PASS(); | |
| 334 | |
| 335 TEST("unauthenticated list → 401"); | |
| 336 { | |
| 337 Seobeo_Request_Entry *req = make_request( | |
| 338 arena, | |
| 339 "Host", "localhost", | |
| 340 "Remote-Addr", "127.0.0.1", | |
| 341 NULL, NULL); | |
| 342 Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena); | |
| 343 ASSERT(strcmp(resp_status(resp), "401") == 0); | |
| 344 /* No page content must leak */ | |
| 345 const char *body = resp_body(resp); | |
| 346 ASSERT(!body || strstr(body, "password") == NULL); | |
| 347 } | |
| 348 PASS(); | |
| 349 | |
| 350 TEST("unauthenticated page → redirect (not content)"); | |
| 351 { | |
| 352 Seobeo_Request_Entry *req = make_request( | |
| 353 arena, | |
| 354 "Host", "localhost", | |
| 355 "Remote-Addr", "127.0.0.1", | |
| 356 NULL, NULL); | |
| 357 Seobeo_Request_Entry *resp = Admin_API_Test_Page_Handler(req, arena); | |
| 358 const char *st = resp_status(resp); | |
| 359 /* Must be 302 redirect; body must be empty/no admin content */ | |
| 360 ASSERT(st && (strcmp(st, "302") == 0 || strcmp(st, "401") == 0)); | |
| 361 const char *body = resp_body(resp); | |
| 362 ASSERT(!body || strstr(body, "<table") == NULL); | |
| 363 } | |
| 364 PASS(); | |
| 365 | |
| 366 Dowa_Arena_Free(arena); | |
| 367 Auth_API_Destroy(); | |
| 368 unlink(db); | |
| 369 } | |
| 370 | |
| 371 /* ------------------------------------------------------------------ */ | |
| 372 /* Test group: forced-password denial */ | |
| 373 /* ------------------------------------------------------------------ */ | |
| 374 | |
| 375 static void test_forced_password_denial(void) | |
| 376 { | |
| 377 printf("\n[forced-password denial]\n"); | |
| 378 | |
| 379 char db[256]; | |
| 380 make_temp_db(db, sizeof(db)); | |
| 381 init_auth(db); | |
| 382 Auth_Store *store = Auth_API_Get_Store(); | |
| 383 | |
| 384 char admin_id[37]; | |
| 385 ASSERT(create_test_user(store, "fadmin", "password123456", "admin", TRUE, admin_id)); | |
| 386 | |
| 387 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 388 | |
| 389 char scookie[512], csrf[512]; | |
| 390 ASSERT(login_user(arena, "fadmin", "password123456", | |
| 391 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 392 | |
| 393 TEST("admin with must_change_password blocked from list (403)"); | |
| 394 { | |
| 395 Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL); | |
| 396 Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena); | |
| 397 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 398 } | |
| 399 PASS(); | |
| 400 | |
| 401 TEST("admin with must_change_password blocked from create (403)"); | |
| 402 { | |
| 403 Seobeo_Request_Entry *req = make_admin_req( | |
| 404 arena, "POST", scookie, csrf, | |
| 405 "{\"username\":\"newu\",\"temporaryPassword\":\"password123456\"}", NULL); | |
| 406 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 407 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 408 } | |
| 409 PASS(); | |
| 410 | |
| 411 Dowa_Arena_Free(arena); | |
| 412 Auth_API_Destroy(); | |
| 413 unlink(db); | |
| 414 } | |
| 415 | |
| 416 /* ------------------------------------------------------------------ */ | |
| 417 /* Test group: create user */ | |
| 418 /* ------------------------------------------------------------------ */ | |
| 419 | |
| 420 static void test_create_user(void) | |
| 421 { | |
| 422 printf("\n[create user]\n"); | |
| 423 | |
| 424 char db[256]; | |
| 425 make_temp_db(db, sizeof(db)); | |
| 426 init_auth(db); | |
| 427 Auth_Store *store = Auth_API_Get_Store(); | |
| 428 | |
| 429 char admin_id[37]; | |
| 430 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 431 | |
| 432 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 433 char scookie[512], csrf[512]; | |
| 434 ASSERT(login_user(arena, "admin1", "password123456", | |
| 435 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 436 | |
| 437 TEST("create user succeeds → 201, mustChangePassword=true"); | |
| 438 { | |
| 439 Seobeo_Request_Entry *req = make_admin_req( | |
| 440 arena, "POST", scookie, csrf, | |
| 441 "{\"username\":\"newuser\",\"temporaryPassword\":\"temppass123456\"}", NULL); | |
| 442 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 443 ASSERT(strcmp(resp_status(resp), "201") == 0); | |
| 444 const char *body = resp_body(resp); | |
| 445 ASSERT(body && strstr(body, "\"mustChangePassword\":true") != NULL); | |
| 446 /* No hash/digest in response */ | |
| 447 ASSERT(strstr(body, "hash") == NULL); | |
| 448 ASSERT(strstr(body, "password_hash") == NULL); | |
| 449 ASSERT(strstr(body, "digest") == NULL); | |
| 450 ASSERT(strstr(body, "session") == NULL); | |
| 451 } | |
| 452 PASS(); | |
| 453 | |
| 454 TEST("duplicate username → 409"); | |
| 455 { | |
| 456 Seobeo_Request_Entry *req = make_admin_req( | |
| 457 arena, "POST", scookie, csrf, | |
| 458 "{\"username\":\"newuser\",\"temporaryPassword\":\"temppass123456\"}", NULL); | |
| 459 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 460 ASSERT(strcmp(resp_status(resp), "409") == 0); | |
| 461 } | |
| 462 PASS(); | |
| 463 | |
| 464 TEST("short password → 400 policy error"); | |
| 465 { | |
| 466 Seobeo_Request_Entry *req = make_admin_req( | |
| 467 arena, "POST", scookie, csrf, | |
| 468 "{\"username\":\"shortpw\",\"temporaryPassword\":\"tooshort\"}", NULL); | |
| 469 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 470 ASSERT(strcmp(resp_status(resp), "400") == 0); | |
| 471 const char *body = resp_body(resp); | |
| 472 ASSERT(body && strstr(body, "password_policy") != NULL); | |
| 473 } | |
| 474 PASS(); | |
| 475 | |
| 476 TEST("invalid role → 400"); | |
| 477 { | |
| 478 Seobeo_Request_Entry *req = make_admin_req( | |
| 479 arena, "POST", scookie, csrf, | |
| 480 "{\"username\":\"badrole\",\"temporaryPassword\":\"temppass123456\",\"role\":\"superuser\"}", NULL); | |
| 481 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 482 ASSERT(strcmp(resp_status(resp), "400") == 0); | |
| 483 } | |
| 484 PASS(); | |
| 485 | |
| 486 TEST("CSRF rejection → 403"); | |
| 487 { | |
| 488 char bad_cookie_hdr[512]; | |
| 489 snprintf(bad_cookie_hdr, sizeof(bad_cookie_hdr), "mjj_session=%s", scookie); | |
| 490 Seobeo_Request_Entry *req = make_request( | |
| 491 arena, | |
| 492 "Host", "localhost", | |
| 493 "Remote-Addr", "127.0.0.1", | |
| 494 "Origin", "http://localhost", | |
| 495 "Cookie", bad_cookie_hdr, | |
| 496 "X-CSRF-Token", "BADCSRF", | |
| 497 "Body", "{\"username\":\"x\",\"temporaryPassword\":\"temppass123456\"}", | |
| 498 NULL, NULL); | |
| 499 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 500 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 501 } | |
| 502 PASS(); | |
| 503 | |
| 504 TEST("origin rejection → 403"); | |
| 505 { | |
| 506 char cookie_hdr[512]; | |
| 507 snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", scookie); | |
| 508 Seobeo_Request_Entry *req = make_request( | |
| 509 arena, | |
| 510 "Host", "localhost", | |
| 511 "Remote-Addr", "127.0.0.1", | |
| 512 "Origin", "http://evil.com", | |
| 513 "Cookie", cookie_hdr, | |
| 514 "X-CSRF-Token", csrf, | |
| 515 "Body", "{\"username\":\"y\",\"temporaryPassword\":\"temppass123456\"}", | |
| 516 NULL, NULL); | |
| 517 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 518 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 519 } | |
| 520 PASS(); | |
| 521 | |
| 522 TEST("audit insertion failure rolls back user creation"); | |
| 523 { | |
| 524 Deita_Connection *connection = Deita_Connection_Create( | |
| 525 DEITA_DATABASE_TYPE_SQLITE3, db); | |
| 526 ASSERT(connection); | |
| 527 ASSERT(Deita_Query_Execute_Update( | |
| 528 connection, | |
| 529 "CREATE TRIGGER fail_admin_api_audit" | |
| 530 " BEFORE INSERT ON admin_audit_log" | |
| 531 " BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END") >= 0); | |
| 532 Deita_Connection_Close(connection); | |
| 533 | |
| 534 Seobeo_Request_Entry *req = make_admin_req( | |
| 535 arena, "POST", scookie, csrf, | |
| 536 "{\"username\":\"auditfail\"," | |
| 537 "\"temporaryPassword\":\"temppass123456\"}", | |
| 538 NULL); | |
| 539 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 540 ASSERT(strcmp(resp_status(resp), "500") == 0); | |
| 541 | |
| 542 Auth_User_Auth_Record record; | |
| 543 memset(&record, 0, sizeof(record)); | |
| 544 ASSERT(Auth_Store_Find_User_By_Username( | |
| 545 store, "auditfail", &record) == AUTH_STORE_NOT_FOUND); | |
| 546 | |
| 547 connection = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, db); | |
| 548 ASSERT(connection); | |
| 549 ASSERT(Deita_Query_Execute_Update( | |
| 550 connection, "DROP TRIGGER fail_admin_api_audit") >= 0); | |
| 551 Deita_Connection_Close(connection); | |
| 552 } | |
| 553 PASS(); | |
| 554 | |
| 555 Dowa_Arena_Free(arena); | |
| 556 Auth_API_Destroy(); | |
| 557 unlink(db); | |
| 558 } | |
| 559 | |
| 560 /* ------------------------------------------------------------------ */ | |
| 561 /* Test group: enable/disable */ | |
| 562 /* ------------------------------------------------------------------ */ | |
| 563 | |
| 564 static void test_enable_disable(void) | |
| 565 { | |
| 566 printf("\n[enable/disable]\n"); | |
| 567 | |
| 568 char db[256]; | |
| 569 make_temp_db(db, sizeof(db)); | |
| 570 init_auth(db); | |
| 571 Auth_Store *store = Auth_API_Get_Store(); | |
| 572 | |
| 573 char admin_id[37], user_id[37]; | |
| 574 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 575 ASSERT(create_test_user(store, "user1", "password123456", "member", FALSE, user_id)); | |
| 576 | |
| 577 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 578 char scookie[512], csrf[512]; | |
| 579 ASSERT(login_user(arena, "admin1", "password123456", | |
| 580 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 581 | |
| 582 const char *user_token_digest = | |
| 583 "1111111111111111111111111111111111111111111111111111111111111111"; | |
| 584 const char *user_csrf_digest = | |
| 585 "2222222222222222222222222222222222222222222222222222222222222222"; | |
| 586 int64 session_now = (int64)time(NULL); | |
| 587 Auth_Session_Record user_session; | |
| 588 ASSERT(Auth_Store_Create_Session( | |
| 589 store, user_id, user_token_digest, user_csrf_digest, | |
| 590 3600, 86400, session_now, &user_session) == AUTH_STORE_OK); | |
| 591 | |
| 592 TEST("disable user → 200, status=disabled"); | |
| 593 { | |
| 594 Seobeo_Request_Entry *req = make_admin_req( | |
| 595 arena, "PATCH", scookie, csrf, "{\"op\":\"disable\"}", user_id); | |
| 596 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 597 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 598 const char *body = resp_body(resp); | |
| 599 ASSERT(body && strstr(body, "\"status\":\"disabled\"") != NULL); | |
| 600 Auth_Session_Record found_session; | |
| 601 Auth_User_Record found_user; | |
| 602 ASSERT(Auth_Store_Find_Session( | |
| 603 store, user_token_digest, session_now + 1, | |
| 604 &found_session, &found_user) == AUTH_STORE_REVOKED); | |
| 605 } | |
| 606 PASS(); | |
| 607 | |
| 608 TEST("enable user → 200, status=active"); | |
| 609 { | |
| 610 Seobeo_Request_Entry *req = make_admin_req( | |
| 611 arena, "PATCH", scookie, csrf, "{\"op\":\"enable\"}", user_id); | |
| 612 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 613 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 614 const char *body = resp_body(resp); | |
| 615 ASSERT(body && strstr(body, "\"status\":\"active\"") != NULL); | |
| 616 Auth_Session_Record found_session; | |
| 617 Auth_User_Record found_user; | |
| 618 ASSERT(Auth_Store_Find_Session( | |
| 619 store, user_token_digest, session_now + 2, | |
| 620 &found_session, &found_user) == AUTH_STORE_REVOKED); | |
| 621 } | |
| 622 PASS(); | |
| 623 | |
| 624 TEST("disable last active admin → 409 last_admin"); | |
| 625 { | |
| 626 /* admin1 is the only admin */ | |
| 627 Seobeo_Request_Entry *req = make_admin_req( | |
| 628 arena, "PATCH", scookie, csrf, "{\"op\":\"disable\"}", admin_id); | |
| 629 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 630 ASSERT(strcmp(resp_status(resp), "409") == 0); | |
| 631 const char *body = resp_body(resp); | |
| 632 ASSERT(body && strstr(body, "last_admin") != NULL); | |
| 633 } | |
| 634 PASS(); | |
| 635 | |
| 636 TEST("response body contains no secrets"); | |
| 637 { | |
| 638 Seobeo_Request_Entry *req = make_admin_req( | |
| 639 arena, "PATCH", scookie, csrf, "{\"op\":\"enable\"}", user_id); | |
| 640 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 641 const char *body = resp_body(resp); | |
| 642 ASSERT(body && strstr(body, "hash") == NULL); | |
| 643 ASSERT(body && strstr(body, "digest") == NULL); | |
| 644 ASSERT(body && strstr(body, "session") == NULL); | |
| 645 } | |
| 646 PASS(); | |
| 647 | |
| 648 Dowa_Arena_Free(arena); | |
| 649 Auth_API_Destroy(); | |
| 650 unlink(db); | |
| 651 } | |
| 652 | |
| 653 /* ------------------------------------------------------------------ */ | |
| 654 /* Test group: role update */ | |
| 655 /* ------------------------------------------------------------------ */ | |
| 656 | |
| 657 static void test_role_update(void) | |
| 658 { | |
| 659 printf("\n[role update]\n"); | |
| 660 | |
| 661 char db[256]; | |
| 662 make_temp_db(db, sizeof(db)); | |
| 663 init_auth(db); | |
| 664 Auth_Store *store = Auth_API_Get_Store(); | |
| 665 | |
| 666 char admin_id[37], user_id[37]; | |
| 667 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 668 ASSERT(create_test_user(store, "user1", "password123456", "member", FALSE, user_id)); | |
| 669 | |
| 670 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 671 char scookie[512], csrf[512]; | |
| 672 ASSERT(login_user(arena, "admin1", "password123456", | |
| 673 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 674 | |
| 675 TEST("promote member to admin → 200"); | |
| 676 { | |
| 677 Seobeo_Request_Entry *req = make_admin_req( | |
| 678 arena, "PATCH", scookie, csrf, | |
| 679 "{\"op\":\"set_role\",\"role\":\"admin\"}", user_id); | |
| 680 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 681 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 682 ASSERT(strstr(resp_body(resp), "\"role\":\"admin\"") != NULL); | |
| 683 } | |
| 684 PASS(); | |
| 685 | |
| 686 TEST("demote admin to member → 200 (2 admins → safe)"); | |
| 687 { | |
| 688 Seobeo_Request_Entry *req = make_admin_req( | |
| 689 arena, "PATCH", scookie, csrf, | |
| 690 "{\"op\":\"set_role\",\"role\":\"member\"}", user_id); | |
| 691 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 692 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 693 } | |
| 694 PASS(); | |
| 695 | |
| 696 TEST("demote last admin → 409"); | |
| 697 { | |
| 698 Seobeo_Request_Entry *req = make_admin_req( | |
| 699 arena, "PATCH", scookie, csrf, | |
| 700 "{\"op\":\"set_role\",\"role\":\"member\"}", admin_id); | |
| 701 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 702 ASSERT(strcmp(resp_status(resp), "409") == 0); | |
| 703 } | |
| 704 PASS(); | |
| 705 | |
| 706 TEST("invalid role value → 400"); | |
| 707 { | |
| 708 Seobeo_Request_Entry *req = make_admin_req( | |
| 709 arena, "PATCH", scookie, csrf, | |
| 710 "{\"op\":\"set_role\",\"role\":\"superuser\"}", user_id); | |
| 711 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 712 ASSERT(strcmp(resp_status(resp), "400") == 0); | |
| 713 } | |
| 714 PASS(); | |
| 715 | |
| 716 Dowa_Arena_Free(arena); | |
| 717 Auth_API_Destroy(); | |
| 718 unlink(db); | |
| 719 } | |
| 720 | |
| 721 /* ------------------------------------------------------------------ */ | |
| 722 /* Test group: temp password reset */ | |
| 723 /* ------------------------------------------------------------------ */ | |
| 724 | |
| 725 static void test_temp_reset(void) | |
| 726 { | |
| 727 printf("\n[temp password reset]\n"); | |
| 728 | |
| 729 char db[256]; | |
| 730 make_temp_db(db, sizeof(db)); | |
| 731 init_auth(db); | |
| 732 Auth_Store *store = Auth_API_Get_Store(); | |
| 733 | |
| 734 char admin_id[37], user_id[37]; | |
| 735 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 736 ASSERT(create_test_user(store, "user1", "password123456", "member", FALSE, user_id)); | |
| 737 | |
| 738 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 739 char scookie[512], csrf[512]; | |
| 740 ASSERT(login_user(arena, "admin1", "password123456", | |
| 741 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 742 | |
| 743 TEST("temp reset sets mustChangePassword=true in response"); | |
| 744 { | |
| 745 Seobeo_Request_Entry *req = make_admin_req( | |
| 746 arena, "PATCH", scookie, csrf, | |
| 747 "{\"op\":\"temp_reset\",\"temporaryPassword\":\"newtemp123456\"}", user_id); | |
| 748 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 749 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 750 const char *body = resp_body(resp); | |
| 751 ASSERT(body && strstr(body, "\"mustChangePassword\":true") != NULL); | |
| 752 /* No raw password, hash, or digest in response. */ | |
| 753 ASSERT(strstr(body, "hash") == NULL); | |
| 754 ASSERT(strstr(body, "digest") == NULL); | |
| 755 ASSERT(strstr(body, "newtemp123456") == NULL); | |
| 756 } | |
| 757 PASS(); | |
| 758 | |
| 759 TEST("temp reset short password → 400"); | |
| 760 { | |
| 761 Seobeo_Request_Entry *req = make_admin_req( | |
| 762 arena, "PATCH", scookie, csrf, | |
| 763 "{\"op\":\"temp_reset\",\"temporaryPassword\":\"short\"}", user_id); | |
| 764 Seobeo_Request_Entry *resp = Admin_API_Test_Update_Handler(req, arena); | |
| 765 ASSERT(strcmp(resp_status(resp), "400") == 0); | |
| 766 } | |
| 767 PASS(); | |
| 768 | |
| 769 TEST("user can login after reset with new temp password"); | |
| 770 { | |
| 771 /* Login as user1 with the new temp password. */ | |
| 772 char u_cookie[512], u_csrf[512]; | |
| 773 boolean ok = login_user(arena, "user1", "newtemp123456", | |
| 774 u_cookie, sizeof(u_cookie), | |
| 775 u_csrf, sizeof(u_csrf)); | |
| 776 ASSERT(ok); | |
| 777 /* Confirm must_change_password is set. */ | |
| 778 char cookie_hdr[512]; | |
| 779 snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", u_cookie); | |
| 780 Seobeo_Request_Entry *sreq = make_request( | |
| 781 arena, | |
| 782 "Host", "localhost", | |
| 783 "Remote-Addr", "127.0.0.1", | |
| 784 "Cookie", cookie_hdr, | |
| 785 NULL, NULL); | |
| 786 Seobeo_Request_Entry *sresp = Auth_API_Test_Session_Handler(sreq, arena); | |
| 787 const char *sbody = resp_body(sresp); | |
| 788 ASSERT(sbody && strstr(sbody, "\"mustChangePassword\":true") != NULL); | |
| 789 } | |
| 790 PASS(); | |
| 791 | |
| 792 Dowa_Arena_Free(arena); | |
| 793 Auth_API_Destroy(); | |
| 794 unlink(db); | |
| 795 } | |
| 796 | |
| 797 /* ------------------------------------------------------------------ */ | |
| 798 /* Test group: session revocation */ | |
| 799 /* ------------------------------------------------------------------ */ | |
| 800 | |
| 801 static void test_session_revocation(void) | |
| 802 { | |
| 803 printf("\n[session revocation]\n"); | |
| 804 | |
| 805 char db[256]; | |
| 806 make_temp_db(db, sizeof(db)); | |
| 807 init_auth(db); | |
| 808 Auth_Store *store = Auth_API_Get_Store(); | |
| 809 | |
| 810 char admin_id[37], user_id[37]; | |
| 811 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 812 ASSERT(create_test_user(store, "user1", "password123456", "member", FALSE, user_id)); | |
| 813 | |
| 814 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 815 char admin_cookie[512], admin_csrf[512]; | |
| 816 ASSERT(login_user(arena, "admin1", "password123456", | |
| 817 admin_cookie, sizeof(admin_cookie), | |
| 818 admin_csrf, sizeof(admin_csrf))); | |
| 819 | |
| 820 /* Login user1 to create a session. */ | |
| 821 char u_cookie[512], u_csrf[512]; | |
| 822 ASSERT(login_user(arena, "user1", "password123456", | |
| 823 u_cookie, sizeof(u_cookie), u_csrf, sizeof(u_csrf))); | |
| 824 | |
| 825 TEST("DELETE /api/admin/users/:id/sessions → 200"); | |
| 826 { | |
| 827 Seobeo_Request_Entry *req = make_admin_req( | |
| 828 arena, "DELETE", admin_cookie, admin_csrf, NULL, user_id); | |
| 829 Seobeo_Request_Entry *resp = | |
| 830 Admin_API_Test_Revoke_Sessions_Handler(req, arena); | |
| 831 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 832 } | |
| 833 PASS(); | |
| 834 | |
| 835 TEST("user session is invalid after revocation"); | |
| 836 { | |
| 837 char cookie_hdr[512]; | |
| 838 snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", u_cookie); | |
| 839 Seobeo_Request_Entry *sreq = make_request( | |
| 840 arena, | |
| 841 "Host", "localhost", | |
| 842 "Remote-Addr", "127.0.0.1", | |
| 843 "Cookie", cookie_hdr, | |
| 844 NULL, NULL); | |
| 845 Seobeo_Request_Entry *sresp = Auth_API_Test_Session_Handler(sreq, arena); | |
| 846 const char *sbody = resp_body(sresp); | |
| 847 /* Should fall back to guest after session revoked */ | |
| 848 ASSERT(sbody && strstr(sbody, "\"kind\":\"user\"") == NULL); | |
| 849 } | |
| 850 PASS(); | |
| 851 | |
| 852 TEST("revoke sessions for non-existent user → 404"); | |
| 853 { | |
| 854 Seobeo_Request_Entry *req = make_admin_req( | |
| 855 arena, "DELETE", admin_cookie, admin_csrf, NULL, | |
| 856 "00000000-0000-0000-0000-000000000000"); | |
| 857 Seobeo_Request_Entry *resp = | |
| 858 Admin_API_Test_Revoke_Sessions_Handler(req, arena); | |
| 859 ASSERT(strcmp(resp_status(resp), "404") == 0); | |
| 860 } | |
| 861 PASS(); | |
| 862 | |
| 863 TEST("CSRF required for DELETE → 403 without CSRF"); | |
| 864 { | |
| 865 char cookie_hdr[512]; | |
| 866 snprintf(cookie_hdr, sizeof(cookie_hdr), "mjj_session=%s", admin_cookie); | |
| 867 Seobeo_Request_Entry *req = make_request( | |
| 868 arena, | |
| 869 "Host", "localhost", | |
| 870 "Remote-Addr", "127.0.0.1", | |
| 871 "Origin", "http://localhost", | |
| 872 "Cookie", cookie_hdr, | |
| 873 ":id", user_id, | |
| 874 NULL, NULL); | |
| 875 Seobeo_Request_Entry *resp = | |
| 876 Admin_API_Test_Revoke_Sessions_Handler(req, arena); | |
| 877 ASSERT(strcmp(resp_status(resp), "403") == 0); | |
| 878 } | |
| 879 PASS(); | |
| 880 | |
| 881 Dowa_Arena_Free(arena); | |
| 882 Auth_API_Destroy(); | |
| 883 unlink(db); | |
| 884 } | |
| 885 | |
| 886 /* ------------------------------------------------------------------ */ | |
| 887 /* Test group: no secrets in responses */ | |
| 888 /* ------------------------------------------------------------------ */ | |
| 889 | |
| 890 static void test_no_secret_fields(void) | |
| 891 { | |
| 892 printf("\n[no secret fields in JSON/page]\n"); | |
| 893 | |
| 894 char db[256]; | |
| 895 make_temp_db(db, sizeof(db)); | |
| 896 init_auth(db); | |
| 897 Auth_Store *store = Auth_API_Get_Store(); | |
| 898 | |
| 899 char admin_id[37]; | |
| 900 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 901 ASSERT(create_test_user(store, "user2", "password123456", "member", FALSE, admin_id)); | |
| 902 | |
| 903 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 904 char scookie[512], csrf[512]; | |
| 905 ASSERT(login_user(arena, "admin1", "password123456", | |
| 906 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 907 | |
| 908 TEST("list response has no password_hash, session, digest, or guest fields"); | |
| 909 { | |
| 910 Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL); | |
| 911 Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena); | |
| 912 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 913 const char *body = resp_body(resp); | |
| 914 ASSERT(body); | |
| 915 ASSERT(strstr(body, "password_hash") == NULL); | |
| 916 ASSERT(strstr(body, "passwordHash") == NULL); | |
| 917 ASSERT(strstr(body, "token_digest") == NULL); | |
| 918 ASSERT(strstr(body, "csrf_digest") == NULL); | |
| 919 ASSERT(strstr(body, "guest_id") == NULL); | |
| 920 ASSERT(strstr(body, "ip_binding") == NULL); | |
| 921 ASSERT(strstr(body, "session") == NULL); | |
| 922 } | |
| 923 PASS(); | |
| 924 | |
| 925 TEST("audit log for create contains no credentials"); | |
| 926 { | |
| 927 /* Create a user, then verify audit log doesn't have hash/password. */ | |
| 928 Seobeo_Request_Entry *req = make_admin_req( | |
| 929 arena, "POST", scookie, csrf, | |
| 930 "{\"username\":\"audituser\",\"temporaryPassword\":\"auditpass123456\"}", NULL); | |
| 931 Seobeo_Request_Entry *resp = Admin_API_Test_Create_Handler(req, arena); | |
| 932 ASSERT(strcmp(resp_status(resp), "201") == 0); | |
| 933 const char *body = resp_body(resp); | |
| 934 ASSERT(body && strstr(body, "auditpass123456") == NULL); | |
| 935 ASSERT(body && strstr(body, "hash") == NULL); | |
| 936 } | |
| 937 PASS(); | |
| 938 | |
| 939 Dowa_Arena_Free(arena); | |
| 940 Auth_API_Destroy(); | |
| 941 unlink(db); | |
| 942 } | |
| 943 | |
| 944 /* ------------------------------------------------------------------ */ | |
| 945 /* Test group: list pagination */ | |
| 946 /* ------------------------------------------------------------------ */ | |
| 947 | |
| 948 static void test_list_pagination(void) | |
| 949 { | |
| 950 printf("\n[list pagination]\n"); | |
| 951 | |
| 952 char db[256]; | |
| 953 make_temp_db(db, sizeof(db)); | |
| 954 init_auth(db); | |
| 955 Auth_Store *store = Auth_API_Get_Store(); | |
| 956 | |
| 957 char admin_id[37]; | |
| 958 ASSERT(create_test_user(store, "admin1", "password123456", "admin", FALSE, admin_id)); | |
| 959 /* Create 5 more users. */ | |
| 960 for (int i = 0; i < 5; i++) | |
| 961 { | |
| 962 char uname[32], uid[37]; | |
| 963 snprintf(uname, sizeof(uname), "user%d", i); | |
| 964 ASSERT(create_test_user(store, uname, "password123456", "member", FALSE, uid)); | |
| 965 } | |
| 966 | |
| 967 Dowa_Arena *arena = Dowa_Arena_Create(128 * 1024); | |
| 968 char scookie[512], csrf[512]; | |
| 969 ASSERT(login_user(arena, "admin1", "password123456", | |
| 970 scookie, sizeof(scookie), csrf, sizeof(csrf))); | |
| 971 | |
| 972 TEST("list all users includes total count"); | |
| 973 { | |
| 974 Seobeo_Request_Entry *req = make_admin_req(arena, "GET", scookie, csrf, NULL, NULL); | |
| 975 Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena); | |
| 976 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 977 const char *body = resp_body(resp); | |
| 978 ASSERT(body && strstr(body, "\"total\":6") != NULL); | |
| 979 ASSERT(body && strstr(body, "\"users\":[") != NULL); | |
| 980 } | |
| 981 PASS(); | |
| 982 | |
| 983 TEST("huge page number returns an empty page without overflow"); | |
| 984 { | |
| 985 Seobeo_Request_Entry *req = | |
| 986 make_admin_req(arena, "GET", scookie, csrf, NULL, NULL); | |
| 987 Dowa_HashMap_Push_Arena( | |
| 988 req, "Query-page", "9223372036854775807", arena); | |
| 989 Dowa_HashMap_Push_Arena(req, "Query-limit", "100", arena); | |
| 990 Seobeo_Request_Entry *resp = Admin_API_Test_List_Handler(req, arena); | |
| 991 ASSERT(strcmp(resp_status(resp), "200") == 0); | |
| 992 const char *body = resp_body(resp); | |
| 993 ASSERT(body && strstr(body, "\"users\":[]") != NULL); | |
| 994 } | |
| 995 PASS(); | |
| 996 | |
| 997 Dowa_Arena_Free(arena); | |
| 998 Auth_API_Destroy(); | |
| 999 unlink(db); | |
| 1000 } | |
| 1001 | |
| 1002 /* ------------------------------------------------------------------ */ | |
| 1003 /* main */ | |
| 1004 /* ------------------------------------------------------------------ */ | |
| 1005 | |
| 1006 int main(void) | |
| 1007 { | |
| 1008 printf("=== admin_api_test ===\n"); | |
| 1009 | |
| 1010 test_non_admin_denial(); | |
| 1011 test_forced_password_denial(); | |
| 1012 test_create_user(); | |
| 1013 test_enable_disable(); | |
| 1014 test_role_update(); | |
| 1015 test_temp_reset(); | |
| 1016 test_session_revocation(); | |
| 1017 test_no_secret_fields(); | |
| 1018 test_list_pagination(); | |
| 1019 | |
| 1020 printf("\n=== ALL TESTS PASSED ===\n"); | |
| 1021 return 0; | |
| 1022 } |