view mrjunejune/test/integration_test.c @ 105:4de2fb74ce82

Adding few comments for future.
author June Park <parkjune1995@gmail.com>
date Sat, 03 Jan 2026 10:28:54 -0800
parents 092afa595764
children 96628cf126a0
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: Send POST request with file data
int send_post_file(Seobeo_Handle *client, const char *path, const char *file_data, size_t file_size)
{
  char request_buffer[8192];
  int header_len = snprintf(
    request_buffer, sizeof(request_buffer),
    "POST %s HTTP/1.1\r\n"
    "Host: %s\r\n"
    "Content-Type: application/octet-stream\r\n"
    "Content-Length: %zu\r\n"
    "Connection: close\r\n"
    "\r\n",
    path, TEST_HOST, file_size
  );

  if (header_len < 0 || header_len >= sizeof(request_buffer))
  {
    fprintf(stderr, "Request header too large\n");
    return -1;
  }

  // Send headers
  Seobeo_Handle_Queue(client, (uint8*)request_buffer, (uint32)header_len);

  // Send file data in chunks if needed
  size_t remaining = file_size;
  const char *ptr = file_data;
  while (remaining > 0)
  {
    size_t chunk_size = remaining > 4096 ? 4096 : remaining;
    Seobeo_Handle_Queue(client, (uint8*)ptr, (uint32)chunk_size);
    ptr += chunk_size;
    remaining -= chunk_size;
  }

  return Seobeo_Handle_Flush(client);
}

// Helper: Extract JSON field value from response body
char* extract_json_field(const char *json, const char *field, char *buffer, size_t buffer_size)
{
  char search_pattern[256];
  snprintf(search_pattern, sizeof(search_pattern), "\"%s\":\"", field);

  const char *start = strstr(json, search_pattern);
  if (!start)
  {
    return NULL;
  }

  start += strlen(search_pattern);
  const char *end = strchr(start, '"');
  if (!end)
  {
    return NULL;
  }

  size_t len = end - start;
  if (len >= buffer_size)
  {
    len = buffer_size - 1;
  }

  memcpy(buffer, start, len);
  buffer[len] = '\0';

  return buffer;
}

// Helper: Test POST file conversion
int test_file_conversion(const char *endpoint, const char *test_file_path,
                         const char *expected_format, pid_t server_pid)
{
  printf("  Testing: POST %s\n", endpoint);

  // Read test file
  size_t file_size;
  char *file_data = read_file(test_file_path, &file_size);
  if (!file_data)
  {
    printf("    ✗ Failed to read test file: %s\n", test_file_path);
    return -1;
  }

  printf("    → Loaded test file (%zu bytes)\n", file_size);

  // Create client and send request
  Seobeo_Handle *client = create_test_client();
  if (!client)
  {
    printf("    ✗ Failed to create client connection\n");
    free(file_data);
    return -1;
  }

  if (send_post_file(client, endpoint, file_data, file_size) < 0)
  {
    printf("    ✗ Failed to send POST request\n");
    free(file_data);
    Seobeo_Handle_Destroy(client);
    return -1;
  }

  free(file_data);

  // Read response
  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;
  }

  Seobeo_Handle_Destroy(client);

  // Parse status
  int status = parse_http_status(response);
  if (status != 200)
  {
    printf("    ✗ Conversion failed with status: %d\n", status);
    printf("    Response: %s\n", response);
    free(response);
    return -1;
  }

  printf("    ✓ Status code: 200\n");

  // Extract download URL from JSON response
  const char *body = strstr(response, "\r\n\r\n");
  if (!body)
  {
    printf("    ✗ No response body found\n");
    free(response);
    return -1;
  }
  body += 4;

  char download_url[512];
  if (!extract_json_field(body, "download_url", download_url, sizeof(download_url)))
  {
    printf("    ✗ Failed to extract download_url from response\n");
    printf("    Response body: %s\n", body);
    free(response);
    return -1;
  }

  printf("    ✓ Conversion succeeded\n");
  printf("    ✓ Download URL: %s\n", download_url);
  free(response);

  // Test downloading the converted file
  printf("    → Testing download: GET %s\n", download_url);

  client = create_test_client();
  if (!client)
  {
    printf("    ✗ Failed to create client for download\n");
    return -1;
  }

  if (send_http_request(client, download_url, NULL) < 0)
  {
    printf("    ✗ Failed to send download request\n");
    Seobeo_Handle_Destroy(client);
    return -1;
  }

  response = NULL;
  response_len = 0;
  if (read_http_response(client, &response, &response_len) < 0)
  {
    printf("    ✗ Failed to read download response\n");
    Seobeo_Handle_Destroy(client);
    return -1;
  }

  Seobeo_Handle_Destroy(client);

  status = parse_http_status(response);
  if (status != 200)
  {
    printf("    ✗ Download failed with status: %d\n", status);
    free(response);
    return -1;
  }

  // Find body in download response
  body = strstr(response, "\r\n\r\n");
  if (!body)
  {
    printf("    ✗ No file data in download response\n");
    free(response);
    return -1;
  }
  body += 4;

  size_t downloaded_size = response_len - (body - response);

  // Verify content type in response headers
  const char *content_type = strstr(response, "Content-Type: ");
  if (!content_type)
  {
    printf("    ✗ No Content-Type header in download\n");
    free(response);
    return -1;
  }

  if (strstr(content_type, expected_format) == NULL)
  {
    printf("    ✗ Wrong content type (expected %s)\n", expected_format);
    free(response);
    return -1;
  }

  printf("    ✓ Downloaded converted file (%zu bytes)\n", downloaded_size);
  printf("    ✓ Content-Type: %s\n", expected_format);

  free(response);
  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},
    {"/resume", 200, NULL, NULL, NULL, 0},
    {"/tools", 200, NULL, NULL, NULL, 0},
    {"/tools/markdown_to_html", 200, NULL, NULL, NULL, 0},
    {"/tools/file_converter", 200, NULL, NULL, NULL, 0},
  };
  int num_success_tests = sizeof(success_tests) / sizeof(success_tests[0]);

  // Define test cases - paths that should redirect (301)
  TestCase redirect_tests[] = {
    {"/index.html", 301, NULL, NULL, NULL, 0},
    {"/resume/index.html", 301, NULL, NULL, NULL, 0},
    {"/tools/index.html", 301, NULL, NULL, NULL, 0},
    {"/tools/markdown_to_html/index.html", 301, NULL, NULL, NULL, 0},
    {"/tools/file_converter/index.html", 301, NULL, NULL, NULL, 0},
  };
  int num_redirect_tests = sizeof(redirect_tests) / sizeof(redirect_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_redirect_tests; i++)
  {
    init_test_case(&redirect_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 redirect tests
  printf("Running tests for paths that should redirect:\n");
  for (int i = 0; i < num_redirect_tests; i++)
  {
    if (execute_test_case(&redirect_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++;
  }

  printf("\n");

  // Test POST endpoints
  printf("Running tests for POST conversion endpoints:\n");

  // Test image-to-webp conversion
  if (test_file_conversion("/api/convert/image-to-webp",
                           "mrjunejune/test/shiba.webp",
                           "image/webp",
                           server_pid) == 0)
  {
    passed_tests++;
  }
  else
  {
    failed_tests++;
  }

  printf("\n");

  // Test video-to-mp4 conversion
  if (test_file_conversion("/api/convert/video-to-mp4",
                           "mrjunejune/test/test_avi.avi",
                           "video/mp4",
                           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_redirect_tests; i++)
  {
    cleanup_test_case(&redirect_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;
}