gohorsejobs/backend/tests/integration/services_integration_test.go
Tiago Yamamoto 6cd8c02252 feat: add test coverage and handler improvements
- Add new test files for handlers (storage, payment, settings)
- Add new test files for services (chat, email, storage, settings, admin)
- Add integration tests for services
- Update handler implementations with bug fixes
- Add coverage reports and test documentation
2026-01-02 08:50:29 -03:00

621 lines
17 KiB
Go

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