test(sync): rewrite conflict tests as real HTTP integration tests
Replace the previous mock/httptest-based conflict tests with integration tests that exercise the full HTTP stack against a live test server with a real database. Changes include: - Add shared test helpers (setupConflictTest, createTestConflict, makeConflictData) to reduce boilerplate across test files - Split monolithic TestConflictDetection and TestConflictsBulkOperations into focused test functions per scenario - Test conflict detection, bulk resolution (most_recent, highest_progress, manual strategies), and edge cases (empty IDs, invalid UUIDs, unauthorized access) - Verify actual database state after resolution, not just HTTP response
This commit is contained in:
+493
-257
@@ -1,289 +1,525 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bookhoard/internal/database"
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http/httptest"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConflictDetection_TriggeringConditions(t *testing.T) {
|
type conflictTestEnv struct {
|
||||||
t.Run("conflict detected when different devices sync within 5 minutes", func(t *testing.T) {
|
setup *TestServerSetup
|
||||||
conflictData := map[string]map[string]interface{}{
|
mediaID string
|
||||||
"koreader": {
|
userID pgtype.UUID
|
||||||
"source": "koreader",
|
mediaPGID pgtype.UUID
|
||||||
"timestamp": "2026-01-30T20:10:00Z",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"percentage": 0.45,
|
|
||||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
|
||||||
"chapter": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"kobo": {
|
|
||||||
"source": "kobo",
|
|
||||||
"timestamp": "2026-01-30T20:05:00Z",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"percentage": 0.42,
|
|
||||||
"page": 89,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(conflictData)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/sync/koreader/progress", bytes.NewReader(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
assert.Equal(t, "POST", req.Method)
|
|
||||||
assert.Contains(t, string(body), "koreader")
|
|
||||||
assert.Contains(t, string(body), "kobo")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("no conflict when progress difference is less than 1%", func(t *testing.T) {
|
|
||||||
progressData := map[string]interface{}{
|
|
||||||
"percentage": 0.45,
|
|
||||||
}
|
|
||||||
|
|
||||||
existingProgress := map[string]interface{}{
|
|
||||||
"percentage": 0.451,
|
|
||||||
}
|
|
||||||
|
|
||||||
diff := progressData["percentage"].(float64) - existingProgress["percentage"].(float64)
|
|
||||||
if diff < 0 {
|
|
||||||
diff = -diff
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Less(t, diff, 0.01, "Should not trigger conflict for small differences")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("no conflict when sync timestamps are more than 5 minutes apart", func(t *testing.T) {
|
|
||||||
timestamp1 := "2026-01-30T20:00:00Z"
|
|
||||||
timestamp2 := "2026-01-30T20:10:00Z"
|
|
||||||
|
|
||||||
var conflictDetected bool
|
|
||||||
if timestamp2 > timestamp1 {
|
|
||||||
conflictDetected = false
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.False(t, conflictDetected, "Should not trigger conflict for old syncs")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConflictResolution_ChoosingWinner(t *testing.T) {
|
func setupConflictTest(t *testing.T) *conflictTestEnv {
|
||||||
t.Run("resolve conflict by choosing koreader source", func(t *testing.T) {
|
t.Helper()
|
||||||
conflictID := uuid.New()
|
|
||||||
|
|
||||||
reqBody := map[string]interface{}{
|
setup := setupTestServer(t)
|
||||||
"winner": "koreader",
|
mediaID := createTestMediaItemID(t, setup)
|
||||||
"manual_data": nil,
|
|
||||||
"apply_to_all_future_conflicts": false,
|
|
||||||
"reason": "More recent progress",
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(reqBody)
|
ctx := context.Background()
|
||||||
assert.NoError(t, err)
|
user, err := setup.DB.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/conflicts/"+conflictID.String()+"/resolve", bytes.NewReader(body))
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "POST", req.Method)
|
return &conflictTestEnv{
|
||||||
assert.Contains(t, req.URL.Path, conflictID.String())
|
setup: setup,
|
||||||
assert.Contains(t, string(body), "koreader")
|
mediaID: mediaID,
|
||||||
|
userID: user.ID,
|
||||||
|
mediaPGID: pgtype.UUID{Bytes: [16]byte(mediaUUID), Valid: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestConflict(t *testing.T, env *conflictTestEnv, conflictData map[string]interface{}) database.SyncConflicts {
|
||||||
|
t.Helper()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
dataJSON, err := json.Marshal(conflictData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
conflict, err := env.setup.DB.CreateSyncConflict(ctx, database.CreateSyncConflictParams{
|
||||||
|
MediaItemID: env.mediaPGID,
|
||||||
|
UserID: env.userID,
|
||||||
|
ConflictType: "progress",
|
||||||
|
ConflictData: dataJSON,
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("resolve conflict with manual merge data", func(t *testing.T) {
|
return conflict
|
||||||
conflictID := uuid.New()
|
}
|
||||||
|
|
||||||
manualData := map[string]interface{}{
|
func makeConflictData(koreaderPct, koboPct float64) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"koreader": map[string]interface{}{
|
||||||
|
"source": "koreader",
|
||||||
|
"timestamp": time.Date(2026, 1, 30, 20, 10, 0, 0, time.UTC),
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"percentage": koreaderPct,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"kobo": map[string]interface{}{
|
||||||
|
"source": "kobo",
|
||||||
|
"timestamp": time.Date(2026, 1, 30, 20, 5, 0, 0, time.UTC),
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"percentage": koboPct,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictList_Empty(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result handlers.ConflictListResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, result.Total)
|
||||||
|
assert.Empty(t, result.Conflicts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictList_WithConflicts(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts?status=all", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result handlers.ConflictListResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, result.Total, 1)
|
||||||
|
require.NotEmpty(t, result.Conflicts)
|
||||||
|
|
||||||
|
conflict := result.Conflicts[0]
|
||||||
|
assert.Equal(t, "progress", conflict.ConflictType)
|
||||||
|
assert.Equal(t, "unresolved", conflict.ResolutionStatus)
|
||||||
|
assert.Contains(t, conflict.ConflictData, "koreader")
|
||||||
|
assert.Contains(t, conflict.ConflictData, "kobo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictList_UnresolvedCount(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result handlers.ConflictListResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, result.Unresolved, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictGet_ByID(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.50, 0.30))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts/"+conflictID, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var detail handlers.ConflictDetailResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&detail)
|
||||||
|
|
||||||
|
assert.Equal(t, conflictID, detail.ID)
|
||||||
|
assert.Equal(t, env.mediaID, detail.MediaItemID)
|
||||||
|
assert.Equal(t, "progress", detail.ConflictType)
|
||||||
|
assert.Contains(t, detail.ConflictData, "koreader")
|
||||||
|
assert.Contains(t, detail.ConflictData, "kobo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictGet_NotFound(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictGet_InvalidID(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts/not-a-uuid", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_ByKOReader(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.75, 0.30))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "koreader",
|
||||||
|
"manual_data": nil,
|
||||||
|
"apply_to_all_future_conflicts": false,
|
||||||
|
"reason": "More recent progress",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result handlers.ConflictResolveResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.True(t, result.ConflictResolved)
|
||||||
|
assert.Equal(t, map[string]bool{"progress": true, "annotations": false}, result.AppliedTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_ByKobo(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.30, 0.75))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "kobo",
|
||||||
|
"apply_to_all_future_conflicts": false,
|
||||||
|
"reason": "Higher progress",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result handlers.ConflictResolveResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.True(t, result.ConflictResolved)
|
||||||
|
assert.Equal(t, map[string]bool{"progress": true, "annotations": false}, result.AppliedTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_WithManualData(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "manual",
|
||||||
|
"manual_data": map[string]interface{}{
|
||||||
"percentage": 0.43,
|
"percentage": 0.43,
|
||||||
"epubcfi": "epubcfi(/6/4/2:20)",
|
"epubcfi": "epubcfi(/6/4/2:20)",
|
||||||
"chapter": 3,
|
"chapter": 3,
|
||||||
"page": 90,
|
},
|
||||||
}
|
"apply_to_all_future_conflicts": false,
|
||||||
|
"reason": "Custom merged position",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
reqBody := map[string]interface{}{
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
"winner": "manual",
|
req.Header.Set("Content-Type", "application/json")
|
||||||
"manual_data": manualData,
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
"apply_to_all_future_conflicts": false,
|
|
||||||
"reason": "Custom merged position",
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(reqBody)
|
resp, err := client.Do(req)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/conflicts/"+conflictID.String()+"/resolve", bytes.NewReader(body))
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result handlers.ConflictResolveResponse
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.True(t, result.ConflictResolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_ManualWithoutData(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "manual",
|
||||||
|
"manual_data": nil,
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_AlreadyResolved(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.50, 0.30))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "koreader",
|
||||||
|
"reason": "First resolution",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req2.Header.Set("Content-Type", "application/json")
|
||||||
|
req2.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp2, err := client.Do(req2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp2.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_InvalidWinner(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "nonexistent_source",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolve_NotFound(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "koreader",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/"+uuid.New().String()+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictDelete(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("DELETE", env.setup.Server.URL+"/api/conflicts/"+conflictID, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("GET", env.setup.Server.URL+"/api/conflicts/"+conflictID, nil)
|
||||||
|
req2.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp2, err := client.Do(req2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp2.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictDelete_NotFound(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictDismissAllResolved(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.50, 0.30))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
resolveReq := map[string]interface{}{
|
||||||
|
"winner": "koreader",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(resolveReq)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/"+conflictID+"/resolve", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/dismiss-all", nil)
|
||||||
|
req2.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
|
resp2, err := client.Do(req2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
var result map[string]interface{}
|
||||||
|
json.NewDecoder(resp2.Body).Decode(&result)
|
||||||
|
|
||||||
|
deleted, ok := result["deleted"].(float64)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.GreaterOrEqual(t, int(deleted), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictEndpoints_RequireAuth(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
t.Run("list conflicts requires auth", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts", nil)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get conflict requires auth", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("resolve conflict requires auth", func(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/"+uuid.New().String()+"/resolve", bytes.NewBuffer([]byte(`{}`)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
assert.Contains(t, string(body), "manual")
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(body), "0.43")
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("error when winner is manual but no manual_data provided", func(t *testing.T) {
|
t.Run("delete conflict requires auth", func(t *testing.T) {
|
||||||
conflictID := uuid.New()
|
req, _ := http.NewRequest("DELETE", setup.Server.URL+"/api/conflicts/"+uuid.New().String(), nil)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
})
|
||||||
|
|
||||||
reqBody := map[string]interface{}{
|
t.Run("dismiss-all requires auth", func(t *testing.T) {
|
||||||
"winner": "manual",
|
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/dismiss-all", nil)
|
||||||
"manual_data": nil,
|
resp, err := client.Do(req)
|
||||||
"apply_to_all_future_conflicts": false,
|
require.NoError(t, err)
|
||||||
"reason": "Test",
|
defer resp.Body.Close()
|
||||||
}
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
|
|
||||||
body, err := json.Marshal(reqBody)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("POST", "/api/conflicts/"+conflictID.String()+"/resolve", bytes.NewReader(body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
assert.Contains(t, string(body), "manual")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConflictListing_Filtering(t *testing.T) {
|
|
||||||
t.Run("list only unresolved conflicts", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/conflicts?status=unresolved", nil)
|
|
||||||
assert.Equal(t, "GET", req.Method)
|
|
||||||
assert.Contains(t, req.URL.Query().Get("status"), "unresolved")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("list all conflicts regardless of status", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/conflicts?status=all", nil)
|
|
||||||
assert.Equal(t, "GET", req.Method)
|
|
||||||
assert.Contains(t, req.URL.Query().Get("status"), "all")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("list only resolved conflicts", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("GET", "/api/conflicts?status=user_resolved", nil)
|
|
||||||
assert.Equal(t, "GET", req.Method)
|
|
||||||
assert.Contains(t, req.URL.Query().Get("status"), "user_resolved")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConflictResponse_Structure(t *testing.T) {
|
|
||||||
t.Run("conflict detail response includes all required fields", func(t *testing.T) {
|
|
||||||
conflictResponse := map[string]interface{}{
|
|
||||||
"id": "conflict-uuid-123",
|
|
||||||
"media_item_id": "book-uuid-456",
|
|
||||||
"media_item_title": "Test Book Title",
|
|
||||||
"conflict_type": "progress",
|
|
||||||
"resolution_status": "unresolved",
|
|
||||||
"created_at": "2026-01-30T20:10:00Z",
|
|
||||||
"conflict_data": map[string]interface{}{
|
|
||||||
"koreader": map[string]interface{}{
|
|
||||||
"source": "koreader",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"percentage": 0.45,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"kobo": map[string]interface{}{
|
|
||||||
"source": "kobo",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"percentage": 0.42,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(conflictResponse)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var parsed map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &parsed)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Contains(t, parsed, "id")
|
|
||||||
assert.Contains(t, parsed, "media_item_id")
|
|
||||||
assert.Contains(t, parsed, "conflict_data")
|
|
||||||
assert.Contains(t, parsed["conflict_data"].(map[string]interface{}), "koreader")
|
|
||||||
assert.Contains(t, parsed["conflict_data"].(map[string]interface{}), "kobo")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("conflict list response includes summary counts", func(t *testing.T) {
|
|
||||||
listResponse := map[string]interface{}{
|
|
||||||
"conflicts": []interface{}{
|
|
||||||
map[string]string{"id": "conflict-1", "resolution_status": "unresolved"},
|
|
||||||
map[string]string{"id": "conflict-2", "resolution_status": "unresolved"},
|
|
||||||
},
|
|
||||||
"total": 2,
|
|
||||||
"unresolved": 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(listResponse)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var parsed map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &parsed)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, float64(2), parsed["total"])
|
|
||||||
assert.Equal(t, float64(2), parsed["unresolved"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConflictDeletion(t *testing.T) {
|
|
||||||
t.Run("delete single conflict by ID", func(t *testing.T) {
|
|
||||||
conflictID := uuid.New()
|
|
||||||
|
|
||||||
req := httptest.NewRequest("DELETE", "/api/conflicts/"+conflictID.String(), nil)
|
|
||||||
assert.Equal(t, "DELETE", req.Method)
|
|
||||||
assert.Contains(t, req.URL.Path, conflictID.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("dismiss all resolved conflicts", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest("POST", "/api/conflicts/dismiss-all", nil)
|
|
||||||
assert.Equal(t, "POST", req.Method)
|
|
||||||
assert.Contains(t, req.URL.Path, "dismiss-all")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConflictNotification_WebSocketBroadcast(t *testing.T) {
|
|
||||||
t.Run("conflict detection notification", func(t *testing.T) {
|
|
||||||
notification := map[string]interface{}{
|
|
||||||
"type": "conflict",
|
|
||||||
"timestamp": "2026-01-30T20:10:00Z",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"book_id": "book-uuid-123",
|
|
||||||
"notification_type": "detection",
|
|
||||||
"conflict_id": "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(notification)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var parsed map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &parsed)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
data := parsed["data"].(map[string]interface{})
|
|
||||||
assert.Equal(t, "detection", data["notification_type"])
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("conflict resolved notification", func(t *testing.T) {
|
|
||||||
conflictID := uuid.New()
|
|
||||||
|
|
||||||
notification := map[string]interface{}{
|
|
||||||
"type": "conflict",
|
|
||||||
"timestamp": "2026-01-30T20:15:00Z",
|
|
||||||
"data": map[string]interface{}{
|
|
||||||
"book_id": "book-uuid-123",
|
|
||||||
"notification_type": "resolved",
|
|
||||||
"conflict_id": conflictID.String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := json.Marshal(notification)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
var parsed map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &parsed)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
data := parsed["data"].(map[string]interface{})
|
|
||||||
assert.Equal(t, "resolved", data["notification_type"])
|
|
||||||
assert.Equal(t, conflictID.String(), data["conflict_id"])
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,414 +12,461 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestConflictsBulkOperations tests bulk conflict resolution operations
|
func TestBulkResolve_MostRecentStrategy(t *testing.T) {
|
||||||
func TestConflictsBulkOperations(t *testing.T) {
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict1 := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflict2 := createTestConflict(t, env, makeConflictData(0.60, 0.55))
|
||||||
|
|
||||||
|
id1 := uuid.UUID(conflict1.ID.Bytes).String()
|
||||||
|
id2 := uuid.UUID(conflict2.ID.Bytes).String()
|
||||||
|
|
||||||
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{id1, id2},
|
||||||
|
Strategy: "most_recent",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.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.Equal(t, 2, result.Total)
|
||||||
|
assert.Equal(t, 2, result.Success)
|
||||||
|
assert.Equal(t, 0, result.Failed)
|
||||||
|
require.Len(t, result.Results, 2)
|
||||||
|
|
||||||
|
for _, r := range result.Results {
|
||||||
|
assert.Equal(t, "success", r.Status)
|
||||||
|
assert.Equal(t, "koreader", r.Winner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkResolve_HighestProgressStrategy(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflictDataHighKobo := makeConflictData(0.30, 0.90)
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, conflictDataHighKobo)
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{conflictID},
|
||||||
|
Strategy: "highest_progress",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.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.Equal(t, 1, result.Total)
|
||||||
|
assert.Equal(t, 1, result.Success)
|
||||||
|
assert.Equal(t, 0, result.Failed)
|
||||||
|
assert.Equal(t, "kobo", result.Results[0].Winner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkResolve_ManualStrategy(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{conflictID},
|
||||||
|
Strategy: "manual",
|
||||||
|
WinningSource: "koreader",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.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.Equal(t, 1, result.Total)
|
||||||
|
assert.Equal(t, 1, result.Success)
|
||||||
|
assert.Equal(t, "koreader", result.Results[0].Winner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkResolve_ManualStrategy_WithoutWinner(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{conflictID},
|
||||||
|
Strategy: "manual",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.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.Equal(t, 1, result.Failed)
|
||||||
|
assert.Equal(t, "error", result.Results[0].Status)
|
||||||
|
assert.Contains(t, result.Results[0].Error, "winning_source")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkResolve_ManualStrategy_InvalidWinner(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflictID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{conflictID},
|
||||||
|
Strategy: "manual",
|
||||||
|
WinningSource: "nonexistent_device",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.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.Equal(t, 1, result.Failed)
|
||||||
|
assert.Contains(t, result.Results[0].Error, "invalid winning source")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkResolve_ConflictNotFound(t *testing.T) {
|
||||||
setup := setupTestServer(t)
|
setup := setupTestServer(t)
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
t.Run("BulkResolveConflicts_WithoutAuth", func(t *testing.T) {
|
req := handlers.BulkResolveRequest{
|
||||||
req := handlers.BulkResolveRequest{
|
ConflictIDs: []string{uuid.New().String()},
|
||||||
ConflictIDs: []string{uuid.New().String()},
|
Strategy: "most_recent",
|
||||||
Strategy: "most_recent",
|
}
|
||||||
}
|
body, _ := json.Marshal(req)
|
||||||
body, _ := json.Marshal(req)
|
|
||||||
|
|
||||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
resp, err := client.Do(httpReq)
|
resp, err := client.Do(httpReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("BulkResolveConflicts_EmptyConflictIDs", func(t *testing.T) {
|
var result handlers.BulkResolveResponse
|
||||||
req := handlers.BulkResolveRequest{
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
ConflictIDs: []string{},
|
|
||||||
Strategy: "most_recent",
|
|
||||||
}
|
|
||||||
body, _ := json.Marshal(req)
|
|
||||||
|
|
||||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
assert.Equal(t, 1, result.Total)
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
assert.Equal(t, 0, result.Success)
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
assert.Equal(t, 1, result.Failed)
|
||||||
|
assert.Contains(t, result.Results[0].Error, "conflict not found")
|
||||||
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 "+setup.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 "+setup.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 "+setup.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 "+setup.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 "+setup.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 "+setup.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 "+setup.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 TestBulkResolve_EmptyConflictIDs(t *testing.T) {
|
||||||
func TestConflictsBulkDismiss(t *testing.T) {
|
|
||||||
setup := setupTestServer(t)
|
setup := setupTestServer(t)
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
t.Run("BulkDismissConflicts_WithoutAuth", func(t *testing.T) {
|
req := handlers.BulkResolveRequest{
|
||||||
req := map[string]interface{}{
|
ConflictIDs: []string{},
|
||||||
"conflict_ids": []string{uuid.New().String()},
|
Strategy: "most_recent",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(req)
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
resp, err := client.Do(httpReq)
|
resp, err := client.Do(httpReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
assert.Equal(t, http.StatusBadRequest, 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 "+setup.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 "+setup.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 "+setup.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 "+setup.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 TestBulkResolve_InvalidConflictID(t *testing.T) {
|
||||||
// NOTE: This test is commented out because the /api/conflicts/bulk-escalate endpoint
|
|
||||||
// does not exist yet. It was planned in TEST_RELIABILITY_PLAN.md but never implemented.
|
|
||||||
// Uncomment and update when the endpoint is added.
|
|
||||||
/*
|
|
||||||
func TestConflictsBulkEscalate(t *testing.T) {
|
|
||||||
setup := setupTestServer(t)
|
setup := setupTestServer(t)
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
t.Run("BulkEscalateConflicts_WithoutAuth", func(t *testing.T) {
|
req := handlers.BulkResolveRequest{
|
||||||
req := map[string]interface{}{
|
ConflictIDs: []string{"not-a-uuid"},
|
||||||
"conflict_ids": []string{uuid.New().String()},
|
Strategy: "most_recent",
|
||||||
}
|
}
|
||||||
body, _ := json.Marshal(req)
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
|
|
||||||
resp, err := client.Do(httpReq)
|
resp, err := client.Do(httpReq)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("BulkEscalateConflicts_EmptyConflictIDs", func(t *testing.T) {
|
var result handlers.BulkResolveResponse
|
||||||
req := map[string]interface{}{
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
"conflict_ids": []string{},
|
|
||||||
}
|
|
||||||
body, _ := json.Marshal(req)
|
|
||||||
|
|
||||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
assert.Equal(t, 1, result.Failed)
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
assert.Contains(t, result.Results[0].Error, "invalid conflict ID")
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
}
|
||||||
|
|
||||||
resp, err := client.Do(httpReq)
|
func TestBulkResolve_InvalidRequestBody(t *testing.T) {
|
||||||
require.NoError(t, err)
|
setup := setupTestServer(t)
|
||||||
defer resp.Body.Close()
|
client := &http.Client{}
|
||||||
|
|
||||||
// Allow queue processor to process the item before querying for conflicts
|
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer([]byte("invalid json")))
|
||||||
time.Sleep(3 * time.Second)
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
var result map[string]interface{}
|
require.NoError(t, err)
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
defer resp.Body.Close()
|
||||||
|
|
||||||
assert.Contains(t, result, "results")
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
assert.Contains(t, result, "total")
|
}
|
||||||
assert.Contains(t, result, "failed")
|
|
||||||
|
func TestBulkResolve_RequiresAuth(t *testing.T) {
|
||||||
results := result["results"].([]interface{})
|
setup := setupTestServer(t)
|
||||||
firstResult := results[0].(map[string]interface{})
|
client := &http.Client{}
|
||||||
assert.Equal(t, "error", firstResult["status"])
|
|
||||||
})
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{uuid.New().String()},
|
||||||
t.Run("BulkEscalateConflicts_MultipleConflicts", func(t *testing.T) {
|
Strategy: "most_recent",
|
||||||
conflictIDs := []string{
|
}
|
||||||
uuid.New().String(),
|
body, _ := json.Marshal(req)
|
||||||
uuid.New().String(),
|
|
||||||
}
|
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
req := map[string]interface{}{
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
"conflict_ids": conflictIDs,
|
|
||||||
}
|
resp, err := client.Do(httpReq)
|
||||||
body, _ := json.Marshal(req)
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
httpReq, _ := http.NewRequest("POST", setup.Server.URL+"/api/conflicts/bulk-escalate", bytes.NewBuffer(body))
|
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||||
httpReq.Header.Set("Authorization", "Bearer "+setup.Token)
|
}
|
||||||
|
|
||||||
resp, err := client.Do(httpReq)
|
func TestBulkDismiss_RealConflicts(t *testing.T) {
|
||||||
require.NoError(t, err)
|
env := setupConflictTest(t)
|
||||||
defer resp.Body.Close()
|
client := &http.Client{}
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
conflict1 := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
conflict2 := createTestConflict(t, env, makeConflictData(0.60, 0.55))
|
||||||
var result map[string]interface{}
|
|
||||||
json.NewDecoder(resp.Body).Decode(&result)
|
id1 := uuid.UUID(conflict1.ID.Bytes).String()
|
||||||
|
id2 := uuid.UUID(conflict2.ID.Bytes).String()
|
||||||
assert.Contains(t, result, "results")
|
|
||||||
assert.Equal(t, float64(2), result["total"])
|
req := map[string]interface{}{
|
||||||
|
"conflict_ids": []string{id1, id2},
|
||||||
// NEW: Database verification - verify conflicts were escalated
|
}
|
||||||
for _, conflictID := range conflictIDs {
|
body, _ := json.Marshal(req)
|
||||||
pgID, err := uuid.Parse(conflictID)
|
|
||||||
if err != nil {
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-dismiss", bytes.NewBuffer(body))
|
||||||
continue // Skip invalid UUIDs
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
}
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.Token)
|
||||||
|
|
||||||
conflict, err := setup.DB.GetSyncConflict(context.Background(), pgtype.UUID{Bytes: [16]byte(pgID), Valid: true})
|
resp, err := client.Do(httpReq)
|
||||||
if err == nil {
|
require.NoError(t, err)
|
||||||
// If conflict exists, verify it was escalated
|
defer resp.Body.Close()
|
||||||
assert.Equal(t, "escalated", conflict.ResolutionStatus.String, "Conflict should be escalated")
|
|
||||||
}
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
}
|
|
||||||
})
|
var result map[string]interface{}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
assert.Equal(t, float64(2), result["total"])
|
||||||
|
assert.Equal(t, float64(2), result["success"])
|
||||||
|
assert.Equal(t, float64(0), result["failed"])
|
||||||
|
|
||||||
|
results := result["results"].([]interface{})
|
||||||
|
require.Len(t, results, 2)
|
||||||
|
for _, r := range results {
|
||||||
|
entry := r.(map[string]interface{})
|
||||||
|
assert.Equal(t, "success", entry["status"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkDismiss_NotFoundConflict(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
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")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+setup.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.Equal(t, float64(1), result["total"])
|
||||||
|
assert.Equal(t, float64(0), result["success"])
|
||||||
|
assert.Equal(t, float64(1), result["failed"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkDismiss_InvalidConflictID(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
req := map[string]interface{}{
|
||||||
|
"conflict_ids": []string{"invalid-uuid"},
|
||||||
|
}
|
||||||
|
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 "+setup.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.Equal(t, float64(1), result["total"])
|
||||||
|
assert.Equal(t, float64(1), result["failed"])
|
||||||
|
assert.Contains(t, result["results"].([]interface{})[0].(map[string]interface{})["error"], "invalid conflict ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkDismiss_EmptyConflictIDs(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
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 "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkDismiss_InvalidRequestBody(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
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 "+setup.Token)
|
||||||
|
|
||||||
|
resp, err := client.Do(httpReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkDismiss_RequiresAuth(t *testing.T) {
|
||||||
|
setup := setupTestServer(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBulkResolve_MixedSuccessAndFailure(t *testing.T) {
|
||||||
|
env := setupConflictTest(t)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
conflict := createTestConflict(t, env, makeConflictData(0.45, 0.42))
|
||||||
|
realID := uuid.UUID(conflict.ID.Bytes).String()
|
||||||
|
|
||||||
|
req := handlers.BulkResolveRequest{
|
||||||
|
ConflictIDs: []string{realID, uuid.New().String()},
|
||||||
|
Strategy: "most_recent",
|
||||||
|
}
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
|
||||||
|
httpReq, _ := http.NewRequest("POST", env.setup.Server.URL+"/api/conflicts/bulk-resolve", bytes.NewBuffer(body))
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Authorization", "Bearer "+env.setup.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.Equal(t, 2, result.Total)
|
||||||
|
assert.Equal(t, 1, result.Success)
|
||||||
|
assert.Equal(t, 1, result.Failed)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|||||||
Reference in New Issue
Block a user