Mercurial
diff mrjunejune/test/integration_test.c @ 67:6626ec933933
[Seobeo] Separated out Client Server logic. Created test tools.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Wed, 24 Dec 2025 09:15:55 -0800 |
| parents | |
| children | 092afa595764 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/test/integration_test.c Wed Dec 24 09:15:55 2025 -0800 @@ -0,0 +1,616 @@ +#include "seobeo/seobeo.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/wait.h> +#include <signal.h> +#include <assert.h> + +#define TEST_PORT "6969" +#define TEST_HOST "127.0.0.1" +#define MAX_RESPONSE_SIZE (1024 * 1024) +#define SNAPSHOT_DIR "mrjunejune/test/snapshots" + +// Test case structure +typedef struct { + const char *path; + int expected_status; + const char *expected_content; // Loaded from file + char *expected_file_path; // Path to expected snapshot file + char *actual_response; + size_t response_len; +} TestCase; + +// Helper: Convert URL path to filename +// "/" -> "root.snapshot" +// "/index.html" -> "index.html.snapshot" +// "/api/users" -> "api_users.snapshot" +void path_to_filename(const char *path, char *filename, size_t max_len) +{ + if (strcmp(path, "/") == 0) + { + snprintf(filename, max_len, "root.snapshot"); + return; + } + + // Remove leading slash and convert remaining slashes to underscores + const char *p = path; + if (*p == '/') + { + p++; + } + + char *out = filename; + size_t remaining = max_len - 1; + + while (*p && remaining > 0) + { + if (*p == '/') + { + *out++ = '_'; + remaining--; + } + else + { + *out++ = *p; + remaining--; + } + p++; + } + + // Add .snapshot extension + snprintf(out, remaining, ".snapshot"); +} + +// Helper: Read file contents into buffer +char* read_file(const char *filepath, size_t *size_out) +{ + FILE *f = fopen(filepath, "rb"); + if (!f) + { + return NULL; + } + + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + + char *buffer = malloc(fsize + 1); + if (!buffer) + { + fclose(f); + return NULL; + } + + size_t read_size = fread(buffer, 1, fsize, f); + buffer[read_size] = '\0'; + fclose(f); + + if (size_out) + { + *size_out = read_size; + } + + return buffer; +} + +// Helper: Load expected content for a test case +int load_expected_content(TestCase *test) +{ + if (!test->expected_file_path) + { + return -1; + } + + size_t size; + test->expected_content = read_file(test->expected_file_path, &size); + + if (!test->expected_content) + { + return -1; + } + + return 0; +} + +// Helper: Create test client +Seobeo_Handle* create_test_client() +{ + Seobeo_Handle *client = Seobeo_Stream_Handle_Client_Create(TEST_HOST, TEST_PORT, FALSE); + if (!client || client->socket < 0) + { + if (client) + { + Seobeo_Handle_Destroy(client); + } + return NULL; + } + return client; +} + +// Helper: Generate default HTTP GET request +int generate_http_get_request(char *buffer, size_t buffer_size, const char *path) +{ + return snprintf( + buffer, buffer_size, + "GET %s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "User-Agent: SeobeoTestClient/1.0\r\n" + "\r\n", + path, TEST_HOST + ); +} + +// Helper: Send HTTP request +int send_http_request(Seobeo_Handle *client, const char *path, const char *custom_request) +{ + char request_buffer[4096]; + int request_len; + + if (custom_request) + { + request_len = snprintf(request_buffer, sizeof(request_buffer), "%s", custom_request); + } + else + { + request_len = generate_http_get_request(request_buffer, sizeof(request_buffer), path); + } + + if (request_len < 0 || request_len >= sizeof(request_buffer)) + { + fprintf(stderr, "Request buffer too small\n"); + return -1; + } + + Seobeo_Handle_Queue(client, (uint8*)request_buffer, (uint32)request_len); + return Seobeo_Handle_Flush(client); +} + +// Helper: Read HTTP response +int read_http_response(Seobeo_Handle *client, char **response_out, size_t *response_len_out) +{ + char *response = malloc(MAX_RESPONSE_SIZE); + if (!response) + { + return -1; + } + + size_t total_bytes = 0; + int attempts = 0; + const int max_attempts = 100; + + while (attempts++ < max_attempts && total_bytes < MAX_RESPONSE_SIZE - 1) + { + int bytes_read = Seobeo_Handle_Read(client); + + if (bytes_read > 0) + { + size_t to_copy = client->read_buffer_len; + if (total_bytes + to_copy > MAX_RESPONSE_SIZE - 1) + { + to_copy = MAX_RESPONSE_SIZE - 1 - total_bytes; + } + + memcpy(response + total_bytes, client->read_buffer, to_copy); + total_bytes += to_copy; + Seobeo_Handle_Consume(client, (uint32)to_copy); + } + else if (bytes_read == -2) + { + // Connection closed + break; + } + else if (bytes_read == 0) + { + // Would block + usleep(10000); + continue; + } + else + { + free(response); + return -1; + } + } + + response[total_bytes] = '\0'; + *response_out = response; + *response_len_out = total_bytes; + + return (total_bytes > 0) ? 0 : -1; +} + +// Helper: Parse HTTP status code +int parse_http_status(const char *response) +{ + if (!response || strlen(response) < 12) + { + return -1; + } + + const char *status_start = strstr(response, "HTTP/1.1 "); + if (!status_start) + { + status_start = strstr(response, "HTTP/1.0 "); + } + + if (!status_start) + { + return -1; + } + + int status_code; + if (sscanf(status_start + 9, "%d", &status_code) == 1) + { + return status_code; + } + + return -1; +} + +// Helper: Check if status is a redirect +int is_redirect_status(int status) +{ + return (status >= 300 && status < 400); +} + +// Helper: Execute a test case +int execute_test_case(TestCase *test, pid_t server_pid) +{ + printf(" Testing: GET %s (expecting %d)\n", test->path, test->expected_status); + + Seobeo_Handle *client = create_test_client(); + if (!client) + { + printf(" ✗ Failed to create client connection\n"); + return -1; + } + + if (send_http_request(client, test->path, NULL) < 0) + { + printf(" ✗ Failed to send request\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + char *response = NULL; + size_t response_len = 0; + if (read_http_response(client, &response, &response_len) < 0) + { + printf(" ✗ Failed to read response\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + test->actual_response = response; + test->response_len = response_len; + + int actual_status = parse_http_status(response); + if (actual_status != test->expected_status) + { + printf(" ✗ Status mismatch: expected %d, got %d\n", + test->expected_status, actual_status); + Seobeo_Handle_Destroy(client); + return -1; + } + + printf(" ✓ Status code: %d\n", actual_status); + + // For redirects, skip content comparison + if (is_redirect_status(actual_status)) + { + printf(" ⚠ Redirect status - skipping content comparison\n"); + Seobeo_Handle_Destroy(client); + return 0; + } + + // Only verify 200 OK responses against snapshots + if (actual_status == 200) + { + if (!test->expected_content) + { + printf(" ✗ No expected snapshot found: %s\n", test->expected_file_path); + printf(" → Run: bazel run //mrjunejune:create_snapshots\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + if (strcmp(response, test->expected_content) != 0) + { + printf(" ✗ Response does not match expected snapshot\n"); + printf(" Expected file: %s\n", test->expected_file_path); + Seobeo_Handle_Destroy(client); + return -1; + } + + printf(" ✓ Response matches snapshot (%zu bytes)\n", response_len); + } + + Seobeo_Handle_Destroy(client); + return 0; +} + +// Helper: Execute custom request test +int execute_custom_request_test(const char *name, const char *custom_request, + int expected_status, pid_t server_pid) +{ + printf(" Testing: %s (expecting %d)\n", name, expected_status); + + Seobeo_Handle *client = create_test_client(); + if (!client) + { + printf(" ✗ Failed to create client connection\n"); + return -1; + } + + if (send_http_request(client, NULL, custom_request) < 0) + { + printf(" ✗ Failed to send request\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + char *response = NULL; + size_t response_len = 0; + if (read_http_response(client, &response, &response_len) < 0) + { + printf(" ✗ Failed to read response\n"); + Seobeo_Handle_Destroy(client); + return -1; + } + + int actual_status = parse_http_status(response); + if (actual_status != expected_status) + { + printf(" ✗ Status mismatch: expected %d, got %d\n", + expected_status, actual_status); + free(response); + Seobeo_Handle_Destroy(client); + return -1; + } + + printf(" ✓ Status code: %d\n", actual_status); + printf(" ✓ Response received (%zu bytes)\n", response_len); + + free(response); + Seobeo_Handle_Destroy(client); + return 0; +} + +// Helper: Start test server +pid_t start_test_server(const char *server_binary) +{ + pid_t server_pid = fork(); + + if (server_pid < 0) + { + perror("fork"); + return -1; + } + + if (server_pid == 0) + { + printf("Starting server on port %s...\n", TEST_PORT); + execl(server_binary, server_binary, NULL); + perror("execl failed"); + exit(1); + } + + printf("Server started (PID: %d)\n", server_pid); + + usleep(100000); + int status; + pid_t result = waitpid(server_pid, &status, WNOHANG); + if (result != 0) + { + if (WIFEXITED(status)) + { + fprintf(stderr, "Server exited immediately with code: %d\n", WEXITSTATUS(status)); + } + else if (WIFSIGNALED(status)) + { + fprintf(stderr, "Server was killed by signal: %d\n", WTERMSIG(status)); + } + return -1; + } + + sleep(2); + printf("Server ready\n\n"); + + return server_pid; +} + +// Helper: Stop test server +void stop_test_server(pid_t server_pid) +{ + if (server_pid > 0) + { + printf("\nStopping server (PID: %d)...\n", server_pid); + kill(server_pid, SIGTERM); + waitpid(server_pid, NULL, 0); + printf("Server stopped\n"); + } +} + +// Helper: Initialize test case with snapshot file +void init_test_case(TestCase *test) +{ + char filename[256]; + path_to_filename(test->path, filename, sizeof(filename)); + + test->expected_file_path = malloc(512); + snprintf(test->expected_file_path, 512, "%s/%s", SNAPSHOT_DIR, filename); + + // Load expected content from snapshot file + load_expected_content(test); +} + +// Helper: Cleanup test case +void cleanup_test_case(TestCase *test) +{ + if (test->actual_response) + { + free(test->actual_response); + } + if (test->expected_file_path) + { + free(test->expected_file_path); + } + if (test->expected_content) + { + free((void*)test->expected_content); + } +} + +// Main integration test +int test_server_client_integration(const char *server_binary) +{ + printf("=== Server-Client Integration Test ===\n"); + printf("MODE: Verifying Against Snapshots\n\n"); + + char cwd[1024]; + if (getcwd(cwd, sizeof(cwd)) != NULL) + { + printf("Working directory: %s\n", cwd); + } + + if (access(server_binary, X_OK) != 0) + { + printf("Server binary not found: %s\n", server_binary); + perror("access"); + return -1; + } + printf("Server binary: %s\n", server_binary); + printf("Snapshot directory: %s\n\n", SNAPSHOT_DIR); + + pid_t server_pid = start_test_server(server_binary); + if (server_pid < 0) + { + return -1; + } + + int failed_tests = 0; + int passed_tests = 0; + + // Define test cases - paths that should succeed (200 OK) + TestCase success_tests[] = { + {"/", 200, NULL, NULL, NULL, 0}, + {"/index.html", 200, NULL, NULL, NULL, 0}, + }; + int num_success_tests = sizeof(success_tests) / sizeof(success_tests[0]); + + // Define test cases - paths that should fail (404) + TestCase failure_tests[] = { + {"/nonexistent", 404, NULL, NULL, NULL, 0}, + {"/does/not/exist", 404, NULL, NULL, NULL, 0}, + {"/missing.html", 404, NULL, NULL, NULL, 0}, + }; + int num_failure_tests = sizeof(failure_tests) / sizeof(failure_tests[0]); + + // Initialize all test cases + for (int i = 0; i < num_success_tests; i++) + { + init_test_case(&success_tests[i]); + } + for (int i = 0; i < num_failure_tests; i++) + { + init_test_case(&failure_tests[i]); + } + + // Run success tests + printf("Running tests for paths that should succeed:\n"); + for (int i = 0; i < num_success_tests; i++) + { + if (execute_test_case(&success_tests[i], server_pid) == 0) + { + passed_tests++; + } + else + { + failed_tests++; + } + } + + printf("\n"); + + // Run failure tests + printf("Running tests for paths that should fail:\n"); + for (int i = 0; i < num_failure_tests; i++) + { + if (execute_test_case(&failure_tests[i], server_pid) == 0) + { + passed_tests++; + } + else + { + failed_tests++; + } + } + + printf("\n"); + + // Test with custom request + printf("Running tests with custom requests:\n"); + char custom_request[4096]; + snprintf(custom_request, sizeof(custom_request), + "GET / HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n" + "X-Custom-Header: TestValue\r\n" + "\r\n", + TEST_HOST); + + if (execute_custom_request_test("Custom headers GET /", custom_request, 200, server_pid) == 0) + { + passed_tests++; + } + else + { + failed_tests++; + } + + // Cleanup test cases + for (int i = 0; i < num_success_tests; i++) + { + cleanup_test_case(&success_tests[i]); + } + for (int i = 0; i < num_failure_tests; i++) + { + cleanup_test_case(&failure_tests[i]); + } + + stop_test_server(server_pid); + + printf("\n=== Test Summary ===\n"); + printf("Passed: %d\n", passed_tests); + printf("Failed: %d\n", failed_tests); + + return (failed_tests == 0) ? 0 : -1; +} + +int main(int argc, char *argv[]) +{ + printf("=== Seobeo Integration Tests ===\n\n"); + + const char *server_binary = "./mrjunejune_server"; + if (argc > 1) + { + server_binary = argv[1]; + } + + int result = test_server_client_integration(server_binary); + + if (result == 0) + { + printf("\n✓ All tests passed!\n"); + } + else + { + printf("\n✗ Some tests failed\n"); + } + + return result; +}