view mrjunejune/test/integration_test.c @ 71:75de5903355c

Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
author June Park <parkjune1995@gmail.com>
date Sun, 28 Dec 2025 20:34:22 -0800
parents 6626ec933933
children 092afa595764
line wrap: on
line source

#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;
}