Files
goyco/internal/e2e/error_recovery_test.go

365 lines
8.2 KiB
Go

package e2e
import (
"context"
"errors"
"net/http"
"sync"
"testing"
"time"
"gorm.io/gorm"
"goyco/internal/database"
"goyco/internal/testutils"
)
func TestE2E_DatabaseFailureRecovery(t *testing.T) {
t.Run("database_unavailable_handles_gracefully", func(t *testing.T) {
ctx := setupTestContext(t)
sqlDB, err := ctx.server.DB.DB()
if err != nil {
t.Fatalf("Failed to get SQL DB: %v", err)
}
sqlDB.Close()
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
testutils.WithStandardHeaders(req)
resp, err := ctx.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
t.Logf("Expected 500 or 503, got %d (acceptable for unavailable DB)", resp.StatusCode)
}
})
t.Run("connection_pool_exhaustion", func(t *testing.T) {
ctx := setupTestContext(t)
sqlDB, err := ctx.server.DB.DB()
if err != nil {
t.Fatalf("Failed to get SQL DB: %v", err)
}
originalMaxOpen := sqlDB.Stats().MaxOpenConnections
if originalMaxOpen == 0 {
originalMaxOpen = 1
}
sqlDB.SetMaxOpenConns(2)
var wg sync.WaitGroup
errors := make(chan error, 10)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
conn, err := sqlDB.Conn(context.Background())
if err != nil {
errors <- err
return
}
defer conn.Close()
time.Sleep(100 * time.Millisecond)
}()
}
wg.Wait()
close(errors)
errorCount := 0
for range errors {
errorCount++
}
if errorCount == 0 {
t.Log("No connection errors occurred (pool handled load)")
}
sqlDB.SetMaxOpenConns(int(originalMaxOpen))
})
t.Run("transaction_rollback_on_error", func(t *testing.T) {
ctx := setupTestContext(t)
testUser := ctx.createUserWithCleanup(t, "rollbackuser", "StrongPass123!")
tx := ctx.server.DB.Begin()
if tx.Error != nil {
t.Fatalf("Failed to begin transaction: %v", tx.Error)
}
post := &database.Post{
Title: "Rollback Test Post",
URL: "https://example.com/rollback",
Content: "This post should be rolled back",
AuthorID: &testUser.ID,
}
err := tx.Create(post).Error
if err != nil {
tx.Rollback()
t.Fatalf("Failed to create post in transaction: %v", err)
}
var postInTx database.Post
err = tx.First(&postInTx, post.ID).Error
if err != nil {
tx.Rollback()
t.Fatalf("Failed to retrieve post in transaction: %v", err)
}
tx.Rollback()
var postAfterRollback database.Post
err = ctx.server.DB.First(&postAfterRollback, post.ID).Error
if err == nil {
t.Error("Expected post to not exist after transaction rollback")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
t.Logf("Post correctly not found after rollback (error: %v)", err)
}
})
t.Run("transaction_commit_succeeds", func(t *testing.T) {
ctx := setupTestContext(t)
testUser := ctx.createUserWithCleanup(t, "commituser", "StrongPass123!")
tx := ctx.server.DB.Begin()
if tx.Error != nil {
t.Fatalf("Failed to begin transaction: %v", tx.Error)
}
post := &database.Post{
Title: "Commit Test Post",
URL: "https://example.com/commit",
Content: "This post should be committed",
AuthorID: &testUser.ID,
}
err := tx.Create(post).Error
if err != nil {
tx.Rollback()
t.Fatalf("Failed to create post in transaction: %v", err)
}
err = tx.Commit().Error
if err != nil {
t.Fatalf("Failed to commit transaction: %v", err)
}
var postAfterCommit database.Post
err = ctx.server.DB.First(&postAfterCommit, post.ID).Error
if err != nil {
t.Errorf("Expected post to exist after transaction commit, got error: %v", err)
}
})
t.Run("database_timeout_handling", func(t *testing.T) {
ctx := setupTestContext(t)
sqlDB, err := ctx.server.DB.DB()
if err != nil {
t.Fatalf("Failed to get SQL DB: %v", err)
}
ctxTimeout, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel()
conn, err := sqlDB.Conn(ctxTimeout)
if err != nil {
return
}
defer conn.Close()
rows, err := conn.QueryContext(ctxTimeout, "SELECT 1")
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
t.Logf("Timeout handled correctly: %v", err)
}
if rows != nil {
rows.Close()
}
})
t.Run("concurrent_transaction_isolation", func(t *testing.T) {
ctx := setupTestContext(t)
testUser := ctx.createUserWithCleanup(t, "isolationuser", "StrongPass123!")
var wg sync.WaitGroup
errors := make(chan error, 2)
wg.Add(2)
go func() {
defer wg.Done()
tx1 := ctx.server.DB.Begin()
if tx1.Error != nil {
errors <- tx1.Error
return
}
post1 := &database.Post{
Title: "Isolation Post 1",
URL: "https://example.com/isolation1",
Content: "First transaction",
AuthorID: &testUser.ID,
}
err := tx1.Create(post1).Error
if err != nil {
tx1.Rollback()
errors <- err
return
}
time.Sleep(50 * time.Millisecond)
tx1.Commit()
}()
go func() {
defer wg.Done()
time.Sleep(25 * time.Millisecond)
tx2 := ctx.server.DB.Begin()
if tx2.Error != nil {
errors <- tx2.Error
return
}
post2 := &database.Post{
Title: "Isolation Post 2",
URL: "https://example.com/isolation2",
Content: "Second transaction",
AuthorID: &testUser.ID,
}
err := tx2.Create(post2).Error
if err != nil {
tx2.Rollback()
errors <- err
return
}
tx2.Commit()
}()
wg.Wait()
close(errors)
for err := range errors {
if err != nil {
t.Errorf("Transaction error: %v", err)
}
}
})
}
func TestE2E_DatabaseConnectionPool(t *testing.T) {
ctx := setupTestContext(t)
t.Run("pool_stats_tracking", func(t *testing.T) {
sqlDB, err := ctx.server.DB.DB()
if err != nil {
t.Fatalf("Failed to get SQL DB: %v", err)
}
stats := sqlDB.Stats()
if stats.MaxOpenConnections == 0 {
t.Error("Expected MaxOpenConnections to be set")
}
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
testutils.WithStandardHeaders(req)
resp, err := ctx.client.Do(req)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
resp.Body.Close()
newStats := sqlDB.Stats()
if newStats.OpenConnections > stats.OpenConnections {
t.Logf("Connection pool used: %d -> %d connections", stats.OpenConnections, newStats.OpenConnections)
}
})
t.Run("pool_reuses_connections", func(t *testing.T) {
sqlDB, err := ctx.server.DB.DB()
if err != nil {
t.Fatalf("Failed to get SQL DB: %v", err)
}
initialStats := sqlDB.Stats()
for i := 0; i < 5; i++ {
req, err := http.NewRequest("GET", ctx.baseURL+"/api/posts", nil)
if err != nil {
continue
}
testutils.WithStandardHeaders(req)
resp, err := ctx.client.Do(req)
if err == nil {
resp.Body.Close()
}
}
finalStats := sqlDB.Stats()
if finalStats.OpenConnections > initialStats.MaxOpenConnections {
t.Errorf("Pool exceeded max connections: %d > %d", finalStats.OpenConnections, initialStats.MaxOpenConnections)
}
})
}
func TestE2E_DatabaseErrorHandling(t *testing.T) {
ctx := setupTestContext(t)
t.Run("invalid_query_returns_error", func(t *testing.T) {
var result struct {
ID int
}
err := ctx.server.DB.Raw("SELECT * FROM nonexistent_table WHERE id = ?", 1).Scan(&result).Error
if err == nil {
t.Error("Expected error for invalid query")
}
})
t.Run("constraint_violation_handled", func(t *testing.T) {
testUser := ctx.createUserWithCleanup(t, "constraintuser", "StrongPass123!")
duplicateUser := &database.User{
Username: testUser.Username,
Email: "different@example.com",
Password: "DifferentPass123!",
EmailVerified: true,
}
err := ctx.server.DB.Create(duplicateUser).Error
if err == nil {
t.Error("Expected error for duplicate username")
}
})
t.Run("null_constraint_violation", func(t *testing.T) {
invalidPost := &database.Post{
Title: "",
URL: "",
Content: "",
}
err := ctx.server.DB.Create(invalidPost).Error
if err == nil {
t.Log("SQLite allows empty strings (constraint validation handled at application level)")
} else {
t.Logf("Database rejected empty values: %v", err)
}
})
}