#include "app.h" #include "web.h" #include "mytime.h" #include #include #include #include #include // @PASTE Stolen from libcurl documentation struct memory { char *body; size_t size; }; static size_t write_to_memory(char *data, size_t size, size_t nmemb, void *clientp) { size_t realsize = size * nmemb; struct memory *mem = (struct memory *)clientp; char *ptr = realloc(mem->body, mem->size + realsize + 1); if(!ptr) return 0; /* out of memory */ mem->body = ptr; memcpy(&(mem->body[mem->size]), data, realsize); mem->size += realsize; mem->body[mem->size] = 0; return realsize; } static size_t read_from_memory(char *data, size_t size, size_t nmemb, void *clientp) { size_t realsize = size * nmemb; struct memory *mem = (struct memory *)clientp; realsize = realsize > mem->size ? mem->size : realsize; memcpy(data, mem->body, realsize); mem->size -= realsize; mem->body += realsize; return realsize; } int main(int argc, char **argv) { enable_fake_time(); // @DEP: The shared-cache mode is deprecated in sqlite, but it's still // there and useful right now. struct App app = { .dbname = "file::memory:?cache=shared", }; app_startup(&app); struct Server server = { .app = &app, }; web_begin(&server, 8080, "admin", "changeme"); curl_global_init(CURL_GLOBAL_ALL); CURLcode curlRes; CURL *curl = curl_easy_init(); assert(curl != NULL); // Default page requires auth, should redirect { curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/"); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); assert(curlRes == CURLE_OK); struct memory body = {0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); curlRes = curl_easy_perform(curl); assert(curlRes == CURLE_OK); long code; curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 302); // Redirect to login } char client_secret[CLIENT_SECRET_LEN] = {0}; { struct CreateClientResult result; int ret = create_client(&app, "client1", &result); assert(ret == 0); assert(result.id == 1); assert(strlen(result.secret) == 32); strncpy(client_secret, result.secret, CLIENT_SECRET_LEN); } { curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/report"); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_POST, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory); assert(curlRes == CURLE_OK); char req_buf[256]; snprintf(req_buf, sizeof(req_buf), "{\"client\": \"client1\", \"secret\": \"%s\", \"event\": \"begin_backup\"}", client_secret); struct memory req_body = { .body = req_buf, .size = strlen(req_buf), }; curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); assert(curlRes == CURLE_OK); struct memory body = {0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); curlRes = curl_easy_perform(curl); assert(curlRes == CURLE_OK); long code; curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 204); char *ct; curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); assert(curlRes == CURLE_OK); assert(ct != NULL); assert(strcmp(ct, "application/json") == 0); assert(body.size == 0); body = (struct memory){0}; } progress_time(TIME_JOIN(0, 30, 0)); { curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/report"); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_POST, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory); assert(curlRes == CURLE_OK); char req_buf[256]; snprintf(req_buf, sizeof(req_buf), "{\"client\": \"client1\", \"secret\": \"%s\", \"event\": \"end_backup\"}", client_secret); struct memory req_body = { .body = req_buf, .size = strlen(req_buf), }; curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); assert(curlRes == CURLE_OK); struct memory body = {0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); curlRes = curl_easy_perform(curl); assert(curlRes == CURLE_OK); long code; curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 204); char *ct; curlRes = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &ct); assert(curlRes == CURLE_OK); assert(ct != NULL); assert(strcmp(ct, "application/json") == 0); assert(body.size == 0); body = (struct memory){0}; } // Test: wrong secret should return 401 { curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/report"); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_POST, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory); assert(curlRes == CURLE_OK); struct memory req_body = { .body = "{\"client\": \"client1\", \"secret\": \"wrongsecret\", \"event\": \"begin_backup\"}", .size = strlen("{\"client\": \"client1\", \"secret\": \"wrongsecret\", \"event\": \"begin_backup\"}"), }; curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); assert(curlRes == CURLE_OK); struct memory body = {0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); curlRes = curl_easy_perform(curl); assert(curlRes == CURLE_OK); long code; curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 401); // Unauthorized } // Test: Starting a new backup should interrupt the previous RUNNING backup { // Begin first backup curlRes = curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:8080/api/report"); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_POST, 1L); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_from_memory); assert(curlRes == CURLE_OK); char req_buf[256]; snprintf(req_buf, sizeof(req_buf), "{\"client\": \"client1\", \"secret\": \"%s\", \"event\": \"begin_backup\"}", client_secret); struct memory req_body = { .body = req_buf, .size = strlen(req_buf) }; curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); assert(curlRes == CURLE_OK); curlRes = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_memory); assert(curlRes == CURLE_OK); struct memory body = {0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); curlRes = curl_easy_perform(curl); assert(curlRes == CURLE_OK); long code; curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 204); body = (struct memory){0}; progress_time(TIME_JOIN(0, 10, 0)); // Advance 10 minutes // Begin second backup (should interrupt the first) snprintf(req_buf, sizeof(req_buf), "{\"client\": \"client1\", \"secret\": \"%s\", \"event\": \"begin_backup\"}", client_secret); req_body = (struct memory){ .body = req_buf, .size = strlen(req_buf) }; curlRes = curl_easy_setopt(curl, CURLOPT_READDATA, &req_body); assert(curlRes == CURLE_OK); body = (struct memory){0}; curlRes = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body); assert(curlRes == CURLE_OK); curlRes = curl_easy_perform(curl); assert(curlRes == CURLE_OK); curlRes = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 204); body = (struct memory){0}; // Use app layer functions to verify backup statuses struct GetClientOnlyResult client_result; int ret = get_client(&app, 1, &client_result); assert(ret == 0); assert(client_result.found == 1); struct GetBackupsResult *backups_result = malloc(sizeof(struct GetBackupsResult) + sizeof(struct BackupRecord) * MAX_BACKUPS_DISPLAY); backups_result->backups_len = MAX_BACKUPS_DISPLAY; ret = get_client_backups(&app, 1, backups_result); assert(ret == 0); // Should have at least 2 backups (the interrupted one and the running one) // Note: There may be more from earlier tests assert(backups_result->backups_len >= 2); // Backups are ordered by started DESC, so newest first // The most recent backup should be RUNNING assert(backups_result->backups[0].status == STATUS_RUNNING); // The previous backup should be INTERRUPTED assert(backups_result->backups[1].status == STATUS_INTERRUPTED); // The interrupted backup should have a completed timestamp assert(backups_result->backups[1].completed.tv_sec > 0); free(backups_result); } // Test: Full login flow { // Use fresh curl with cookie jar CURL *auth_curl = curl_easy_init(); assert(auth_curl != NULL); curl_easy_setopt(auth_curl, CURLOPT_COOKIEFILE, ""); // Enable cookie engine curl_easy_setopt(auth_curl, CURLOPT_FOLLOWLOCATION, 0L); curl_easy_setopt(auth_curl, CURLOPT_WRITEFUNCTION, write_to_memory); struct memory body = {0}; curl_easy_setopt(auth_curl, CURLOPT_WRITEDATA, &body); // Step 1: Unauthenticated access redirects to /login curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/clients"); curl_easy_perform(auth_curl); long code; curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 302); // Step 2: Wrong credentials show error (not redirect) free(body.body); body = (struct memory){0}; curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/login"); curl_easy_setopt(auth_curl, CURLOPT_POSTFIELDS, "username=admin&password=wrong"); curl_easy_perform(auth_curl); curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 200); assert(strstr(body.body, "Invalid") != NULL); // Step 3: Correct credentials redirect and set cookie free(body.body); body = (struct memory){0}; curl_easy_setopt(auth_curl, CURLOPT_POSTFIELDS, "username=admin&password=changeme"); curl_easy_perform(auth_curl); curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 303); // Step 4: Authenticated access works free(body.body); body = (struct memory){0}; curl_easy_setopt(auth_curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/clients"); curl_easy_perform(auth_curl); curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 200); assert(strstr(body.body, "Clients") != NULL); // Step 5: Logout clears cookie and redirects free(body.body); body = (struct memory){0}; curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/logout"); curl_easy_setopt(auth_curl, CURLOPT_POST, 1L); curl_easy_setopt(auth_curl, CURLOPT_POSTFIELDS, ""); curl_easy_perform(auth_curl); curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 303); // Redirect to login // Step 6: No longer authenticated after logout free(body.body); body = (struct memory){0}; curl_easy_setopt(auth_curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(auth_curl, CURLOPT_URL, "http://localhost:8080/clients"); curl_easy_perform(auth_curl); curl_easy_getinfo(auth_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 302); // Redirect to login (not authenticated) free(body.body); curl_easy_cleanup(auth_curl); } // Test: Invalid session cookie rejected { CURL *bad_curl = curl_easy_init(); assert(bad_curl != NULL); curl_easy_setopt(bad_curl, CURLOPT_FOLLOWLOCATION, 0L); curl_easy_setopt(bad_curl, CURLOPT_COOKIE, "session=invalidtoken"); curl_easy_setopt(bad_curl, CURLOPT_URL, "http://localhost:8080/clients"); curl_easy_setopt(bad_curl, CURLOPT_WRITEFUNCTION, write_to_memory); struct memory body = {0}; curl_easy_setopt(bad_curl, CURLOPT_WRITEDATA, &body); curl_easy_perform(bad_curl); long code; curl_easy_getinfo(bad_curl, CURLINFO_RESPONSE_CODE, &code); assert(code == 302); // Redirect to login free(body.body); curl_easy_cleanup(bad_curl); } web_join(&server); }