fetch_ml/tests/integration/storage_redis_integration_test.go
Jeremie Fraeys c980167041 test: implement comprehensive test suite with multiple test types
- Add end-to-end tests for complete workflow validation
- Include integration tests for API and database interactions
- Add unit tests for all major components and utilities
- Include performance tests for payload handling
- Add CLI API integration tests
- Include Podman container integration tests
- Add WebSocket and queue execution tests
- Include shell script tests for setup validation

Provides comprehensive test coverage ensuring platform reliability
and functionality across all components and interactions.
2025-12-04 16:55:13 -05:00

477 lines
12 KiB
Go

package tests
import (
"context"
"testing"
"time"
"github.com/jfraeys/fetch_ml/internal/storage"
"github.com/redis/go-redis/v9"
)
// setupRedis creates a Redis client for testing
func setupRedis(t *testing.T) *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 1, // Use DB 1 for tests to avoid conflicts
})
ctx := context.Background()
if err := rdb.Ping(ctx).Err(); err != nil {
t.Skipf("Redis not available, skipping integration test: %v", err)
return nil
}
// Clean up the test database
rdb.FlushDB(ctx)
t.Cleanup(func() {
rdb.FlushDB(ctx)
rdb.Close()
})
return rdb
}
func TestStorageRedisIntegration(t *testing.T) {
t.Parallel() // Enable parallel execution
// Setup Redis and storage
redisHelper := setupRedis(t)
defer redisHelper.Close()
tempDir := t.TempDir()
db, err := storage.NewDBFromPath(tempDir + "/test.db")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
// Initialize database schema
schema := `
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
job_name TEXT NOT NULL,
args TEXT,
status TEXT NOT NULL DEFAULT 'pending',
priority INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
ended_at DATETIME ,
worker_id TEXT,
error TEXT,
datasets TEXT,
metadata TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workers (
id TEXT PRIMARY KEY,
hostname TEXT NOT NULL,
last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL DEFAULT 'active',
current_jobs INTEGER DEFAULT 0,
max_jobs INTEGER DEFAULT 1,
metadata TEXT
);
`
err = db.Initialize(schema)
if err != nil {
t.Fatalf("Failed to initialize database: %v", err)
}
// Test 1: Create job in storage and queue in Redis
job := &storage.Job{
ID: "test-job-1",
JobName: "Test Job",
Status: "pending",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Args: "",
Priority: 0,
}
// Store job in database
err = db.CreateJob(job)
if err != nil {
t.Fatalf("Failed to create job: %v", err)
}
// Queue job in Redis
ctx := context.Background()
err = redisHelper.RPush(ctx, "ml:queue", job.ID).Err()
if err != nil {
t.Fatalf("Failed to queue job in Redis: %v", err)
}
// Verify job exists in both systems
retrievedJob, err := db.GetJob(job.ID)
if err != nil {
t.Fatalf("Failed to retrieve job from database: %v", err)
}
if retrievedJob.ID != job.ID {
t.Errorf("Expected job ID %s, got %s", job.ID, retrievedJob.ID)
}
// Verify job is in Redis queue
queueLength := redisHelper.LLen(ctx, "ml:queue").Val()
if queueLength != 1 {
t.Errorf("Expected queue length 1, got %d", queueLength)
}
queuedJobID := redisHelper.LIndex(ctx, "ml:queue", 0).Val()
if queuedJobID != job.ID {
t.Errorf("Expected queued job ID %s, got %s", job.ID, queuedJobID)
}
}
func TestStorageRedisWorkerIntegration(t *testing.T) {
t.Parallel() // Enable parallel execution
// Setup Redis and storage
redisHelper := setupRedis(t)
defer redisHelper.Close()
tempDir := t.TempDir()
db, err := storage.NewDBFromPath(tempDir + "/test.db")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
schema := `
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
job_name TEXT NOT NULL,
args TEXT,
status TEXT NOT NULL DEFAULT 'pending',
priority INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
ended_at DATETIME,
worker_id TEXT,
error TEXT,
datasets TEXT,
metadata TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workers (
id TEXT PRIMARY KEY,
hostname TEXT NOT NULL,
last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL DEFAULT 'active',
current_jobs INTEGER DEFAULT 0,
max_jobs INTEGER DEFAULT 1,
metadata TEXT
);
CREATE TABLE IF NOT EXISTS job_metrics (
job_id TEXT NOT NULL,
metric_name TEXT NOT NULL,
metric_value TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (job_id, metric_name),
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
);
`
err = db.Initialize(schema)
if err != nil {
t.Fatalf("Failed to initialize database: %v", err)
}
// Test 2: Worker registration and heartbeat integration
worker := &storage.Worker{
ID: "worker-1",
Hostname: "test-host",
LastHeartbeat: time.Now(),
Status: "active",
CurrentJobs: 0,
MaxJobs: 1,
}
// Register worker in database
err = db.RegisterWorker(worker)
if err != nil {
t.Fatalf("Failed to register worker: %v", err)
}
// Update worker heartbeat in Redis
ctx := context.Background()
heartbeatKey := "ml:workers:heartbeat"
err = redisHelper.HSet(ctx, heartbeatKey, worker.ID, time.Now().Unix()).Err()
if err != nil {
t.Fatalf("Failed to set worker heartbeat in Redis: %v", err)
}
// Verify worker exists in database
activeWorkers, err := db.GetActiveWorkers()
if err != nil {
t.Fatalf("Failed to get active workers: %v", err)
}
if len(activeWorkers) != 1 {
t.Errorf("Expected 1 active worker, got %d", len(activeWorkers))
}
if activeWorkers[0].ID != worker.ID {
t.Errorf("Expected worker ID %s, got %s", worker.ID, activeWorkers[0].ID)
}
// Verify heartbeat exists in Redis
heartbeatTime := redisHelper.HGet(ctx, heartbeatKey, worker.ID).Val()
if heartbeatTime == "" {
t.Error("Worker heartbeat not found in Redis")
}
}
func TestStorageRedisMetricsIntegration(t *testing.T) {
t.Parallel() // Enable parallel execution
// Setup Redis and storage
redisHelper := setupRedis(t)
defer redisHelper.Close()
tempDir := t.TempDir()
db, err := storage.NewDBFromPath(tempDir + "/test.db")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
schema := `
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
job_name TEXT NOT NULL,
args TEXT,
status TEXT NOT NULL DEFAULT 'pending',
priority INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
ended_at DATETIME,
worker_id TEXT,
error TEXT,
datasets TEXT,
metadata TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workers (
id TEXT PRIMARY KEY,
hostname TEXT NOT NULL,
last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL DEFAULT 'active',
current_jobs INTEGER DEFAULT 0,
max_jobs INTEGER DEFAULT 1,
metadata TEXT
);
CREATE TABLE IF NOT EXISTS job_metrics (
job_id TEXT NOT NULL,
metric_name TEXT NOT NULL,
metric_value TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (job_id, metric_name),
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
);
`
err = db.Initialize(schema)
if err != nil {
t.Fatalf("Failed to initialize database: %v", err)
}
// Test 3: Metrics recording in both systems
jobID := "metrics-job-1"
// Create job first to satisfy foreign key constraint
job := &storage.Job{
ID: jobID,
JobName: "Metrics Test Job",
Status: "running",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Args: "",
Priority: 0,
}
err = db.CreateJob(job)
if err != nil {
t.Fatalf("Failed to create job: %v", err)
}
// Record job metrics in database
err = db.RecordJobMetric(jobID, "cpu_usage", "75.5")
if err != nil {
t.Fatalf("Failed to record job metric: %v", err)
}
err = db.RecordJobMetric(jobID, "memory_usage", "1024.0")
if err != nil {
t.Fatalf("Failed to record job metric: %v", err)
}
// Record system metrics in Redis
ctx := context.Background()
systemMetricsKey := "ml:metrics:system"
metricsData := map[string]interface{}{
"timestamp": time.Now().Unix(),
"cpu_total": 85.2,
"memory_total": 4096.0,
"disk_usage": 75.0,
}
err = redisHelper.HMSet(ctx, systemMetricsKey, metricsData).Err()
if err != nil {
t.Fatalf("Failed to record system metrics in Redis: %v", err)
}
// Verify job metrics in database
jobMetrics, err := db.GetJobMetrics(jobID)
if err != nil {
t.Fatalf("Failed to get job metrics: %v", err)
}
if len(jobMetrics) != 2 {
t.Errorf("Expected 2 job metrics, got %d", len(jobMetrics))
}
// Verify system metrics in Redis
cpuTotal := redisHelper.HGet(ctx, systemMetricsKey, "cpu_total").Val()
if cpuTotal != "85.2" {
t.Errorf("Expected CPU total 85.2, got %s", cpuTotal)
}
memoryTotal := redisHelper.HGet(ctx, systemMetricsKey, "memory_total").Val()
if memoryTotal != "4096" {
t.Errorf("Expected memory total 4096, got %s", memoryTotal)
}
}
func TestStorageRedisJobStatusIntegration(t *testing.T) {
t.Parallel() // Enable parallel execution
// Setup Redis and storage
redisHelper := setupRedis(t)
defer redisHelper.Close()
tempDir := t.TempDir()
db, err := storage.NewDBFromPath(tempDir + "/test.db")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
schema := `
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
job_name TEXT NOT NULL,
args TEXT,
status TEXT NOT NULL DEFAULT 'pending',
priority INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME,
ended_at DATETIME,
worker_id TEXT,
error TEXT,
datasets TEXT,
metadata TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS workers (
id TEXT PRIMARY KEY,
hostname TEXT NOT NULL,
last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL DEFAULT 'active',
current_jobs INTEGER DEFAULT 0,
max_jobs INTEGER DEFAULT 1,
metadata TEXT
);
CREATE TABLE IF NOT EXISTS job_metrics (
job_id TEXT NOT NULL,
metric_name TEXT NOT NULL,
metric_value TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (job_id, metric_name),
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
);
`
err = db.Initialize(schema)
if err != nil {
t.Fatalf("Failed to initialize database: %v", err)
}
// Test 4: Job status updates across both systems
jobID := "status-job-1"
// Create initial job
job := &storage.Job{
ID: jobID,
JobName: "Status Test Job",
Status: "pending",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Args: "",
Priority: 0,
}
err = db.CreateJob(job)
if err != nil {
t.Fatalf("Failed to create job: %v", err)
}
// Update job status to running
err = db.UpdateJobStatus(jobID, "running", "worker-1", "")
if err != nil {
t.Fatalf("Failed to update job status: %v", err)
}
// Set job status in Redis for real-time tracking
ctx := context.Background()
statusKey := "ml:status:" + jobID
err = redisHelper.Set(ctx, statusKey, "running", time.Hour).Err()
if err != nil {
t.Fatalf("Failed to set job status in Redis: %v", err)
}
// Verify status in database
updatedJob, err := db.GetJob(jobID)
if err != nil {
t.Fatalf("Failed to get updated job: %v", err)
}
if updatedJob.Status != "running" {
t.Errorf("Expected job status 'running', got '%s'", updatedJob.Status)
}
// Verify status in Redis
redisStatus := redisHelper.Get(ctx, statusKey).Val()
if redisStatus != "running" {
t.Errorf("Expected Redis status 'running', got '%s'", redisStatus)
}
// Test status progression to completed
err = db.UpdateJobStatus(jobID, "completed", "worker-1", "")
if err != nil {
t.Fatalf("Failed to update job status to completed: %v", err)
}
err = redisHelper.Set(ctx, statusKey, "completed", time.Hour).Err()
if err != nil {
t.Fatalf("Failed to update Redis status: %v", err)
}
// Final verification
finalJob, err := db.GetJob(jobID)
if err != nil {
t.Fatalf("Failed to get final job: %v", err)
}
if finalJob.Status != "completed" {
t.Errorf("Expected final job status 'completed', got '%s'", finalJob.Status)
}
// Final Redis verification
finalRedisStatus := redisHelper.Get(ctx, statusKey).Val()
if finalRedisStatus != "completed" {
t.Errorf("Expected final Redis status 'completed', got '%s'", finalRedisStatus)
}
}