view mrjunejune/test/integration_test.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents 1f9877b637e9
children
line wrap: on
line source

#include "mrjunejune/test/test.h"

// 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;

void debug_diff(const char *expected, const char *actual)
{
    printf("\n--- DIFF (Expected vs Actual) ---\n");
    
    // Create copies to use with strtok (strtok modifies the string)
    char *exp_copy = strdup(expected);
    char *act_copy = strdup(actual);
    
    char *exp_line = strtok(exp_copy, "\n");
    char *act_line = strtok(act_copy, "\n");
    
    int line_num = 1;
    while (exp_line != NULL || act_line != NULL) {
        if (exp_line && act_line && strcmp(exp_line, act_line) == 0) {
            // Lines match - optional: print nothing or a dot
        } else {
            printf("Line %d mismatch:\n", line_num);
            printf("  EXP: [%s]\n", exp_line ? exp_line : "(end of string)");
            printf("  ACT: [%s]\n", act_line ? act_line : "(end of string)");
            printf("  -----------------------------------\n");
        }
        
        exp_line = strtok(NULL, "\n");
        act_line = strtok(NULL, "\n");
        line_num++;
    }
    
    free(exp_copy);
    free(act_copy);
}

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

int execute_test_case(TestCase *test, pid_t server_pid)
{
  printf("  Testing: GET %s (expecting %d)\n", test->path, test->expected_status);
  
  int32 max_url_length = 1024*3;
  char *url = malloc(sizeof(char)*max_url_length);
  snprintf(url, max_url_length, "%s%s", TEST_URL, test->path);
  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(url);
  if (!p_req)
  {
    printf("Can't create requests");
    return -1;
  }

  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
  if (!p_resp)
  {
    printf("No response");
    return -1;
  }

  if (p_resp->status_code != test->expected_status)
  {
    printf("    ✗ Status mismatch: expected %d, got %d\n",
           test->expected_status, p_resp->status_code);
    Seobeo_Client_Response_Destroy(p_resp);
    Seobeo_Client_Request_Destroy(p_req);
    return -1;
  }

  printf("    ✓ Status code: %d\n", p_resp->status_code);

  if (p_resp->status_code == 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_Client_Response_Destroy(p_resp);
      Seobeo_Client_Request_Destroy(p_req);
      return -1;
    }

    if (strcmp(p_resp->body, test->expected_content) != 0)
    {
      printf("    ✗ Response does not match expected snapshot\n");
      printf("    Expected file: %s\n", test->expected_file_path);
      debug_diff(p_resp->body, test->expected_content);
      Seobeo_Client_Response_Destroy(p_resp);
      Seobeo_Client_Request_Destroy(p_req);
      return -1;
    }

    printf("    ✓ Response matches snapshot (%zu bytes)\n", p_resp->body_length);
  }

  Seobeo_Client_Response_Destroy(p_resp);
  Seobeo_Client_Request_Destroy(p_req);
  return 0;
}

int send_post_file(Seobeo_Handle *p_req, 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(p_req, (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(p_req, (uint8*)ptr, (uint32)chunk_size);
    ptr += chunk_size;
    remaining -= chunk_size;
  }

  return Seobeo_Handle_Flush(p_req);
}

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

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

  char url[1024];
  snprintf(url, sizeof(url), "%s%s", TEST_URL, endpoint);

  Seobeo_Client_Request *p_req = Seobeo_Client_Request_Create(url);
  Seobeo_Client_Request_Set_Method(p_req, "POST");
  Seobeo_Client_Request_Set_Body(p_req, (uint8*)file_data, (uint32)file_size);
  Seobeo_Client_Request_Add_Header_Array(p_req, "Content-Type: application/octet-stream");

  Seobeo_Client_Response *p_resp = Seobeo_Client_Request_Execute(p_req);
  free(file_data);

  if (!p_resp || p_resp->status_code != 200) {
    printf("    ✗ Conversion failed with status: %d, %s\n", p_resp ? p_resp->status_code : 0, p_resp->body);
    if (p_resp) Seobeo_Client_Response_Destroy(p_resp);
    Seobeo_Client_Request_Destroy(p_req);
    return -1;
  }

  // Extract download URL from JSON body
  char download_url_path[512];
  if (!extract_json_field((char*)p_resp->body, "download_url", download_url_path, sizeof(download_url_path))) {
    printf("    ✗ Failed to extract download_url\n");
    Seobeo_Client_Response_Destroy(p_resp);
    Seobeo_Client_Request_Destroy(p_req);
    return -1;
  }

  printf("    ✓ Conversion succeeded. Download URL: %s\n", download_url_path);
  Seobeo_Client_Response_Destroy(p_resp);
  Seobeo_Client_Request_Destroy(p_req);

  usleep(100000);
  printf("    → Testing download: GET %s\n", download_url_path);
  char full_download_url[1024];
  snprintf(full_download_url, sizeof(full_download_url), "%s%s", TEST_URL, download_url_path);
  
  p_req = Seobeo_Client_Request_Create(full_download_url);
  p_resp = Seobeo_Client_Request_Execute(p_req);

  if (!p_resp || p_resp->status_code != 200)
  {
    printf("    ✗ Download failed\n");
    printf("%s\n", p_resp->body);
    if (p_resp) Seobeo_Client_Response_Destroy(p_resp);
    Seobeo_Client_Request_Destroy(p_req);
    return -1;
  }

  printf("    ✓ Downloaded converted file (%u bytes)\n", p_resp->body_length);

  Seobeo_Client_Response_Destroy(p_resp);
  Seobeo_Client_Request_Destroy(p_req);
  return 0;
}

// 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-p_req 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;

  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},
    {"/tools/hls_player", 200, NULL, NULL, NULL, 0},
    {"/tools/latex_editor", 200, NULL, NULL, NULL, 0},
    {"/talk", 200, NULL, NULL, NULL, 0},
    {"/jrpg", 200, NULL, NULL, NULL, 0},
  };
  int num_success_tests = sizeof(success_tests) / sizeof(success_tests[0]);

  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},
    {"/tools/hls_player/index.html", 301, NULL, NULL, NULL, 0},
    {"/tools/latex_editor/index.html", 301, NULL, NULL, NULL, 0},
    {"/jrpg/index.html", 301, NULL, NULL, NULL, 0},
  };
  int num_redirect_tests = sizeof(redirect_tests) / sizeof(redirect_tests[0]);

  TestCase failure_tests[] = {
    {"/nonexistent", 404, NULL, NULL, NULL, 0},
    {"/does/not/exist", 404, NULL, NULL, NULL, 0},
    {"/missing.html", 404, NULL, NULL, NULL, 0},
    {"/api/inference/health", 503, NULL, NULL, NULL, 0},
  };
  int num_failure_tests = sizeof(failure_tests) / sizeof(failure_tests[0]);

  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]);

  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");

  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");

  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");

  printf("Running tests for POST conversion endpoints:\n");
  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");

  if (test_file_conversion("/api/convert/video-to-mp4",
                           "mrjunejune/test/test_avi.avi",
                           "video/mp4",
                           server_pid) == 0)
    passed_tests++;
  else
    failed_tests++;

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