Files
bookhoard/cmd/server/tests/conflicts_bulk_test.go
T
john-okeefe 0ac1c58bc7 fix(tests): fix timing issue in TestConflictsBulkEscalate
Add delay to allow queue processor to process sync queue items
before querying for escalated conflicts

Resolves race condition between queue item creation and conflict lookup
2026-02-13 20:18:46 -05:00

428 lines
13 KiB
Go

package main
import (
"bookhoard/internal/handlers"
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestConflictsBulkOperations tests bulk conflict resolution operations
func TestConflictsBulkOperations(t *testing.T) {
setup := setupTestServer(t)
token := loginTestUser(t, setup.Server, setup.DB)
client := &http.Client{}
t.Run("BulkResolveConflicts_WithoutAuth", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{uuid.New().String()},
Strategy: "most_recent",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("BulkResolveConflicts_EmptyConflictIDs", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{},
Strategy: "most_recent",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("BulkResolveConflicts_InvalidConflictID", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{"invalid-uuid"},
Strategy: "most_recent",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result handlers.BulkResolveResponse
json.NewDecoder(resp.Body).Decode(&result)
assert.NotEmpty(t, result.Results, "Should have results")
assert.Equal(t, 1, result.Total)
assert.Equal(t, 0, result.Success)
assert.Greater(t, result.Failed, 0)
firstResult := result.Results[0]
assert.Equal(t, "error", firstResult.Status)
})
t.Run("BulkResolveConflicts_InvalidStrategy", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{uuid.New().String()},
Strategy: "invalid_strategy",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
// Bulk operations return 200 OK with individual error results
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result handlers.BulkResolveResponse
json.NewDecoder(resp.Body).Decode(&result)
assert.NotEmpty(t, result.Results, "Should have results")
assert.Greater(t, result.Total, 0)
assert.Greater(t, result.Failed, 0)
firstResult := result.Results[0]
assert.Equal(t, "error", firstResult.Status)
// The error will be "conflict not found" since we're using a random UUID
// The invalid strategy would be caught for valid conflict IDs
assert.Contains(t, firstResult.Error, "conflict")
})
t.Run("BulkResolveConflicts_MostRecentStrategy", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{uuid.New().String(), uuid.New().String()},
Strategy: "most_recent",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result handlers.BulkResolveResponse
json.NewDecoder(resp.Body).Decode(&result)
assert.NotEmpty(t, result.Results, "Should have results")
assert.Equal(t, 2, result.Total)
})
t.Run("BulkResolveConflicts_HighestProgressStrategy", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{uuid.New().String(), uuid.New().String()},
Strategy: "highest_progress",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result handlers.BulkResolveResponse
json.NewDecoder(resp.Body).Decode(&result)
assert.NotEmpty(t, result.Results, "Should have results")
assert.Equal(t, 2, result.Total)
})
t.Run("BulkResolveConflicts_ManualStrategy_WithoutWinner", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{uuid.New().String()},
Strategy: "manual",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result handlers.BulkResolveResponse
json.NewDecoder(resp.Body).Decode(&result)
assert.NotEmpty(t, result.Results, "Should have results")
})
t.Run("BulkResolveConflicts_ManualStrategy_WithWinner", func(t *testing.T) {
req := handlers.BulkResolveRequest{
ConflictIDs: []string{uuid.New().String()},
Strategy: "manual",
WinningSource: "device",
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
})
t.Run("BulkResolveConflicts_InvalidRequestBody", func(t *testing.T) {
// Send invalid JSON
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer([]byte("invalid json")))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
// TestConflictsBulkDismiss tests bulk dismiss operations
func TestConflictsBulkDismiss(t *testing.T) {
setup := setupTestServer(t)
token := loginTestUser(t, setup.Server, setup.DB)
client := &http.Client{}
t.Run("BulkDismissConflicts_WithoutAuth", func(t *testing.T) {
req := map[string]interface{}{
"conflict_ids": []string{uuid.New().String()},
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("BulkDismissConflicts_EmptyConflictIDs", func(t *testing.T) {
req := map[string]interface{}{
"conflict_ids": []string{},
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("BulkDismissConflicts_InvalidConflictID", func(t *testing.T) {
req := map[string]interface{}{
"conflict_ids": []string{"invalid-uuid", uuid.New().String()},
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result, "results")
assert.Contains(t, result, "total")
assert.Contains(t, result, "success")
assert.Contains(t, result, "failed")
results := result["results"].([]interface{})
firstResult := results[0].(map[string]interface{})
assert.Equal(t, "error", firstResult["status"])
})
t.Run("BulkDismissConflicts_MultipleConflicts", func(t *testing.T) {
req := map[string]interface{}{
"conflict_ids": []string{
uuid.New().String(),
uuid.New().String(),
uuid.New().String(),
},
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result, "results")
assert.Equal(t, float64(3), result["total"])
})
t.Run("BulkDismissConflicts_InvalidRequestBody", func(t *testing.T) {
// Send invalid JSON
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer([]byte("invalid json")))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
// TestConflictsBulkEscalate tests bulk escalate operations
func TestConflictsBulkEscalate(t *testing.T) {
setup := setupTestServer(t)
token := loginTestUser(t, setup.Server, setup.DB)
client := &http.Client{}
t.Run("BulkEscalateConflicts_WithoutAuth", func(t *testing.T) {
req := map[string]interface{}{
"conflict_ids": []string{uuid.New().String()},
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("BulkEscalateConflicts_EmptyConflictIDs", func(t *testing.T) {
req := map[string]interface{}{
"conflict_ids": []string{},
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
// Allow queue processor to process the item before querying for conflicts
time.Sleep(3 * time.Second)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result, "results")
assert.Contains(t, result, "total")
assert.Contains(t, result, "failed")
results := result["results"].([]interface{})
firstResult := results[0].(map[string]interface{})
assert.Equal(t, "error", firstResult["status"])
})
t.Run("BulkEscalateConflicts_MultipleConflicts", func(t *testing.T) {
conflictIDs := []string{
uuid.New().String(),
uuid.New().String(),
}
req := map[string]interface{}{
"conflict_ids": conflictIDs,
}
body, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(httpReq)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
assert.Contains(t, result, "results")
assert.Equal(t, float64(2), result["total"])
// NEW: Database verification - verify conflicts were escalated
for _, conflictID := range conflictIDs {
pgID, err := uuid.Parse(conflictID)
if err != nil {
continue // Skip invalid UUIDs
}
conflict, err := setup.DB.GetSyncConflict(context.Background(), pgtype.UUID{Bytes: [16]byte(pgID), Valid: true})
if err == nil {
// If conflict exists, verify it was escalated
assert.Equal(t, "escalated", conflict.ResolutionStatus.String, "Conflict should be escalated")
}
}
})
}