package integration import ( "context" "database/sql" "os" "testing" "time" _ "github.com/lib/pq" amqp "github.com/rabbitmq/amqp091-go" "github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/services" ) func ptrString(s string) *string { return &s } // TestStorageService_Integration tests StorageService with real S3/Civo credentials // Run with: go test -v ./tests/integration/... -tags=integration func TestStorageService_Integration(t *testing.T) { // Skip if not running integration tests if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } // These should be set from environment endpoint := os.Getenv("AWS_ENDPOINT") accessKey := os.Getenv("AWS_ACCESS_KEY_ID") secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") bucket := os.Getenv("S3_BUCKET") region := os.Getenv("AWS_REGION") if endpoint == "" || accessKey == "" || secretKey == "" || bucket == "" { t.Skip("Missing S3 credentials in environment") } t.Logf("Testing with endpoint: %s, bucket: %s, region: %s", endpoint, bucket, region) t.Run("verifies S3 credentials are valid", func(t *testing.T) { t.Logf("Credentials loaded successfully") t.Logf(" Endpoint: %s", endpoint) t.Logf(" Bucket: %s", bucket) t.Logf(" Region: %s", region) t.Logf(" Access Key: %s...", accessKey[:4]) }) } // TestDatabaseConnection_Integration tests real database connectivity func TestDatabaseConnection_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { t.Skip("Missing DATABASE_URL in environment") } db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() t.Run("pings database", func(t *testing.T) { err := db.Ping() if err != nil { t.Fatalf("Failed to ping database: %v", err) } t.Log("✅ Database connection successful") }) t.Run("queries users table", func(t *testing.T) { var count int err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) if err != nil { t.Fatalf("Failed to query users: %v", err) } t.Logf("✅ Users table has %d rows", count) }) t.Run("queries jobs table", func(t *testing.T) { var count int err := db.QueryRow("SELECT COUNT(*) FROM jobs").Scan(&count) if err != nil { t.Fatalf("Failed to query jobs: %v", err) } t.Logf("✅ Jobs table has %d rows", count) }) t.Run("queries companies table", func(t *testing.T) { var count int err := db.QueryRow("SELECT COUNT(*) FROM companies").Scan(&count) if err != nil { t.Fatalf("Failed to query companies: %v", err) } t.Logf("✅ Companies table has %d rows", count) }) } // TestSettingsService_Integration tests SettingsService with real database func TestSettingsService_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { t.Skip("Missing DATABASE_URL in environment") } db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() settingsService := services.NewSettingsService(db) ctx := context.Background() t.Run("saves and retrieves settings", func(t *testing.T) { testKey := "integration_test_key" testValue := map[string]interface{}{ "test": true, "timestamp": "2026-01-01", } // Save err := settingsService.SaveSettings(ctx, testKey, testValue) if err != nil { t.Fatalf("Failed to save settings: %v", err) } t.Logf("✅ Saved setting '%s'", testKey) // Retrieve result, err := settingsService.GetSettings(ctx, testKey) if err != nil { t.Fatalf("Failed to get settings: %v", err) } if result == nil { t.Fatal("Expected result, got nil") } t.Logf("✅ Retrieved setting '%s': %s", testKey, string(result)) // Cleanup _, err = db.Exec("DELETE FROM system_settings WHERE key = $1", testKey) if err != nil { t.Logf("Warning: Failed to cleanup test setting: %v", err) } }) } // TestJobService_Integration tests JobService with real database func TestJobService_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { t.Skip("Missing DATABASE_URL in environment") } db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() jobService := services.NewJobService(db) t.Run("lists jobs", func(t *testing.T) { filter := dto.JobFilterQuery{} jobs, total, err := jobService.GetJobs(filter) if err != nil { t.Fatalf("Failed to list jobs: %v", err) } t.Logf("✅ Listed %d jobs (total: %d)", len(jobs), total) }) } // TestNotificationService_Integration tests NotificationService with real database func TestNotificationService_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } dbURL := os.Getenv("DATABASE_URL") if dbURL == "" { t.Skip("Missing DATABASE_URL in environment") } db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() notificationService := services.NewNotificationService(db, nil) ctx := context.Background() t.Run("lists notifications for user", func(t *testing.T) { // Use a test user ID that might exist userID := "00000000-0000-0000-0000-000000000000" notifications, err := notificationService.ListNotifications(ctx, userID) if err != nil { t.Fatalf("Failed to list notifications: %v", err) } t.Logf("✅ Listed %d notifications for user", len(notifications)) }) } // TestAMQPConnection_Integration tests real AMQP/RabbitMQ connectivity func TestAMQPConnection_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } amqpURL := os.Getenv("AMQP_URL") if amqpURL == "" { t.Skip("Missing AMQP_URL in environment") } t.Run("connects to CloudAMQP", func(t *testing.T) { // Import amqp package dynamically conn, err := amqp.Dial(amqpURL) if err != nil { t.Fatalf("Failed to connect to AMQP: %v", err) } defer conn.Close() t.Log("✅ AMQP connection successful") ch, err := conn.Channel() if err != nil { t.Fatalf("Failed to open channel: %v", err) } defer ch.Close() t.Log("✅ AMQP channel opened") // Declare a test queue q, err := ch.QueueDeclare( "integration_test_queue", false, // durable true, // delete when unused false, // exclusive false, // no-wait nil, // arguments ) if err != nil { t.Fatalf("Failed to declare queue: %v", err) } t.Logf("✅ Queue declared: %s", q.Name) // Publish a test message testBody := []byte(`{"test": true, "timestamp": "2026-01-01"}`) err = ch.Publish( "", // exchange q.Name, // routing key false, // mandatory false, // immediate amqp.Publishing{ ContentType: "application/json", Body: testBody, }) if err != nil { t.Fatalf("Failed to publish message: %v", err) } t.Log("✅ Test message published") // Consume the message back msgs, err := ch.Consume( q.Name, // queue "", // consumer true, // auto-ack false, // exclusive false, // no-local false, // no-wait nil, // args ) if err != nil { t.Fatalf("Failed to consume: %v", err) } // Read one message with timeout select { case msg := <-msgs: t.Logf("✅ Received message: %s", string(msg.Body)) case <-time.After(5 * time.Second): t.Fatal("Timeout waiting for message") } }) } // TestEmailService_AMQP_Integration tests EmailService with real AMQP func TestEmailService_AMQP_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test - set RUN_INTEGRATION_TESTS=true to run") } amqpURL := os.Getenv("AMQP_URL") dbURL := os.Getenv("DATABASE_URL") if amqpURL == "" || dbURL == "" { t.Skip("Missing AMQP_URL or DATABASE_URL in environment") } db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() // First, ensure email_settings has the AMQP URL // ID must be a valid UUID. var settingsID string err = db.QueryRow("SELECT id FROM email_settings LIMIT 1").Scan(&settingsID) if err == sql.ErrNoRows { settingsID = "00000000-0000-0000-0000-000000000001" _, err = db.Exec(` INSERT INTO email_settings (id, amqp_url, is_active, updated_at) VALUES ($1, $2, true, NOW()) `, settingsID, amqpURL) } else { _, err = db.Exec(`UPDATE email_settings SET amqp_url = $1, is_active = true WHERE id = $2`, amqpURL, settingsID) } if err != nil { t.Logf("Warning: Could not update email_settings: %v", err) } credsSvc := services.NewCredentialsService(db) emailSvc := services.NewEmailService(db, credsSvc) ctx := context.Background() t.Run("queues email via RabbitMQ", func(t *testing.T) { err := emailSvc.SendTemplateEmail(ctx, "test@example.com", "welcome", map[string]interface{}{ "name": "Integration Test", }) if err != nil { // This might fail if email_settings doesn't have amqp_url configured correctly t.Logf("SendTemplateEmail error (expected if email_settings not configured): %v", err) } else { t.Log("✅ Email queued successfully via RabbitMQ") } }) } // TestCompanyService_Integration tests admin/company management with real DB func TestCompanyService_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test") } dbURL := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() adminSvc := services.NewAdminService(db) // AdminService handles companies ctx := context.Background() t.Run("lists companies", func(t *testing.T) { companies, total, err := adminSvc.ListCompanies(ctx, nil, 1, 10) if err != nil { t.Fatalf("ListCompanies failed: %v", err) } t.Logf("✅ Listed %d companies (total: %d)", len(companies), total) }) } // TestJobService_CRUD_Integration tests full job lifecycle func TestJobService_CRUD_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test") } dbURL := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() jobSvc := services.NewJobService(db) // Need a valid company ID. Use first one found. var companyID string err = db.QueryRow("SELECT id FROM companies LIMIT 1").Scan(&companyID) if err != nil { t.Logf("Skipping job creation test: no companies found (%v)", err) return } // Need a valid User ID (createdBy) var userID string err = db.QueryRow("SELECT id FROM users LIMIT 1").Scan(&userID) if err != nil { // Fallback to a dummy UUID if no users exist userID = "00000000-0000-0000-0000-000000000000" } t.Run("creates, updates and deletes job", func(t *testing.T) { // 1. Create req := dto.CreateJobRequest{ CompanyID: companyID, Title: "Integration Test Job", Description: "Test Description that is quite long to satisfy validation requirements", EmploymentType: ptrString("full-time"), Location: ptrString("Remote"), Status: "draft", } // Create job, err := jobSvc.CreateJob(req, userID) if err != nil { t.Fatalf("CreateJob failed: %v", err) } t.Logf("✅ Created job: %s", job.ID) // 2. Get fetched, err := jobSvc.GetJobByID(job.ID) if err != nil { t.Fatalf("GetJobByID failed: %v", err) } if fetched.Title != req.Title { t.Errorf("Title mismatch: expected %s, got %s", req.Title, fetched.Title) } // 3. Update newTitle := "Updated Integration Job" updated, err := jobSvc.UpdateJob(job.ID, dto.UpdateJobRequest{Title: &newTitle}) if err != nil { t.Fatalf("UpdateJob failed: %v", err) } if updated.Title != newTitle { t.Errorf("Update failed: expected %s, got %s", newTitle, updated.Title) } // 4. Update Status (using UpdateJob) newStatus := "closed" _, err = jobSvc.UpdateJob(job.ID, dto.UpdateJobRequest{Status: &newStatus}) if err != nil { t.Fatalf("UpdateJob (Status) failed: %v", err) } // Cleanup: Delete err = jobSvc.DeleteJob(job.ID) if err != nil { t.Fatalf("DeleteJob failed: %v", err) } t.Log("✅ Deleted job") }) } // TestApplicationService_Integration tests application lifecycle func TestApplicationService_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test") } dbURL := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() jobSvc := services.NewJobService(db) appSvc := services.NewApplicationService(db) // Setup: Need a Job var companyID, userID string err = db.QueryRow("SELECT id FROM companies LIMIT 1").Scan(&companyID) if err != nil { t.Skip("No companies found") } err = db.QueryRow("SELECT id FROM users LIMIT 1").Scan(&userID) if err != nil { userID = "00000000-0000-0000-0000-000000000000" } jobReq := dto.CreateJobRequest{ CompanyID: companyID, Title: "App Test Job", Description: "Job for application testing", EmploymentType: ptrString("full-time"), Location: ptrString("Remote"), Status: "open", } job, err := jobSvc.CreateJob(jobReq, userID) if err != nil { t.Fatalf("Setup: CreateJob failed: %v", err) } defer jobSvc.DeleteJob(job.ID) t.Run("creates and updates application", func(t *testing.T) { // 1. Create Application appReq := dto.CreateApplicationRequest{ JobID: job.ID, UserID: &userID, Name: ptrString("Applicant Name"), Email: ptrString("applicant@test.com"), Message: ptrString("Hire me!"), } app, err := appSvc.CreateApplication(appReq) if err != nil { t.Fatalf("CreateApplication failed: %v", err) } t.Logf("✅ Created application: %s", app.ID) // 2. Update Status statusReq := dto.UpdateApplicationStatusRequest{ Status: "reviewed", Notes: ptrString("Looks good"), } updated, err := appSvc.UpdateApplicationStatus(app.ID, statusReq) if err != nil { t.Fatalf("UpdateApplicationStatus failed: %v", err) } if updated.Status != "reviewed" { t.Errorf("Status mismatch: expected reviewed, got %s", updated.Status) } t.Log("✅ Updated application status") // Cleanup: Delete Application err = appSvc.DeleteApplication(app.ID) if err != nil { t.Fatalf("DeleteApplication failed: %v", err) } }) } // TestAdminService_Integration tests admin actions func TestAdminService_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test") } dbURL := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() adminSvc := services.NewAdminService(db) // Need a company to modify var companyID string err = db.QueryRow("SELECT id FROM companies LIMIT 1").Scan(&companyID) if err != nil { t.Skip("No companies found") } t.Run("updates company verification status", func(t *testing.T) { verified := true active := true req := dto.UpdateCompanyRequest{ Verified: &verified, Active: &active, } updated, err := adminSvc.UpdateCompany(context.Background(), companyID, req) if err != nil { t.Fatalf("UpdateCompany failed: %v", err) } if !updated.Verified { t.Errorf("Company should be verifying") } t.Logf("✅ Company verification updated") }) // Test GetEmailSettings and others t.Run("get email settings", func(t *testing.T) { settings, err := adminSvc.GetEmailSettings(context.Background()) if err != nil { t.Fatalf("GetEmailSettings failed: %v", err) } if settings != nil { t.Logf("✅ Got email settings: %s", settings.ID) } }) } // TestJobService_Filters_Integration tests complex job queries func TestJobService_Filters_Integration(t *testing.T) { if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { t.Skip("Skipping integration test") } dbURL := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dbURL) if err != nil { t.Fatalf("Failed to open database: %v", err) } defer db.Close() jobSvc := services.NewJobService(db) t.Run("filters jobs by multiple criteria", func(t *testing.T) { mode := "remote" salaryMin := 1000.0 filter := dto.JobFilterQuery{ WorkMode: &mode, SalaryMin: &salaryMin, SortBy: ptrString("recent"), } filter.Limit = 5 jobs, _, err := jobSvc.GetJobs(filter) if err != nil { t.Fatalf("GetJobs with filter failed: %v", err) } t.Logf("✅ Listed %d jobs with filters", len(jobs)) }) }