To gitea and beyond, let's go(-yco)
This commit is contained in:
364
internal/e2e/error_recovery_test.go
Normal file
364
internal/e2e/error_recovery_test.go
Normal file
@@ -0,0 +1,364 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user