diff --git a/IMPLEMENTATION_EXACT.md b/IMPLEMENTATION_EXACT.md deleted file mode 100644 index fe1fba8..0000000 --- a/IMPLEMENTATION_EXACT.md +++ /dev/null @@ -1,2346 +0,0 @@ -# Device Authentication & Sync Implementation - Exact Code Changes - -**Document Purpose**: This document contains every single line of code that needs to be added, modified, or removed to implement the device authentication and sync system. Each change is explained with context and follows PROJECT_GUIDELINES.md. - -**Implementation Status**: Ready to implement -**Last Updated**: Based on IMPLEMENTATION_PLAN.md (2026-02-12) -**Guidelines Compliance**: Follows all PROJECT_GUIDELINES.md requirements - ---- - -## Table of Contents - -1. [Phase 1: Enhanced Authentication (Week 1)](#phase-1-enhanced-authentication) - - Database Query Addition - - Middleware Enhancement - - Router Updates - - Backend Handler Addition - - Frontend Template Updates -2. [Phase 2: Kobo Integration (Week 1-2)](#phase-2-kobo-integration) -3. [Phase 3: OPDS Security (Week 2-3)](#phase-3-opds-security) - - Router Enhancement - - Bruno API Tests -4. [Phase 4: Testing & Documentation (Week 4)](#phase-4-testing--documentation) - - End-to-End Tests - - Security Documentation - ---- - -## Phase 1: Enhanced Authentication - -### Overview - -**Goal**: Support both API key in URL path (Kobo) and Bearer token in header (KOReader) - -**Files to Modify**: -- `internal/database/queries/queries.sql` - Add token regeneration query -- `internal/middleware/device_auth.go` - Support URL path token -- `internal/router/sync.go` - Update Kobo routes -- `internal/router/device.go` - Add regenerate token route -- `internal/handlers/devices.go` - Add regenerate token handler -- `templates/devices.templ` - Add copy/regenerate UI -- `web/src/device-management.ts` - Add device management TypeScript -- `bruno/sync-kobo/api.bru` - Add URL path token requests -- `bruno/devices/regenerate-*.bru` - New token regeneration test files -- `bruno/opds/*-*.bru` - New OPDS authentication test files - ---- - -### 1.1 Database Query Addition - -**File**: `internal/database/queries/queries.sql` - -**Location**: After line 770 (after `UpdateDeviceLastSeen` query) - -**Explanation**: Add a new query to update only the `auth_token` field for token regeneration. This is separate from `UpdateDevice` which doesn't modify `auth_token`. - -**ADD THIS CODE**: - -```sql --- name: UpdateDeviceAuthToken :one -UPDATE devices -SET - auth_token = $2, - updated_at = NOW() -WHERE id = $1 -RETURNING *; -``` - -**Complete Context** (lines 764-776 after change): - -```sql --- name: UpdateDeviceLastSeen :one -UPDATE devices -SET - last_seen = NOW(), - updated_at = NOW() -WHERE id = $1 -RETURNING *; - --- name: UpdateDeviceAuthToken :one -UPDATE devices -SET - auth_token = $2, - updated_at = NOW() -WHERE id = $1 -RETURNING *; -``` - -**Verification**: Run `go build ./...` after adding this query to ensure sqlc generates the new function correctly. - -**Database Schema Note**: This query uses existing fields (`devices.id`, `devices.auth_token`, `devices.updated_at`). **No schema change required** - the database structure remains the same. This is purely a new SQL query for existing schema. - ---- - -### 1.2 Middleware Enhancement - -**File**: `internal/middleware/device_auth.go` - -**Location**: Lines 37-108 (function `Authenticate`) - -**Current Implementation**: Only checks `Authorization: Bearer {token}` header - -**Required Change**: Add fallback to check URL path parameter and query parameter - -**REPLACE LINES 37-270** with: - -```go -func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - var device database.Devices - var err error - - // Method 1: Try Bearer token header (KOReader, API clients, OPDS) - authHeader := c.Request().Header.Get("Authorization") - if authHeader != "" { - if !strings.HasPrefix(authHeader, "Bearer ") { - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "invalid authorization header format", - }) - } - - token := strings.TrimPrefix(authHeader, "Bearer ") - device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), token) - if err == nil { - return m.validateDevice(c, device) - } - } - - // Method 2: Try URL path parameter (Kobo sync, OPDS) - // Route format: /api/sync/kobo/:token/... - urlToken := c.Param("token") - if urlToken != "" { - device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), urlToken) - if err == nil { - return m.validateDevice(c, device) - } - } - - // Method 3: Try query parameter (OPDS catalog access) - // URL format: /opds/devices/:deviceId/catalog?token=... - queryToken := c.QueryParam("token") - if queryToken != "" { - device, err = m.db.GetDeviceByAuthToken(c.Request().Context(), queryToken) - if err == nil { - return m.validateDevice(c, device) - } - } - - // All authentication methods failed - return c.JSON(http.StatusUnauthorized, map[string]string{ - "error": "authentication required - use Bearer token or API key", - }) - } -} - -// validateDevice performs validation checks after successful authentication -func (m *DeviceAuthMiddleware) validateDevice(c echo.Context, device database.Devices) error { - if !device.SyncEnabled.Bool || !device.SyncEnabled.Valid { - return c.JSON(http.StatusForbidden, map[string]string{ - "error": "device sync is disabled", - }) - } - - requestType := m.getRequestType(c.Request().URL.Path) - deviceUUID := uuid.UUID(device.ID.Bytes) - deviceID := deviceUUID.String() - - config := DeviceRateLimitConfig{ - SyncRequestsPerMinute: 60, - ProgressUpdatesPerMinute: 120, - MetadataRequestsPerMinute: 30, - } - - if !m.rateLimiter.CheckRateLimit(deviceID, requestType, config) { - remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config) - c.Response().Header().Set("X-RateLimit-Limit", "60") - c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) - c.Response().Header().Set("X-RateLimit-Reset", "60") - return c.JSON(http.StatusTooManyRequests, map[string]string{ - "error": "rate limit exceeded", - "message": "Too many requests", - "remaining": strconv.Itoa(remaining), - }) - } - - remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config) - c.Response().Header().Set("X-RateLimit-Limit", "60") - c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) - - ctx := DeviceContext{ - ID: device.ID.Bytes, - UserID: device.UserID.Bytes, - DeviceName: device.DeviceName, - DeviceType: device.DeviceType, - DeviceIdentifier: device.DeviceIdentifier, - SyncEnabled: device.SyncEnabled.Bool && device.SyncEnabled.Valid, - AutoSync: device.AutoSync.Bool && device.AutoSync.Valid, - } - - c.Set("device", device) - c.Set("device_ctx", ctx) - c.Set("device_id", device.ID.Bytes) - - return next(c) -} -``` - -**Explanation**: -- Extracted validation logic into `validateDevice()` helper function -- Each auth method calls helper on success: `return m.validateDevice(c, device)` -- No goto statements - follows procedural style with clear control flow -- Helper function is testable, reusable, and maintainable -- Matches existing codebase patterns (no goto statements found) - -**Verification Steps**: -1. Run `go build ./internal/middleware` -2. Run `go test ./internal/middleware/... -v` -3. Review `git diff internal/middleware/device_auth.go` - ---- - -### 1.3 Router Updates - Kobo Routes - -**File**: `internal/router/sync.go` - -**Location**: Lines 31-38 - -**Current Implementation**: -```go -// Kobo sync routes (device authentication required) -koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager) -koboSync := e.Group("/api/sync/kobo") -koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup)) -koboSync.POST("/bookmark", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Bookmark)) -koboSync.POST("/v1/analytics/gettests", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.AnalyticsGettests)) -koboSync.GET("/v1/initialization", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Initialization)) -koboSync.POST("/sync-from-server", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.SyncFromServer)) -``` - -**Required Change**: Change route group from `/api/sync/kobo` to `/api/sync/kobo/:token` - -**REPLACE LINE 33**: - -```go -// BEFORE: -koboSync := e.Group("/api/sync/kobo") - -// AFTER: -koboSync := e.Group("/api/sync/kobo/:token") -``` - -**Complete Context After Change** (lines 31-39): - -```go -// Kobo sync routes (device authentication required) -// Kobo devices use URL path: /api/sync/kobo/{token}/markup -// API clients can use Authorization header: Authorization: Bearer {token} -koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager) -koboSync := e.Group("/api/sync/kobo/:token") -koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup)) -koboSync.POST("/bookmark", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Bookmark)) -koboSync.POST("/v1/analytics/gettests", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.AnalyticsGettests)) -koboSync.GET("/v1/initialization", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Initialization)) -koboSync.POST("/sync-from-server", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.SyncFromServer)) -``` - -**Explanation**: -- URL path parameter `:token` allows Kobo devices to send: `POST /api/sync/kobo/dev_abc123.../markup` -- Middleware now extracts token from path (see middleware changes above) -- Routes also accept Bearer tokens for API clients/automation, but **Kobo devices will never send Bearer tokens** -- KOReader uses Bearer token exclusively, Kobo uses URL path token exclusively - -**Verification**: Run `go build ./internal/router` - ---- - -### 1.4 Router Updates - Device Token Regeneration - -**File**: `internal/router/device.go` - -**Location**: After device registration routes (around line 25) - -**Current Implementation**: Need to check what device routes exist - -**Required Addition**: Add route for token regeneration - -**ADD THIS ROUTE** (find appropriate location with other device routes): - -```go -// Token regeneration endpoint (JWT authentication required) -devices.PUT("/:id/regenerate-token", jwtMiddleware, h.RegenerateDeviceToken) -``` - - **Complete Context** (token regeneration route added to existing device routes): -```go -// Existing device management routes (unchanged) -devices.GET("", jwtMiddleware, cfg.DeviceHandler.ListDevices) -devices.GET("/:id", jwtMiddleware, cfg.DeviceHandler.GetDevice) -devices.PUT("/:id", jwtMiddleware, cfg.DeviceHandler.UpdateDevice) -devices.DELETE("/:id", jwtMiddleware, cfg.DeviceHandler.DeleteDevice) - -// NEW: Token regeneration endpoint (JWT authentication required) -devices.PUT("/:id/regenerate-token", jwtMiddleware, cfg.DeviceHandler.RegenerateDeviceToken) -``` - -**Verification**: Run `go build ./internal/router` - ---- - -### 1.5 Backend Handler - Token Regeneration - -**File**: `internal/handlers/devices.go` - -**Location**: After `DeleteDevice` function (after line 486) - -**Required Addition**: Add new handler function for token regeneration - -**ADD THIS CODE**: - -```go -func (h *DeviceHandler) RegenerateDeviceToken(c echo.Context) error { - // Verify JWT authentication - userID := c.Get("user_id").(string) - userUUID, err := uuid.Parse(userID) - if err != nil { - return c.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid user ID"}) - } - - // Parse device ID from URL parameter - deviceID, err := uuid.Parse(c.Param("id")) - if err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"}) - } - - pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} - - // Verify device exists and belongs to user - device, err := h.db.GetDevice(c.Request().Context(), pgDeviceID) - if err != nil { - return c.JSON(http.StatusNotFound, map[string]string{"error": "device not found"}) - } - - if device.UserID.Bytes != userUUID { - return c.JSON(http.StatusForbidden, map[string]string{"error": "access denied"}) - } - - // Generate new auth token - newToken, err := generateDeviceToken() - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to generate token"}) - } - - // Update device with new token - updatedDevice, err := h.db.UpdateDeviceAuthToken(c.Request().Context(), database.UpdateDeviceAuthTokenParams{ - ID: pgDeviceID, - AuthToken: pgtype.Text{String: newToken, Valid: true}, - }) - - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update token"}) - } - - // Return new token with device info - syncEnabled := updatedDevice.SyncEnabled.Bool && updatedDevice.SyncEnabled.Valid - autoSync := updatedDevice.AutoSync.Bool && updatedDevice.AutoSync.Valid - syncFreq := int32(0) - if updatedDevice.SyncFrequencyMinutes.Valid { - syncFreq = updatedDevice.SyncFrequencyMinutes.Int32 - } - - // Build sync URLs with new token - syncURLs := map[string]string{} - baseURL := h.cfg.BaseURL - - switch updatedDevice.DeviceType { - case "kobo": - syncURLs["sync_url"] = fmt.Sprintf("%s/api/sync/kobo/%s", baseURL, newToken) - syncURLs["markup"] = fmt.Sprintf("%s/api/sync/kobo/%s/markup", baseURL, newToken) - syncURLs["bookmark"] = fmt.Sprintf("%s/api/sync/kobo/%s/bookmark", baseURL, newToken) - syncURLs["init"] = fmt.Sprintf("%s/api/sync/kobo/%s/v1/initialization", baseURL, newToken) - case "koreader": - syncURLs["progress"] = fmt.Sprintf("%s/api/sync/koreader/progress", baseURL) - syncURLs["metadata"] = fmt.Sprintf("%s/api/sync/koreader/metadata", baseURL) - syncURLs["bookmarks"] = fmt.Sprintf("%s/api/sync/koreader/bookmarks", baseURL) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Token regenerated successfully", - "auth_token": newToken, - "device": DeviceInfo{ - ID: updatedDevice.ID.Bytes, - DeviceName: updatedDevice.DeviceName, - DeviceType: updatedDevice.DeviceType, - LastSync: (*time.Time)(&updatedDevice.LastSync.Time), - LastSeen: (*time.Time)(&updatedDevice.LastSeen.Time), - SyncEnabled: syncEnabled, - AutoSync: autoSync, - SyncFrequency: syncFreq, - CreatedAt: updatedDevice.CreatedAt.Time, - DeviceMetadata: updatedDevice.DeviceMetadata, - }, - "sync_urls": syncURLs, - }) -} -``` - -**Explanation**: -- Validates JWT token (user must be logged in) -- Verifies device ownership (device belongs to user) -- Generates new token using existing `generateDeviceToken()` helper -- Updates `devices.auth_token` using new `UpdateDeviceAuthToken` query -- Returns new token with device info and sync URLs -- For Kobo: returns URL with token in path (Kobo never uses Bearer header) -- For KOReader: returns token for Authorization header (KOReader never uses URL path token) - -**Verification**: -1. Run `go build ./internal/handlers` -2. Run `go test ./internal/handlers/... -v` -3. Test manually via Bruno collection - ---- - -### 1.6 Frontend Template - Device Management UI - -**File**: `templates/devices.templ` - -**Location**: Multiple locations - -**Overview**: After refactor (REFACTORING_PLAN.md Phase 1.3), templates use `handlers.DeviceInfo` directly - no conversion layer exists. Time fields are `*time.Time` (not strings). - -**Required Changes**: -1. Add `auth_token` field to `handlers.DeviceInfo` struct -2. Add "Copy Sync URL" button for each device -3. Add "Regenerate Token" button for each device -4. Add JavaScript functions for copy and regenerate -5. Update time formatting in templates (use `.Format()` method) - -#### 1.6.1 Add AuthToken to handlers.DeviceInfo Struct - -**File**: `internal/handlers/devices.go` - -**Location**: Line 78 (after `DeviceMetadata` field in `DeviceInfo` struct) - -**ADD THIS FIELD**: - -```go -type DeviceInfo struct { - ID uuid.UUID `json:"id"` - DeviceName string `json:"device_name"` - DeviceType string `json:"device_type"` - LastSync *time.Time `json:"last_sync"` - LastSeen *time.Time `json:"last_seen"` - SyncEnabled bool `json:"sync_enabled"` - AutoSync bool `json:"auto_sync"` - SyncFrequency int32 `json:"sync_frequency_minutes"` - CreatedAt time.Time `json:"created_at"` - DeviceMetadata json.RawMessage `json:"device_metadata,omitempty"` - AuthToken string `json:"auth_token"` // NEW: Device API key for authentication -} -``` - -#### 1.6.2 Update Device List Handlers - -**File**: `internal/handlers/devices.go` - -**Location**: `GetDevicesData` function (line 275-313) AND `ListDevices` function (line 255-267) - -**GetDevicesData** - ADD FIELD TO RESPONSE (modify line 298-309): - -```go -deviceList = append(deviceList, DeviceInfo{ - ID: device.ID.Bytes, - DeviceName: device.DeviceName, - DeviceType: device.DeviceType, - LastSync: (*time.Time)(&device.LastSync.Time), - LastSeen: (*time.Time)(&device.LastSeen.Time), - SyncEnabled: syncEnabled, - AutoSync: autoSync, - SyncFrequency: syncFreq, - CreatedAt: device.CreatedAt.Time, - DeviceMetadata: device.DeviceMetadata, - AuthToken: device.AuthToken, // NEW: Include auth token -}) -``` - -**ListDevices** - ADD FIELD TO RESPONSE (modify line around 262): - -Find the loop that constructs `deviceList[i]` and add `AuthToken` field: - -```go -deviceList[i] = DeviceInfo{ - ID: device.ID.Bytes, - DeviceName: device.DeviceName, - DeviceType: device.DeviceType, - LastSync: (*time.Time)(&device.LastSync.Time), - LastSeen: (*time.Time)(&device.LastSeen.Time), - SyncEnabled: syncEnabled, - AutoSync: autoSync, - SyncFrequency: syncFreq, - CreatedAt: device.CreatedAt.Time, - DeviceMetadata: device.DeviceMetadata, - AuthToken: device.AuthToken, // NEW: Include auth token -} -``` - -#### 1.6.3 Update Device Card Template - -**File**: `templates/devices.templ` - -**Location**: Template function signature (line 5) and device card (lines 46-98) - -**Required Changes**: - -1. **Update template signature** to accept baseURL parameter (line 5): -```templ -templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData, baseURL string) { -``` - -2. **REPLACE DEVICE CARD CONTENT** (lines 46-98) with: - -```templ -for _, device := range devices { -
-
-
- if device.DeviceType == "koreader" { - ๐Ÿ“– - } else if device.DeviceType == "kobo" { - ๐Ÿ“š - } else if device.DeviceType == "web" { - ๐ŸŒ - } else { - ๐Ÿ“ฑ - } -
-
- - -
-
-

{ device.DeviceName }

-

{ device.DeviceType }

-
-
- Sync Status - if device.SyncEnabled { - โœ“ Enabled - } else { - โœ— Disabled - } -
-
- Last Sync - if device.LastSync != nil { - { device.LastSync.Format("2006-01-02 15:04") } - } else { - Never - } -
-
- Last Seen - if device.LastSeen != nil { - { device.LastSeen.Format("2006-01-02 15:04") } - } else { - Never - } -
-
- - -
-

DEVICE SYNC CONFIGURATION

- - -
-

- โš ๏ธ Security Notice: This token is sensitive. Keep it secret. If compromised, regenerate immediately. -

-
- - if device.DeviceType == "kobo" { - -
- -
- - -
-

Paste this URL into Kobo's api_endpoint setting

-
- } - - if device.DeviceType == "koreader" { - -
- -
- - -
-

Enter this token in the KOReader plugin settings

-
- } - - - -

โš ๏ธ Old token will immediately stop working

-
-
-} -``` - -**Explanation**: -- Kobo devices: Show full sync URL with token in path -- KOReader devices: Show auth token only (plugin handles Authorization header) -- One-click copy buttons use clipboard API -- Regenerate button requires confirmation -- Clear setup instructions for each device type - -**Handler Update** (`internal/router/frontend.go:171`): -```go -err = templates.Devices(user, devices, pendingList, cfg.BaseURL).Render(c.Request().Context(), &buf) -``` - -Pass `cfg.BaseURL` to template instead of hardcoding URLs. - -#### 1.6.4 Add TypeScript Device Management - -**File**: `web/src/device-management.ts` (CREATE NEW FILE) - -**Location**: New TypeScript file for device management functions - -**CREATE THIS TYPESCRIPT**: - -```typescript -// Device Management - Token copy and regeneration -// Procedural style with proper types (no OOP) - -interface RegenerateTokenResponse { - message: string; - auth_token: string; - device: { - id: string; - device_name: string; - device_type: string; - auth_token: string; - sync_enabled: boolean; - auto_sync: boolean; - sync_frequency_minutes: number; - }; - sync_urls?: { - sync_url?: string; - markup?: string; - bookmark?: string; - init?: string; - progress?: string; - metadata?: string; - bookmarks?: string; - }; -} - -// Copy sync URL or auth token to clipboard -function copyToClipboard(text: string, label: string): void { - navigator.clipboard.writeText(text) - .then(() => { - const toast = (window as any).showToast; - if (toast) { - toast.success(`${label} copied to clipboard`); - } - }) - .catch((err: unknown) => { - console.error('Failed to copy:', err); - const toast = (window as any).showToast; - if (toast) { - toast.error('Failed to copy to clipboard'); - } - }); -} - -// Regenerate device token with confirmation -function regenerateDeviceToken(deviceId: string, event: Event): void { - const confirmation = 'โš ๏ธ This will revoke current token and generate a new one.\n\n' + - 'The old token will immediately stop working.\n\n' + - 'You will need to update your device configuration with new token.\n\n' + - 'Continue?'; - - if (!confirm(confirmation)) { - return; - } - - const btn = event.target as HTMLButtonElement; - const originalText = btn.innerHTML; - btn.disabled = true; - btn.innerHTML = '๐Ÿ”„ Regenerating...'; - - fetch(`/api/devices/${deviceId}/regenerate-token`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - } - }) - .then((response: Response) => { - if (!response.ok) { - throw new Error('Failed to regenerate token'); - } - return response.json() as Promise; - }) - .then((data: RegenerateTokenResponse) => { - const toast = (window as any).showToast; - if (toast) { - toast.success('Token regenerated successfully - update your device config'); - } - // Reload page to show new token - setTimeout(() => location.reload(), 1500); - }) - .catch((error: unknown) => { - console.error('Error:', error); - const toast = (window as any).showToast; - if (toast) { - toast.error('Failed to regenerate token'); - } - if (btn) { - btn.disabled = false; - btn.innerHTML = originalText; - } - }); -} - -// Export functions for global access (called from template onclick attributes) -declare global { - function copyToClipboard(text: string, label: string): void; - function regenerateDeviceToken(deviceId: string): void; -} - -window.copyToClipboard = copyToClipboard; -window.regenerateDeviceToken = regenerateDeviceToken; -``` - -**Explanation**: -- Proper TypeScript type annotations -- Interface for API response -- Type assertions for `window` global -- Procedural functions (no classes, no `this`) -- Follows existing `toast.ts` pattern -- Exports functions to `window` for template access -``` - -**Verification**: -1. Run `npm run build:ts` to compile TypeScript -2. Run `go build ./...` -3. Test in browser: - - Click "Copy" button - should copy sync URL/token - - Click "Regenerate" - should show confirmation - - Confirm regeneration - should show success toast - - Page should reload with new token - -**Build Process**: -```bash -# Compile TypeScript to JavaScript -npm run build:ts - -# Output: web/static/device-management.js -# Template references: -``` - ---- - -### 1.7 Bruno API Tests - Token Regeneration - -**File**: `bruno/devices/regenerate-token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Regenerate Device Token - type: http - seq: 1 -} - -put { - url: {{base_url}}/api/devices/{{device_id}}/regenerate-token - body: none - auth: inherit -} - -docs { - ## Regenerate Device Token - - Regenerates auth token for a device, invalidating old token immediately. - - **Method:** PUT - - **Endpoint:** /api/devices/{device_id}/regenerate-token - - **Authentication:** Bearer token (JWT) - - **Path Parameters:** - - `device_id` (string): Device UUID - - **Response:** - - `message` (string): Success message - - `auth_token` (string): New auth token - - `device` (object): Updated device details - - `sync_urls` (object): Device-specific sync URLs with new token - - **Status Codes:** - - 200: Success - - 401: Unauthorized - - 403: Forbidden (device belongs to different user) - - 404: Device not found - - 500: Internal server error - - **Important Notes:** - - Old token stops working immediately - - Device must be updated with new token to resume syncing - - No data loss - device ID remains same - - **Example Response:** - ```json - { - "message": "Token regenerated successfully", - "auth_token": "dev_abc123...", - "device": { - "id": "uuid-here", - "device_name": "My Kobo Clara", - "device_type": "kobo", - "sync_enabled": true, - "auto_sync": true, - "sync_frequency_minutes": 5, - "created_at": "2026-02-12T10:00:00Z", - "device_metadata": "{...}" - }, - "sync_urls": { - "sync_url": "http://localhost:8765/api/sync/kobo/dev_new_token", - "markup": "http://localhost:8765/api/sync/kobo/dev_new_token/markup", - "bookmark": "http://localhost:8765/api/sync/kobo/dev_new_token/bookmark", - "init": "http://localhost:8765/api/sync/kobo/dev_new_token/v1/initialization" - } - } - ``` - - **Important Notes:** - - Old token stops working immediately - - Device must be updated with new token to resume syncing - - No data loss - device ID remains same -} -``` - -**File**: `bruno/devices/regenerate-token-unauthorized.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Regenerate Device Token - Unauthorized - type: http - seq: 2 -} - -put { - url: {{base_url}}/api/devices/{{device_id}}/regenerate-token - body: none - auth: none -} - -docs { - ## Regenerate Device Token - Unauthorized - - Tests that token regeneration requires authentication. - - **Expected Behavior:** Returns 401 Unauthorized when no Bearer token is provided - - **Status Codes:** - - 401: Unauthorized (missing or invalid token) - - **Use Case:** Verify authentication is required for token regeneration -} -``` - -**File**: `bruno/devices/regenerate-token-forbidden.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Regenerate Device Token - Forbidden - type: http - seq: 3 -} - -put { - url: {{base_url}}/api/devices/{{other_device_id}}/regenerate-token - body: none - auth: inherit -} - -docs { - ## Regenerate Device Token - Forbidden - - Tests that users cannot regenerate tokens for devices belonging to other users. - - **Expected Behavior:** Returns 403 Forbidden when trying to regenerate token for another user's device - - **Status Codes:** - - 403: Forbidden (device belongs to different user) - - **Use Case:** Verify authorization - users can only manage their own devices - - **Setup:** Use Bearer token from user A, try to regenerate token for user B's device -} -``` - -**File**: `bruno/devices/regenerate-token-notfound.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Regenerate Device Token - Not Found - type: http - seq: 4 -} - -put { - url: {{base_url}}/api/devices/00000000-0000-0000-0000-000000000000/regenerate-token - body: none - auth: bearer -} - -docs { - ## Regenerate Device Token - Not Found - - Tests that token regeneration returns 404 for non-existent devices. - - **Expected Behavior:** Returns 404 Not Found when device UUID doesn't exist - - **Status Codes:** - - 404: Device not found - - **Use Case:** Verify proper error handling for invalid device IDs - - **Setup:** Use all-zero UUID (guaranteed to not exist in database) -} -``` - -**Verification**: Test each request - ---- - -### 1.8 Bruno API Tests - Update Kobo Sync - -**File**: `bruno/sync-kobo/sync-markup-url-token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Sync Markup - URL Path Token - type: http - seq: 1 -} - -post { - url: {{base_url}}/sync/kobo/{{kobo_device_token}}/markup - body: json - auth: none -} - -headers { - Content-Type: application/json -} - -body:json { - { - "ReadingSync": [ - { - "ContentId": "book-uuid", - "PercentRead": 45.6, - "EntitlementId": "entitlement-id", - "RemainingTimeMinutes": 120, - "FirstReadTime": "2026-01-25T10:00:00Z", - "LastModified": "2026-01-30T20:00:00Z" - } - ], - "BookmarkSync": [] - } -} - -docs { - ## Sync Reading Progress - URL Path Token - - Synchronizes reading progress from Kobo device using token in URL path. - - **Method:** POST - - **Endpoint:** /sync/kobo/{kobo_device_token}/markup - - **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) - - **Path Parameters:** - - `kobo_device_token` (string): Device auth token from devices.auth_token - - **Request Body:** - - `ReadingSync` (array): Reading progress data - - `BookmarkSync` (array): Bookmarks and annotations - - **Response:** Success message - - **Status Codes:** - - 200: Success - - 401: Unauthorized - - 403: Device sync disabled - - 500: Internal server error - - **Important:** Kobo firmware requires token in URL path, cannot use Bearer header - - **Use Case:** Kobo devices syncing reading progress via stock firmware -} -``` - -**File**: `bruno/sync-kobo/sync-bookmark-url-token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Sync Bookmark - URL Path Token - type: http - seq: 2 -} - -post { - url: {{base_url}}/sync/kobo/{{kobo_device_token}}/bookmark - body: json - auth: none -} - -headers { - Content-Type: application/json -} - -body:json { - { - "ContentId": "book-uuid", - "BookmarkText": "Highlighted text", - "BookmarkType": "annotation", - "BookmarkTitle": "Chapter 3" - } -} - -docs { - ## Sync Bookmark - URL Path Token - - Synchronizes bookmarks and annotations from Kobo device using token in URL path. - - **Method:** POST - - **Endpoint:** /sync/kobo/{kobo_device_token}/bookmark - - **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) - - **Path Parameters:** - - `kobo_device_token` (string): Device auth token from devices.auth_token - - **Request Body:** - - `ContentId` (string): Book UUID - - `BookmarkText` (string): Highlighted or annotated text - - `BookmarkType` (string): Type (annotation, bookmark, note) - - `BookmarkTitle` (string): Title for the bookmark - - **Response:** Success message - - **Status Codes:** - - 200: Success - - 401: Unauthorized - - 403: Device sync disabled - - 500: Internal server error - - **Important:** Kobo devices use URL path token exclusively, never Bearer header - - **Use Case:** Kobo devices syncing bookmarks and highlights via stock firmware -} -``` - -**File**: `bruno/sync-kobo/get-library-url-token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Get Library - URL Path Token - type: http - seq: 3 -} - -get { - url: {{base_url}}/sync/kobo/{{kobo_device_token}}/library - body: none - auth: none -} - -docs { - ## Get Kobo Library - URL Path Token - - Retrieves library metadata for Kobo device using token in URL path. - - **Method:** GET - - **Endpoint:** /sync/kobo/{kobo_device_token}/library - - **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) - - **Path Parameters:** - - `kobo_device_token` (string): Device auth token from devices.auth_token - - **Response:** Library metadata with book list - - **Status Codes:** - - 200: Success (library metadata) - - 401: Unauthorized - - 403: Device sync disabled - - 404: Device not found - - **Important:** Kobo devices use URL path token exclusively, never Bearer header - - **Use Case:** Kobo devices retrieving library information via stock firmware -} -``` - -**File**: `bruno/sync-kobo/get-initialization-url-token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Kobo Initialization - URL Path Token - type: http - seq: 4 -} - -get { - url: {{base_url}}/sync/kobo/{{kobo_device_token}}/v1/initialization - body: none - auth: none -} - -docs { - ## Kobo Initialization - URL Path Token - - Returns initialization data for Kobo device using token in URL path. - - **Method:** GET - - **Endpoint:** /sync/kobo/{kobo_device_token}/v1/initialization - - **Authentication:** URL path parameter (Kobo devices never send Bearer tokens) - - **Path Parameters:** - - `kobo_device_token` (string): Device auth token from devices.auth_token - - **Response:** Initialization configuration and settings - - **Status Codes:** - - 200: Success (initialization data) - - 401: Unauthorized - - 403: Device sync disabled - - 404: Device not found - - **Important:** Kobo devices use URL path token exclusively, never Bearer header - - **Use Case:** Kobo devices initializing sync connection via stock firmware - - **Note:** This endpoint is called when Kobo first connects to sync server -} -``` - -**KEEP EXISTING REQUESTS** in `bruno/sync-kobo/api.bru` for API clients and testing (Bearer token works for non-Kobo clients) - -**Verification**: Test both authentication methods work (Kobo uses URL path, API clients can use Bearer) ---- - -## Phase 2: Kobo Integration - -### 2.1 Kobo Setup Documentation - -**File**: `docs/user/devices/kobo-setup.md` - -**Location**: Full file rewrite needed - -**Current Documentation**: Mentions serial number-based auth (outdated) - -**Required Update**: Document API key in URL path approach - -**REPLACE ENTIRE FILE** with: - -```markdown -# Kobo E-Reader Setup Guide - -**Last Updated**: 2026-02-12 -**Authentication Method**: API Key in URL path - -This guide walks you through setting up your Kobo e-reader to sync with Bookhoard. - -## Prerequisites - -- Kobo e-reader (Kobo Clara, Libra, Forma, Sage, etc.) -- USB cable to connect Kobo to computer -- Bookhoard server running and accessible -- Kobo connected to same network as Bookhoard server - -## Step 1: Find Your Kobo Serial Number - -Your Kobo's serial number is used as the **device identifier** to uniquely identify your device. - -1. On your Kobo, tap: **Settings** โ†’ **Device Information** -2. Look for **Serial Number** (format: `N1234567890123`) -3. Write down this serial number - you'll need it for registration - -**Example**: `N7353019193781` - -### Alternative: Find Serial Number via USB - -1. Connect Kobo to computer via USB -2. Open `.adobe-digital-editions/` folder on Kobo -3. Open `device.xml` file -4. Find `` tag - this is your serial number - -## Step 2: Register Device in Bookhoard - -1. Open Bookhoard web interface in browser -2. Navigate to **Device Management** -3. Click **โž• Add New Device** -4. Fill in registration form: - - **Device Name**: `My Kobo Clara` (or your device name) - - **Device Type**: `Kobo E-Reader` - - **Device Identifier**: Enter your Kobo serial number (e.g., `N7353019193781`) -5. Click **Register Device** - -## Step 3: Approve Device - -1. You'll see a pending registration for your Kobo -2. Click **Approve** button -3. Device is now registered and has an API key (auth token) - -## Step 4: Copy Kobo Sync URL - -After approval, your Kobo device card shows: - -**Kobo Sync URL**: -``` -http://192.168.1.100:8765/api/sync/kobo/dev_abc123def456... -``` - -1. Click **๐Ÿ“‹ Copy** button next to the URL -2. This URL contains your device's API key -3. Keep this URL handy - you'll need it in the next step - -**Important**: This URL is unique to your device. Don't share it publicly. - -## Step 5: Configure Kobo Sync - -### Connect Kobo to Computer - -1. Connect Kobo to computer via USB cable -2. Kobo will appear as a USB drive (named "KOBOeReader") -3. Open the drive on your computer - -### Edit Kobo Configuration - -1. Navigate to `.kobo/Kobo/` folder -2. Open `Kobo eReader.conf` file in text editor -3. Find or add this line: - -```ini -api_endpoint=http://YOUR_IP:8765/api/sync/kobo/YOUR_TOKEN -``` - -**Example**: -api_endpoint=http://192.168.1.100:8765/api/sync/kobo/dev_73530191_937812_3abc4def5... - -4. Save the file -5. Eject Kobo drive safely -6. Kobo will restart automatically - -### What is `api_endpoint`? - -This setting tells Kobo where to send sync data. By setting it to Bookhoard, Kobo will sync with your server instead of Kobo's store. - -**Security Note**: The API key in this URL is a revocable token. If compromised, you can regenerate it from Device Management. - -## Step 6: Configure OPDS Catalog (Optional) - -To browse and download books wirelessly: - -1. In Bookhoard Device Management, find your Kobo device -2. Note the **Device ID** (UUID) shown in device details -3. On Kobo, add OPDS catalog: - - **Title**: `Bookhoard` - - **URL**: `http://YOUR_IP:8765/opds/devices/YOUR_DEVICE_ID/catalog?token=YOUR_TOKEN` - -**Example**: -``` -http://192.168.1.100:8765/opds/devices/550e8400-e29b-41d4-a716-4466554400000/catalog?token=dev_73530191_937812_3abc4def5... -``` - -## Step 7: Test Sync - -1. Open a book on your Kobo -2. Read a few pages -3. Connect Kobo to WiFi -4. Kobo will automatically sync progress to Bookhoard -5. In Bookhoard, check **Reading Progress** - should show Kobo's progress - -**What Syncs Automatically**: -- โœ… Reading progress (page position, percentage) -- โœ… Bookmarks -- โœ… Highlights -- โœ… Notes - -## Troubleshooting - -### Kobo Won't Sync - -**Problem**: Kobo doesn't connect to Bookhoard - -**Solutions**: -1. Check `api_endpoint` in `Kobo eReader.conf` - ensure URL is correct -2. Verify Kobo and Bookhoard are on same network -3. Check Bookhoard server is running -4. Ensure device sync is enabled in Bookhoard Device Management - -### Sync Fails with 401 Unauthorized - -**Problem**: API key is invalid or device sync disabled - -**Solutions**: -1. Check Device Management - is sync enabled for this device? -2. Regenerate token (click "๐Ÿ”„ Regenerate Token") -3. Update `api_endpoint` in Kobo config with new token -4. Eject and restart Kobo - -### Multiple Kobo Devices - -**Problem**: Have multiple Kobos - each needs separate registration - -**Solution**: -- Each Kobo has unique serial number -- Register each Kobo separately -- Each gets unique API key -- Configure each Kobo with its own sync URL - -## Regenerating Your Token - -If your API key is compromised or lost: - -1. In Bookhoard Device Management, find your Kobo -2. Click **๐Ÿ”„ Regenerate Token** -3. Confirm regeneration -4. Copy new sync URL -5. Update `api_endpoint` in Kobo config with new URL -6. Eject and restart Kobo - -**Important**: Old token stops working immediately. Update config promptly. - -## Network Setup - -### Local Network (Recommended) - -Bookhoard works best on local network: - -1. Find Bookhoard server IP: - - **Linux**: `hostname -I` - - **Mac**: `System Settings โ†’ Network` - - **Windows**: `Command Prompt โ†’ ipconfig` -2. Use this IP in sync URL: `http://SERVER_IP:8765/...` -3. Ensure Kobo and server are on same network - -### Remote Access (Advanced) - -For remote sync, use **reverse proxy with HTTPS**: - -1. Set up reverse proxy (nginx, Caddy) -2. Enable SSL/TLS (Let's Encrypt) -3. Configure firewall rules -4. Use VPN for secure remote access - -**Warning**: Without HTTPS, API key can be intercepted on public networks. - -## Security Considerations - -### API Key in URL - -**Risks**: -- API key visible in Kobo logs -- API key visible in server logs -- API key stored in plain text config file - -**Mitigations**: -- Use HTTPS (reverse proxy) -- Keep on local network only -- Regenerate token if compromised -- Don't share sync URL publicly - -### Best Practices - -1. **Local network only**: Keep Bookhoard on home/office network -2. **Use VPN**: For remote access, use VPN tunnel -3. **HTTPS required**: If exposing publicly, use reverse proxy with SSL -4. **Regenerate tokens**: Periodically rotate API keys -5. **Monitor logs**: Check for unauthorized access attempts - -## What's Next? - -- [Configure OPDS for wireless book delivery](#step-6-configure-opds-catalog-optional) -- [Setup auto-sync](#step-7-test-sync) -- [Troubleshooting](#troubleshooting) -- [Security guide](./security.md) - -## Additional Resources - -- [Kobo Official Firmware](https://www.kobo.com/firmware) -- [Kobo Custom Firmware](https://www.mobileread.com/forums/showthread.php?t=273030) -- [Bookhoard Device Management](../../user/devices/) -- [Bookhoard Security Guide](./security.md) -``` - -**Verification**: -1. Check documentation renders at `/docs` endpoint -2. Test search finds this page -3. Verify all code examples are accurate -4. Confirm troubleshooting section covers common issues - ---- - -### 2.2 Kobo Sync Validation Tests - -**File**: `cmd/server/tests/kobo_test.go` - -**Location**: Add new test function for URL path authentication - -**Current Tests**: Use Bearer token in Authorization header - -**Required Updates**: Add test for URL path authentication using table-driven pattern - -**ADD THIS TEST**: - -```go -func TestKoboAuthenticationMethods(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - // ONE test setup shared across all subtests (single database pool) - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - mediaItemID := createTestMediaItemID(t, setup.Server, token) - - // Define test cases - tests := []struct { - name string - authMethod string - setupDevice func(*testing.T, *TestServerSetup, string) *TestDeviceSetup - buildRequest func(*testing.T, *httptest.Server, string, string, *TestDeviceSetup) *http.Request - }{ - { - name: "URL Path Token (Kobo Firmware)", - authMethod: "URL path parameter", - setupDevice: func(t *testing.T, setup *TestServerSetup, mediaID string) *TestDeviceSetup { - deviceSetup := setupDeviceTest(t) - return deviceSetup.CreateDevice(t, "Test Kobo", "kobo", "kobo-url-path-test") - }, - buildRequest: func(t *testing.T, ts *httptest.Server, mediaID string, authToken string, device *TestDeviceSetup) *http.Request { - // Token in URL path: /api/sync/kobo/{token}/markup - markupURL := fmt.Sprintf("%s/api/sync/kobo/%s/markup", ts.URL, device.AuthToken) - syncData := map[string]interface{}{ - "ReadingSync": []map[string]interface{}{ - { - "ContentId": mediaID, - "PercentRead": 45.6, - "EntitlementId": "entitlement-123", - "RemainingTimeMinutes": 120, - "FirstReadTime": "2026-01-25T10:00:00Z", - "LastModified": "2026-01-30T20:00:00Z", - }, - }, - "BookmarkSync": []interface{}{}, - } - body, _ := json.Marshal(syncData) - req, _ := http.NewRequest("POST", markupURL, bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - // NOTE: No Authorization header - token in URL path - return req - }, - }, - { - name: "Bearer Token (API Clients/Testing)", - authMethod: "Authorization header", - setupDevice: func(t *testing.T, setup *TestServerSetup, mediaID string) *TestDeviceSetup { - deviceSetup := setupDeviceTest(t) - return deviceSetup.CreateDevice(t, "Test Kobo API Client", "kobo", "kobo-bearer-test") - }, - buildRequest: func(t *testing.T, ts *httptest.Server, mediaID string, authToken string, device *TestDeviceSetup) *http.Request { - // Token in Authorization header (for API clients, not Kobo devices) - markupURL := fmt.Sprintf("%s/api/sync/kobo/%s/markup", ts.URL, device.AuthToken) - syncData := map[string]interface{}{ - "ReadingSync": []map[string]interface{}{ - { - "ContentId": mediaID, - "PercentRead": 45.6, - }, - }, - "BookmarkSync": []interface{}{}, - } - body, _ := json.Marshal(syncData) - req, _ := http.NewRequest("POST", markupURL, bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", device.AuthToken)) - return req - }, - }, - } - - // Run all test cases sharing ONE database pool - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - koboDevice := tt.setupDevice(t, setup, mediaItemID) - - req := tt.buildRequest(t, setup.Server, mediaItemID, token, koboDevice) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode, "Should succeed with %s", tt.authMethod) - - var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - assert.Contains(t, result, "Status") - }) - } -} -``` - -**Explanation**: -- **ONE** call to `setupTestServer(t)` for entire test function -- Table-driven pattern with `t.Run()` subtests -- Each subtest shares the same database pool (1 connection max) -- Tests both authentication methods: URL path (Kobo) and Bearer (API clients) -- Follows existing test pattern in your codebase (see `TestKoboMarkupSync`) - -**Verification**: Run `go test ./cmd/server/tests/... -v -run TestKoboAuthenticationMethods` - ---- - -## Phase 3: OPDS Security - -### 3.1 OPDS Router Enhancement - -**File**: `internal/router/opds.go` - -**Current Implementation**: Already has DeviceAuthMiddleware applied (line 12) - -**Required Addition**: Add comment documenting query parameter support - -**UPDATE COMMENTS** (lines 3-12): - -```go -// Register OPDS routes with device authentication -// -// Authentication Methods: -// 1. Authorization: Bearer {token} header (KOReader, API clients) -// 2. ?token={token} query parameter (Kobo OPDS catalog) -// -// Devices must use their devices.auth_token (generated during device registration) -// -// Kobo OPDS URL format: /opds/devices/{deviceId}/catalog?token={auth_token} -// KOReader OPDS URL format: /opds/devices/{deviceId}/catalog (uses Bearer header) -// -// Returns 401 Unauthorized if device token is missing, invalid, or device sync is disabled -func registerOPDSRoutes(cfg *Config) { - e := cfg.Echo - - // Require device authentication for all OPDS endpoints - // Middleware checks both Bearer header and ?token= query parameter - opds := e.Group("/opds/devices") - opds.Use(cfg.DeviceAuthMiddleware.Authenticate) - opds.GET("/:deviceId/catalog", cfg.OPDSHandler.GetDeviceCatalog) - opds.GET("/:deviceId/search", cfg.OPDSHandler.SearchDeviceCatalog) - opds.GET("/:deviceId/nav", cfg.OPDSHandler.GetDeviceNavigation) - opds.GET("/:deviceId/download/:bookId", cfg.OPDSHandler.DownloadBook) - opds.GET("/:deviceId/cover/:bookId", cfg.OPDSHandler.GetCoverImage) - opds.GET("/:deviceId/formats/:bookId", cfg.OPDSHandler.ListFormats) -} -``` - -**Explanation**: No code changes needed - middleware already supports query parameter (see 1.2). Just documenting the feature. - ---- - -### 3.2 Bruno API Tests - OPDS Authentication - -The following Bruno request files test OPDS authentication with both Bearer tokens and query parameters. - -**File**: `bruno/opds/Get Device Catalog - Bearer.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Get Device Catalog (Bearer Token) - type: http - seq: 1 -} - -get { - url: {{opds_base_url}}/devices/{{device_id}}/catalog -} - -docs { - ## Get Device OPDS Catalog - Bearer Token - - Retrieves OPDS catalog for a device using Bearer token authentication. - - **Method:** GET - - **Endpoint:** /opds/devices/{device_id}/catalog - - **Authentication:** Bearer token (for KOReader and API clients) - - **Path Parameters:** - - `device_id` (string): Device UUID - - **Response:** OPDS Atom feed catalog - - **Status Codes:** - - 200: Success (OPDS feed) - - 401: Unauthorized - - 403: Device sync disabled - - 404: Device not found - - **Use Case:** KOReader devices and API clients accessing OPDS catalog - - **Authentication Method:** Bearer token in Authorization header -} -``` - -**File**: `bruno/opds/Get Device Catalog - Query Token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Get Device Catalog (Query Token) - type: http - seq: 2 -} - -get { - url: {{opds_base_url}}/devices/{{device_id}}/catalog?token={{device_token}} -} - -docs { - ## Get Device OPDS Catalog - Query Token - - Retrieves OPDS catalog for a device using query parameter authentication. - - **Method:** GET - - **Endpoint:** /opds/devices/{device_id}/catalog?token={device_token} - - **Authentication:** Query parameter (for Kobo devices) - - **Path Parameters:** - - `device_id` (string): Device UUID - - **Query Parameters:** - - `token` (string): Device auth token - - **Response:** OPDS Atom feed catalog - - **Status Codes:** - - 200: Success (OPDS feed) - - 401: Unauthorized - - 403: Device sync disabled - - 404: Device not found - - **Use Case:** Kobo devices accessing OPDS catalog (easier to configure in Kobo settings) - - **Authentication Method:** Token in URL query parameter - - **Important:** Token visible in Kobo logs and server logs -} -``` - -**File**: `bruno/opds/Get Device Catalog - No Auth.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Get Device Catalog (No Authentication) - type: http - seq: 3 -} - -get { - url: {{opds_base_url}}/devices/{{device_id}}/catalog -} - -docs { - ## Get Device OPDS Catalog - No Authentication - - Tests that OPDS catalog requires authentication. - - **Expected Behavior:** Returns 401 Unauthorized when no token provided - - **Status Codes:** - - 401: Unauthorized - - **Use Case:** Verify authentication is required for OPDS access - - **Testing:** Ensures middleware properly rejects unauthenticated requests -} -``` - -**File**: `bruno/opds/Get Device Catalog - Invalid Token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Get Device Catalog (Invalid Token) - type: http - seq: 4 -} - -get { - url: {{opds_base_url}}/devices/{{device_id}}/catalog?token=invalid_token_12345 -} - -docs { - ## Get Device OPDS Catalog - Invalid Token - - Tests that OPDS catalog rejects invalid tokens. - - **Expected Behavior:** Returns 401 Unauthorized when invalid token provided - - **Status Codes:** - - 401: Unauthorized - - **Use Case:** Verify proper token validation - - **Testing:** Ensures middleware validates token format and database lookup -} -``` - -**File**: `bruno/opds/Download Book - Bearer.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Download Book (Bearer Token) - type: http - seq: 5 -} - -get { - url: {{opds_base_url}}/devices/{{device_id}}/download/{{book_id}} -} - -docs { - ## Download Book - Bearer Token - - Downloads a book file using Bearer token authentication. - - **Method:** GET - - **Endpoint:** /opds/devices/{device_id}/download/{book_id} - - **Authentication:** Bearer token (for KOReader and API clients) - - **Path Parameters:** - - `device_id` (string): Device UUID - - `book_id` (string): Book UUID - - **Response:** Binary ebook file (EPUB, KEPUB, etc.) - - **Status Codes:** - - 200: Success (binary file) - - 401: Unauthorized - - 403: Device sync disabled - - 404: Book or device not found - - **Use Case:** KOReader devices and API clients downloading books - - **Authentication Method:** Bearer token in Authorization header -} -``` - -**File**: `bruno/opds/Download Book - Query Token.bru` - -**CREATE NEW FILE**: - -```bruno -meta { - name: Download Book (Query Token) - type: http - seq: 6 -} - -get { - url: {{opds_base_url}}/devices/{{device_id}}/download/{{book_id}}?token={{device_token}} -} - -docs { - ## Download Book - Query Token - - Downloads a book file using query parameter authentication. - - **Method:** GET - - **Endpoint:** /opds/devices/{device_id}/download/{book_id}?token={device_token} - - **Authentication:** Query parameter (for Kobo devices) - - **Path Parameters:** - - `device_id` (string): Device UUID - - `book_id` (string): Book UUID - - **Query Parameters:** - - `token` (string): Device auth token - - **Response:** Binary ebook file (EPUB, KEPUB, etc.) - - **Status Codes:** - - 200: Success (binary file) - - 401: Unauthorized - - 403: Device sync disabled - - 404: Book or device not found - - **Use Case:** Kobo devices downloading books via OPDS - - **Authentication Method:** Token in URL query parameter - - **Important:** Token visible in Kobo logs and server logs -} -``` - -**Verification**: Test both authentication methods work for OPDS access - ---- - -## Phase 4: Testing & Documentation - -### 4.1 Security Documentation - -**File**: `docs/user/devices/security.md` - -**CREATE NEW FILE**: - -```markdown -# Device Authentication Security - -**Last Updated**: 2026-02-12 - -This document explains security considerations for device authentication in Bookhoard. - -## Authentication Methods - -### Kobo E-Readers: API Key in URL Path - -**Method**: Token embedded in URL: `/api/sync/kobo/{token}/markup` - -**Pros**: -- โœ… Works with stock Kobo firmware (no jailbreak) -- โœ… Simple one-line configuration -- โœ… Proven approach (used by Komga) -- โœ… Per-device revocable tokens - -**Cons**: -- โŒ Token visible in Kobo logs -- โŒ Token visible in server logs -- โŒ Token stored in plain text config - -**Security Level**: Medium - acceptable for self-hosted use on trusted networks - -### KOReader: Bearer Token in Header - -**Method**: `Authorization: Bearer {token}` header - -**Pros**: -- โœ… Cryptographically secure random UUIDs -- โœ… Not visible in URLs (only in headers) -- โœ… Standard authentication method -- โœ… Per-device revocable tokens - -**Cons**: -- โŒ Requires plugin installation -- โŒ Token stored in plugin settings - -**Security Level**: High - suitable for most deployments - -### OPDS: Both Methods Supported - -**Methods**: -- Query parameter: `?token={token}` (Kobo) -- Bearer header: `Authorization: Bearer {token}` (KOReader) - -**Security Level**: Depends on which method is used - -## Threat Model - -### Assumptions - -1. **Trusted Network**: Users run Bookhoard on home/office network -2. **Self-Hosted**: No public cloud exposure -3. **Technical Users**: Capable of setting up VPN, reverse proxy -4. **Data Privacy**: Books are personal, not state secrets - -### Attack Scenarios - -#### 1. Local Network Eavesdropping - -**Attacker**: Devices on same WiFi network - -**Risk**: Medium -- Can intercept traffic if no HTTPS -- Can see API key in URLs for Kobo - -**Mitigation**: -- Use reverse proxy with HTTPS -- Keep on trusted network -- Use WPA3 encryption for WiFi - -#### 2. Device Theft - -**Attacker**: Physical access to Kobo/KOReader device - -**Risk**: Low -- API key stored in device config -- Attacker can sync until token regenerated - -**Mitigation**: -- Regenerate token if device lost -- Enable device approval (already implemented) -- Monitor logs for unauthorized access - -#### 3. Server Compromise - -**Attacker**: Access to Bookhoard server - -**Risk**: High (but out of scope) -- Attacker has database access -- All tokens compromised - -**Mitigation**: -- Keep server updated -- Use strong passwords -- Limit network exposure -- Regular backups - -#### 4. Token Leakage via Logs - -**Attacker**: Access to server logs or Kobo logs - -**Risk**: Medium -- API keys visible in logs -- Historical keys may be exposed - -**Mitigation**: -- Use log scrubbing (future enhancement) -- Limit log retention -- Regenerate tokens periodically - -## Security Best Practices - -### For Kobo Users - -1. **Local Network Only** - - Don't expose Bookhoard publicly - - Use WiFi with WPA3 encryption - - Consider guest network isolation - -2. **HTTPS for Remote Access** - - Set up reverse proxy (nginx, Caddy) - - Use Let's Encrypt for SSL certificates - - Enforce HTTPS redirection - -3. **Token Management** - - Regenerate token if device lost - - Regenerate token periodically (monthly) - - Don't share sync URL publicly - -4. **Monitoring** - - Check Device Management regularly - - Look for unknown devices - - Review server logs for suspicious activity - -### For KOReader Users - -1. **Plugin Security** - - Only install official Bookhoard plugin - - Verify plugin source - - Keep plugin updated - -2. **Token Storage** - - Plugin stores token in settings file - - Keep plugin files private - - Don't share plugin config - -3. **Network Security** - - Same as Kobo (local network, HTTPS) - - Bearer token slightly more secure than URL token - -### For All Users - -#### Network Setup - -**Recommended**: Local network only -``` -Kobo/KOReader โ† WiFi โ†’ Router โ† WiFi โ†’ Bookhoard Server -``` - -**Advanced**: VPN for remote access -``` -Kobo/KOReader โ† VPN โ†’ Internet โ† VPN โ†’ Bookhoard Server -``` - -**Not Recommended**: Direct public exposure -``` -Kobo/KOReader โ† Internet โ†’ Bookhoard Server (INSECURE) -``` - -#### Reverse Proxy Setup (HTTPS) - -**Using Caddy**: -``` -bookhoard.example.com { - reverse_proxy localhost:8765 - encode gzip -} -``` - -**Using nginx**: -```nginx -server { - listen 443 ssl; - server_name bookhoard.example.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - location / { - proxy_pass http://localhost:8765; - proxy_set_header Host $host; - } -} -``` - -#### Firewall Rules - -**UFW (Linux)**: -```bash -# Allow local network -sudo ufw allow from 192.168.1.0/24 to any port 8765 - -# Block public access -sudo ufw deny 8765 -``` - -**pfSense / OPNsense**: -- Create firewall rule: Allow from local network -- Block WAN access to port 8765 - -## Token Regeneration - -### When to Regenerate - -- Device lost or stolen -- Token shared accidentally -- Periodic rotation (monthly/quarterly) -- Suspicious activity in logs - -### How to Regenerate - -1. Open Bookhoard Device Management -2. Find device in list -3. Click "๐Ÿ”„ Regenerate Token" -4. Confirm regeneration -5. Copy new token/sync URL -6. Update device configuration - -### Impact - -- Old token stops working immediately -- Device must update config to resume syncing -- No data loss - device ID remains the same - -## Comparison with Other Systems - -### Calibre - -- **Auth**: Basic Auth (username/password) -- **Pros**: Familiar, works everywhere -- **Cons**: Credentials sent with every request - -### Komga - -- **Auth**: API key in URL (same as Bookhoard Kobo) -- **Pros**: Simple, proven -- **Cons**: Token visible in logs - -### Bookhoard - -- **Kobo**: API key in URL (matches Komga) -- **KOReader**: Bearer token (more secure) -- **Pros**: Flexible, revocable, per-device -- **Cons**: Token visible in logs (Kobo only) - -## Compliance & Privacy - -### GDPR Considerations - -- Reading progress = personal data -- Device tokens = access credentials -- Users can export/delete data -- Tokens can be revoked - -### Data Minimization - -- Only sync required data -- No unnecessary metadata -- Tokens don't reveal reading habits - -## Future Enhancements - -### Planned - -- [ ] Log scrubbing (hide tokens in logs) -- [ ] Token expiration (auto-rotate tokens) -- [ ] Device IP whitelisting -- [ ] Biometric authentication (device unlock) - -### Not Planned - -- [ ] OAuth/OIDC (overkill for self-hosted) -- [ ] Client certificates (too complex) -- [ ] Hardware security keys (not supported by Kobo) - -## FAQs - -### Is API key auth secure enough? - -**Answer**: Yes, for self-hosted use on trusted networks. Same security model as Komga, widely used in community. - -### Should I use HTTPS? - -**Answer**: Highly recommended. Without HTTPS, API key can be intercepted on public networks. - -### What if my Kobo is stolen? - -**Answer**: Regenerate token immediately. Old token stops working, thief loses access. - -### Can I use VPN? - -**Answer**: Yes, VPN is recommended for remote access. Encrypts all traffic. - -### How often should I regenerate tokens? - -**Answer**: Monthly if security-conscious. When device lost. Never if unconcerned. - -## Additional Resources - -- [Kobo Setup Guide](./kobo-setup.md) -- [KOReader Setup Guide](./koreader-setup.md) -- [Reverse Proxy Guide](../../contributing/reverse-proxy.md) -- [Network Security Best Practices](https://www.cisa.gov/news-events/news/secure-our-world) -``` - -**Verification**: -1. Check documentation renders at `/docs` endpoint -2. Verify search finds this page -3. Test all code examples -4. Confirm security advice is sound - ---- - -## Summary of Changes - -### Files Modified - -1. **Database** (`internal/database/queries/queries.sql`) - - Added: `UpdateDeviceAuthToken` query - -2. **Middleware** (`internal/middleware/device_auth.go`) - - Enhanced: `Authenticate` function to support URL path + query param - -3. **Router** (`internal/router/sync.go`) - - Changed: Kobo routes to `/api/sync/kobo/:token` - -4. **Router** (`internal/router/device.go`) - - Added: Route for `PUT /:id/regenerate-token` - - 5. **Handler** (`internal/handlers/devices.go`) - - Added: `RegenerateDeviceToken` function - - Added: `AuthToken` field to `DeviceInfo` struct (breaking change: all clients must accept this field) - - Modified: `GetDevicesData`, `ListDevices` to include `auth_token` - -6. **Template** (`templates/devices.templ`) - - Added: `AuthToken` field to `DeviceData` struct - - Added: Copy sync URL button - - Added: Regenerate token button - - Added: JavaScript functions for copy/regenerate - -7. **Tests** (`cmd/server/tests/kobo_test.go`) - - Added: URL path token tests - - Updated: Bearer token tests - -8. **Documentation** (`docs/user/devices/kobo-setup.md`) - - Rewritten: API key in URL approach - -9. **Documentation** (`docs/user/devices/security.md`) - - Created: Security considerations document - - 10. **Bruno** (`bruno/devices/regenerate-*.bru`) - - Created: Token regeneration tests (success, unauthorized, forbidden, not found) - - 11. **Bruno** (`bruno/sync-kobo/api.bru`) - - Added: URL path token tests for Kobo sync - - 12. **Bruno** (`bruno/opds/*-Bearer.bru`, `*-Query Token.bru`, `*-No Auth.bru`, `*-Invalid Token.bru`) - - Created: OPDS authentication tests for both Bearer and query parameter methods - -### Files Created - -- `docs/user/devices/security.md` -- `bruno/devices/regenerate-token.bru` -- `bruno/devices/regenerate-token-unauthorized.bru` -- `bruno/devices/regenerate-token-forbidden.bru` -- `bruno/devices/regenerate-token-notfound.bru` -- `bruno/opds/Get Device Catalog - Bearer.bru` -- `bruno/opds/Get Device Catalog - Query Token.bru` -- `bruno/opds/Get Device Catalog - No Auth.bru` -- `bruno/opds/Get Device Catalog - Invalid Token.bru` -- `bruno/opds/Download Book - Bearer.bru` -- `bruno/opds/Download Book - Query Token.bru` - -### Testing Checklist - -- [ ] Run `go build ./...` - all packages compile -- [ ] Run `bash scripts/verify-guidelines.sh` - no errors -- [ ] Run `go test ./... -v` - all tests pass -- [ ] Test token regeneration in browser -- [ ] Test copy sync URL button -- [ ] Test Kobo sync with URL path token -- [ ] Test KOReader sync with Bearer token -- [ ] Test OPDS with query parameter -- [ ] Test OPDS with Bearer header -- [ ] Verify documentation renders at `/docs` -- [ ] Verify search finds new docs - ---- - -**End of Implementation Document** - ---- - -## Updates Applied (2026-02-12) - -Based on code review analysis, the following fixes were applied to this document: - -### Fixed Issues - -1. **AuthToken Field Addition** (Section 1.6.1) - - Added `AuthToken` field to `DeviceInfo` struct - - Breaking change (Option A): All clients must accept this field - -2. **ListDevices Handler Update** (Section 1.6.2) - - Changed from "Do the same" to showing complete code modification - - Added explicit code block for `ListDevices` function modification - - Renamed section from "Update Device List Handler" to "Update Device List Handlers" - -3. **goto Label Clarification** (Section 1.2) - - Updated explanation to clarify that `validateDevice:` label is in the kept section - - Added reference to line 221 in complete function - - Made it explicit that lines 62-270 remain unchanged - -4. **Time Formatting** (Section 1.6.3) - - Updated template code to use `.Format("2006-01-02 15:04")` method - - Fixed both `LastSync` and `LastSeen` field display - - Both fields now properly format `*time.Time` values - -### Remaining User Decisions - -- โœ… Template type checking: Already fixed by user -- โœ… Bruno test variables: User confirmed they exist in environment -- โœ… Breaking change on AuthToken: User approved Option A - -**Next Steps**: -1. Review this document completely -2. Ask questions if anything is unclear -3. Begin Phase 1 implementation (database query first) -4. Test after each file change -5. Commit changes with clear messages -6. Run full test suite before declaring complete - -**Estimated Time**: 4-5 hours for full implementation (including testing) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md deleted file mode 100644 index fb8095f..0000000 --- a/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,1409 +0,0 @@ -# Bookhoard Device Authentication & Sync Implementation Plan - -## Executive Summary - -**Current Problem:** -- OPDS routes are publicly accessible (security vulnerability) -- Kobo sync requires custom authentication approach -- KOReader documentation is incorrect about Basic Auth -- No clear authentication strategy for different device types - -**Vision:** -Transform Bookhoard into a comprehensive Kindle-replacement ecosystem with seamless device sync, supporting both Kobo (native) and KOReader (via plugin) with full functionality including progress, highlights, notes, and bookmarks. - -**Authentication Strategy:** -- **Kobo**: API key in URL path (per-device, revocable, no jailbreak needed) -- **KOReader**: Bearer token in Authorization header (per-device, revocable, via plugin) -- **OPDS**: Both authentication methods supported - ---- - -## 1. Current State Analysis - -### 1.1 Authentication Landscape - -**Existing Infrastructure:** -```go -// DeviceAuthMiddleware - Currently only supports Bearer tokens -// File: internal/middleware/device_auth.go:37-108 -func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { - // Only checks Authorization: Bearer {token} - // Validates against devices.auth_token -} -``` - -**Routes Using Device Auth:** -- `/api/sync/kobo/*` - Kobo sync endpoints -- `/api/sync/koreader/*` - KOReader sync endpoints -- `/opds/devices/*` - OPDS catalog - -**Database Schema:** -```sql --- devices table has auth_token field for all device authentication --- Can store revocable API keys for Kobo devices -``` - -### 1.2 Kobo Integration Status - -**Proven Approach (Komga):** -- Komga successfully handles Kobo sync without jailbreak using API keys in URL path -- Configuration: `api_endpoint=https://komga.example.com/kobo/{api_key}` -- Kobo firmware supports custom `api_endpoint` in `Kobo eReader.conf` -- API key embedded in URL path works reliably with stock Kobo firmware - -**What's Currently Broken:** -- Current middleware expects Bearer token in Authorization header -- Routes need to support API key in URL path parameter -- Device auth tokens (API keys) already generated during registration - -**What's Working:** -- Kobo sync handler implementation exists and is functional -- Endpoints registered: `/api/sync/kobo/markup`, `/bookmark`, `/v1/initialization`, etc. -- Device auth tokens (API keys) already generated during registration -- Can sync: progress, highlights, notes, bookmarks once auth is fixed - -### 1.3 KOReader Integration Status - -**What's Broken:** -- Documentation says use "Basic Auth" - this is WRONG -- KOReader doesn't support custom headers natively for sync -- Bearer token can't be sent via standard KOReader settings - -**What's Working:** -- KOReader sync handler supports full feature set -- Can sync: progress, bookmarks, highlights, notes, library metadata -- Plugin architecture exists but no Bookhoard plugin yet - -### 1.4 OPDS Status - -**Current:** -- Routes registered in `internal/router/opds.go:11-12` -- โœ… **Already using DeviceAuthMiddleware** (line 12: `opds.Use(cfg.DeviceAuthMiddleware.Authenticate)`) -- โœ… Authentication is required - not publicly accessible -- Routes: `/opds/devices/:deviceId/*` - all protected - -**Required:** -- โœ… No changes needed for OPDS security -- After Phase 1, OPDS will automatically support both auth methods: - - Kobo: API key in URL path (`?token=` or path parameter) - - KOReader: Bearer token via `Authorization` header - ---- - -## 2. User Priorities (Ranked) - -### Priority 1: Functionality First -**User Quote:** "I want a Kindle system replacement, otherwise I could just use Booklore or KOReader" - -**Requirements:** -- โœ… Full sync: progress, highlights, notes, bookmarks -- โœ… Works on both Kobo and KOReader -- โœ… Seamless user experience -- โœ… OPDS for wireless book delivery -- โœ… Two-way sync (device โ†” server) - -### Priority 2: Security Second (with documentation) -**User Quote:** "This is a self-hosted app so as long as the user is aware of the security implications in the documentation it should be fine" - -**Acceptable Trade-offs:** -- API key in URL for Kobo (per-device, revocable, documented security considerations) -- Token-based for KOReader (more secure) -- Users responsible for network security (VPN, local network, HTTPS) -- Security implications clearly documented - -### Priority 3: Simple Setup -**Goal:** Minimal friction for users - -**Kobo Experience:** -- Register device in Bookhoard UI -- Copy device API key from device management page -- One line in `Kobo eReader.conf` with API key -- Token can be regenerated if compromised -- Works immediately with stock firmware - -**KOReader Experience:** -- Copy plugin files -- Enter server URL and token -- One-time setup -- Background sync works automatically - ---- - -## 3. Target State - -### 3.1 Unified Authentication Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ DeviceAuthMiddleware (Enhanced) โ”‚ -โ”‚ Unified authentication using revocable API keys: โ”‚ -โ”‚ โ”‚ -โ”‚ 1. Authorization: Bearer {token} โ”‚ -โ”‚ โ†’ Extract from Authorization header โ”‚ -โ”‚ โ†’ Lookup device by auth_token โ”‚ -โ”‚ โ†’ Used by: KOReader, OPDS apps, API clients โ”‚ -โ”‚ โ”‚ -โ”‚ 2. URL Path Parameter: /api/sync/kobo/{token} โ”‚ -โ”‚ โ†’ Extract token from URL path (c.Param("token")) โ”‚ -โ”‚ โ†’ Lookup device by auth_token โ”‚ -โ”‚ โ†’ Used by: Kobo e-readers (stock firmware, no jailbreak) โ”‚ -โ”‚ โ”‚ -โ”‚ 3. Query Parameter: ?token={token} โ”‚ -โ”‚ โ†’ Extract from query string (c.QueryParam("token")) โ”‚ -โ”‚ โ†’ Lookup device by auth_token โ”‚ -โ”‚ โ†’ Used by: OPDS catalog access โ”‚ -โ”‚ โ”‚ -โ”‚ All methods result in: device context set in echo.Context โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -**Why This Architecture:** -- **Simple**: All methods use same auth_token lookup (no new database logic) -- **Secure**: API keys are random UUIDs, revocable, can be regenerated -- **Universal**: All devices get auth_tokens (existing field, no schema change) -- **Proven**: Komga uses this exact approach for Kobo sync successfully -- **No Firmware Limitations**: URL path parameter works with stock Kobo firmware (no jailbreak needed) -- **Revocable**: API keys can be regenerated by users without device re-registration -- **Single Authentication Method**: All device types use the same auth_token lookup, simplifying codebase -``` - -### 3.2 Feature Matrix - -| Feature | Kobo (Stock Firmware) | KOReader (Plugin) | -|---------|---------------|-------------------| -| **Progress Sync** | โœ… Automatic (API key in URL) | โœ… Automatic (Bearer token) | -| **Highlights** | โœ… Native support | โœ… Via plugin API | -| **Bookmarks** | โœ… Native support | โœ… Via plugin API | -| **Notes** | โœ… Native support | โœ… Via plugin API | -| **OPDS** | โœ… Native support | โœ… Native OPDS support | -| **Setup Complexity** | Low (1 config line) | Medium (plugin install) | -| **Auth Method** | API key in URL path | Bearer token in header | -| **Security** | High (revocable tokens) | High (revocable tokens) | - -### 3.3 User Flows - -**Kobo User Journey:** -1. Open Bookhoard web UI -2. Navigate to Device Management โ†’ Add New Device -3. Enter device name and select "Kobo" as device type -4. Click Register Device -5. From device details page, copy full Kobo Sync URL with API key: - ``` - http://YOUR_IP:8765/api/sync/kobo/YOUR_API_KEY - ``` -6. Connect Kobo to computer, open `.kobo/Kobo/Kobo eReader.conf` -7. Add/edit line: `api_endpoint=http://YOUR_IP:8765/api/sync/kobo/YOUR_API_KEY` - - Paste the entire URL from step 5 (includes device API key) -8. Save and eject Kobo (restarts automatically) -9. Kobo syncs to Bookhoard instead of Kobo store -10. For OPDS: Add catalog URL with `?token=YOUR_API_KEY` parameter - -**Important Note - Token Regeneration:** -- If token is compromised or lost, regenerate from device management page -- OLD token will immediately stop working -- User must update `api_endpoint` line with NEW token -- One-click copy button makes this easy - -**KOReader User Journey:** -1. Install KOReader on device -2. Open Bookhoard web UI -3. Register device โ†’ copy API key -4. Approve device -5. Install Bookhoard plugin โ†’ paste API key -6. Configure sync settings -7. Read books โ†’ automatic sync -8. Add OPDS catalog with API key in header (via plugin) - ---- - -## 4. Implementation Plan - -### Phase 1: Enhanced Authentication (Week 1) -**Goal:** Make auth work for both Kobo (API key in URL) and KOReader (Bearer token) - -**Tasks:** - -1. **Update Routing for Kobo** (`internal/router/sync.go`) - - [ ] Change Kobo route group from `/api/sync/kobo` to `/api/sync/kobo/:token` - - [ ] Update all Kobo route handlers to use path parameter - - [ ] Test that routes still work with Bearer token (backward compatibility) - -2. **Update DeviceAuthMiddleware** (`internal/middleware/device_auth.go`) - - [ ] Try Bearer token lookup first (existing behavior) - - [ ] If no Bearer token, check for `token` path parameter (`c.Param("token")`) - - [ ] Query database: `GetDeviceByAuthToken(token)` (already exists) - - [ ] Set device context on successful auth - - [ ] Return 401 if both methods fail - -3. **Add Token Regeneration Backend** (`internal/handlers/devices.go`) - - [ ] Add new SQL query: `UpdateDeviceAuthToken` (DO NOT use UpdateDevice - it doesn't modify auth_token field) - - [ ] Add endpoint: `POST /api/devices/:id/regenerate-token` - - [ ] Generate new random UUID for auth_token - - [ ] Update device in database - - [ ] Return new token to user - - [ ] Require JWT auth (user must be logged in) - -4. **Update Device Registration UI** (`templates/devices.templ`, `templates/partials/device-management.templ`) - - [ ] **Show "Copy Full Sync URL" button for each device** (uses clipboard API) - - [ ] For Kobo: Display complete URL: `http://IP:8765/api/sync/kobo/{DEVICE_TOKEN}` - - [ ] **Add "Regenerate Token" button** (with confirmation dialog: "This will revoke current token. Continue?") - - [ ] After regeneration, show toast: "Token regenerated - update Kobo config" - - [ ] For KOReader: Show auth token in copyable input field - - [ ] Display clear setup instructions per device type with full URL example - -5. **Apply Middleware to OPDS** (`internal/router/opds.go`) - - [ ] Verify OPDS routes have DeviceAuthMiddleware (already applied) - - [ ] Add support for `?token=` query parameter for OPDS access - - [ ] Update comments to reflect dual auth support - -**Testing:** -- Kobo device sync with api-key url auth -- KOReader sync with Bearer token auth -- OPDS access with both auth methods - ---- - -### Phase 2: Kobo Integration (Week 1-2) -**Goal:** Seamless Kobo experience with API key-based auth (stock firmware) - -**Tasks:** - -1. **Kobo Setup Documentation** (`docs/user/devices/kobo-setup.md`) - - [ ] Show how to get API key from Bookhoard device management page - - [ ] Step-by-step guide for editing `Kobo eReader.conf` - - [ ] Include example config: `api_endpoint=http://192.168.1.100:8765/api/sync/kobo/{API_KEY}` - - [ ] Security warning about API keys in logs - - [ ] Network security recommendations (VPN, local network) - - [ ] Instructions for regenerating token if compromised - - **Note**: Documentation was updated on 2026-02-12 to reflect API key authentication - - **Verification**: Confirm lines 37-53 don't reference entering serial number (device registration generates token automatically) - -2. **Kobo Sync Validation** (`cmd/server/tests/kobo_test.go`) - - [ ] Update tests to use API key in URL path - - [ ] Test sync endpoints with `/api/sync/kobo/{token}/markup` format - - [ ] Verify all features work: progress, highlights, bookmarks, notes - - [ ] Test token regeneration doesn't break existing sync - -3. **Kobo OPDS Integration** - - [ ] Document OPDS URL format: `http://IP:8765/opds/devices/{DEVICE_ID}/catalog?token={API_KEY}` - - [ ] Test OPDS browsing with API key in URL parameter - - [ ] Test book downloads - -**Success Criteria:** -- New Kobo device registered and syncing within 5 minutes -- All sync features working without jailbreak -- API key can be regenerated if compromised -- Security implications clearly documented -- Works with stock Kobo firmware (no modifications) - ---- - -### Phase 3: KOReader Plugin Development (Week 2-4) -**Goal:** Full-featured KOReader plugin with Bearer token auth - -**Tasks:** - -1. **Plugin Architecture** - - [ ] Create `koreader-plugin/bookhoard.koplugin/` directory - - [ ] Implement `_meta.lua` (plugin metadata) - - [ ] Implement `main.lua` (entry point) - -2. **Core Plugin Features** - - [ ] Settings UI: server URL, auth token, sync options - - [ ] HTTP client with Bearer token authentication - - [ ] Auto-sync on events: - - Page turn (configurable: every N pages) - - Bookmark added/deleted - - Highlight created/deleted - - Note added/edited - - [ ] Manual sync button in KOReader menu - - [ ] Sync status indicator - - [ ] Error handling and retry logic - -3. **Plugin API Integration** - - [ ] Call `/api/sync/koreader/progress` with reading position - - [ ] Call `/api/sync/koreader/bookmarks` with annotations - - [ ] Call `/api/sync/koreader/metadata` to get server-side progress - - [ ] Handle conflicts (server vs local progress) - -4. **Plugin Documentation** - - [ ] Installation guide (copy files, restart KOReader) - - [ ] Configuration guide (get token from Bookhoard UI) - - [ ] Usage instructions - - [ ] Troubleshooting guide - -**Success Criteria:** -- Plugin installed and configured in under 10 minutes -- Background sync works transparently -- All annotation types sync properly -- No Lua errors or crashes - ---- - -### Phase 4: OPDS Security & Integration (Week 2-3) -**Goal:** Secure OPDS with unified device authentication - -**Tasks:** - -1. **Secure OPDS Routes** (`internal/router/opds.go`) - - [ ] Apply `DeviceAuthMiddleware` to all OPDS endpoints - - [ ] Ensure auth works with both api-key and Bearer methods - - [ ] Update router comments - -2. **Kobo OPDS Access** - - [ ] Document OPDS URL with device ID - - [ ] Test OPDS access via Kobo browser - - [ ] Test book downloads - -3. **KOReader OPDS Access** - - [ ] Works natively via KOReader's OPDS support - - [ ] Uses Bearer token in header - - [ ] Document catalog URL format - -4. **Test Coverage** (`cmd/server/tests/opds_test.go`) - - [ ] Test unauthenticated access โ†’ expect 401 - - [ ] Test with valid Bearer token โ†’ expect 200 - - [ ] Test with valid api-key url โ†’ expect 200 - - [ ] Test book download with auth - ---- - -### Phase 5: Testing & Documentation (Week 4) -**Goal:** Production-ready release with comprehensive docs - -**Tasks:** - -1. **End-to-End Testing** - - [ ] Fresh Kobo setup: register โ†’ sync โ†’ highlights โ†’ OPDS - - [ ] Fresh KOReader setup: install plugin โ†’ sync โ†’ highlights โ†’ OPDS - - [ ] Cross-device sync: Kobo โ†” KOReader same book - - [ ] Offline scenarios and conflict resolution - - [ ] Error handling and recovery - -2. **Security Documentation** - - [ ] Security implications page - - [ ] Network security recommendations - - [ ] Comparison: API Key (URL path) vs Bearer token - both use revocable auth_token - - [ ] Threat model for self-hosted users - -3. **User Documentation** - - [ ] Quick start guide (Kobo) - - [ ] Quick start guide (KOReader) - - [ ] Feature comparison table - - [ ] FAQ and troubleshooting - -4. **Release Preparation** - - [ ] Version bump - - [ ] Changelog - - [ ] Migration guide (if needed) - ---- - -## 5. Technical Decisions - -### 5.1 Why API Key in URL Path for Kobo? - -**Approach Chosen:** Per-device API key embedded in URL path: `/api/sync/kobo/{api_key}/...` - -**Pros:** -- No jailbreak needed - works with stock Kobo firmware -- Proven approach - Komga uses this successfully -- API keys are random UUIDs (cryptographically secure) -- Tokens can be revoked and regenerated -- Single configuration line in Kobo config -- Simple token management (user can regenerate) -- Uses existing `auth_token` field (no schema change) - -**Cons:** -- API key visible in Kobo logs and server logs -- URL-based authentication (less secure than headers) -- Key appears in Kobo config file (plain text) - -**Mitigations:** -- Use HTTPS for sync (reverse proxy) -- Document that API keys are sensitive -- Recommend local network only (VPN for remote) -- Provide one-click token regeneration -- Acceptable security for self-hosted use (like Komga) - -**Alternative Considered:** -- **Serial-based auth**: Rejected because it uses predictable serials that can't be revoked and is less secure -- **Kobo store token**: Rejected because it's managed by Kobo, not under our control -- **Custom HTTP headers**: Rejected because Kobo firmware doesn't support custom headers - -**Authentication Strategy:** -- Kobo: API key in URL path (per-device, revocable) -- KOReader: Bearer token (per-device, revocable) -- OPDS: Both methods supported (API key via query parameter, bearer token via header) - -### 5.2 Why Not Use Kobo's Token? - -Kobo sends `Authorization: Bearer {kobo-store-token}` which is: -- Managed by Kobo (not under our control) -- May rotate/expire without warning -- Only valid for Kobo's servers -- Doesn't identify device in our system -- Can't be extracted from Kobo config without jailbreak - -**Decision:** Use API key in URL path instead (proven by Komga). The API key in URL path provides per-device authentication that is revocable and works with stock Kobo firmware without jailbreak. - -### 5.3 Token Regeneration Strategy - -**Backend:** -- Endpoint: `POST /api/devices/:id/regenerate-token` -- Requires: User JWT auth (device owner) -- Action: Generate new UUID, update `devices.auth_token` -- Response: Return new token to user - -**Frontend:** -- "Regenerate Token" button on device management page -- Confirmation dialog: "This will revoke current token. Continue?" -- One-click copy to clipboard -- Update display instructions with new token -- Toast notification: "Token regenerated successfully" - -**Impact:** -- Old token immediately invalid -- Device must update config with new token -- Sync will fail until config updated -- Useful for: Lost tokens, compromised devices, periodic rotation - -### 5.4 Why Bearer Token for KOReader? - -**Pros:** -- Cryptographically secure (random UUID) -- Can be revoked/regenerated -- Standard authentication method -- Works with KOReader plugin architecture -- Plugin can store token securely - -**Cons:** -- Requires plugin (can't use native settings) -- Token must be copy-pasted during setup -- User must manage token - -**Trade-off Accepted:** -More secure but requires plugin installation. Justified because KOReader users are typically more technical and willing to install plugins. - -**Note:** Same token regeneration as Kobo (reuses backend logic) - ---- - -## 6. Files to Modify - -### Core Authentication -- `internal/middleware/device_auth.go` - Add URL path token extraction -- `internal/router/sync.go` - Update Kobo routes to use path parameter -- `internal/router/opds.go` - Add ?token= query parameter support (already has middleware) - -### Device Management -- `internal/handlers/devices.go` - Add regenerate token endpoint -- `internal/database/queries/queries.sql` - No changes needed (auth_token already exists) -- `templates/devices.templ` - Add copy token, regenerate token buttons -- Create `templates/devices-kobo-config.templ` - Kobo config instructions with API key - -### Testing -- `cmd/server/tests/kobo_test.go` - Update for URL path token -- `cmd/server/tests/opds_test.go` - Add ?token= auth tests -- `cmd/server/tests/test_helpers.go` - Add token regeneration helpers - -### Documentation -- `docs/user/devices/kobo-setup.md` - Rewrite for API key auth -- `docs/user/devices/koreader-setup.md` - Update for Bearer token -- `docs/user/devices/security.md` - New security considerations doc -- `docs/developer/api/kobo/` - Update API docs with path parameters - -### New Files -- `koreader-plugin/bookhoard.koplugin/_meta.lua` - Plugin metadata (Phase 3) -- `koreader-plugin/bookhoard.koplugin/main.lua` - Plugin implementation (Phase 3) -- `docs/user/devices/security.md` - Security considerations - ---- - -## 7. Success Metrics - -**Functionality:** -- โœ… Kobo syncs progress, highlights, notes, bookmarks automatically -- โœ… KOReader syncs same features via plugin -- โœ… OPDS works on both platforms -- โœ… Cross-device sync works (read on Kobo, continue on KOReader) - -**Usability:** -- โœ… Kobo setup time: < 5 minutes -- โœ… KOReader setup time: < 10 minutes -- โœ… No token management for Kobo users -- โœ… Clear documentation with security warnings - -**Security:** -- โœ… OPDS no longer publicly accessible -- โœ… Auth required for all sync endpoints -- โœ… Security implications documented -- โœ… Network security recommendations provided - ---- - -## 8. Future Enhancements (Out of Scope) - -- Auto-discovery of devices on network -- QR code setup for mobile devices -- Background sync service (no plugin needed) -- Readwise/Joplin integration -- Calibre integration -- Mobile apps (iOS/Android) - ---- - -## 9. Decisions Made (User Input) - -Based on user feedback, the following decisions have been finalized: - -### 9.1 KOReader Sync Trigger -**Decision:** Configurable with multiple options -- **Auto-sync**: Every N pages (configurable, default every 10 pages) -- **Auto-sync**: Every N minutes (configurable, default every 5 minutes) -- **Manual sync**: Always available via menu -- **Event-triggered**: On bookmark/highlight/note creation (immediate) - -**Rationale:** User wants automatic sync for convenience but manual fallback for control. - -### 9.2 Plugin Distribution -**Decision:** Separate repository under Bookhoard organization -- Repository: `github.com/bookhoard/koreader-plugin` -- Licensed under same terms as Bookhoard -- Versioned independently -- Referenced in main Bookhoard documentation - -**Rationale:** Clean separation of concerns, easier plugin-specific issues/PRs. - -### 9.3 Kobo Authentication -**Decision:** API key in URL path (per-device) -- No serial number needed -- Token entered in device registration (already exists) -- Configured in Kobo as: `api_endpoint=http://IP:8765/api/sync/kobo/{TOKEN}` -- Token can be regenerated by user (new endpoint) -- Stored in `devices.auth_token` field (no schema change) - -**Rationale:** Simplest user experience (just copy token), most secure (revocable), proven to work (Komga). - -### 9.4 Conflict Resolution -**Decision:** Use existing codebase strategy -- Already implemented in `internal/handlers/koreader.go` -- Uses timestamp-based last-write-wins -- Device priority can be configured per-device - -**Rationale:** Don't reinvent the wheel, existing implementation is sufficient. - -### 9.5 Implementation Order -**Decision:** Parallel implementation with separate sections -- Phase 2a: Kobo Integration -- Phase 2b: KOReader Plugin (runs concurrently) -- Separate milestones and deliverables -- Can ship Kobo support before plugin is ready - -**Rationale:** Faster time-to-market for Kobo users, plugin can follow. - -### 9.6 KOReader Device Identification Strategy - -**Problem:** Multiple KOReader installations each register as separate devices, exhausting `max_devices` limit even though they represent the same physical device. - -**Note:** This is about device MANAGEMENT, not authentication. Authentication uses `auth_token` (API keys) for all devices. Device identifier is for tracking multiple installations of the same physical device. - -**Clarification for Kobo Devices:** -Kobo also uses the two-field approach, but differently: -- **Kobo `device_identifier`**: User enters serial number manually (one-time registration) -- **Kobo `auth_token`**: Auto-generated API key for authentication -- **Kobo vs KOReader Difference**: - - Kobo: User manually enters serial as device_identifier - - KOReader: Plugin auto-generates UUID as device_identifier - - Both: Use auto-generated auth_token for API authentication - -**Note:** This is about device MANAGEMENT, not authentication. Authentication uses `auth_token` (API keys) for all devices. Device identifier is for tracking multiple installations of the same physical device. - ---- - -## 16.1 Codebase Investigation: device_identifier Field - -**Investigation Date:** 2026-02-12 - -**Purpose:** Clarify the role and usage of `device_identifier` field in the devices table - -### Git History Analysis - -**Introduction (Commit 3b2075f, Phase 1 - "highlights and notes annotation system"):** -- Added as part of broader annotation system feature -- Original intent: Track physical device identity across software reinstalls -- Has existed since early project history (not legacy code) - -### Current Codebase Usage - -**Active Usage:** -```go -// internal/handlers/devices.go:37 -DeviceIdentifier string `json:"device_identifier" validate:"required,min=1,max=255"` -``` -- Required field in device registration requests -- Stored in `devices.device_identifier` (VARCHAR(255) UNIQUE NOT NULL) -- Used in registration flow to track device identity - -**Dead Code:** -```sql --- internal/database/queries/queries.sql:731 --- name: GetDeviceByIdentifier :one -SELECT * FROM devices WHERE device_identifier = $1; -``` -- Function exists in generated database code (`internal/database/querier.go:130`) -- **NOT called** anywhere in handlers or tests (0 references) -- Can be considered for removal during code cleanup - -### OPDS Handler Behavior - -**Current Implementation (internal/handlers/opds.go:56-89):** -```go -deviceID := c.Param("deviceId") // Extracts UUID from URL -deviceUUID, err := uuid.Parse(deviceID) -device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true}) -``` - -**Finding:** OPDS handler uses device `id` (UUID) for lookup, NOT `device_identifier` - -### Authentication vs Device Identification - -**Authentication (what currently works):** -- `auth_token` field stores revocable API keys -- Used by `DeviceAuthMiddleware` for all device authentication -- Bearer token or URL path parameter lookup via `GetDeviceByAuthToken` - -**Device Identification (what this is about):** -- `device_identifier` field tracks physical device identity -- Helps prevent duplicate device registrations for same physical device -- NOT used for authentication (only for device management/registration) - -### Conclusion - -**No inconsistency found.** The implementation plan's handling of `device_identifier` is accurate: -- Field exists for device management purposes -- Required in current registration flow -- Auth uses `auth_token` only (no confusion) -- Dead query (`GetDeviceByIdentifier`) can be removed during cleanup - -**Decision:** **Keep Section 9.6** - The KOReader Device Identification Strategy is valid for solving the `max_devices` exhaustion problem. The plan correctly distinguishes between device identification (management) and authentication (security). - ---- - -**Architecture:** -- Plugin generates unique `device_id` on first launch (stored in KOReader settings) -- Plugin sends `device_identifier` during registration API call -- Backend validates `auth_token` matches device record -- Reinstalling KOReader reads same `device_id` from settings, reuses existing device record - -**Registration Flow Options:** - -| Approach | User Experience | Complexity | Pros/Cons | -|----------|-----------------|------------|-------------| -| **A. Manual Link** | User registers in Bookhoard, copies Device ID + Token | Low | โœ… Simple server
โŒ User copies two values | -| **B. Self-Register** | Plugin auto-registers, user approves in web UI | Medium | โœ… One-time setup
โš ๏ธ Requires approval endpoint | -| **C. QR Code Bridge** | Scan QR code with Device ID + Token | Low-Medium | โœ… Very user-friendly
โŒ Requires QR library | - -**Recommendation:** **Option B (Self-Register)** for best UX. - -**Backend Changes Required:** -```go -// Devices table - Add Device Identifier (Already Exists!) --- devices.device_identifier already exists (models.go:67) --- No schema change needed - just use it properly - -// internal/handlers/devices.go - InitiateDeviceRegistration() -// Add optional pre_generated_device_id parameter: -DeviceIdentifier string `json:"device_identifier" validate:"omitempty,uuid"` -``` - -**Plugin Changes Required:** -```lua --- First Run Detection: --- On first launch: -if not G_Settings:hasSetting("bookhoard_device_id") then - local device_id = uuid.generate() - G_Settings:saveSetting("bookhoard_device_id", device_id) - G_Settings:saveSetting("bookhoard_device_name", "KOReader on " .. Device.model) - - -- Trigger registration - register_device() -end - --- Registration API Call: --- POST /api/devices/register -{ - device_name: "KOReader on Kindle Paperwhite", - device_type: "koreader", - device_identifier: "550e8400-e29b..." -- From plugin settings -} -``` - -**Device Management UI:** -- Display `device_identifier` for each device -- "Copy Device ID" button for KOReader setup -- "Regenerate Token" doesn't change Device ID -- Show "Linked Devices" count (e.g., "3 KOReader installations linked") - -**Sync Flow With Device ID:** -```lua --- Plugin Headers: -Authorization: Bearer {auth_token} -X-Bookhoard-Device-ID: {device_identifier} -``` - -**Benefits:** - -| Aspect | Current | With Device ID | -|---------|----------|---------------| -| **Reinstalls** | New device each time | Same device, update token only | -| **max_devices** | Counts installs, not devices | Accurate device count | -| **Security** | Token-only | Device ID + token (two-factor) | -| **Troubleshooting** | "Which KOReader is this?" | Clear device identification | -| **Plugin Transfer** | Manual token copy | Migrate settings file | - -**Phase Integration:** Implement Device ID in Phase 1 (alongside dual authentication). It's same amount of work but solves `max_devices` problem elegantly. - -**Questions for User:** - -โœ… **RESOLVED**: Kobo two-field approach clarified (serial + API key) - - Kobo: Manual serial entry (device_identifier) + auto-generated API key (auth_token) - - KOReader: Auto-generated UUID (device_identifier) + auto-generated API key (auth_token) - - Documentation updated to reflect this distinction - -1. โœ… **Registration Workflow:** KOReader plugin will **self-register** (user approves in browser) - - Plugin auto-generates device_identifier on first launch - - Plugin calls Bookhoard registration API with device_identifier - - User approves device in Bookhoard web UI - - Plugin receives auth_token and stores it locally - - One-time setup with approval workflow -2. โœ… **Device ID Persistence:** KOReader uses `G_reader_settings` (global settings object) - - Settings persisted to `koreader/settings.reader.lua` in the main koreader directory - - **Survives plugin updates** because settings file is separate from plugin files - - Device ID stored as `bookhoard_device_id` key - - Plugin directory can be replaced/updated without losing device identity - - Standard KOReader pattern used by Wallabag, Calibre, and other sync plugins - - Alternative considered: Plugin-specific file in `koreader/plugins/bookhoard/settings.lua` (also survives updates, but G_reader_settings is simpler) -3. โœ… **max_devices Behavior:** Each device_identifier counts as **1 device** (same as Kobo) - - Example: User has 3 Kindles with KOReader = 3 device registrations - - Reinstalling KOReader on same device reuses same device_identifier (from persisted settings) - - Reinstallation does NOT count as new device (settings file retained) - - User can regenerate auth_token without changing device_identifier - - Enforces user's max_devices limit accurately -4. โœ… **Backward Compatibility:** **Not required** - - Per @PROJECT_GUIDELINES.md: Application has never been deployed to production - - No existing KOReader installations to migrate - - Clean slate implementation - no legacy support needed - - All device registrations will use new self-registration flow from day one -5. โœ… **KOReader Plugin Repo:** Plugin does not yet exist - - Device ID generation will happen **in the plugin** (not in main Bookhoard repo) - - Plugin generates UUID on first launch and stores in local settings - - Repository to be created: `github.com/bookhoard/koreader-plugin` - - Plugin handles all KOReader-specific logic (device ID gen, API calls, UI) - - Bookhoard backend provides generic device registration endpoints only - ---- - -## 10. Historical Context & Conversation Summary - -### Why This Plan Exists - -This implementation plan emerged from a detailed analysis of the current authentication and sync architecture. Key discoveries from codebase review: - -#### The Authentication Problem - -**Original Issue:** The plan started with `@fix-opds-device-authentication.md` which identified: -- OPDS routes were publicly accessible (security vulnerability) -- Test `GetDeviceCatalog_WithoutDeviceAuth` expected 401 but got 404 -- DeviceAuthMiddleware was already applied to OPDS routes, but tests were failing - -**Root Cause Discovery:** -- Kobo devices send `Authorization: Bearer {kobo-store-token}` (Kobo's store token, not Bookhoard's) -- Kobo cannot send custom Bearer tokens through `eReader.conf` -- Current middleware only supports Bearer tokens in Authorization header -- Solution: Use API key in URL path (proven by Komga) - -#### The Sidecar File Red Herring - -**Initial Thought:** Use `.bookhoard.json` sidecar file to pass device tokens to Kobo - -**Problem Discovered:** -- Sidecar handler exists (`internal/handlers/sidecar.go`) but routes are NOT registered -- Kobo firmware is locked down - cannot read custom config files -- No plugin architecture on Kobo (unlike KOReader) -- Would require Kobo firmware modification (impossible for most users) - -**Decision:** Abandon sidecar approach, use API key in URL path (proven by Komga) -- API keys are revocable and regenerable -- Works with stock Kobo firmware -- Simple user configuration - -#### KOReader Documentation Bug - -**Original Documentation:** Claimed KOReader uses "Basic Auth" with username/password - -**Reality:** -- KOReader sync sends `Authorization: Bearer {token}` to sync servers -- The official KOReader sync protocol uses MD5 hashed passwords -- Bookhoard's implementation expects Bearer tokens in `DeviceAuthMiddleware` -- KOReader cannot send custom headers through native settings - -**Solution:** Create KOReader plugin that can send proper Bearer tokens - -#### Booklore Analysis - -**Investigation:** Analyzed how Booklore handles Kobo authentication - -**Booklore Approach:** -- Token embedded in URL path: `/api/kobo/{token}/...` -- Works but token appears in logs -- One token per user (not per device) -- OPDS uses separate Basic Auth - -**Decision:** Don't copy Booklore - less secure. Use device-specific auth with proper tokens for KOReader. - -#### Plugin Feasibility Research - -**Concern:** Is KOReader a "moving target" for plugin development? - -**Findings:** -- UI plugins are unstable (Issue #13942) - frequent breaking changes -- Backend/sync plugins (like Wallabag) are stable -- Wallabag2 plugin has minimal updates over years -- HTTP client APIs don't change often - -**Decision:** Proceed with plugin - sync plugins are low-risk - -### Technical Constraints Discovered - -1. **Kobo Firmware Limitations:** - - Cannot send custom HTTP headers - - Cannot install plugins - - Settings in `eReader.conf` are limited to specific keys - - Solution: API key in URL path works with stock firmware - -2. **KOReader Capabilities:** - - Can install plugins (copy files to `/koreader/plugins/`) - - Lua-based plugin architecture - - Can send custom HTTP requests - - Plugin can register itself - - Native OPDS client support - -3. **Security Trade-offs:** - - API Key in URl: Which means it's stored in logs - - Bearer tokens: Cryptographically secure, requires setup - - User accepted security/usability trade-off for self-hosted use - -### Why Functionality Trumps Security - -**User Quote:** "I want a Kindle system replacement, otherwise I could just use booklore or KOReader. My biggest concern is do any of these limit any of the syncing/downloading implementation I already have. Functionality and features are more important than anything else. Although security does come in at a close second." - -**Translation:** -- Self-hosted = trusted network environment -- User understands and accepts risks -- Willing to document security implications -- Prioritizes working features over perfect security - -**Resulting Architecture:** -- Kobo: API key in URL path (revocable, simple) -- KOReader: Bearer token (high security, requires plugin setup) -- Both get full feature parity -- Security implications clearly documented - ---- - -## 11. Updated Implementation Timeline - -### Parallel Track Structure - -**Track A: Kobo Integration** -- Phase 1: Enhanced Auth (Week 1) -- Phase 2a: Kobo Implementation (Week 1-2) -- Phase 4: Kobo Testing (Week 3) - -**Track B: KOReader Plugin** -- Phase 1: Enhanced Auth (Week 1) [shared] -- Phase 2b: Plugin Development (Week 2-4) -- Phase 4: Plugin Testing (Week 4) - -**Track C: OPDS Security** -- Phase 3: OPDS Integration (Week 2-3) - -**Track D: Documentation** -- Phase 5: Final Docs & Release (Week 4) - -### Milestones - -**Milestone 1 (End of Week 2):** Kobo Fully Functional -- Kobo: API key in URL (works with stock firmware) -- All sync features operational -- OPDS access secured -- Can ship to Kobo users - -**Milestone 2 (End of Week 4):** KOReader Plugin Ready -- Plugin released in separate repo -- Full feature parity with Kobo -- Documentation complete -- Production release - ---- - -## 12. Files & Components Reference - -### Critical Files for Implementation - -**Authentication Core:** -- `internal/middleware/device_auth.go:37-108` - Main auth middleware -- `internal/database/queries/queries.sql` - Device queries -- `internal/database/models.go` - Device model (has device_identifier) - -**Routing:** -- `internal/router/opds.go:11-12` - OPDS routes (need middleware) -- `internal/router/sync.go:17-50` - Sync routes -- `internal/router/device.go` - Device management routes - -**Handlers:** -- `internal/handlers/kobo.go:26-679` - Kobo sync implementation -- `internal/handlers/koreader.go:23-897` - KOReader sync implementation -- `internal/handlers/opds.go` - OPDS handler - -**Tests:** -- `cmd/server/tests/kobo_test.go` - Kobo tests -- `cmd/server/tests/opds_test.go` - OPDS tests -- `cmd/server/tests/test_helpers.go` - Test utilities - -**Documentation (to update):** -- `docs/user/devices/kobo-setup.md` - Currently has wrong info -- `docs/user/devices/koreader-setup.md` - Needs Bearer token info - -**Sidecar (reference only, not implementing):** -- `internal/handlers/sidecar.go` - Exists but not wired up -- Routes not registered in router - ---- - -## 13. Next Steps - -### Immediate Actions (When Ready to Proceed) - -1. **Review this plan** - Ensure all decisions and context are captured - - **NEW**: Review updated clarification on two-field approach (device_identifier vs auth_token) - - **NEW**: Confirm understanding that Kobo requires manual serial entry (device_identifier) - - **NEW**: Confirm understanding that auth_token (API key) is auto-generated for both Kobo and KOReader -2. **Create KOReader plugin repo** - Set up `github.com/bookhoard/koreader-plugin` -3. **Begin Phase 1** - Enhanced DeviceAuthMiddleware -4. **Parallel development** - Kobo and KOReader tracks - -### Success Criteria (Reiterated) - -**Functional:** -- โœ… Kobo: Enter serial โ†’ register โ†’ sync works immediately -- โœ… KOReader: Install plugin โ†’ plugin registers โ†’ sync works -- โœ… Both: Full sync (progress, highlights, notes, bookmarks) -- โœ… OPDS: Authenticated access on both platforms -- โœ… Cross-device: Read on one, continue on another - -**Security:** -- โœ… OPDS no longer publicly accessible (authenticated with API key or Bearer token) -- โœ… All sync endpoints require device authentication (API key or bearer token) -- โœ… Device authentication uses revocable API keys (Kobo/KOReader) -- โœ… Security implications documented -- โœ… Network security recommendations provided - -**User Experience:** -- โœ… Kobo setup: < 5 minutes -- โœ… KOReader setup: < 10 minutes -- โœ… Clear documentation -- โœ… Working examples - ---- - -**Plan Status:** Ready for implementation -**Last Updated:** Based on conversation ending with user decisions -**Note:** User indicated it's late and not proceeding tonight - -**Ready to implement when you are. This plan captures all our discussion and decisions.** - ---- - -## 16. Documentation Updates Required (Pre-Implementation Checklist) - -Based on codebase analysis, the following documentation updates are needed: - -### koreader-setup.md (docs/user/devices/koreader-setup.md) -**Current Issues:** -- Line 126: Says "Basic Auth" - should be "Bearer Token" -- Lines 127-128: Reference username/password - should reference auth_token -- Documentation states KOReader uses Basic Auth (incorrect) - -**Required Changes:** -```diff -- 1. **Authentication Method**: Select "Basic Auth" -- 2. **Username**: Your Bookhoard email or username -- 3. **Password**: Your Bookhoard password -+ 1. **Authentication Method**: Bearer Token (API Key) -+ 2. **Auth Token**: Copy from Bookhoard Device Management page -+ 3. **Setup**: Plugin will include token in Authorization header automatically -``` - -### kobo-setup.md (docs/user/devices/kobo-setup.md) -**Current Status:** -- Updated on 2026-02-12 to reflect API key authentication -- Shows full URL format with token - -**Verification Needed:** -- Confirm lines 37-53 correctly show TWO-FIELD approach: - 1. **device_identifier**: Serial number (user enters manually - identifies WHICH device) - 2. **auth_token**: API key (auto-generated - authenticates API requests) -- Verify registration flow explains: - - User enters serial number as device_identifier (step 1: "Find Your Kobo Serial Number") - - System generates auth_token (API key) after registration - - User copies full sync URL with auth_token (not device_identifier) -- Ensure "Copy Full Sync URL" button displays auth_token (API key), not device_identifier (serial number) -- Clarify in documentation that serial number is ONLY for device identity during registration -- Auth token (API key) is what user copies for Kobo configuration - -**IMPORTANT: Two-Field Distinction for Kobo Devices** - -The Kobo setup uses TWO separate database fields that serve different purposes: - -1. **`device_identifier`** (Device Identity - User Entered) - - **Purpose**: Identify WHICH physical device this is - - **User Action**: Manually enter Kobo serial number (e.g., "N1234567890123") - - **Storage**: VARCHAR(255) UNIQUE NOT NULL - - **Persistence**: Remains constant for device lifetime - - **Example**: "N1234567890123" - - **Used For**: Preventing duplicate registrations, tracking device identity - -2. **`auth_token`** (API Key - System Generated) - - **Purpose**: Authenticate API requests from this device - - **User Action**: Auto-generated by backend during registration, copied for configuration - - **Storage**: VARCHAR(255) UNIQUE NOT NULL - - **Persistence**: Can be revoked and regenerated without changing device_identifier - - **Example**: "dev_550e8400-e29b-41d4-a716-446655440000" - - **Used For**: Kobo sync URL, OPDS access, authentication - -**Why Two Fields?** -- **Security**: `auth_token` can be revoked/regenerated if compromised -- **Flexibility**: Same physical device can get new tokens without re-registration -- **Tracking**: `device_identifier` persists across token regenerations -- **User Experience**: Register once with serial, regenerate tokens as needed - -**What This Means for Documentation:** -- โœ… Step 1 (lines 37-43): Should show user finding/entering serial number (CORRECT) -- โœ… Step 2 (lines 45-72): Should show serial entered in device_identifier field (CORRECT) -- โœ… Configuration (lines 102-149): Should use auth_token in sync URL (NOT serial number) -- โœ… Sync URL format: `http://IP:8765/api/sync/kobo/{auth_token}` (serial not used) - -### New Documentation: security.md -**Required Content:** -- API key in URL path security considerations -- Comparison: API key (URL) vs Bearer token (header) -- Network security recommendations (HTTPS, VPN, local-only) -- Token regeneration best practices -- Risk mitigations for self-hosted deployments - ---- - -## 10. Risk Assessment - -**Low Risk:** -- API key in URL path (well-understood pattern) -- OPDS security (straightforward middleware application) -- Kobo sync (already implemented, just fixing auth route) - -**Medium Risk:** -- KOReader plugin development (Lua learning curve, potential API changes) -- Cross-device sync conflicts (need clear resolution strategy) - -**High Risk:** -- KOReader API instability (if they change plugin APIs frequently) -- User adoption (may resist plugin installation) - -**Mitigations:** -- Start with Kobo to prove concept -- Simple plugin architecture (minimize breaking changes impact) -- Excellent documentation to reduce friction -- Community feedback loop - ---- - -## 11. Timeline Summary - -| Phase | Duration | Key Deliverable | -|-------|----------|----------------| -| Phase 1 | Week 1 | Enhanced auth middleware working | -| Phase 2 | Week 1-2 | Kobo fully functional | -| Phase 3 | Week 2-4 | KOReader plugin complete | -| Phase 4 | Week 2-3 | OPDS secured | -| Phase 5 | Week 4 | Testing & docs complete | - -**Total Duration: 4 weeks** - ---- - -**Next Steps:** -1. User reviews and approves plan -2. Answer open questions (Section 9) -3. Begin Phase 1 implementation -4. Weekly check-ins on progress - -**Ready to proceed?** - ---- - -## 14. Authentication Strategy Confirmation - -### 14.1 Kobo Authentication: API Key in URL Path - -**Decision:** Use per-device API keys embedded in URL path (proven by Komga) - -**Architecture:** -- Kobo route: `/api/sync/kobo/{api_key}/*` -- API key generated during device registration -- User copies full URL from device management page -- Paste into Kobo's `api_endpoint` configuration - -**Why This Approach:** -- โœ… Proven by Komga (production-tested, works reliably) -- โœ… No jailbreak needed - works with stock Kobo firmware -- โœ… API keys are revocable and can be regenerated -- โœ… Uses existing `auth_token` field (no schema change) -- โœ… Simple user configuration (one line in config file) -- โœ… More secure than serial-based authentication - -**Rejection: Serial-based via X-Kobo-Device Header** -- Serials are predictable and cannot be revoked -- More complex implementation (JSON parsing, device_identifier lookup) -- Less secure than random API keys -- No benefit over URL path approach - ---- - -### 14.2 Router Configuration - -**Required Change:** -```go -// Current routing -koboSync := e.Group("/api/sync/kobo") -koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(...)) - -// New routing (supports API key in URL path) -koboSync := e.Group("/api/sync/kobo/:token") -koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(...)) -``` - -**Middleware Enhancement:** -```go -// Add path parameter extraction (supports both Bearer token and URL token) -func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc { - // 1. Try Bearer token header (KOReader, API clients) - authHeader := c.Request().Header.Get("Authorization") - if authHeader != "" { - token := strings.TrimPrefix(authHeader, "Bearer ") - device, err := m.db.GetDeviceByAuthToken(c.Request().Context(), token) - if err == nil { - return m.setDeviceContext(c, device) - } - } - - // 2. Try URL path parameter (Kobo, OPDS) - urlToken := c.Param("token") - if urlToken == "" { - urlToken = c.QueryParam("token") // Fallback to query param - } - if urlToken != "" { - device, err := m.db.GetDeviceByAuthToken(c.Request().Context(), urlToken) - if err == nil { - return m.setDeviceContext(c, device) - } - } - - return c.JSON(401, map[string]string{"error": "authentication required"}) -} -``` - -**Frontend:** -- Display `auth_token` for each device (API key for Kobo, token for KOReader) -- "Copy Device ID" button for KOReader setup -- "Regenerate Token" doesn't change auth_token - ---- - -### 14.3 Token Regeneration - -**Required Addition:** Backend endpoint to regenerate tokens - -**Implementation:** -```go -// internal/handlers/devices.go -func (h *Handlers) RegenerateDeviceToken(c echo.Context) error { - deviceID, err := uuid.Parse(c.Param("id")) - // ... user auth check ... - - newToken := fmt.Sprintf("dev_%s", uuid.New().String()) - - // Use new query (to be created in queries.sql) - _, err = h.db.UpdateDeviceAuthToken(c.Request().Context(), database.UpdateDeviceAuthTokenParams{ - ID: pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}, - AuthToken: pgtype.Text{String: newToken, Valid: true}, - }) - - return c.JSON(200, map[string]string{ - "auth_token": newToken, - "message": "Token regenerated successfully", - }) -} -``` - -**Route:** -```go -// internal/router/device.go -devices.PUT("/:id/regenerate-token", jwtMiddleware, h.RegenerateDeviceToken) -``` - -**Frontend:** -- "Regenerate Token" button on device details -- Confirmation dialog -- One-click copy to clipboard -- Update Kobo config example with new token - -**Status**: โœ… New feature needed - ---- - -### 14.4 Enhanced Security Documentation - -**Add to `docs/user/security.md`** (new file): - -## API Key Authentication Security Considerations - -### Risks - -- **URL Exposure**: API keys visible in Kobo logs and server logs -- **Config File Storage**: API key appears in Kobo config file (plain text) -- **Network Interception**: On public networks, API key in URL could be intercepted -- **Device Theft**: Physical access to device grants access until token is regenerated - -### Recommended Mitigations - -1. **Network Security** (REQUIRED for self-hosted app) - - Use HTTPS for sync (reverse proxy with SSL/TLS) - - If you expose Bookhoard publicly: Use reverse proxy with SSL/TLS termination - - Ensure API keys are transmitted over encrypted connection - - Keep firewall rules restricting access by IP (fail2ban for repeated failed auth attempts) - -2. **Token Management** - - Regenerate API keys if compromised or lost - - One-click regeneration in device management UI - - Automatic expiration and rotation (optional, for high-security deployments) - -3. **Security Documentation** - - Document that API keys are sensitive (like passwords) - - Recommend local network only (home or VPN) - - For remote access, use reverse proxy with valid SSL certificates - - Provide clear warnings in UI when API keys are displayed or copied - -# Kobo OPDS -http://IP:8765/opds/devices/{DEVICE_ID}/catalog?token={API_KEY} - -# KOReader OPDS (via plugin - uses header automatically) -http://IP:8765/opds/devices/{DEVICE_ID}/catalog -``` - -**Phase 1 Tasks:** - -1. **[CORE]** Implement enhanced DeviceAuthMiddleware with multi-method auth: - - Try Bearer token from Authorization header (KOReader, API clients) - - Try URL path parameter: `c.Param("token")` (Kobo sync) - - Try query parameter: `c.QueryParam("token")` (OPDS access) - - All methods lookup device via `GetDeviceByAuthToken` - - Set device context on successful auth - -2. **[CORE]** Update Kobo routing to use path parameter: - - Change route from `/api/sync/kobo` to `/api/sync/kobo/:token` - - All Kobo sync endpoints inherit token from path - - Update handler to use path parameter - -3. **[CORE]** Add token regeneration endpoint: - - Create new SQL query: `UpdateDeviceAuthToken(id, auth_token)` (NOT UpdateDevice - it doesn't modify auth_token) - - Add handler: `POST /api/devices/:id/regenerate-token` - - Generate new API key, update database - - Return new token to user - -4. **[CRITICAL]** Update kobo-setup.md documentation: - - Remove Username/Password references (if any remain) - - Verify registration flow reflects API key generation: - - Device registration automatically generates `auth_token` - - User copies full sync URL from device management page (not just token) - - Example: `http://IP:8765/api/sync/kobo/{API_KEY}` - - Explain API key in URL configuration - - Show full URL with token: `http://IP:8765/api/sync/kobo/{API_KEY}` - - Document token regeneration process - - **Status**: Documentation was updated on 2026-02-12 to reflect API key auth - - **Verification Needed**: Ensure no references to entering serial number remain (lines 37-53) - -5. **[CRITICAL]** Update koreader-setup.md documentation: - - Change "Basic Auth" to "Bearer Token" authentication (line 126) - - Remove incorrect username/password references (lines 127-128) - - Currently: "Username: Your Bookhoard email or username" - - Currently: "Password: Your Bookhoard password" - - Should be: "Auth Token: Your device API key from Bookhoard" - - Explain plugin token management - - **Note**: Current docs mention "Basic Auth" which is INCORRECT - - **Note**: KOReader uses Bearer token in Authorization header (not Basic Auth) - -6. **[TESTING]** Update test coverage: - - Test Kobo sync with API key in URL path - - Test KOReader sync with Bearer token header - - Test OPDS access with both auth methods (path param and query param) - - Test token regeneration invalidates old token - ---- - -## 15. Updated Success Criteria - -**Authentication:** -- โœ… Kobo API key auth working via URL path parameter -- โœ… KOReader Bearer token auth working via Authorization header -- โœ… OPDS accessible via BOTH authentication methods (middleware handles both) -- โœ… Token regeneration endpoint functional -- โœ… Security implications documented with mitigation strategies - -**Database:** -- โœ… `auth_token` field used for all devices (no schema change) -- โœ… Token regeneration updates existing field -- โœ… `device_identifier` column available for KOReader device linking (not used for auth) - -**Frontend:** -- โœ… Device management UI has copy/regenerate token buttons -- โœ… Kobo config shows API key in URL format -- โœ… Clear setup instructions per device type - -**Documentation:** -- โœ… kobo-setup.md reflects API key authentication -- โœ… koreader-setup.md updated for Bearer token -- โœ… security.md created with token management considerations -- โœ… OPDS authentication clearly documented - ---- - -**Plan Status**: โœ… Corrections Applied - Plan is now accurate and ready for implementation - -**Summary**: Original plan was 95% solid. Added critical clarifications: - -1. **Two-Field Approach for Kobo**: - - `device_identifier` (serial): User enters manually, identifies WHICH device - - `auth_token` (API key): Auto-generated, authenticates API requests - - Users enter serial ONCE during registration, then use auto-generated API key for configuration - -2. **Verification Step Correction**: - - Changed from: "Confirm lines 37-53 don't reference entering serial number" - - Changed to: "Confirm lines 37-53 correctly show two-field approach (serial + generated token)" - - Serial entry is CORRECT and REQUIRED for Kobo - -3. **Core Strategy (Unchanged)**: - - Kobo: API key in URL path (works with stock firmware) - - KOReader: Bearer token in header (via plugin) - - Both: Use revocable `auth_token` for authentication - - OPDS: Support both methods diff --git a/TEST_RELIABILITY_PLAN.md b/TEST_RELIABILITY_PLAN.md deleted file mode 100644 index 8da7f2c..0000000 --- a/TEST_RELIABILITY_PLAN.md +++ /dev/null @@ -1,3388 +0,0 @@ -# Test Reliability Improvement Plan -## Bookhoard Pre-Production Test Overhaul - -**Status**: Planning Phase - DO NOT IMPLEMENT YET -**Date**: 2025-02-13 -**Scope**: Comprehensive test refactoring for production-grade reliability -**Goal**: Transform tests from brittle map-based assertions to robust struct-based validation with full database verification and edge case coverage - ---- - -## ๐ŸŽฏ Executive Summary - -### Current State -- **507+ instances** of `map[string]interface{}` instead of structs -- **Zero concurrency tests** for race conditions -- **Minimal database verification** after mutations -- **Happy-path only** for WebSocket, sync, OPDS, and book matching -- **No null/empty/missing** field edge case coverage - -### Target State -- โœ… **100% struct-based assertions** for API responses (compile-time safety) -- โœ… **100% database verification** for all mutation operations -- โœ… **Comprehensive edge cases**: null/empty/missing, boundaries, Unicode -- โœ… **Concurrency coverage** for all critical sync paths -- โœ… **Error path coverage** for WebSocket, sync, OPDS, matching - -### Impact -- **Silent API contract changes** โ†’ Compile-time detection -- **Data corruption bugs** โ†’ Pre-deployment prevention -- **Race conditions** โ†’ Caught in testing -- **Edge case crashes** โ†’ Eliminated before production - ---- - -## ๐Ÿ“‹ Table of Contents - -1. [Phase 1: Stop Silent Failures](#phase-1-stop-silent-failures) - **HIGHEST ROI** -2. [Phase 2: Concurrency Protection](#phase-2-concurrency-protection) -3. [Phase 3: Hardened Error Handling](#phase-3-hardened-error-handling) -4. [Phase 4: Load and Security](#phase-4-load-and-security) -5. [Implementation Order](#implementation-order) -6. [Verification Checklist](#verification-checklist) - ---- - -## Phase 1: Stop Silent Failures - -**Priority**: ๐Ÿ”ด CRITICAL - **Do this first** -**Time Estimate**: 20-25 files -**Risk Level**: Medium (compilation errors will guide fixes) -**ROI**: Highest - catches API changes and data corruption before production - -### Objectives -1. Replace `map[string]interface{}` with actual structs in all assertions -2. Add database state verification after all mutation operations -3. Add null/empty/missing field edge cases for critical paths - ---- - -### 1.1 Replace Map-Based Assertions with Structs - -#### Problem Areas (507+ instances) - -| File | Lines | Issue | Impact | -|-------|--------|--------|--------| -| `cmd/server/tests/registration_test.go` | 18-305 | Registration requests/responses use maps | Silent field renames | -| `cmd/server/tests/user_test.go` | 14-488 | Profile updates use maps | API contract drift | -| `cmd/server/tests/device_test.go` | 18-529 | Device CRUD uses maps | Missing field validation | -| `cmd/server/tests/collections_bulk_test.go` | 1-200 | Bulk operations use maps | Data corruption undetected | -| `cmd/server/tests/media_bulk_test.go` | 14-100 | Bulk updates use maps | Schema changes missed | -| `cmd/server/tests/conflicts_bulk_test.go` | 1-150 | Conflict resolution uses maps | Resolution failures silent | -| `cmd/server/tests/analytics_test.go` | 1-250 | Analytics queries use maps | Wrong field types undetected | -| `cmd/server/tests/search_test.go` | 21-244 | Search responses use maps | API changes break users | -| `cmd/server/tests/filtering_test.go` | 20-262 | Filter results use maps | Null handling untested | -| `cmd/server/tests/sorting_test.go` | 1-150 | Sort responses use maps | Type errors silent | -| `cmd/server/tests/library_test.go` | 15-378 | Library CRUD uses maps | Missing fields in tests | -| `cmd/server/tests/auth_test.go` | 19-300 | Auth responses use maps | Security issues undetected | - -#### Conversion Pattern - -**BEFORE** (brittle): -```go -// device_test.go:138-141 -var response map[string]interface{} -json.Unmarshal(rec.Body.Bytes(), &response) -devices := response["devices"].([]interface{}) -firstDevice := devices[0].(map[string]interface{}) -deviceName, ok := firstDevice["device_name"].(string) -assert.True(t, ok, "Should have device_name") -assert.Equal(t, "Test Device", deviceName) -// โŒ If "device_name" โ†’ "device_name_display", test passes with empty string -``` - -**AFTER** (safe): -```go -// Import handler types -import "bookhoard/internal/handlers" - -// device_test.go:138-141 (NEW) -var response handlers.DeviceListResponse -err := json.Unmarshal(rec.Body.Bytes(), &response) -require.NoError(t, err, "Response should match DeviceListResponse schema") -require.Greater(t, len(response.Devices), 0, "Should have at least one device") -assert.Equal(t, "Test Device", response.Devices[0].DeviceName) -// โœ… If DeviceName field renamed, COMPILATION FAILS -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/device_test.go` - -**Lines to modify**: -- 18-529: Full file conversion - -**Specific changes**: - -| Line Range | Current | New | -|------------|---------|-----| -| 36-38 | `var regResponse map[string]interface{}` | `var response handlers.DeviceRegistrationResponse` | -| 60-61 | `var statusResponse map[string]interface{}` | `var response handlers.DeviceAuthStatusResponse` | -| 82-85 | `var loginResponse map[string]interface{}` | `var response handlers.LoginResponse` | -| 131-135 | `var response map[string]interface{}` | `var response handlers.DeviceListResponse` | -| 167-173 | `var response map[string]interface{}` | `var response handlers.DeviceUpdateResponse` | -| 342-345 | `var response map[string]interface{}` | `var response handlers.DeviceTokenResponse` | - -**Required imports** (add to top): -```go -import ( - "bookhoard/internal/handlers" // NEW - // ... existing imports -) -``` - -**Action**: Convert all response parsing to use handler structs. Update assertions to access struct fields directly. - ---- - -##### File: `cmd/server/tests/user_test.go` - -**Lines to modify**: -- 14-488: Full file conversion - -**Specific changes**: - -| Line Range | Current | New | -|------------|---------|-----| -| 43-50 | `profile := map[string]interface{}` | Use `handlers.UserInfo` struct | -| 258-260 | `var result map[string]interface{}` | `var response handlers.UpdateResponse` | -| 472-481 | `users := []map[string]interface{}` | `var response handlers.UsersListResponse` | - -**Action**: Import `handlers` package. Convert all user profile and list responses to structs. - ---- - -##### File: `cmd/server/tests/registration_test.go` - -**Lines to modify**: -- 18-305: Full file conversion - -**Specific changes**: - -| Test Function | Lines | Change | -|--------------|--------|--------| -| `TestRegistration_Flow` | 18-85 | Use `handlers.DeviceRegistrationResponse` | -| `TestRegistration_StatusCheck` | 87-120 | Use `handlers.DeviceAuthStatusResponse` | -| `TestRegistration_Approval` | 122-160 | Use `handlers.ApprovalResponse` | -| `TestRegistration_Rejection` | 162-200 | Use `handlers.RejectionResponse` | -| `TestRegistration_QRCode` | 202-240 | Use `handlers.QRCodeResponse` | -| `TestRegistration_Expiry` | 242-280 | Use `handlers.ExpiryResponse` | -| `TestRegistration_Concurrent` | 282-305 | Use all above structs | - -**Action**: Import `handlers` package. Replace all map-based response parsing with struct-based unmarshaling. - ---- - -##### File: `cmd/server/tests/collections_bulk_test.go` - -**Lines to modify**: -- 1-200: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestCollectionsBulk_Add` | 14-50 | `map[string]interface{}` | `handlers.BulkAddResponse` | -| `TestCollectionsBulk_Remove` | 52-100 | `map[string]interface{}` | `handlers.BulkRemoveResponse` | -| `TestCollectionsBulk_Move` | 102-150 | `map[string]interface{}` | `handlers.BulkMoveResponse` | -| `TestCollectionsBulk_Validation` | 152-200 | `map[string]interface{}` | `handlers.ValidationErrorResponse` | - -**Action**: -1. Import `handlers` package -2. Create response structs in `handlers/collections.go` if they don't exist -3. Update all bulk operation tests to use structs - ---- - -##### File: `cmd/server/tests/media_bulk_test.go` - -**Lines to modify**: -- 14-100: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestMediaBulk_Delete` | 14-50 | `map[string]interface{}` | `handlers.BulkDeleteResponse` | -| `TestMediaBulk_Update` | 52-100 | `map[string]interface{}` | `handlers.BulkUpdateResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure `handlers.MediaHandler` has response structs for bulk operations -3. Update all bulk media tests to use structs - ---- - -##### File: `cmd/server/tests/conflicts_bulk_test.go` - -**Lines to modify**: -- 1-150: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestConflictsBulk_Resolve` | 14-50 | `map[string]interface{}` | `handlers.BulkResolveResponse` | -| `TestConflictsBulk_Dismiss` | 52-100 | `map[string]interface{}` | `handlers.BulkDismissResponse` | -| `TestConflictsBulk_Escalate` | 102-150 | `map[string]interface{}` | `handlers.BulkEscalateResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure conflict response structs exist -3. Update all bulk conflict tests to use structs - ---- - -##### File: `cmd/server/tests/analytics_test.go` - -**Lines to modify**: -- 1-250: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestAnalytics_ReadingStats` | 14-50 | `map[string]interface{}` | `handlers.ReadingStatsResponse` | -| `TestAnalytics_PopularBooks` | 52-100 | `map[string]interface{}` | `handlers.PopularBooksResponse` | -| `TestAnalytics_DeviceUsage` | 102-150 | `map[string]interface{}` | `handlers.DeviceUsageResponse` | -| `TestAnalytics_UserActivity` | 152-200 | `map[string]interface{}` | `handlers.UserActivityResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure analytics response structs exist -3. Update all analytics tests to use structs - ---- - -##### File: `cmd/server/tests/search_test.go` - -**Lines to modify**: -- 21-244: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestSearch_Books` | 21-80 | `map[string]interface{}` | `handlers.SearchBooksResponse` | -| `TestSearch_Authors` | 82-140 | `map[string]interface{}` | `handlers.SearchAuthorsResponse` | -| `TestSearch_Series` | 142-200 | `map[string]interface{}` | `handlers.SearchSeriesResponse` | -| `TestSearch_Advanced` | 202-244 | `map[string]interface{}` | `handlers.AdvancedSearchResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure search response structs exist -3. Update all search tests to use structs - ---- - -##### File: `cmd/server/tests/filtering_test.go` - -**Lines to modify**: -- 20-262: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestFiltering_ByStatus` | 20-60 | `map[string]interface{}` | `handlers.FilteredBooksResponse` | -| `TestFiltering_ByCollection` | 62-120 | `map[string]interface{}` | `handlers.CollectionBooksResponse` | -| `TestFiltering_ByDateRange` | 122-180 | `map[string]interface{}` | `handlers.DateRangeResponse` | -| `TestFiltering_Advanced` | 182-262 | `map[string]interface{}` | `handlers.AdvancedFilterResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure filtering response structs exist -3. Update all filtering tests to use structs - ---- - -##### File: `cmd/server/tests/sorting_test.go` - -**Lines to modify**: -- 1-150: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestSorting_ByTitle` | 14-40 | `map[string]interface{}` | `handlers.SortedBooksResponse` | -| `TestSorting_ByAuthor` | 42-80 | `map[string]interface{}` | `handlers.SortedBooksResponse` | -| `TestSorting_ByDateAdded` | 82-120 | `map[string]interface{}` | `handlers.SortedBooksResponse` | -| `TestSorting_ByRating` | 122-150 | `map[string]interface{}` | `handlers.SortedBooksResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure sorting response struct exists -3. Update all sorting tests to use structs - ---- - -##### File: `cmd/server/tests/library_test.go` - -**Lines to modify**: -- 15-378: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestLibraryCreation` | 91-161 | `map[string]interface{}` | `handlers.LibraryResponse` | -| `TestLibraryTypes` | 164-205 | `map[string]interface{}` | `handlers.LibraryTypesResponse` | -| `TestUserVisibleLibraries` | 208-238 | `map[string]interface{}` | `handlers.VisibleLibrariesResponse` | -| `TestMediaItemsList` | 241-269 | `map[string]interface{}` | `handlers.MediaItemsResponse` | -| `TestJSONValidation` | 272-333 | `map[string]interface{}` | `handlers.ValidationResponse` | -| `TestErrorHandling` | 336-377 | `map[string]interface{}` | `handlers.ErrorResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure library response structs exist -3. Update all library tests to use structs - ---- - -##### File: `cmd/server/tests/auth_test.go` - -**Lines to modify**: -- 19-300: Full file conversion - -**Specific changes**: - -| Test | Lines | Current | New | -|-------|--------|---------|-----| -| `TestAuth_Login` | 19-80 | `map[string]interface{}` | `handlers.LoginResponse` | -| `TestAuth_Register` | 82-140 | `map[string]interface{}` | `handlers.RegisterResponse` | -| `TestAuth_Refresh` | 142-200 | `map[string]interface{}` | `handlers.RefreshResponse` | -| `TestAuth_Logout` | 202-250 | `map[string]interface{}` | `handlers.LogoutResponse` | -| `TestAuth_PasswordReset` | 252-300 | `map[string]interface{}` | `handlers.PasswordResetResponse` | - -**Action**: -1. Import `handlers` package -2. Ensure auth response structs exist -3. Update all auth tests to use structs - ---- - -### 1.2 Add Database State Verification - -#### Problem Areas - -| File | Tests Missing DB Verification | Risk | -|------|---------------------------|-------| -| `device_test.go` | `TestUpdateDevice`, `TestDeleteDevice` | API says success but DB unchanged | -| `user_test.go` | All profile update tests | Silent data loss | -| `collections_bulk_test.go` | All bulk operations | Orphaned collection_books records | -| `media_bulk_test.go` | All bulk deletes | Files deleted but DB records remain | -| `conflicts_bulk_test.go` | All conflict resolutions | Conflicts marked resolved but aren't | -| `library_test.go` | Create, update, delete libraries | Schema violations undetected | -| `registration_test.go` | Device approval flow | Device created but not linked to user | -| `sync_integration_test.go` | Sync success but no DB verification | Progress lost | - -#### Verification Pattern - -**BEFORE** (insufficient): -```go -// device_test.go:191 -assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete device") -// โŒ No DB query - device might still exist -``` - -**AFTER** (complete): -```go -// device_test.go:191 (NEW) -assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete device") - -// NEW: Verify device actually deleted from database -pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true} -_, err := setup.DB.GetDevice(context.Background(), pgDeviceID) -assert.Error(t, err, "Device should be deleted from database") -// โœ… Now we know delete actually worked -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/device_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestUpdateDevice` | 144-176 | Checks HTTP 200 only | Query DB to verify name, sync_enabled, sync_frequency actually updated | -| `TestDeleteDevice` | 178-197 | Already has DB check | โœ… Already good - keep as example | -| `TestRegenerateDeviceToken_Success` | 326-362 | Checks HTTP 200 only | Query DB to verify auth_token actually changed | -| `TestRegenerateDeviceToken_OldTokenInvalidated` | 364-407 | Checks token works only | Query DB to verify only one valid token exists | -| `TestDeviceRegistrationFlow` | 18-114 | Checks response codes | Query DB after step 4 to verify device exists and is linked to user | - -**Action**: -After each successful mutation, add DB query to verify state change: - -```go -// Example for TestUpdateDevice (after line 175) -// NEW: Verify database state -updatedDevice, err := setup.DB.GetDevice(context.Background(), pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true}) -require.NoError(t, err, "Should retrieve updated device") -assert.Equal(t, "Updated Device Name", updatedDevice.DeviceName) -assert.Equal(t, false, updatedDevice.SyncEnabled.Bool) -assert.Equal(t, int32(10), updatedDevice.SyncFrequencyMinutes.Int32) -``` - ---- - -##### File: `cmd/server/tests/user_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestUserProfileEndpoints` | 15-77 | Checks HTTP 200 only | Query DB after PUT to verify first_name, last_name updated | -| `TestUserUpdateEndpoints` | 80-336 | All checks HTTP codes only | Query DB after each successful update (email, username, password, theme) | -| `TestAccountDeletion` | 340-440 | Checks HTTP codes only | Query DB after delete to verify user record removed | - -**Action**: -Add DB verification queries after successful updates: - -```go -// Example for email update (after line 112) -// NEW: Verify database state -updatedUser, err := setup.DB.GetUserByEmail(context.Background(), "newemail@example.com") -require.NoError(t, err, "Should find user with new email") -assert.Equal(t, "newemail@example.com", updatedUser.Email) -``` - ---- - -##### File: `cmd/server/tests/collections_bulk_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestCollectionsBulk_Add` | All | Checks response only | Query collection_items table to verify all book IDs added | -| `TestCollectionsBulk_Remove` | All | Checks response only | Query collection_items table to verify all book IDs removed | -| `TestCollectionsBulk_Move` | All | Checks response only | Query collection_items table to verify books moved to target collection | -| `TestCollectionsBulk_DuplicateHandling` | All | Checks response only | Query collection_items to verify no duplicate entries created | - -**Action**: -After bulk operations, query junction table: - -```go -// Example for TestCollectionsBulk_Add (after assertion) -// NEW: Verify database state -collectionItems, err := setup.DB.GetCollectionItems(context.Background(), collectionID) -require.NoError(t, err, "Should retrieve collection items") -require.Equal(t, len(bookIDs), len(collectionItems), "All books should be in collection") - -// Verify each book ID exists -actualIDs := make([]uuid.UUID, len(collectionItems)) -for i, item := range collectionItems { - actualIDs[i] = item.MediaItemID -} -assert.ElementsMatch(t, bookIDs, actualIDs, "All book IDs should match") -``` - ---- - -##### File: `cmd/server/tests/media_bulk_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestMediaBulk_Delete` | 80-100 | Checks response only | Query media_items table to verify records deleted (not just file_deleted flag) | -| `TestMediaBulk_Update` | All (if exists) | Checks response only | Query media_items table to verify all fields actually updated | - -**Action**: -Add DB verification after bulk deletes: - -```go -// Example for TestMediaBulk_Delete (after line 99) -// NEW: Verify database state -for _, mediaID := range mediaIDs { - pgID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - _, err := setup.DB.GetMediaItem(context.Background(), pgID) - assert.Error(t, err, "Media item should be deleted from database") -} -``` - ---- - -##### File: `cmd/server/tests/conflicts_bulk_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestConflictsBulk_Resolve` | All | Checks response only | Query sync_conflicts table to verify resolution_status updated | -| `TestConflictsBulk_Dismiss` | All | Checks response only | Query sync_conflicts table to verify dismissed status | -| `TestConflictsBulk_Escalate` | All | Checks response only | Query sync_conflicts table to verify escalated to admin | - -**Action**: -Add DB verification after conflict operations: - -```go -// Example for TestConflictsBulk_Resolve (after assertion) -// NEW: Verify database state -for _, conflictID := range conflictIDs { - pgID := pgtype.UUID{Bytes: [16]byte(conflictID), Valid: true} - conflict, err := setup.DB.GetSyncConflict(context.Background(), pgID) - require.NoError(t, err, "Should retrieve conflict") - assert.Equal(t, "resolved", conflict.ResolutionStatus.String) -} -``` - ---- - -##### File: `cmd/server/tests/sync_integration_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestSyncFull_Initial` | All | Checks response only | Query reading_progress, media_notes, media_highlights tables to verify sync data saved | -| `TestSyncFull_Incremental` | All | Checks response only | Query DB to verify only changed fields updated | -| `TestSyncConflict_Resolution` | All | Checks response only | Query sync_conflicts table to verify conflict created and resolved | - -**Action**: -Add comprehensive DB verification after sync: - -```go -// Example for TestSyncFull_Initial (after success assertion) -// NEW: Verify database state -progress, err := setup.DB.GetReadingProgress(context.Background(), database.GetReadingProgressParams{ - MediaItemID: pgtype.UUID{Bytes: [16]byte(mediaItemID), Valid: true}, - UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true}, -}) -require.NoError(t, err, "Should retrieve reading progress") -assert.Equal(t, testProgress.Percentage, progress.Percentage.Float64) -``` - ---- - -##### File: `cmd/server/tests/registration_test.go` - -**Tests needing DB verification**: - -| Test Function | Lines | Current State | Required Addition | -|--------------|--------|---------------|-------------------| -| `TestRegistrationFlow` | 18-114 | Checks responses only | After approval, query devices table to verify device exists and linked to user | -| `TestRegistrationApproval` | All | Checks response only | Query devices table to verify auth_token set and device active | - -**Action**: -Add DB verification after registration approval: - -```go -// Example for TestRegistrationFlow (after line 93) -// NEW: Verify database state -pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} -device, err := setup.DB.GetDevice(context.Background(), pgDeviceID) -require.NoError(t, err, "Device should exist in database") -assert.NotEmpty(t, device.AuthToken, "Device should have auth token after approval") -``` - ---- - -### 1.3 Add Null/Empty/Missing Edge Cases - -#### Problem Areas - -| Field Type | Missing Tests | Production Risk | -|-----------|---------------|-----------------| -| **Text fields** (`first_name`, `last_name`, `description`) | `null` vs `""` vs missing | Constraint violations, different behavior | -| **Optional arrays** (`tags`, `contributors`) | `null` vs `[]` vs missing | Search failures | -| **Optional dates** (`date_published`) | `null` vs invalid vs missing | Display corruption | -| **Optional IDs** (`asin`, `isbn`) | `null` vs `""` vs missing | External API failures | -| **Optional enums** (`reading_status`) | `null` vs invalid vs missing | Filter breaks | - -#### Test Patterns to Add - -##### Pattern 1: Null vs Empty String vs Missing - -```go -// NEW TEST: Add to user_test.go -func TestUserProfile_NullVsEmptyVsMissing(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - testCases := []struct { - name string - payload map[string]interface{} - expectOK bool - }{ - { - name: "Null first_name", - payload: map[string]interface{}{ - "first_name": nil, - }, - expectOK: true, // null should be allowed (optional field) - }, - { - name: "Empty string first_name", - payload: map[string]interface{}{ - "first_name": "", - }, - expectOK: true, // empty string should be allowed - }, - { - name: "Missing first_name", - payload: map[string]interface{}{ - // first_name not included - }, - expectOK: true, // missing should be allowed (optional) - }, - { - name: "Valid first_name", - payload: map[string]interface{}{ - "first_name": "John", - }, - expectOK: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - jsonData, _ := json.Marshal(tc.payload) - req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - rec := httptest.NewRecorder() - setup.Server.Config.Handler.ServeHTTP(rec, req) - - if tc.expectOK { - assert.Equal(t, http.StatusOK, rec.Code) - } else { - assert.NotEqual(t, http.StatusOK, rec.Code) - } - }) - } -} -``` - -##### Pattern 2: Null vs Empty Array vs Missing - -```go -// NEW TEST: Add to media_bulk_test.go -func TestMediaTags_NullVsEmptyVsMissing(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - mediaID := createTestMediaItemID(t, setup.Server, token) - - testCases := []struct { - name string - tags interface{} - expectOK bool - }{ - { - name: "Null tags", - tags: nil, - expectOK: true, // null should clear tags - }, - { - name: "Empty array tags", - tags: []string{}, - expectOK: true, // empty array should clear tags - }, - { - name: "Missing tags", - // tags field not included - expectOK: true, // missing should not change tags - }, - { - name: "Valid tags", - tags: []string{"fiction", "science-fiction"}, - expectOK: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload := map[string]interface{}{"tags": tc.tags} - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", fmt.Sprintf("/api/media-items/%s", mediaID), bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - rec := httptest.NewRecorder() - setup.Server.Config.Handler.ServeHTTP(rec, req) - - if tc.expectOK { - assert.Equal(t, http.StatusOK, rec.Code) - - // NEW: Verify database state - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - media, err := setup.DB.GetMediaItem(context.Background(), pgMediaID) - require.NoError(t, err) - - if tc.tags == nil { - assert.Empty(t, media.Tags, "Tags should be null/empty") - } else if tags, ok := tc.tags.([]string); ok && len(tags) == 0 { - assert.Empty(t, media.Tags, "Tags should be empty") - } else if tags, ok := tc.tags.([]string); ok { - assert.ElementsMatch(t, tags, media.Tags, "Tags should match") - } - } - }) - } -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/user_test.go` - -**Add new test function after line 336**: - -```go -// NEW TEST FUNCTION -func TestUserProfile_NullVsEmptyVsMissing(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - testCases := []struct { - name string - field string - value interface{} - expectOK bool - }{ - // First name tests - {"Null first_name", "first_name", nil, true}, - {"Empty first_name", "first_name", "", true}, - {"Missing first_name", "first_name", nil, true}, // Not included in payload - {"Valid first_name", "first_name", "Jane", true}, - - // Last name tests - {"Null last_name", "last_name", nil, true}, - {"Empty last_name", "last_name", "", true}, - {"Valid last_name", "last_name", "Doe", true}, - - // Email tests (required field, different expectations) - {"Null email", "email", nil, false}, // Email required, null should fail - {"Empty email", "email", "", false}, // Email required, empty should fail - {"Valid email", "email", "new@example.com", true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload := map[string]interface{}{tc.field: tc.value} - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - rec := httptest.NewRecorder() - setup.Server.Config.Handler.ServeHTTP(rec, req) - - if tc.expectOK { - assert.Equal(t, http.StatusOK, rec.Code, tc.name) - } else { - assert.NotEqual(t, http.StatusOK, rec.Code, tc.name) - } - }) - } -} -``` - -**Action**: Add new test file or append to existing `user_test.go` after line 336. - ---- - -##### File: `cmd/server/tests/media_bulk_test.go` - -**Add new test function**: - -```go -// NEW TEST FUNCTION -func TestMediaItem_NullVsEmptyVsMissing(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - mediaID := createTestMediaItemID(t, setup.Server, token) - - testCases := []struct { - name string - field string - value interface{} - expectOK bool - verifyDB bool - }{ - // Tags tests (array field) - {"Null tags", "tags", nil, true, true}, - {"Empty array tags", "tags", []string{}, true, true}, - {"Valid tags", "tags", []string{"fiction", "sci-fi"}, true, true}, - - // ISBN tests (optional text) - {"Null ISBN", "isbn", nil, true, true}, - {"Empty ISBN", "isbn", "", true, true}, - {"Valid ISBN", "isbn", "978-0-123456-78-9", true, true}, - - // ASIN tests (optional text) - {"Null ASIN", "asin", nil, true, true}, - {"Empty ASIN", "asin", "", true, true}, - {"Valid ASIN", "asin", "B08XXXXXXX", true, true}, - - // Date published tests (optional date) - {"Null date_published", "date_published", nil, true, true}, - {"Invalid date_published", "date_published", "not-a-date", false, false}, - {"Valid date_published", "date_published", "2024-01-15", true, true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload := map[string]interface{}{tc.field: tc.value} - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", fmt.Sprintf("/api/media-items/%s", mediaID), bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - rec := httptest.NewRecorder() - setup.Server.Config.Handler.ServeHTTP(rec, req) - - if tc.expectOK { - assert.Equal(t, http.StatusOK, rec.Code, tc.name) - - // NEW: Verify database state - if tc.verifyDB { - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - media, err := setup.DB.GetMediaItem(context.Background(), pgMediaID) - require.NoError(t, err, "Should retrieve media item") - - // Verify field value matches expected - switch tc.field { - case "tags": - if tc.value == nil { - assert.Empty(t, media.Tags, "Tags should be null/empty") - } else if tags, ok := tc.value.([]string); ok { - assert.ElementsMatch(t, tags, media.Tags, "Tags should match") - } - case "isbn": - if tc.value == nil || tc.value == "" { - assert.False(t, media.Isbn.Valid, "ISBN should be null") - } else { - assert.True(t, media.Isbn.Valid, "ISBN should be set") - assert.Equal(t, tc.value.(string), media.Isbn.String) - } - case "asin": - if tc.value == nil || tc.value == "" { - assert.False(t, media.Asin.Valid, "ASIN should be null") - } else { - assert.True(t, media.Asin.Valid, "ASIN should be set") - assert.Equal(t, tc.value.(string), media.Asin.String) - } - } - } - } else { - assert.NotEqual(t, http.StatusOK, rec.Code, tc.name) - } - }) - } -} -``` - -**Action**: Add new test function to `media_bulk_test.go`. - ---- - -##### File: `cmd/server/tests/library_test.go` - -**Add new test function**: - -```go -// NEW TEST FUNCTION -func TestLibrary_NullVsEmptyVsMissing(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - userID := getTestUserID(t, setup.DB) - - // Create library first - libReq := map[string]interface{}{ - "name": "Test Library", - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode) - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - testCases := []struct { - name string - field string - value interface{} - expectOK bool - }{ - {"Null description", "description", nil, true}, - {"Empty description", "description", "", true}, - {"Valid description", "description", "A test library", true}, - - {"Null name", "name", nil, false}, // Name required - {"Empty name", "name", "", false}, // Name required - {"Valid name", "name", "Updated Library", true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload := map[string]interface{}{tc.field: tc.value} - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", fmt.Sprintf("%s/api/libraries/%s", setup.Server.URL, libraryID), bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - if tc.expectOK { - assert.Equal(t, http.StatusOK, resp.StatusCode, tc.name) - } else { - assert.NotEqual(t, http.StatusOK, resp.StatusCode, tc.name) - } - }) - } -} -``` - -**Action**: Add new test function to `library_test.go`. - ---- - -### Phase 1 Summary - -**Files Modified**: 12+ test files -**Lines Changed**: ~2000+ lines -**New Test Functions**: 15+ -**Compilation Checks**: 100% (struct-based assertions) -**Database Verification**: 100% of mutations - -**Verification Steps**: -1. โœ… All tests compile with struct-based assertions -2. โœ… All mutation operations verify database state -3. โœ… Null/empty/missing edge cases covered for critical fields - ---- - -## Phase 2: Concurrency Protection - -**Priority**: ๐ŸŸ  HIGH - **Do after Phase 1** -**Time Estimate**: 8-10 new test functions -**Risk Level**: Low (new tests only) -**ROI**: High - prevents lost updates and deadlocks - -### Objectives -1. Add race condition tests for all sync operations -2. Add concurrent bulk operation tests -3. Add concurrent profile + device update tests - ---- - -### 2.1 Sync Operation Race Conditions - -#### Problem Areas - -| Operation | Missing Tests | Risk | -|-----------|---------------|-------| -| **Reading progress sync** | Multiple devices updating same book simultaneously | Lost updates, last-write-wins | -| **Conflict resolution** | Two users resolving same conflict | Duplicate resolution records | -| **Bulk operations** | Bulk delete + individual update simultaneously | Orphaned records | -| **Device registration** | Multiple devices registering simultaneously | Duplicate device IDs | -| **Token regeneration** | Regenerate token while old token in use | Auth failures | - -#### Test Pattern - -```go -// NEW TEST TEMPLATE: Concurrent sync -func TestSyncConcurrent_ProgressUpdates(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-concurrent") - mediaID := createTestMediaItemID(t, setup.Server, token) - - // Start with initial progress - initialProgress := map[string]interface{}{ - "media_item_id": mediaID, - "percentage": 25.0, - "current_page": 50, - "total_pages": 200, - } - - // Simulate 3 concurrent updates from different "sources" - var wg sync.WaitGroup - errors := make(chan error, 3) - - for i := 0; i < 3; i++ { - wg.Add(1) - go func(updateNum int) { - defer wg.Done() - - progressData := initialProgress - progressData["percentage"] = 25.0 + float64(updateNum * 10) - progressData["current_page"] = 50 + (updateNum * 20) - - jsonData, _ := json.Marshal(progressData) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/sync/koreader/progress", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+device.AuthToken) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - errors <- err - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusConflict { - errors <- fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - }(i) - } - - wg.Wait() - close(errors) - - // Check for errors (conflicts are OK, but not failures) - for err := range errors { - t.Logf("Concurrent update error: %v", err) - // We expect either success or conflict, not failures - } - - // NEW: Verify final state is consistent - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - pgUserID := pgtype.UUID{Bytes: [16]byte(setup.User.ID), Valid: true} - progress, err := setup.DB.GetReadingProgress(context.Background(), database.GetReadingProgressParams{ - MediaItemID: pgMediaID, - UserID: pgUserID, - }) - require.NoError(t, err, "Should retrieve final progress") - - // Verify one of the updates won (no corruption) - assert.GreaterOrEqual(t, progress.Percentage.Float64, 25.0) - assert.LessOrEqual(t, progress.Percentage.Float64, 55.0) - assert.True(t, progress.Percentage.Float64 == 25.0 || - progress.Percentage.Float64 == 35.0 || - progress.Percentage.Float64 == 45.0, - "Final progress should be one of the concurrent updates") -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/sync_integration_test.go` - -**Add new test functions**: - -1. **TestSyncConcurrent_ProgressUpdates** (after existing sync tests) - - Lines: Add ~80 lines - - Purpose: Multiple devices updating same book's progress - - Verification: Final progress is one of the updates (no corruption) - -2. **TestSyncConcurrent_NotesUpdates** (after progress test) - - Lines: Add ~80 lines - - Purpose: Multiple devices adding notes to same book - - Verification: All notes saved, no duplicates - -3. **TestSyncConcurrent_HighlightsUpdates** (after notes test) - - Lines: Add ~80 lines - - Purpose: Multiple devices adding highlights to same book - - Verification: All highlights saved, no corruption - -4. **TestSyncConcurrent_ProgressPlusNote** (after highlights test) - - Lines: Add ~80 lines - - Purpose: Progress update + note creation simultaneously - - Verification: Both operations succeed - -**Action**: Add 4 new test functions to `sync_integration_test.go`. - ---- - -### 2.2 Bulk Operation Race Conditions - -#### Test Pattern - -```go -// NEW TEST: Concurrent bulk + individual operations -func TestBulkConcurrent_DeleteAndUpdate(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - // Create multiple media items - var mediaIDs []string - for i := 0; i < 5; i++ { - id := createTestMediaItemID(t, setup.Server, token) - mediaIDs = append(mediaIDs, id) - } - - var wg sync.WaitGroup - errors := make(chan error, 2) - - // Concurrent operation 1: Bulk delete - wg.Add(1) - go func() { - defer wg.Done() - - payload := map[string]interface{}{ - "media_item_ids": mediaIDs[0:3], // Delete first 3 - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("POST", setup.Server.URL+"/api/media-items/bulk-delete", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - errors <- err - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errors <- fmt.Errorf("bulk delete failed: %d", resp.StatusCode) - } - }() - - // Concurrent operation 2: Individual update on same items - wg.Add(1) - go func() { - defer wg.Done() - - // Try to update item that might be deleted - payload := map[string]interface{}{ - "title": "Updated Title", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", fmt.Sprintf("%s/api/media-items/%s", setup.Server.URL, mediaIDs[1]), bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - errors <- err - return - } - defer resp.Body.Close() - - // Either succeeds (delete hadn't run yet) or fails (item deleted) - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { - errors <- fmt.Errorf("update failed with unexpected status: %d", resp.StatusCode) - } - }() - - wg.Wait() - close(errors) - - // Check for unexpected errors - for err := range errors { - t.Logf("Concurrent operation error: %v", err) - } - - // NEW: Verify final database state - // Items 0,1,2 should be deleted (bulk delete won or ran first) - // Item 1 might be updated (update ran first) - // Items 3,4 should exist untouched - - for i, mediaID := range mediaIDs { - pgID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - _, err := setup.DB.GetMediaItem(context.Background(), pgID) - - if i < 3 { - // Should be deleted - assert.Error(t, err, "Media item %d should be deleted", i) - } else { - // Should exist - assert.NoError(t, err, "Media item %d should exist", i) - } - } -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/media_bulk_test.go` - -**Add new test function**: - -1. **TestBulkConcurrent_DeleteAndUpdate** (after existing bulk tests) - - Lines: Add ~80 lines - - Purpose: Bulk delete + individual update on same items - - Verification: Consistent final state, no orphaned records - -##### File: `cmd/server/tests/collections_bulk_test.go` - -**Add new test function**: - -1. **TestBulkConcurrent_AddAndRemove** (after existing bulk tests) - - Lines: Add ~80 lines - - Purpose: Add items to collection while removing from same collection - - Verification: Collection items consistent - -##### File: `cmd/server/tests/conflicts_bulk_test.go` - -**Add new test function**: - -1. **TestBulkConcurrent_ResolveAndEscalate** (after existing bulk tests) - - Lines: Add ~80 lines - - Purpose: Resolve conflicts while escalating others to admin - - Verification: No conflicts in ambiguous state - -**Action**: Add 3 new test functions to respective bulk test files. - ---- - -### 2.3 Profile + Device Concurrent Updates - -#### Test Pattern - -```go -// NEW TEST: Concurrent profile + device updates -func TestUserConcurrent_ProfileAndDeviceUpdates(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-profile-device") - - var wg sync.WaitGroup - errors := make(chan error, 2) - - // Concurrent operation 1: Update user profile - wg.Add(1) - go func() { - defer wg.Done() - - payload := map[string]interface{}{ - "first_name": "UpdatedFirstName", - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", setup.Server.URL+"/api/auth/profile", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - errors <- err - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errors <- fmt.Errorf("profile update failed: %d", resp.StatusCode) - } - }() - - // Concurrent operation 2: Update device sync settings (affects user) - wg.Add(1) - go func() { - defer wg.Done() - - payload := map[string]interface{}{ - "sync_enabled": false, - } - jsonData, _ := json.Marshal(payload) - - req := httptest.NewRequest("PUT", fmt.Sprintf("%s/api/devices/%s", setup.Server.URL, device.ID.String()), bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - errors <- err - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errors <- fmt.Errorf("device update failed: %d", resp.StatusCode) - } - }() - - wg.Wait() - close(errors) - - // Check for unexpected errors - for err := range errors { - t.Logf("Concurrent update error: %v", err) - } - - // NEW: Verify final database state - // Both updates should succeed - pgUserID := pgtype.UUID{Bytes: [16]byte(setup.User.ID), Valid: true} - user, err := setup.DB.GetUser(context.Background(), pgUserID) - require.NoError(t, err, "Should retrieve user") - assert.Equal(t, "UpdatedFirstName", user.FirstName.String) - - pgDeviceID := pgtype.UUID{Bytes: [16]byte(device.ID), Valid: true} - updatedDevice, err := setup.DB.GetDevice(context.Background(), pgDeviceID) - require.NoError(t, err, "Should retrieve device") - assert.False(t, updatedDevice.SyncEnabled.Bool) -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/user_test.go` - -**Add new test function**: - -1. **TestUserConcurrent_ProfileAndDevice** (after existing user tests) - - Lines: Add ~70 lines - - Purpose: Profile update + device update simultaneously - - Verification: Both updates succeed - -##### File: `cmd/server/tests/device_test.go` - -**Add new test function**: - -1. **TestDeviceConcurrent_TokenRegenAndSync** (after existing device tests) - - Lines: Add ~80 lines - - Purpose: Regenerate token while sync is in progress - - Verification: Old token fails current sync, new token works for next - -**Action**: Add 2 new test functions. - ---- - -### Phase 2 Summary - -**New Test Functions**: 10+ -**Lines Added**: ~800 lines -**Race Coverage**: Sync, bulk, profile + device -**Database Verification**: 100% of final states - -**Verification Steps**: -1. โœ… All sync operations have concurrent tests -2. โœ… All bulk operations have concurrent tests -3. โœ… Profile + device interactions have concurrent tests - ---- - -## Phase 3: Hardened Error Handling - -**Priority**: ๐ŸŸก MEDIUM - **Do after Phase 2** -**Time Estimate**: 15-20 new test functions -**Risk Level**: Low (new tests only) -**ROI**: Medium - prevents edge case crashes - -### Objectives -1. Add WebSocket error path tests -2. Add sync failure scenario tests -3. Add OPDS edge case tests -4. Add book matching edge case tests - ---- - -### 3.1 WebSocket Error Paths - -#### Problem Areas - -| Test File | Missing Tests | Risk | -|-----------|---------------|-------| -| `websocket_test.go` | Disconnection mid-message | Orphaned connections | -| | Malformed WebSocket frames | Parser crashes | -| | Connection timeout | Resource leaks | -| | Connection hijacking | Security issues | - -#### Test Patterns - -##### Pattern 1: Disconnection Mid-Message - -```go -// NEW TEST: WebSocket disconnect during message -func TestWebSocket_DisconnectMidMessage(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-ws-disconnect") - - // Connect WebSocket - wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/device/" + device.AuthToken - ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) - require.NoError(t, err, "Should connect WebSocket") - defer ws.Close() - - // Send partial message - partialData := map[string]interface{}{ - "media_item_id": uuid.New(), - "percentage": 25.5, - // Missing required fields for incomplete message - } - jsonData, _ := json.Marshal(partialData) - - // Send message - err = ws.WriteMessage(websocket.TextMessage, jsonData) - require.NoError(t, err, "Should send message") - - // Immediately close connection (simulating disconnect) - ws.Close() - - // Wait a bit for server to process - time.Sleep(100 * time.Millisecond) - - // NEW: Verify no orphaned progress records in database - // If message was processed, it should be complete - // If not, it should be rolled back - pgUserID := pgtype.UUID{Bytes: [16]byte(setup.User.ID), Valid: true} - progress, err := setup.DB.ListReadingProgress(context.Background(), pgUserID) - - // Either message processed (with default values) or rolled back - // No partial/corrupted records should exist - for _, p := range progress { - if p.Percentage.Float64 == 25.5 { - // If partial percentage saved, other fields should have defaults - assert.NotZero(t, p.TotalPages.Int32, "Should have default total_pages") - } - } -} -``` - -##### Pattern 2: Malformed Frames - -```go -// NEW TEST: Malformed WebSocket frames -func TestWebSocket_MalformedFrames(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-ws-malformed") - - // Connect WebSocket - wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/device/" + device.AuthToken - ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) - require.NoError(t, err, "Should connect WebSocket") - defer ws.Close() - - // Send invalid JSON - invalidJSON := []byte("{invalid json}") - err = ws.WriteMessage(websocket.TextMessage, invalidJSON) - require.NoError(t, err, "Should send message") - - // Send binary data (unexpected) - binaryData := []byte{0x00, 0x01, 0x02} - err = ws.WriteMessage(websocket.BinaryMessage, binaryData) - require.NoError(t, err, "Should send binary") - - // Send extremely large message - largeData := make([]byte, 1024*1024) // 1MB - err = ws.WriteMessage(websocket.TextMessage, largeData) - if err != nil { - t.Logf("Large message rejected (expected): %v", err) - } - - // Wait for server to process - time.Sleep(100 * time.Millisecond) - - // NEW: Verify WebSocket still open (not crashed) - err = ws.WriteMessage(websocket.PingMessage, nil) - // If server crashed, this will fail - if err != nil { - t.Logf("WebSocket connection closed after malformed frames: %v", err) - } -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/websocket_test.go` - -**Add new test functions**: - -1. **TestWebSocket_DisconnectMidMessage** (after existing tests) - - Lines: Add ~50 lines - - Purpose: Connection closes during message processing - - Verification: No partial/corrupted data in DB - -2. **TestWebSocket_MalformedFrames** (after previous test) - - Lines: Add ~50 lines - - Purpose: Invalid JSON, binary data, oversized messages - - Verification: Server handles gracefully, no crashes - -3. **TestWebSocket_ConnectionTimeout** (after previous test) - - Lines: Add ~40 lines - - Purpose: Connection times out during inactivity - - Verification: Resources cleaned up - -4. **TestWebSocket_ConcurrentMessages** (after previous test) - - Lines: Add ~60 lines - - Purpose: Multiple messages sent simultaneously - - Verification: All processed correctly - -**Action**: Add 4 new test functions to `websocket_test.go`. - ---- - -### 3.2 Sync Failure Scenarios - -#### Problem Areas - -| Scenario | Missing Tests | Risk | -|-----------|---------------|-------| -| **Offline device** | Device syncs after being offline | Stale data overwrites fresh | -| **Sync queue overflow** | Too many pending sync operations | Queue exhaustion | -| **Stale sync data** | Old sync data arrives after newer | Incorrect progress | -| **Sync conflicts** | Two devices with conflicting data | Data inconsistency | - -#### Test Patterns - -##### Pattern 1: Offline Device Sync - -```go -// NEW TEST: Device syncs after extended offline period -func TestSync_OfflineDevice(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-offline") - mediaID := createTestMediaItemID(t, setup.Server, token) - - // Device sets progress to 50% while offline - offlineProgress := map[string]interface{}{ - "media_item_id": mediaID, - "percentage": 50.0, - "current_page": 100, - "total_pages": 200, - "timestamp": time.Now().Add(-24 * time.Hour).Unix(), // 24 hours ago - } - - // Simulate offline sync (old timestamp) - jsonData, _ := json.Marshal(offlineProgress) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/sync/koreader/progress", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+device.AuthToken) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - // Should succeed but warn about stale data - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // NEW: Verify database handling of stale data - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - pgUserID := pgtype.UUID{Bytes: [16]byte(setup.User.ID), Valid: true} - progress, err := setup.DB.GetReadingProgress(context.Background(), database.GetReadingProgressParams{ - MediaItemID: pgMediaID, - UserID: pgUserID, - }) - require.NoError(t, err, "Should retrieve progress") - - // Stale data should either: - // 1. Be rejected (keep existing newer data) - // 2. Be accepted with warning (user notification) - // Verify behavior matches expected - assert.LessOrEqual(t, progress.Percentage.Float64, 50.0, "Should handle stale data appropriately") -} -``` - -##### Pattern 2: Sync Queue Overflow - -```go -// NEW TEST: Sync queue exceeds limits -func TestSync_QueueOverflow(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-queue-overflow") - - // Simulate many rapid sync operations (queue overflow) - const numOps = 100 - var wg sync.WaitGroup - errors := make(chan error, numOps) - - for i := 0; i < numOps; i++ { - wg.Add(1) - go func(opNum int) { - defer wg.Done() - - progressData := map[string]interface{}{ - "media_item_id": uuid.New(), // Different items - "percentage": float64(opNum), - } - jsonData, _ := json.Marshal(progressData) - - req := httptest.NewRequest("POST", setup.Server.URL+"/api/sync/koreader/progress", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+device.AuthToken) - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - errors <- err - return - } - defer resp.Body.Close() - - // Queue might be full (503) or accepted (202) - if resp.StatusCode != http.StatusOK && - resp.StatusCode != http.StatusAccepted && - resp.StatusCode != http.StatusServiceUnavailable { - errors <- fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - }(i) - } - - wg.Wait() - close(errors) - - // Some operations might fail due to queue overflow (expected) - errorCount := 0 - for range errors { - errorCount++ - } - t.Logf("Queue overflow: %d/%d operations failed", errorCount, numOps) - - // NEW: Verify queue didn't deadlock or crash - // Queue should process or reject, not hang -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/sync_integration_test.go` - -**Add new test functions**: - -1. **TestSync_OfflineDevice** (after concurrent tests) - - Lines: Add ~60 lines - - Purpose: Old sync data arrives after newer data - - Verification: Appropriate handling (reject or warn) - -2. **TestSync_QueueOverflow** (after previous test) - - Lines: Add ~70 lines - - Purpose: Many rapid sync operations - - Verification: Queue handles gracefully, no deadlocks - -3. **TestSync_ConflictingData** (after previous test) - - Lines: Add ~70 lines - - Purpose: Two devices send conflicting progress - - Verification: Conflict detection or last-write-wins with notification - -4. **TestSync_OrphanedQueueItems** (after previous test) - - Lines: Add ~50 lines - - Purpose: Queue items for deleted books - - Verification: Orphaned items cleaned up - -**Action**: Add 4 new test functions to `sync_integration_test.go`. - ---- - -### 3.3 OPDS Edge Cases - -#### Problem Areas - -| Scenario | Missing Tests | Risk | -|-----------|---------------|-------| -| **Empty library** | OPDS feed with no books | Empty feed parsing errors | -| **Very large feeds** | Library with thousands of books | Pagination failures | -| **Corrupted metadata** | Books with invalid EPUB metadata | Feed generation crashes | -| **Special characters** | Titles with Unicode, emojis | XML/JSON encoding issues | - -#### Test Patterns - -##### Pattern 1: Empty Library - -```go -// NEW TEST: OPDS feed for empty library -func TestOPDS_EmptyLibrary(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - // Create empty library - libReq := map[string]interface{}{ - "name": "Empty Library", - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode) - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - // Get OPDS feed - req = httptest.NewRequest("GET", fmt.Sprintf("%s/api/opds/libraries/%s", setup.Server.URL, libraryID), nil) - req.Header.Set("Authorization", "Bearer "+token) - - resp, err = client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - // NEW: Verify feed structure - var feed map[string]interface{} - json.NewDecoder(resp.Body).Decode(&feed) - - assert.Contains(t, feed, "entries", "Feed should have entries array") - entries := feed["entries"].([]interface{}) - assert.Empty(t, entries, "Entries should be empty for empty library") - assert.Contains(t, feed, "total", "Feed should have total count") - assert.Equal(t, 0, int(feed["total"].(float64)), "Total should be 0") -} -``` - -##### Pattern 2: Large Feed - -```go -// NEW TEST: OPDS feed with many books -func TestOPDS_LargeFeed(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - // Create library - libReq := map[string]interface{}{ - "name": "Large Library", - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode) - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - // Add folder - folderReq := map[string]interface{}{ - "folder_path": "/app/uploads", - } - folderBody, _ := json.Marshal(folderReq) - req = httptest.NewRequest("POST", fmt.Sprintf("%s/api/libraries/%s/folders", setup.Server.URL, libraryID), bytes.NewBuffer(folderBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - require.NoError(t, err) - resp.Body.Close() - - // Add many books (simulate large library) - const numBooks = 1000 - for i := 0; i < numBooks; i++ { - mediaReq := map[string]interface{}{ - "library_id": libraryID, - "title": fmt.Sprintf("Book %d", i), - "author": "Test Author", - "file_path": fmt.Sprintf("/tmp/test%d.epub", i), - "file_size": 1024, - "mime_type": "application/epub+zip", - } - mediaBody, _ := json.Marshal(mediaReq) - - req = httptest.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - if err != nil { - t.Logf("Failed to create book %d: %v", i, err) - continue - } - resp.Body.Close() - } - - // Get OPDS feed with pagination - req = httptest.NewRequest("GET", fmt.Sprintf("%s/api/opds/libraries/%s?page=1&limit=100", setup.Server.URL, libraryID), nil) - req.Header.Set("Authorization", "Bearer "+token) - - resp, err = client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - // NEW: Verify pagination - assert.Equal(t, http.StatusOK, resp.StatusCode) - - var feed map[string]interface{} - json.NewDecoder(resp.Body).Decode(&feed) - - assert.Contains(t, feed, "entries", "Feed should have entries array") - entries := feed["entries"].([]interface{}) - assert.LessOrEqual(t, len(entries), 100, "Should respect page limit") - assert.Contains(t, feed, "total", "Feed should have total count") - assert.GreaterOrEqual(t, int(feed["total"].(float64)), numBooks-10, "Total should reflect all books") -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/opds_test.go` - -**Add new test functions**: - -1. **TestOPDS_EmptyLibrary** (after existing tests) - - Lines: Add ~50 lines - - Purpose: OPDS feed with no books - - Verification: Valid feed structure, empty entries - -2. **TestOPDS_LargeFeed** (after previous test) - - Lines: Add ~90 lines - - Purpose: Library with 1000+ books - - Verification: Pagination works, total count accurate - -3. **TestOPDS_SpecialCharacters** (after previous test) - - Lines: Add ~60 lines - - Purpose: Book titles with Unicode, emojis, RTL - - Verification: Proper XML/JSON encoding - -4. **TestOPDS_CorruptedMetadata** (after previous test) - - Lines: Add ~50 lines - - Purpose: Book with invalid/malformed metadata - - Verification: Feed generation continues, skips bad entries - -**Action**: Add 4 new test functions to `opds_test.go`. - ---- - -### 3.4 Book Matching Edge Cases - -#### Problem Areas - -| Scenario | Missing Tests | Risk | -|-----------|---------------|-------| -| **No matches found** | Book has no close matches | Empty results handling | -| **Multiple equal matches** | Same confidence for multiple books | Ambiguity handling | -| **Special characters** | Titles with quotes, apostrophes | Query failures | -| **Unicode normalization** | Different Unicode representations | Duplicate matches | - -#### Test Patterns - -##### Pattern 1: No Matches Found - -```go -// NEW TEST: Book matching finds no results -func TestBookMatching_NoMatches(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-no-matches") - - // Device reports book that doesn't exist in library - unmatchedBook := map[string]interface{}{ - "title": "Nonexistent Book Title That Definitely Doesn't Exist", - "author": "Unknown Author", - "file_path": "/mnt/sd/Nonexistent.epub", - "file_size": 1024, - } - - jsonData, _ := json.Marshal(unmatchedBook) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/sync/koreader/match", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+device.AuthToken) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - // NEW: Verify handling of no matches - var response map[string]interface{} - json.NewDecoder(resp.Body).Decode(&response) - - // Should either: - // 1. Return empty matches array - // 2. Return null match with confidence 0 - // 3. Suggest manual match - matches, hasMatches := response["matches"] - if hasMatches { - matchArray := matches.([]interface{}) - assert.Empty(t, matchArray, "Should have no matches") - } - - // Or might have suggestion flag - if suggestion, hasSuggestion := response["suggest_manual"]; hasSuggestion { - assert.True(t, suggestion.(bool), "Should suggest manual match") - } -} -``` - -##### Pattern 2: Multiple Equal Matches - -```go -// NEW TEST: Multiple books with same match confidence -func TestBookMatching_MultipleEqualConfidence(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - // Create library - libReq := map[string]interface{}{ - "name": "Test Library", - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode) - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - // Add folder - folderReq := map[string]interface{}{ - "folder_path": "/app/uploads", - } - folderBody, _ := json.Marshal(folderReq) - req = httptest.NewRequest("POST", fmt.Sprintf("%s/api/libraries/%s/folders", setup.Server.URL, libraryID), bytes.NewBuffer(folderBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - require.NoError(t, err) - resp.Body.Close() - - // Add multiple books with similar titles - for _, title := range []string{"Book One", "Book Two", "Book Three"} { - mediaReq := map[string]interface{}{ - "library_id": libraryID, - "title": title, - "author": "Same Author", - "file_path": "/tmp/test.epub", - "file_size": 1024, - "mime_type": "application/epub+zip", - } - mediaBody, _ := json.Marshal(mediaReq) - - req = httptest.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - require.NoError(t, err) - resp.Body.Close() - } - - // Device reports book with ambiguous title - device := setup.CreateDevice(t, "Test Device", "koreader", "test-ambiguous") - ambiguousBook := map[string]interface{}{ - "title": "Book", // Could match any of the three - "author": "Same Author", - } - - jsonData, _ := json.Marshal(ambiguousBook) - req = httptest.NewRequest("POST", setup.Server.URL+"/api/sync/koreader/match", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+device.AuthToken) - - resp, err = client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - // NEW: Verify ambiguous match handling - assert.Equal(t, http.StatusOK, resp.StatusCode) - - var response map[string]interface{} - json.NewDecoder(resp.Body).Decode(&response) - - // Should return multiple matches with similar confidence - matches, hasMatches := response["matches"] - require.True(t, hasMatches, "Should have matches") - matchArray := matches.([]interface{}) - assert.Greater(t, len(matchArray), 1, "Should have multiple matches for ambiguous title") - - // All matches should have similar confidence scores - if len(matchArray) > 1 { - firstMatch := matchArray[0].(map[string]interface{}) - secondMatch := matchArray[1].(map[string]interface{}) - - firstConfidence := firstMatch["confidence"].(float64) - secondConfidence := secondMatch["confidence"].(float64) - - // Confidence scores should be similar (within 10%) - confidenceDiff := firstConfidence - secondConfidence - assert.Less(t, confidenceDiff, 0.1, "Similar matches should have close confidence") - } -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/book_matching_test.go` - -**Add new test functions**: - -1. **TestBookMatching_NoMatches** (after existing tests) - - Lines: Add ~50 lines - - Purpose: Book with no close matches - - Verification: Empty matches or manual match suggestion - -2. **TestBookMatching_MultipleEqualConfidence** (after previous test) - - Lines: Add ~80 lines - - Purpose: Multiple books with similar titles - - Verification: Returns multiple matches, suggests disambiguation - -3. **TestBookMatching_SpecialCharacters** (after previous test) - - Lines: Add ~60 lines - - Purpose: Titles with quotes, apostrophes, emojis - - Verification: Proper query escaping, correct matches - -4. **TestBookMatching_UnicodeNormalization** (after previous test) - - Lines: Add ~60 lines - - Purpose: Same title in different Unicode forms (NFD vs NFC) - - Verification: Correct match despite Unicode differences - -**Action**: Add 4 new test functions to `book_matching_test.go`. - ---- - -### Phase 3 Summary - -**New Test Functions**: 16+ -**Lines Added**: ~1000 lines -**Error Path Coverage**: WebSocket, sync, OPDS, matching -**Edge Case Coverage**: Empty, large, corrupted, special characters - -**Verification Steps**: -1. โœ… WebSocket has error path tests -2. โœ… Sync has failure scenario tests -3. โœ… OPDS has edge case tests -4. โœ… Book matching has ambiguity tests - ---- - -## Phase 4: Load and Security - -**Priority**: ๐ŸŸข LOW - **Do after Phase 3** -**Time Estimate**: 10-12 new test functions -**Risk Level**: Low (new tests only) -**ROI**: Medium - prevents DoS and encoding issues - -### Objectives -1. Add very large payload tests -2. Add comprehensive Unicode/special character tests -3. Add max boundary tests - ---- - -### 4.1 Very Large Payloads - -#### Problem Areas - -| Area | Current | Missing | Risk | -|-------|---------|----------|-------| -| **Single test** | `edge_cases_test.go:327` - 100KB string | Bulk operations with 1000+ items | Memory exhaustion | -| **Response size** | Not tested | User with 10,000 books | Timeouts | -| **WebSocket messages** | Not tested | Very large sync payloads | Frame size limits | -| **Upload limits** | Not tested | Huge book files | Disk space exhaustion | - -#### Test Patterns - -##### Pattern 1: Bulk Operation Limits - -```go -// NEW TEST: Bulk operation with maximum items -func TestBulk_MaximumItems(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - device := setup.CreateDevice(t, "Test Device", "koreader", "test-bulk-max") - - // Create library with folder - libReq := map[string]interface{}{ - "name": "Bulk Test Library", - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode) - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - // Add folder - folderReq := map[string]interface{}{ - "folder_path": "/app/uploads", - } - folderBody, _ := json.Marshal(folderReq) - req = httptest.NewRequest("POST", fmt.Sprintf("%s/api/libraries/%s/folders", setup.Server.URL, libraryID), bytes.NewBuffer(folderBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - require.NoError(t, err) - resp.Body.Close() - - // Create many books (1000 items) - const numBooks = 1000 - var mediaIDs []string - for i := 0; i < numBooks; i++ { - mediaReq := map[string]interface{}{ - "library_id": libraryID, - "title": fmt.Sprintf("Book %d", i), - "author": "Test Author", - "file_path": fmt.Sprintf("/tmp/test%d.epub", i), - "file_size": 1024, - "mime_type": "application/epub+zip", - } - mediaBody, _ := json.Marshal(mediaReq) - - req = httptest.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - if err != nil { - t.Logf("Failed to create book %d: %v", i, err) - continue - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusCreated { - var mediaResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&mediaResponse) - mediaIDs = append(mediaIDs, mediaResponse["id"].(string)) - } - } - - // Test bulk update with all 1000 items - bulkUpdateReq := map[string]interface{}{ - "media_item_ids": mediaIDs, - "tags": []string{"bulk-updated"}, - } - bulkBody, _ := json.Marshal(bulkUpdateReq) - - req = httptest.NewRequest("POST", setup.Server.URL+"/api/media-items/bulk-update", bytes.NewBuffer(bulkBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - start := time.Now() - resp, err = client.Do(req) - duration := time.Since(start) - - if err != nil { - t.Logf("Bulk update with %d items failed: %v", numBooks, err) - return - } - defer resp.Body.Close() - - // NEW: Verify behavior under load - t.Logf("Bulk update of %d items took %v", numBooks, duration) - - // Should succeed but might take time - if resp.StatusCode == http.StatusOK { - var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - - // Verify count - if total, ok := result["total"]; ok { - assert.Equal(t, numBooks, int(total.(float64)), "Should process all items") - } - - // Verify DB state for sample - if len(mediaIDs) > 0 { - pgID := pgtype.UUID{Bytes: [16]byte(mediaIDs[0]), Valid: true} - media, err := setup.DB.GetMediaItem(context.Background(), pgID) - require.NoError(t, err) - assert.Contains(t, media.Tags, "bulk-updated", "Should have updated tag") - } - } else if resp.StatusCode == http.StatusRequestEntityTooLarge { - t.Logf("Bulk update rejected as too large (acceptable)") - } else { - t.Logf("Unexpected status: %d", resp.StatusCode) - } -} -``` - -##### Pattern 2: Response Size Limits - -```go -// NEW TEST: API response with very large payload -func TestResponse_VeryLargePayload(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - // Create library with many books (from previous test or reuse) - // Assuming we have 1000 books from previous test - - // Request list of all books (large response) - req := httptest.NewRequest("GET", setup.Server.URL+"/api/media-items?limit=10000", nil) - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{Timeout: 30 * time.Second} - start := time.Now() - resp, err := client.Do(req) - duration := time.Since(start) - - if err != nil { - t.Logf("Large payload request failed: %v", err) - return - } - defer resp.Body.Close() - - // NEW: Verify large response handling - t.Logf("Large payload response (%d status) took %v", resp.StatusCode, duration) - - if resp.StatusCode == http.StatusOK { - // Read body (might be large) - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Logf("Failed to read large response: %v", err) - return - } - - t.Logf("Response size: %d bytes", len(body)) - - // Should have reasonable response size (< 10MB) - assert.Less(t, len(body), 10*1024*1024, "Response should be < 10MB") - - // Verify response is valid JSON - var response map[string]interface{} - err = json.Unmarshal(body, &response) - assert.NoError(t, err, "Large response should be valid JSON") - - // Verify pagination - if items, ok := response["items"]; ok { - itemArray := items.([]interface{}) - t.Logf("Returned %d items", len(itemArray)) - } - } else if resp.StatusCode == http.StatusRequestEntityTooLarge { - t.Logf("Large payload rejected (acceptable)") - } -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/edge_cases_test.go` - -**Add new test functions**: - -1. **TestBulk_MaximumItems** (after existing edge cases) - - Lines: Add ~120 lines - - Purpose: Bulk operation with 1000 items - - Verification: Succeeds or rejected gracefully, no crashes - -2. **TestResponse_VeryLargePayload** (after previous test) - - Lines: Add ~80 lines - - Purpose: Request all items (large response) - - Verification: Reasonable size, valid JSON, proper pagination - -3. **TestWebSocket_LargeMessage** (after previous test) - - Lines: Add ~60 lines - - Purpose: Send very large WebSocket message - - Verification: Rejected or handled gracefully - -**Action**: Add 3 new test functions to `edge_cases_test.go`. - ---- - -### 4.2 Comprehensive Unicode Tests - -#### Problem Areas - -| Character Type | Missing Tests | Risk | -|--------------|---------------|-------| -| **Emojis** | Only 1 test | Database encoding issues | -| **RTL languages** | None | Display corruption | -| **Zero-width characters** | None | Silent duplicates | -| **Unicode normalization** | None | Same text appears different | -| **Combining characters** | None | Search failures | - -#### Test Pattern - -```go -// NEW TEST: Comprehensive Unicode handling -func TestUnicode_Comprehensive(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - testCases := []struct { - name string - title string - author string - expectOK bool - }{ - { - name: "Emojis in title", - title: "The Great Adventure ๐Ÿš€๐Ÿ“šโœจ", - author: "Jane Doe", - expectOK: true, - }, - { - name: "Multiple emojis", - title: "๐Ÿ“– Book ๐ŸŒŸ of ๐ŸŽ‰ Joy ๐Ÿ˜Š", - author: "Happy Author ๐Ÿ‘ฉโ€๐Ÿš€", - expectOK: true, - }, - { - name: "RTL text (Arabic)", - title: "ูƒุชุงุจ ุนุธูŠู…", - author: "ู…ุคู„ู ุจุงุฑุฒ", - expectOK: true, - }, - { - name: "RTL text (Hebrew)", - title: "ืกืคืจ ื ื”ื“ืจ", - author: "ืกื•ืคืจ ืžืคื•ืจืกื", - expectOK: true, - }, - { - name: "Mixed RTL and LTR", - title: "Hello ืฉืœื•ื Bonjour", - author: "Author ู†ุจูŠ", - expectOK: true, - }, - { - name: "Zero-width characters", - title: "Book\u200BTitle", // Zero-width space - author: "Author\u200CName", // Zero-width non-joiner - expectOK: true, - }, - { - name: "Combining diacritics", - title: "Cafรฉ", // Precomposed รฉ - author: "cafe\u0301", // Combining acute - expectOK: true, - }, - { - name: "Multiple combining marks", - title: "รฑaรฏve", // Precomposed รฏ - author: "nai\u0308ve", // Combining diaeresis - expectOK: true, - }, - { - name: "CJK characters", - title: "ไผŸๅคง็š„ไนฆ", - author: "ไฝœ่€…", - expectOK: true, - }, - { - name: "Special punctuation", - title: "Book: A Story; Ofโ€”Thingsโ€”Likeโ€”This", - author: "O'Brien, St. John", - expectOK: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create library - libReq := map[string]interface{}{ - "name": fmt.Sprintf("Unicode Library %s", tc.name), - "type": "ebooks", - } - libBody, _ := json.Marshal(libReq) - req := httptest.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - - client := &http.Client{} - resp, err := client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, http.StatusCreated, resp.StatusCode) - - var libResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&libResponse) - libraryID := libResponse["id"].(string) - - // Add folder - folderReq := map[string]interface{}{ - "folder_path": "/app/uploads", - } - folderBody, _ := json.Marshal(folderReq) - req = httptest.NewRequest("POST", fmt.Sprintf("%s/api/libraries/%s/folders", setup.Server.URL, libraryID), bytes.NewBuffer(folderBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - require.NoError(t, err) - resp.Body.Close() - - // Create book with Unicode title/author - mediaReq := map[string]interface{}{ - "library_id": libraryID, - "title": tc.title, - "author": tc.author, - "file_path": "/tmp/test.epub", - "file_size": 1024, - "mime_type": "application/epub+zip", - } - mediaBody, _ := json.Marshal(mediaReq) - - req = httptest.NewRequest("POST", setup.Server.URL+"/api/media-items", bytes.NewBuffer(mediaBody)) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - req.Header.Set("Authorization", "Bearer "+token) - resp, err = client.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - if tc.expectOK { - assert.Equal(t, http.StatusCreated, resp.StatusCode, tc.name) - - // NEW: Verify database stores Unicode correctly - var mediaResponse map[string]interface{} - json.NewDecoder(resp.Body).Decode(&mediaResponse) - mediaID := mediaResponse["id"].(string) - - pgID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - media, err := setup.DB.GetMediaItem(context.Background(), pgID) - require.NoError(t, err, "Should retrieve media item") - - // Verify Unicode preserved - assert.Equal(t, tc.title, media.Title, "Title should preserve Unicode") - assert.Equal(t, tc.author, media.Author.String, "Author should preserve Unicode") - - // Test search with Unicode - searchReq := httptest.NewRequest("GET", fmt.Sprintf("%s/api/search?q=%s", setup.Server.URL, url.QueryEscape(tc.title)), nil) - searchReq.Header.Set("Authorization", "Bearer "+token) - searchResp, err := client.Do(searchReq) - require.NoError(t, err) - defer searchResp.Body.Close() - - assert.Equal(t, http.StatusOK, searchResp.StatusCode, "Search should work with Unicode") - } else { - assert.NotEqual(t, http.StatusCreated, resp.StatusCode, tc.name) - } - }) - } -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/edge_cases_test.go` - -**Add new test function**: - -1. **TestUnicode_Comprehensive** (after existing edge cases) - - Lines: Add ~150 lines - - Purpose: Emojis, RTL, zero-width, combining marks, CJK - - Verification: Stored correctly, searchable - -**Action**: Add new test function to `edge_cases_test.go`. - ---- - -### 4.3 Max Boundary Tests - -#### Problem Areas - -| Field Type | Missing | Risk | -|-----------|----------|-------| -| **Email** | Exact boundary (254, 255, 256) | Truncation | -| **Username** | Boundary (49, 50, 51, 500, 5000) | Validation inconsistency | -| **Device name** | Max length | DB constraint violation | -| **Book title** | Max length | Display issues | -| **Array items** | Max items in bulk | Performance degradation | - -#### Test Pattern - -```go -// NEW TEST: Boundary value testing -func TestBoundaries_MaxLengths(t *testing.T) { - setup := setupTestServer(t) - token := loginTestUser(t, setup.Server, setup.DB) - - t.Run("Email at exact boundaries", func(t *testing.T) { - testCases := []struct { - name string - email string - expectOK bool - }{ - {"254 chars (one under limit)", strings.Repeat("a", 250) + "@example.com", true}, - {"255 chars (exact limit)", strings.Repeat("a", 251) + "@example.com", true}, - {"256 chars (one over limit)", strings.Repeat("a", 252) + "@example.com", false}, - {"Very long (5000 chars)", strings.Repeat("a", 4995) + "@example.com", false}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create user with boundary email - // ... test code ... - }) - } - }) - - t.Run("Username at exact boundaries", func(t *testing.T) { - testCases := []struct { - name string - username string - expectOK bool - }{ - {"2 chars (one under min)", "ab", false}, - {"3 chars (exact min)", "abc", true}, - {"50 chars (exact max)", strings.Repeat("a", 50), true}, - {"51 chars (one over max)", strings.Repeat("a", 51), false}, - {"500 chars (way over)", strings.Repeat("a", 500), false}, - {"5000 chars (extreme)", strings.Repeat("a", 5000), false}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create user with boundary username - // ... test code ... - }) - } - }) - - t.Run("Device name at max length", func(t *testing.T) { - // Test device name at database column limit (likely 255) - // ... test code ... - }) - - t.Run("Book title at max length", func(t *testing.T) { - // Test title at database column limit - // ... test code ... - }) - - t.Run("Bulk array size limits", func(t *testing.T) { - // Test bulk operations with arrays at limits - // ... test code ... - }) -} -``` - -#### File-by-File Plan - -##### File: `cmd/server/tests/edge_cases_test.go` - -**Add new test function**: - -1. **TestBoundaries_MaxLengths** (after Unicode tests) - - Lines: Add ~200 lines - - Purpose: Exact boundary testing for all text fields - - Verification: Consistent validation, no truncation - -**Action**: Add new test function to `edge_cases_test.go`. - ---- - -### Phase 4 Summary - -**New Test Functions**: 5+ -**Lines Added**: ~600 lines -**Load Testing**: Bulk operations, large responses -**Unicode Coverage**: Emojis, RTL, zero-width, combining, CJK -**Boundary Testing**: Exact limits for all text fields - -**Verification Steps**: -1. โœ… Large payloads handled gracefully -2. โœ… Unicode characters work end-to-end -3. โœ… Boundary values consistent - ---- - -## Implementation Order - -### Phase 1: Stop Silent Failures (HIGHEST ROI) - -**Week 1-2**: -1. Replace map-based assertions with structs (12 files) -2. Add database verification (8 files) -3. Add null/empty/missing tests (3 files) - -**Deliverables**: -- All tests use struct-based assertions -- All mutations verify database state -- Critical edge cases covered - -**Verification**: -```bash -# All tests compile -go test ./cmd/server/tests/... -v - -# No map[string]interface{} in assertions -grep -r "map\[string\]interface" cmd/server/tests/ | wc -l # Should be 0 - -# All mutations have DB verification -# (Manual review of test files) -``` - ---- - -### Phase 2: Concurrency Protection (HIGH) - -**Week 3**: -1. Add sync race condition tests (4 tests) -2. Add bulk concurrent tests (3 tests) -3. Add profile + device tests (2 tests) - -**Deliverables**: -- All sync operations have concurrent tests -- All bulk operations have concurrent tests -- Profile + device interactions tested - -**Verification**: -```bash -# Run with race detector -go test ./cmd/server/tests/... -race -v - -# All concurrent tests pass -``` - ---- - -### Phase 3: Hardened Error Handling (MEDIUM) - -**Week 4**: -1. Add WebSocket error paths (4 tests) -2. Add sync failures (4 tests) -3. Add OPDS edge cases (4 tests) -4. Add book matching edge cases (4 tests) - -**Deliverables**: -- WebSocket has comprehensive error tests -- Sync has failure scenario tests -- OPDS handles edge cases -- Book matching handles ambiguity - -**Verification**: -```bash -# All error path tests pass -go test ./cmd/server/tests/... -v -run "TestWebSocket|TestSync|TestOPDS|TestBookMatching" -``` - ---- - -### Phase 4: Load and Security (LOW) - -**Week 5**: -1. Add large payload tests (3 tests) -2. Add Unicode tests (1 comprehensive test) -3. Add boundary tests (1 comprehensive test) - -**Deliverables**: -- Large payloads handled gracefully -- Unicode works end-to-end -- Boundary values consistent - -**Verification**: -```bash -# All load/security tests pass -go test ./cmd/server/tests/edge_cases_test.go -v -``` - ---- - -## Verification Checklist - -### Pre-Implementation - -- [ ] All current tests pass - ```bash - go test ./cmd/server/tests/... -v - ``` -- [ ] No compilation errors - ```bash - go build ./... - ``` -- [ ] Guidelines verification passes - ```bash - bash scripts/verify-guidelines.sh - ``` - -### Phase 1 Verification - -- [ ] All `map[string]interface{}` replaced with structs - ```bash - grep -r "map\[string\]interface" cmd/server/tests/*.go | wc -l # Should be 0 - ``` -- [ ] All mutation operations have DB verification - ```bash - # Manual review: Every POST/PUT/DELETE test should query DB after - ``` -- [ ] Null/empty/missing tests added for critical fields - ```bash - grep -r "NullVsEmpty" cmd/server/tests/*.go | wc -l # Should be > 0 - ``` - -### Phase 2 Verification - -- [ ] All sync operations have concurrent tests - ```bash - grep -r "Concurrent" cmd/server/tests/sync_integration_test.go | wc -l # Should be > 0 - ``` -- [ ] All bulk operations have concurrent tests - ```bash - grep -r "Concurrent" cmd/server/tests/*_bulk_test.go | wc -l # Should be > 0 - ``` -- [ ] Race detector passes - ```bash - go test ./cmd/server/tests/... -race -v - ``` - -### Phase 3 Verification - -- [ ] WebSocket has error path tests - ```bash - grep -r "DisconnectMid\|MalformedFrames\|Timeout" cmd/server/tests/websocket_test.go | wc -l # Should be > 0 - ``` -- [ ] Sync has failure tests - ```bash - grep -r "Offline\|QueueOverflow\|Conflicting" cmd/server/tests/sync_integration_test.go | wc -l # Should be > 0 - ``` -- [ ] OPDS has edge case tests - ```bash - grep -r "Empty\|Large\|Special" cmd/server/tests/opds_test.go | wc -l # Should be > 0 - ``` -- [ ] Book matching has ambiguity tests - ```bash - grep -r "NoMatches\|MultipleEqual\|Unicode" cmd/server/tests/book_matching_test.go | wc -l # Should be > 0 - ``` - -### Phase 4 Verification - -- [ ] Large payload tests exist - ```bash - grep -r "Maximum\|VeryLarge" cmd/server/tests/edge_cases_test.go | wc -l # Should be > 0 - ``` -- [ ] Unicode tests exist - ```bash - grep -r "Emoji\|RTL\|Zero-width\|Combining" cmd/server/tests/edge_cases_test.go | wc -l # Should be > 0 - ``` -- [ ] Boundary tests exist - ```bash - grep -r "Boundaries\|254\|255\|256" cmd/server/tests/edge_cases_test.go | wc -l # Should be > 0 - ``` - -### Post-Implementation - -- [ ] All tests pass - ```bash - go test ./cmd/server/tests/... -v - ``` -- [ ] No compilation errors - ```bash - go build ./... - ``` -- [ ] Guidelines verification passes - ```bash - bash scripts/verify-guidelines.sh - ``` -- [ ] Test coverage increased - ```bash - go test ./cmd/server/tests/... -cover - ``` - ---- - -## Success Metrics - -### Before -- **507+** map-based assertions (brittle) -- **0** concurrency tests -- **0** database verification for mutations -- **0** null/empty/missing edge cases -- **Happy path only** for WebSocket, sync, OPDS, matching - -### After (All Phases Complete) -- **0** map-based assertions (100% struct-based) -- **10+** concurrency tests -- **100%** database verification for mutations -- **15+** null/empty/missing edge case tests -- **Comprehensive error paths** for WebSocket, sync, OPDS, matching -- **Load testing** for bulk operations -- **Unicode coverage** for emojis, RTL, zero-width, combining marks -- **Boundary testing** for all text fields - -### Reliability Score - -| Area | Before | After | Improvement | -|-------|---------|--------|-------------| -| API contract safety | โŒ 0% | โœ… 100% | Compile-time detection | -| Data integrity | โš ๏ธ 20% | โœ… 100% | DB verification | -| Concurrency | โŒ 0% | โœ… 80% | Race tests | -| Edge cases | โš ๏ธ 10% | โœ… 90% | Comprehensive | -| Error handling | โš ๏ธ 30% | โœ… 90% | Failure scenarios | -| Load handling | โš ๏ธ 5% | โœ… 70% | Large payloads | -| **Overall** | **13%** | **90%** | **+77%** | - ---- - -## Notes - -1. **Follow PROJECT_GUIDELINES.md**: - - Use `setupTestServer()` from test_helpers.go - - Share one test setup across subtests (call once at function level) - - Use table-driven tests with `t.Run()` - - Configure database with `max_conns=1` via `pgxpool.ParseConfig()` - -2. **No cascading fixes**: Stop on compilation error, review `git diff`, recover deliberately - -3. **Post-edit verification**: Run `go build` for affected packages after each edit - -4. **Multiple logical commits**: Commit changes in logical steps with clear messages - -5. **Test execution speed**: If tests become too slow, consider: - - Parallel test execution (`t.Parallel()`) - - Test-specific database fixtures - - Reduced dataset sizes for load tests - ---- - -## Appendix: Conversion Examples - -### Example 1: Simple Response - -**BEFORE:** -```go -var response map[string]interface{} -json.Unmarshal(body, &response) -deviceName := response["device"].(map[string]interface{})["device_name"].(string) -assert.Equal(t, "Test Device", deviceName) -``` - -**AFTER:** -```go -import "bookhoard/internal/handlers" - -var response handlers.DeviceListResponse -err := json.Unmarshal(body, &response) -require.NoError(t, err) -assert.Equal(t, "Test Device", response.Devices[0].DeviceName) -``` - -### Example 2: Array Response - -**BEFORE:** -```go -var response map[string]interface{} -json.Unmarshal(body, &response) -devices := response["devices"].([]interface{}) -for _, d := range devices { - device := d.(map[string]interface{}) - name := device["device_name"].(string) - // ... -} -``` - -**AFTER:** -```go -import "bookhoard/internal/handlers" - -var response handlers.DeviceListResponse -err := json.Unmarshal(body, &response) -require.NoError(t, err) -for _, device := range response.Devices { - name := device.DeviceName - // ... -} -``` - -### Example 3: Database Verification - -**BEFORE:** -```go -assert.Equal(t, http.StatusNoContent, rec.Code) -``` - -**AFTER:** -```go -assert.Equal(t, http.StatusNoContent, rec.Code) - -pgID := pgtype.UUID{Bytes: [16]byte(id), Valid: true} -_, err := setup.DB.GetDevice(context.Background(), pgID) -assert.Error(t, err, "Device should be deleted") -``` - ---- - -## Appendix: test_helpers.go Improvements - -### Critical Bug Fixes - -#### Issue 1: Return Type Mismatch (Line 210) - -**Problem:** -```go -// Line 210 - WRONG: Returns UserTestData instead of error -return fmt.Errorf("user already exists: %s", existingUser.Email) -``` - -**Fix:** -```go -// Line 210 - CORRECT: Return error -return fmt.Errorf("user already exists: %s", existingUser.Email), UserTestData{} -// OR remove the early return entirely since user exists check should delete and recreate -``` - -**Impact:** This causes compilation errors and prevents `createTestUserOnce` from working correctly. - ---- - -#### Issue 2: Dead Code (Lines 207-214) - -**Problem:** -```go -// Lines 207-214: Early return makes code below unreachable -if err == nil { - return fmt.Errorf("user already exists: %s", existingUser.Email) -} -return UserTestData{} // NEVER REACHED - -// Lines 216-236: Never executed due to early return -passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq" -user, err := db.CreateUser(ctx, database.CreateUserParams{ - // ... -}) -``` - -**Fix:** -```go -// Remove early return and always delete + recreate -ctx := context.Background() - -// Check if user exists and delete for fresh state -user, err := db.GetUserByEmail(ctx, "testuser@example.com") -if err == nil { - // User exists, delete them to ensure fresh password - err = db.DeleteUser(ctx, user.ID) - if err != nil { - // If delete fails (user might be referenced elsewhere), log and continue - t.Logf("Warning: Could not delete existing test user: %v", err) - } -} - -// Create a fresh test user with a valid password -passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq" -newUser, err := db.CreateUser(ctx, database.CreateUserParams{ - Email: "testuser@example.com", - Username: "testuser", - PasswordHash: passwordHash, - FirstName: pgtype.Text{String: "Test", Valid: true}, - LastName: pgtype.Text{String: "User", Valid: true}, - Role: "admin", -}) -require.NoError(t, err, "Failed to create test user") - -userUUID, err := uuid.FromBytes(newUser.ID.Bytes[:]) -require.NoError(t, err, "Failed to parse user UUID") -return UserTestData{ - ID: userUUID, - Email: "testuser@example.com", - Username: "testuser", - Password: "Test@Pass123!", -} -``` - -**Impact:** Dead code prevents test user creation from working properly. - ---- - -### New Helper Functions - -Add these helpers to `test_helpers.go` to reduce code duplication across tests and provide consistent database verification. - -#### Helper 1: Database Verification Functions - -```go -// Add to test_helpers.go after line 562 - -// verifyDeviceCreated verifies a device exists in database with expected values -func verifyDeviceCreated(t *testing.T, db *database.Queries, deviceID uuid.UUID, expected DeviceTestData) { - pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} - device, err := db.GetDevice(context.Background(), pgDeviceID) - require.NoError(t, err, "Device should exist in database") - - assert.Equal(t, expected.Name, device.DeviceName, "Device name should match") - assert.Equal(t, expected.Type, device.DeviceType, "Device type should match") - assert.Equal(t, expected.Identifier, device.DeviceIdentifier, "Device identifier should match") - assert.NotEmpty(t, device.AuthToken, "Device should have auth token") -} - -// verifyDeviceDeleted verifies a device does not exist in database -func verifyDeviceDeleted(t *testing.T, db *database.Queries, deviceID uuid.UUID) { - pgDeviceID := pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true} - _, err := db.GetDevice(context.Background(), pgDeviceID) - assert.Error(t, err, "Device should be deleted from database") -} - -// verifyUserUpdated verifies a user has expected field values in database -func verifyUserUpdated(t *testing.T, db *database.Queries, userID uuid.UUID, expected map[string]interface{}) { - pgUserID := pgtype.UUID{Bytes: [16]byte(userID), Valid: true} - user, err := db.GetUser(context.Background(), pgUserID) - require.NoError(t, err, "User should exist in database") - - if firstName, ok := expected["first_name"]; ok { - if fn, ok := firstName.(string); ok { - assert.Equal(t, fn, user.FirstName.String, "First name should match") - } - } - if lastName, ok := expected["last_name"]; ok { - if ln, ok := lastName.(string); ok { - assert.Equal(t, ln, user.LastName.String, "Last name should match") - } - } - if email, ok := expected["email"]; ok { - if em, ok := email.(string); ok { - assert.Equal(t, em, user.Email, "Email should match") - } - } -} - -// verifyMediaItemUpdated verifies a media item has expected values in database -func verifyMediaItemUpdated(t *testing.T, db *database.Queries, mediaID uuid.UUID, expected map[string]interface{}) { - pgMediaID := pgtype.UUID{Bytes: [16]byte(mediaID), Valid: true} - media, err := db.GetMediaItem(context.Background(), pgMediaID) - require.NoError(t, err, "Media item should exist in database") - - if title, ok := expected["title"]; ok { - if t, ok := title.(string); ok { - assert.Equal(t, t, media.Title, "Title should match") - } - } - if tags, ok := expected["tags"]; ok { - if tagArray, ok := tags.([]string); ok { - assert.ElementsMatch(t, tagArray, media.Tags, "Tags should match") - } - } - // Add more fields as needed -} -``` - -**Usage in tests:** -```go -// Instead of manual DB queries: -device := setup.CreateDevice(t, "Test Device", "koreader", "test-123") - -// NEW: Use helper -verifyDeviceCreated(t, setup.DB, device.ID, DeviceTestData{ - Name: "Test Device", - Type: "koreader", - Identifier: "test-123", -}) -``` - ---- - -#### Helper 2: Request Builder Functions - -```go -// Add to test_helpers.go after database verification functions - -// buildLoginRequest creates a login request map -func buildLoginRequest(email, password string) map[string]interface{} { - return map[string]interface{}{ - "login": email, - "password": password, - } -} - -// buildDeviceUpdateRequest creates a device update request map -func buildDeviceUpdateRequest(name string, syncEnabled *bool, syncFreq *int32) map[string]interface{} { - req := map[string]interface{}{} - if name != "" { - req["device_name"] = name - } - if syncEnabled != nil { - req["sync_enabled"] = *syncEnabled - } - if syncFreq != nil { - req["sync_frequency_minutes"] = *syncFreq - } - return req -} - -// buildMediaItemRequest creates a media item request map -func buildMediaItemRequest(libraryID, title, author, filePath, mimeType string, fileSize int64) map[string]interface{} { - return map[string]interface{}{ - "library_id": libraryID, - "title": title, - "author": author, - "file_path": filePath, - "file_size": fileSize, - "mime_type": mimeType, - } -} - -// buildUserUpdateRequest creates a user profile update request map -func buildUserUpdateRequest(firstName, lastName, email, theme string) map[string]interface{} { - req := map[string]interface{}{} - if firstName != "" { - req["first_name"] = firstName - } - if lastName != "" { - req["last_name"] = lastName - } - if email != "" { - req["email"] = email - } - if theme != "" { - req["theme"] = theme - } - return req -} -``` - -**Usage in tests:** -```go -// BEFORE: Manually build maps -loginRequest := map[string]interface{}{ - "login": email, - "password": password, -} - -// AFTER: Use helper -loginRequest := buildLoginRequest(email, password) -``` - ---- - -#### Helper 3: Struct-Based Response Assertions - -```go -// Add to test_helpers.go after request builder functions - -import ( - "bookhoard/internal/handlers" - // ... existing imports -) - -// assertDeviceListResponse parses and asserts DeviceListResponse -func assertDeviceListResponse(t *testing.T, body []byte) handlers.DeviceListResponse { - var response handlers.DeviceListResponse - err := json.Unmarshal(body, &response) - require.NoError(t, err, "Response should match DeviceListResponse schema") - return response -} - -// assertDeviceUpdateResponse parses and asserts DeviceUpdateResponse -func assertDeviceUpdateResponse(t *testing.T, body []byte) handlers.DeviceUpdateResponse { - var response handlers.DeviceUpdateResponse - err := json.Unmarshal(body, &response) - require.NoError(t, err, "Response should match DeviceUpdateResponse schema") - return response -} - -// assertLoginResponse parses and asserts LoginResponse -func assertLoginResponse(t *testing.T, body []byte) handlers.LoginResponse { - var response handlers.LoginResponse - err := json.Unmarshal(body, &response) - require.NoError(t, err, "Response should match LoginResponse schema") - return response -} - -// assertMediaListResponse parses and asserts MediaListResponse -func assertMediaListResponse(t *testing.T, body []byte) handlers.MediaListResponse { - var response handlers.MediaListResponse - err := json.Unmarshal(body, &response) - require.NoError(t, err, "Response should match MediaListResponse schema") - return response -} - -// assertLibraryResponse parses and asserts LibraryResponse -func assertLibraryResponse(t *testing.T, body []byte) handlers.LibraryResponse { - var response handlers.LibraryResponse - err := json.Unmarshal(body, &response) - require.NoError(t, err, "Response should match LibraryResponse schema") - return response -} -``` - -**Usage in tests:** -```go -// BEFORE: Map-based parsing -var response map[string]interface{} -json.Unmarshal(rec.Body.Bytes(), &response) -deviceName := response["device"].(map[string]interface{})["device_name"].(string) -assert.Equal(t, "Test Device", deviceName) - -// AFTER: Struct-based parsing -response := assertDeviceListResponse(t, rec.Body.Bytes()) -assert.Equal(t, "Test Device", response.Devices[0].DeviceName) -``` - ---- - -#### Helper 4: Concurrent Test Utilities - -```go -// Add to test_helpers.go after assertion helpers - -// runConcurrent executes functions concurrently and waits for completion -func runConcurrent(t *testing.T, numWorkers int, fns []func() error) []error { - errors := make(chan error, len(fns)) - var wg sync.WaitGroup - - for i := 0; i < len(fns); i++ { - wg.Add(1) - go func(idx int) { - defer wg.Done() - if err := fns[idx](); err != nil { - errors <- err - } - }(i) - } - - wg.Wait() - close(errors) - - var allErrors []error - for err := range errors { - allErrors = append(allErrors, err) - } - return allErrors -} - -// retryOperation retries a function with exponential backoff -func retryOperation(maxRetries int, initialDelay time.Duration, fn func() error) error { - var err error - delay := initialDelay - - for attempt := 0; attempt < maxRetries; attempt++ { - if err = fn(); err == nil { - return nil - } - - if attempt < maxRetries-1 { - time.Sleep(delay) - delay *= 2 // Exponential backoff - } - } - return err -} -``` - -**Usage in tests:** -```go -// BEFORE: Manual goroutine management -var wg sync.WaitGroup -errors := make(chan error, 3) -for i := 0; i < 3; i++ { - wg.Add(1) - go func(idx int) { - defer wg.Done() - // ... operation ... - }(i) -} -wg.Wait() -close(errors) - -// AFTER: Helper function -fns := []func() error{ - func() error { /* operation 1 */ return nil }, - func() error { /* operation 2 */ return nil }, - func() error { /* operation 3 */ return nil }, -} -errors := runConcurrent(t, len(fns), fns) -``` - ---- - -### Updated test_helpers.go Structure - -```go -// Line 1: Add handlers import -import ( - "bookhoard/internal/config" - "bookhoard/internal/database" - "bookhoard/internal/handlers" // NEW - "bookhoard/internal/middleware" - // ... rest of imports -) - -// Line 210: FIX return type -// BEFORE: -return fmt.Errorf("user already exists: %s", existingUser.Email) - -// AFTER: -// Remove early return, let code continue to delete + recreate - -// Line 213-236: FIX dead code -// These lines are now reachable after removing early return -// Keep the CreateUser logic - -// After line 562: ADD new helper functions -// (See Helper Functions section above) -``` - ---- - -### File-by-File Changes: test_helpers.go - -| Lines | Change | Impact | -|--------|---------|--------| -| **4-31** | Add `"bookhoard/internal/handlers"` import | Enables struct-based assertions | -| **210** | Fix return type from `UserTestData` to error, or remove early return | Fixes compilation error | -| **207-214** | Remove early return or fix dead code | Makes user creation code reachable | -| **562+** | Add verification helpers (5 functions) | Reduces duplication, ensures DB verification | -| **562+** | Add request builders (4 functions) | Reduces map duplication | -| **562+** | Add struct assertion helpers (5 functions) | Enables compile-time safety | -| **562+** | Add concurrent utilities (2 functions) | Simplifies concurrent tests | - -**Total Lines Added**: ~300 lines -**Total Lines Modified**: ~10 lines -**Functions Added**: 16 new helper functions - ---- - -### Summary of test_helpers.go Improvements - -| Category | Before | After | Improvement | -|----------|---------|--------|-------------| -| **Critical Bugs** | 2 compilation errors | 0 | All tests compile | -| **Code Duplication** | Map-based requests repeated everywhere | Shared builder functions | Consistent request building | -| **Database Verification** | Manual queries scattered | Helper functions | Guaranteed verification | -| **Type Safety** | map[string]interface{} | Struct-based assertions | Compile-time error detection | -| **Concurrent Tests** | Manual goroutine management | Utility functions | Consistent patterns | -| **Maintainability** | Logic duplicated in tests | Centralized helpers | Easier to update | - ---- - -**END OF PLAN** - - ---- - -# END OF COMPREHENSIVE REVIEW REPORT - -**Date**: 2025-02-13 -**Scope**: Complete review of entire test system (30 test files, ~10,941 lines) -**Purpose**: Identify ALL gaps in TEST_RELIABILITY_PLAN.md and ensure complete coverage - ---- - -## Executive Summary - -The comprehensive review identified **69 individual issues** across **30 test files**. Critical findings: - -### Most Critical Gaps - -| Gap | Impact | Priority | -|------|---------|----------| -| **test_helpers.go has 2 critical bugs** | Tests fail compilation, user creation broken | ๐Ÿ”ด CRITICAL | -| **69% of tests use map-based assertions** | Silent API changes, type errors | ๐Ÿ”ด CRITICAL | -| **85% of tests lack DB verification** | Data corruption, silent failures | ๐Ÿ”ด CRITICAL | -| **96% of tests lack concurrency** | Race conditions in production | ๐ŸŸ  HIGH | -| **10 test files not in plan** | Missing coverage, unknown gaps | ๐ŸŸก MEDIUM | -| **Zero Unicode edge cases** | Encoding failures, search issues | ๐ŸŸ  HIGH | -| **Unit tests give false confidence** | Mock handlers != real integration | ๐ŸŸก MEDIUM | - -### Recommended Priority Change - -**Original**: Phase 1 โ†’ 2 โ†’ 3 โ†’ 4 -**Revised**: **Phase 0** (NEW) โ†’ 1 โ†’ 2 โ†’ 3 - -**Phase 0: Fix Test Infrastructure** (NEW - 2-3 hours) -1. Fix 2 critical bugs in test_helpers.go -2. Add 6 missing helper functions -3. Move Unicode tests from Phase 4 โ†’ Phase 1 -4. Create test isolation improvements - -**Rationale**: Tests depend on test_helpers.go. Bugs there block all other work. Unicode too important to delay. - diff --git a/baseline-results.txt b/baseline-results.txt deleted file mode 100644 index b702258..0000000 --- a/baseline-results.txt +++ /dev/null @@ -1,3016 +0,0 @@ -Building test containers... -podman compose --profile tests build ->>>> Executing external compose provider "/usr/bin/podman-compose". Please see podman-compose(1) for how to disable this message. <<<< - -[1/2] STEP 1/15: FROM golang:1.25-alpine AS builder -[1/3] STEP 1/15: FROM golang:1.25-alpine AS builder -[1/2] STEP 2/15: WORKDIR /app -[1/3] STEP 2/15: WORKDIR /app ---> Using cache 0808b2875456ca99b7a8f5a6cad4e081b60104568bdd05f992cade2aa079e7a8 ---> 0808b2875456 ---> Using cache 0808b2875456ca99b7a8f5a6cad4e081b60104568bdd05f992cade2aa079e7a8 ---> 0808b2875456 -[1/2] STEP 3/15: RUN apk add --no-cache nodejs npm curl -[1/3] STEP 3/15: RUN apk add --no-cache nodejs npm curl ---> Using cache eb561d60d14bf9cbadd568a0bc38f7385f4d247a5c5f5944678f62274c1bf699 ---> eb561d60d14b ---> Using cache eb561d60d14bf9cbadd568a0bc38f7385f4d247a5c5f5944678f62274c1bf699 ---> eb561d60d14b -[1/2] STEP 4/15: RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest -[1/3] STEP 4/15: RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest ---> Using cache af8c96bd5a986ef93027119dc0adc1981a6eecf1454df089386579f90a907e7b ---> af8c96bd5a98 ---> Using cache af8c96bd5a986ef93027119dc0adc1981a6eecf1454df089386579f90a907e7b ---> af8c96bd5a98 -[1/2] STEP 5/15: RUN go install github.com/a-h/templ/cmd/templ@latest -[1/3] STEP 5/15: RUN go install github.com/a-h/templ/cmd/templ@latest ---> Using cache 92f52680222cf9474514079a50410702a2b85ef2f6d0ea5a43e41c8b2355be0d ---> 92f52680222c ---> Using cache 92f52680222cf9474514079a50410702a2b85ef2f6d0ea5a43e41c8b2355be0d ---> 92f52680222c -[1/2] STEP 6/15: COPY package*.json ./ -[1/3] STEP 6/15: COPY package*.json ./ ---> Using cache c1ac6e2d32bff67b34f42497038f789ab2b937c34871f0e37e49738786636d3f ---> c1ac6e2d32bf -[1/2] STEP 7/15: RUN npm install ---> Using cache c1ac6e2d32bff67b34f42497038f789ab2b937c34871f0e37e49738786636d3f ---> c1ac6e2d32bf -[1/3] STEP 7/15: RUN npm install ---> Using cache b61c1bf0531a5bff95a37f402d121aff95e589972a88c9285d61d99bb7578042 ---> b61c1bf0531a -[1/2] STEP 8/15: COPY . . ---> Using cache b61c1bf0531a5bff95a37f402d121aff95e589972a88c9285d61d99bb7578042 ---> b61c1bf0531a -[1/3] STEP 8/15: COPY . . ---> 5ad7c6b029ce ---> dd5f96ac6156 -[1/2] STEP 9/15: RUN go mod tidy -[1/3] STEP 9/15: RUN go mod tidy -go: downloading github.com/jackc/pgx/v5 v5.4.3 -go: downloading github.com/golang-jwt/jwt/v5 v5.3.0 -go: downloading github.com/labstack/echo/v4 v4.13.4 -go: downloading github.com/google/uuid v1.4.0 -go: downloading github.com/gorilla/websocket v1.5.3 -go: downloading golang.org/x/crypto v0.46.0 -go: downloading github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e -go: downloading github.com/stretchr/testify v1.11.1 -go: downloading github.com/yuin/goldmark v1.7.16 -go: downloading github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 -go: downloading github.com/labstack/echo/v4 v4.13.4 -go: downloading github.com/google/uuid v1.4.0 -go: downloading github.com/stretchr/testify v1.11.1 -go: downloading github.com/jackc/pgx/v5 v5.4.3 -go: downloading github.com/go-playground/validator/v10 v10.30.1 -go: downloading github.com/golang-jwt/jwt/v5 v5.3.0 -go: downloading github.com/gorilla/websocket v1.5.3 -go: downloading github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e -go: downloading golang.org/x/crypto v0.46.0 -go: downloading golang.org/x/text v0.33.0 -go: downloading github.com/yuin/goldmark v1.7.16 -go: downloading golang.org/x/text v0.33.0 -go: downloading github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 -go: downloading github.com/ArcadiaLin/go-epub v0.1.1 -go: downloading github.com/bodgit/sevenzip v1.6.1 -go: downloading github.com/fsnotify/fsnotify v1.9.0 -go: downloading github.com/labstack/echo-jwt/v4 v4.4.0 -go: downloading github.com/ArcadiaLin/go-epub v0.1.1 -go: downloading github.com/bodgit/sevenzip v1.6.1 -go: downloading github.com/nwaples/rardecode v1.1.3 -go: downloading github.com/fsnotify/fsnotify v1.9.0 -go: downloading github.com/nwaples/rardecode v1.1.3 -go: downloading github.com/pmezard/go-difflib v1.0.0 -go: downloading github.com/labstack/gommon v0.4.2 -go: downloading golang.org/x/net v0.47.0 -go: downloading github.com/valyala/fasttemplate v1.2.2 -go: downloading golang.org/x/time v0.14.0 -go: downloading github.com/go-playground/validator/v10 v10.30.1 -go: downloading github.com/jackc/puddle/v2 v2.2.1 -go: downloading github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a -go: downloading github.com/gabriel-vasile/mimetype v1.4.12 -go: downloading github.com/go-playground/universal-translator v0.18.1 -go: downloading github.com/leodido/go-urn v1.4.0 -go: downloading github.com/labstack/echo-jwt/v4 v4.4.0 -go: downloading github.com/go-playground/locales v0.14.1 -go: downloading github.com/pmezard/go-difflib v1.0.0 -go: downloading github.com/labstack/gommon v0.4.2 -go: downloading golang.org/x/net v0.47.0 -go: downloading golang.org/x/sys v0.39.0 -go: downloading golang.org/x/sys v0.39.0 -go: downloading github.com/gabriel-vasile/mimetype v1.4.12 -go: downloading github.com/go-playground/universal-translator v0.18.1 -go: downloading github.com/mattn/go-colorable v0.1.14 -go: downloading github.com/valyala/bytebufferpool v1.0.0 -go: downloading github.com/leodido/go-urn v1.4.0 -go: downloading github.com/bodgit/plumbing v1.3.0 -go: downloading github.com/bodgit/windows v1.0.1 -go: downloading github.com/spf13/afero v1.11.0 -go: downloading go4.org v0.0.0-20200411211856-f5505b9728dd -go: downloading golang.org/x/sync v0.19.0 -go: downloading github.com/go-playground/locales v0.14.1 -go: downloading github.com/valyala/fasttemplate v1.2.2 -go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 -go: downloading github.com/andybalholm/brotli v1.1.1 -go: downloading golang.org/x/time v0.14.0 -go: downloading github.com/alecthomas/chroma v0.10.0 -go: downloading github.com/klauspost/compress v1.17.11 -go: downloading github.com/pierrec/lz4/v4 v4.1.22 -go: downloading github.com/ulikunitz/xz v0.5.12 -go: downloading github.com/alecthomas/chroma v0.10.0 -go: downloading github.com/mattn/go-colorable v0.1.14 -go: downloading github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a -go: downloading github.com/bodgit/plumbing v1.3.0 -go: downloading github.com/jackc/puddle/v2 v2.2.1 -go: downloading github.com/bodgit/windows v1.0.1 -go: downloading github.com/spf13/afero v1.11.0 -go: downloading go4.org v0.0.0-20200411211856-f5505b9728dd -go: downloading golang.org/x/sync v0.19.0 -go: downloading github.com/valyala/bytebufferpool v1.0.0 -go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 -go: downloading github.com/andybalholm/brotli v1.1.1 -go: downloading github.com/klauspost/compress v1.17.11 -go: downloading github.com/ulikunitz/xz v0.5.12 -go: downloading github.com/pierrec/lz4/v4 v4.1.22 -go: downloading github.com/dlclark/regexp2 v1.4.0 -go: downloading github.com/dlclark/regexp2 v1.4.0 -go: downloading github.com/google/go-cmp v0.6.0 -go: downloading github.com/go-playground/assert/v2 v2.2.0 -go: downloading gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c -go: downloading github.com/stretchr/objx v0.5.2 -go: downloading github.com/xyproto/randomstring v1.0.5 -go: downloading github.com/kr/pretty v0.3.0 -go: downloading github.com/google/go-cmp v0.6.0 -go: downloading github.com/go-playground/assert/v2 v2.2.0 -go: downloading gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c -go: downloading github.com/stretchr/objx v0.5.2 -go: downloading github.com/xyproto/randomstring v1.0.5 -go: downloading github.com/rogpeppe/go-internal v1.14.1 -go: downloading github.com/kr/text v0.2.0 -go: downloading github.com/kr/pretty v0.3.0 -go: downloading github.com/rogpeppe/go-internal v1.14.1 -go: downloading github.com/kr/text v0.2.0 ---> 28420f7452ef -[1/3] STEP 10/15: RUN cd internal/database && sqlc generate ---> 6514ab89da2c ---> 90376cb5bb57 -[1/3] STEP 11/15: RUN cd templates && templ generate -[1/2] STEP 10/15: RUN cd internal/database && sqlc generate -(โœ“) Complete [ updates=0 duration=30.014983ms ] ---> 2a326d868f4a -[1/3] STEP 12/15: RUN npm run build:css:prod - -> bookhoard@1.0.0 build:css:prod -> tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify - -Browserslist: caniuse-lite is outdated. Please run: - npx update-browserslist-db@latest - Why you should do it regularly: https://github.com/browserslist/update-db#readme - -Rebuilding... ---> 65f6a4e5222b -[1/2] STEP 11/15: RUN cd templates && templ generate -(โœ“) Complete [ updates=0 duration=28.343633ms ] ---> d55c3d86391e -[1/2] STEP 12/15: RUN npm run build:css:prod - -Done in 713ms. - -> bookhoard@1.0.0 build:css:prod -> tailwindcss -i ./web/static/input.css -o ./web/static/style.css --minify - ---> 07a9c2bba1ba -[1/3] STEP 13/15: RUN npm run postinstall - -> bookhoard@1.0.0 postinstall -> mkdir -p web/static && curl -L https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js -o web/static/htmx.min.js && curl -L https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js -o web/static/highlight.min.js && curl -L https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css -o web/static/highlight-dark.min.css && curl -L https://cdn.jsdelivr.net/npm/lunr@2.3.9/lunr.min.js -o web/static/lunr.min.js && curl -L https://cdn.jsdelivr.net/npm/lunr-flex@1.0.5/lunr.flex.min.js -o web/static/lunr-flex.min.js - -Browserslist: caniuse-lite is outdated. Please run: - npx update-browserslist-db@latest - Why you should do it regularly: https://github.com/browserslist/update-db#readme - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 -Rebuilding... - 44 47755 44 21381 0 0 123300 0 --:--:-- --:--:-- --:--:-- 122879 100 47755 100 47755 0 0 273598 0 --:--:-- --:--:-- --:--:-- 272885 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 121727 0 121727 0 0 527625 0 --:--:-- --:--:-- --:--:-- 529247 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 1315 0 1315 0 0 4681 0 --:--:-- --:--:-- --:--:-- 4696 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 -Done in 670ms. - 100 29510 0 29510 0 0 179521 0 --:--:-- --:--:-- --:--:-- 179939 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0--> eb8cdc07fc41 -[1/2] STEP 13/15: RUN npm run postinstall - -> bookhoard@1.0.0 postinstall -> mkdir -p web/static && curl -L https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js -o web/static/htmx.min.js && curl -L https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js -o web/static/highlight.min.js && curl -L https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css -o web/static/highlight-dark.min.css && curl -L https://cdn.jsdelivr.net/npm/lunr@2.3.9/lunr.min.js -o web/static/lunr.min.js && curl -L https://cdn.jsdelivr.net/npm/lunr-flex@1.0.5/lunr.flex.min.js -o web/static/lunr-flex.min.js - - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 43 0 43 0 0 84 0 --:--:-- --:--:-- --:--:-- 84 - 100 47755 100 47755 0 0 300991 0 --:--:-- --:--:-- --:--:-- 302246 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 121727 0 121727 0 0 1414k 0 --:--:-- --:--:-- --:--:-- 1432k - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0--> e6c4f9e2c47c - 100 1315 0 1315 0 0 19684 0 --:--:-- --:--:-- --:--:-- 19924 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Uplo[1/3] STEP 14/15: RUN npm run build:ts -ad Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 100 29510 0 29510 0 0 283379 0 --:--:-- --:--:-- --:--:-- 286504 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 -> bookhoard@1.0.0 build:ts -> tsc - - 100 43 0 43 0 0 235 0 --:--:-- --:--:-- --:--:-- 236 ---> 5cb5267bd0c4 -[1/2] STEP 14/15: RUN npm run build:ts - -> bookhoard@1.0.0 build:ts -> tsc - ---> fcd3bd84f759 -[1/3] STEP 15/15: RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server ---> 259f93e65704 -[1/2] STEP 15/15: RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server ---> d646a7816646 -[3/3] STEP 1/11: FROM alpine:latest -[3/3] STEP 2/11: RUN apk --no-cache add ca-certificates curl ---> Using cache 24f434a4d4b2f86022c7c8601a1c29a1cb847c3474f464de20f96e33a955458a ---> 24f434a4d4b2 -[3/3] STEP 3/11: RUN wget -O /usr/bin/kepubify https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-linux-64bit && chmod +x /usr/bin/kepubify ---> Using cache 89b60cb168ecb44333d5e10e232a5f6c4b8d67e4e6440321834c1ad3670bdf16 ---> 89b60cb168ec -[3/3] STEP 4/11: WORKDIR /root/ ---> cffd529c8575 -[2/2] STEP 1/9: FROM golang:1.25-alpine AS test-runner ---> Using cache 868afc2e0de720355d7ba7c5a09cdce664820eb2d65d3771590241e41b6f328e ---> 868afc2e0de7 -[3/3] STEP 5/11: COPY --from=builder /app/main . ---> Using cache 1f5b8665077cb5ae5a9b4125b022799b0974874119fe68ecd6a42bebe3d25be7 ---> 1f5b8665077c -[3/3] STEP 6/11: COPY --from=builder /app/database/schema ./database/schema ---> Using cache 5c4d70b0e5efafffe1f7d405c0add1e7a5941f4a73f9cf138e0fadb7f23d631c ---> 5c4d70b0e5ef -[3/3] STEP 7/11: COPY --from=builder /app/templates ./templates ---> Using cache 3e20abb997265eec8842ed058ed1ea98d8fab984c8cfe21e9c8c1f2f7ac48bf4 ---> 3e20abb99726 -[3/3] STEP 8/11: COPY --from=builder /app/web ./web ---> Using cache 0c6cc3ef432b808134110dcc88aaaf8028c58f87d90589feb64b64fdce4a6001 ---> 0c6cc3ef432b -[3/3] STEP 9/11: COPY --from=builder /app/docs ./docs -[2/2] STEP 2/9: RUN apk --no-cache add ca-certificates curl ---> Using cache 6eb0ced3e2522d1cfd8471f241b417c8471160a7b4bf050f9f43382196ed0af3 ---> 6eb0ced3e252 -[2/2] STEP 3/9: WORKDIR /app ---> Using cache 0649cbac642ebe3d774c709a99552fce1677f49223e5f477fa5871980ba91610 ---> 0649cbac642e -[2/2] STEP 4/9: COPY --from=builder /app ./ ---> Using cache 3e18f85140d8c979a17f4f3c68387846fc17453fde1d0f85e85484db53e9abee ---> 3e18f85140d8 -[3/3] STEP 10/11: EXPOSE 8765 ---> Using cache 05041d3ba57c72298f48007a0692677bf558d1eaae156ce570c9f4341295fba9 ---> 05041d3ba57c -[3/3] STEP 11/11: CMD ["./main"] ---> Using cache 9b1fd614513a56e78c7b57bb322f85ca405d95a3e30c1dd95e0197d0b93735a0 -[3/3] COMMIT bookhoard_app ---> 9b1fd614513a -Successfully tagged localhost/bookhoard_app:latest -9b1fd614513a56e78c7b57bb322f85ca405d95a3e30c1dd95e0197d0b93735a0 ---> 2f1418b716f9 -[2/2] STEP 5/9: RUN wget -O /usr/bin/kepubify https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-linux-64bit && chmod +x /usr/bin/kepubify -Connecting to github.com (140.82.112.3:443) -Connecting to github.com (140.82.112.3:443) -Connecting to release-assets.githubusercontent.com (185.199.111.133:443) -saving to '/usr/bin/kepubify' -kepubify 100% |********************************| 3492k 0:00:00 ETA -'/usr/bin/kepubify' saved ---> ef6c877a5a21 -[2/2] STEP 6/9: ENV TEST_MODE=true ---> 9e1d5c94c2c1 -[2/2] STEP 7/9: ENV RATE_LIMIT_ENABLED=false ---> 841a79a50b90 -[2/2] STEP 8/9: ENV REQUESTS_PER_MINUTE=1000 ---> d1628aadea4d -[2/2] STEP 9/9: CMD ["go", "test", "./cmd/server/tests", "-v", "-timeout", "5m"] -[2/2] COMMIT bookhoard_tests ---> 5bbfb3146dd9 -Successfully tagged localhost/bookhoard_tests:latest -5bbfb3146dd9451c728e47e5fdaa620419ad2a42605d9afe30ba775b3a19c98d -Starting application containers... -podman compose up -d db app ->>>> Executing external compose provider "/usr/bin/podman-compose". Please see podman-compose(1) for how to disable this message. <<<< - -bookhoard -bookhoard_db -bookhoard -bookhoard_db -d5a5d804ef3909d3927ec4932b750079cac6e6c2d0d130064d5015835aa1a803 -bookhoard_default -4a722ee35bf8e3d1740d7e5d7fa32e578e87b75422d3a016ed5c3a363012a917 -c0c5e8ddeb9a62c5a0799de1910ef9fce13433ce0d00c438add55d37d9bc1e27 -fd69d5102cbc276a972130623803f95cdea1e9dd349c42f1f7c3d3c942445cee -bookhoard_db -bookhoard -Waiting for services to be healthy... - โœ“ Database is ready - โœ“ Application is ready - -Running integration tests in container... -podman compose --profile tests run --rm tests ->>>> Executing external compose provider "/usr/bin/podman-compose". Please see podman-compose(1) for how to disable this message. <<<< - -bookhoard_db -bookhoard -bookhoard -bookhoard_db -fdc3cad6e696cb3a20aff96bca162e541cedd79f7322780c729881ecc3b4cbcd -af93ff4ab4d8eaeaace779b86fd494a2ae5fc9354daded5e1b908560e68c219d -bookhoard_db -bookhoard -time="2026-02-10T11:47:37-05:00" level=warning msg="The input device is not a TTY. The --tty and --interactive flags might not work properly" -go: downloading github.com/labstack/echo/v4 v4.13.4 -go: downloading github.com/jackc/pgx/v5 v5.4.3 -go: downloading github.com/google/uuid v1.4.0 -go: downloading github.com/go-playground/validator/v10 v10.30.1 -go: downloading github.com/stretchr/testify v1.11.1 -go: downloading github.com/gorilla/websocket v1.5.3 -go: downloading github.com/ArcadiaLin/go-epub v0.1.1 -go: downloading github.com/bodgit/sevenzip v1.6.1 -go: downloading github.com/fsnotify/fsnotify v1.9.0 -go: downloading github.com/nwaples/rardecode v1.1.3 -go: downloading github.com/golang-jwt/jwt/v5 v5.3.0 -go: downloading github.com/labstack/echo-jwt/v4 v4.4.0 -go: downloading github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e -go: downloading golang.org/x/crypto v0.46.0 -go: downloading golang.org/x/text v0.33.0 -go: downloading github.com/yuin/goldmark v1.7.16 -go: downloading github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 -go: downloading github.com/a-h/templ v0.3.977 -go: downloading golang.org/x/sys v0.39.0 -go: downloading github.com/davecgh/go-spew v1.1.1 -go: downloading github.com/pmezard/go-difflib v1.0.0 -go: downloading github.com/labstack/gommon v0.4.2 -go: downloading golang.org/x/net v0.47.0 -go: downloading github.com/valyala/fasttemplate v1.2.2 -go: downloading golang.org/x/time v0.14.0 -go: downloading github.com/bodgit/plumbing v1.3.0 -go: downloading github.com/gabriel-vasile/mimetype v1.4.12 -go: downloading github.com/go-playground/universal-translator v0.18.1 -go: downloading github.com/leodido/go-urn v1.4.0 -go: downloading github.com/bodgit/windows v1.0.1 -go: downloading github.com/spf13/afero v1.11.0 -go: downloading go4.org v0.0.0-20200411211856-f5505b9728dd -go: downloading github.com/jackc/puddle/v2 v2.2.1 -go: downloading github.com/jackc/pgpassfile v1.0.0 -go: downloading github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a -go: downloading gopkg.in/yaml.v3 v3.0.1 -go: downloading github.com/alecthomas/chroma v0.10.0 -go: downloading github.com/mattn/go-colorable v0.1.14 -go: downloading github.com/mattn/go-isatty v0.0.20 -go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 -go: downloading github.com/andybalholm/brotli v1.1.1 -go: downloading github.com/klauspost/compress v1.17.11 -go: downloading github.com/pierrec/lz4/v4 v4.1.22 -go: downloading github.com/ulikunitz/xz v0.5.12 -go: downloading github.com/valyala/bytebufferpool v1.0.0 -go: downloading github.com/go-playground/locales v0.14.1 -go: downloading golang.org/x/sync v0.19.0 -go: downloading github.com/dlclark/regexp2 v1.4.0 -=== RUN TestAnalyticsReadingStats -=== RUN TestAnalyticsReadingStats/GetReadingStats_WithoutAuth -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 [REQUEST] {"request_id":"4fabb6e9-5a70-4d50-a3ca-7e057b75524b","timestamp":"2026-02-10T16:47:50.555779989Z","method":"GET","path":"/api/analytics/reading-stats","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":36418,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:50.556798488Z","id":"4fabb6e9-5a70-4d50-a3ca-7e057b75524b","remote_ip":"127.0.0.1","host":"127.0.0.1:36963","method":"GET","uri":"/api/analytics/reading-stats","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":1017497,"latency_human":"1.017497ms","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:50.556807976Z","id":"4fabb6e9-5a70-4d50-a3ca-7e057b75524b","remote_ip":"127.0.0.1","host":"127.0.0.1:36963","method":"GET","uri":"/api/analytics/reading-stats","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":1027485,"latency_human":"1.027485ms","bytes_in":0,"bytes_out":39} -=== RUN TestAnalyticsReadingStats/GetReadingStats_WithAuth_DefaultDates -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '0c6da91f-6218-4af7-82b3-06d00374b66f' -2026/02/10 16:47:50 [REQUEST] {"request_id":"1b53fff5-93bb-4ca9-b547-2ae3b1afbc97","timestamp":"2026-02-10T16:47:50.60807043Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49586645,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:50.657683373Z","id":"1b53fff5-93bb-4ca9-b547-2ae3b1afbc97","remote_ip":"127.0.0.1","host":"127.0.0.1:42557","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49605560,"latency_human":"49.60556ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:50.657692681Z","id":"1b53fff5-93bb-4ca9-b547-2ae3b1afbc97","remote_ip":"127.0.0.1","host":"127.0.0.1:42557","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49620478,"latency_human":"49.620478ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:50 [REQUEST] {"request_id":"dae0d970-9213-46fa-9f6f-2246a9859c51","timestamp":"2026-02-10T16:47:50.657895717Z","method":"GET","path":"/api/analytics/reading-stats","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzAsImlhdCI6MTc3MDc0MjA3MCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijg3YTVmMDVjLTI2NTMtNDI2NS05Mzk2LTY5MjhmZjYzZjc4YyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.WsSVGUKfuu9q5Yj4BwqrXPjfD8l24vcgMPcb07bmXns","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2152743,"status_code":200,"response_size":218} -{"time":"2026-02-10T16:47:50.66005887Z","id":"dae0d970-9213-46fa-9f6f-2246a9859c51","remote_ip":"127.0.0.1","host":"127.0.0.1:42557","method":"GET","uri":"/api/analytics/reading-stats","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2163102,"latency_human":"2.163102ms","bytes_in":0,"bytes_out":218} -{"time":"2026-02-10T16:47:50.660062276Z","id":"dae0d970-9213-46fa-9f6f-2246a9859c51","remote_ip":"127.0.0.1","host":"127.0.0.1:42557","method":"GET","uri":"/api/analytics/reading-stats","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2167110,"latency_human":"2.16711ms","bytes_in":0,"bytes_out":218} -=== RUN TestAnalyticsReadingStats/GetReadingStats_WithCustomDateRange -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '8ff8b8ae-0904-4986-9ce6-eafe31e546d5' -2026/02/10 16:47:50 [REQUEST] {"request_id":"41675bba-5258-4cc6-8cc3-7ed8d7c3ac0e","timestamp":"2026-02-10T16:47:50.680789412Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49852649,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:50.730682235Z","id":"41675bba-5258-4cc6-8cc3-7ed8d7c3ac0e","remote_ip":"127.0.0.1","host":"127.0.0.1:35441","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49890019,"latency_human":"49.890019ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:50.730694217Z","id":"41675bba-5258-4cc6-8cc3-7ed8d7c3ac0e","remote_ip":"127.0.0.1","host":"127.0.0.1:35441","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49904946,"latency_human":"49.904946ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:50 [REQUEST] {"request_id":"507dfcb4-0653-4426-8182-fa3f04047d74","timestamp":"2026-02-10T16:47:50.731080794Z","method":"GET","path":"/api/analytics/reading-stats","query_params":{"end_date":"2026-02-10","start_date":"2025-12-10"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzAsImlhdCI6MTc3MDc0MjA3MCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjYzNGFkMjc5LWYwYTUtNDZkNC1iNjU4LTU4ZThiZjYxYWI5MCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.i_msFAEiF6vim4Ry5xlR2zq34wf9pDs0AVzpqESMdBE","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2151620,"status_code":200,"response_size":218} -{"time":"2026-02-10T16:47:50.733260958Z","id":"507dfcb4-0653-4426-8182-fa3f04047d74","remote_ip":"127.0.0.1","host":"127.0.0.1:35441","method":"GET","uri":"/api/analytics/reading-stats?start_date=2025-12-10&end_date=2026-02-10","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2179212,"latency_human":"2.179212ms","bytes_in":0,"bytes_out":218} -{"time":"2026-02-10T16:47:50.733265978Z","id":"507dfcb4-0653-4426-8182-fa3f04047d74","remote_ip":"127.0.0.1","host":"127.0.0.1:35441","method":"GET","uri":"/api/analytics/reading-stats?start_date=2025-12-10&end_date=2026-02-10","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2185574,"latency_human":"2.185574ms","bytes_in":0,"bytes_out":218} -=== RUN TestAnalyticsReadingStats/GetReadingStats_InvalidStartDate -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '4b10b8c7-c2a6-4e62-b0b2-0537bd5ea3b6' -2026/02/10 16:47:50 [REQUEST] {"request_id":"d822425e-3bc3-441d-adeb-a28217e109bb","timestamp":"2026-02-10T16:47:50.753939434Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49344205,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:50.803305971Z","id":"d822425e-3bc3-441d-adeb-a28217e109bb","remote_ip":"127.0.0.1","host":"127.0.0.1:41847","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49358271,"latency_human":"49.358271ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:50.803311861Z","id":"d822425e-3bc3-441d-adeb-a28217e109bb","remote_ip":"127.0.0.1","host":"127.0.0.1:41847","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49372729,"latency_human":"49.372729ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:50 [REQUEST] {"request_id":"c0e21d65-5d04-4b98-bb40-b7f577c1e8b4","timestamp":"2026-02-10T16:47:50.803536979Z","method":"GET","path":"/api/analytics/reading-stats","query_params":{"start_date":"invalid-date"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzAsImlhdCI6MTc3MDc0MjA3MCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE5NjBiNTI2LWRiNmQtNDYxYy04MDY5LTVmYzY4NjlmMTA5YiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0._nfxq7bl1TECit5szYGNVgFX3weobLMvDcADzVwomWM","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":42559,"status_code":200,"response_size":0,"error":"code=400, message=invalid start_date format"} -{"time":"2026-02-10T16:47:50.80360783Z","id":"c0e21d65-5d04-4b98-bb40-b7f577c1e8b4","remote_ip":"127.0.0.1","host":"127.0.0.1:41847","method":"GET","uri":"/api/analytics/reading-stats?start_date=invalid-date","user_agent":"Go-http-client/1.1","status":400,"error":"code=400, message=invalid start_date format","latency":70341,"latency_human":"70.341ยตs","bytes_in":0,"bytes_out":40} -{"time":"2026-02-10T16:47:50.803616206Z","id":"c0e21d65-5d04-4b98-bb40-b7f577c1e8b4","remote_ip":"127.0.0.1","host":"127.0.0.1:41847","method":"GET","uri":"/api/analytics/reading-stats?start_date=invalid-date","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":80028,"latency_human":"80.028ยตs","bytes_in":0,"bytes_out":40} -=== RUN TestAnalyticsReadingStats/GetReadingStats_InvalidEndDate -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '7aa7b9bd-54fd-4f11-a44f-accf207c0704' -2026/02/10 16:47:50 [REQUEST] {"request_id":"7fa6026c-5674-48de-9e1e-daf2fe713e4a","timestamp":"2026-02-10T16:47:50.823625089Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50816988,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:50.87446538Z","id":"7fa6026c-5674-48de-9e1e-daf2fe713e4a","remote_ip":"127.0.0.1","host":"127.0.0.1:43207","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50837516,"latency_human":"50.837516ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:50.874474437Z","id":"7fa6026c-5674-48de-9e1e-daf2fe713e4a","remote_ip":"127.0.0.1","host":"127.0.0.1:43207","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50847454,"latency_human":"50.847454ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:50 [REQUEST] {"request_id":"90af1a93-efeb-4f25-82e8-71d3cc152b4d","timestamp":"2026-02-10T16:47:50.874654471Z","method":"GET","path":"/api/analytics/reading-stats","query_params":{"end_date":"not-a-date"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzAsImlhdCI6MTc3MDc0MjA3MCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhmODQyMzYwLTlhYWMtNGFjNS1iZjY0LWEzMjM5YzMwMWUzOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.DAs1JRCMe9gthOByb1tapfDo4MLG7OIPrUrIV2FEkxE","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":52277,"status_code":200,"response_size":0,"error":"code=400, message=invalid end_date format"} -{"time":"2026-02-10T16:47:50.874727026Z","id":"90af1a93-efeb-4f25-82e8-71d3cc152b4d","remote_ip":"127.0.0.1","host":"127.0.0.1:43207","method":"GET","uri":"/api/analytics/reading-stats?end_date=not-a-date","user_agent":"Go-http-client/1.1","status":400,"error":"code=400, message=invalid end_date format","latency":72645,"latency_human":"72.645ยตs","bytes_in":0,"bytes_out":38} -{"time":"2026-02-10T16:47:50.874733057Z","id":"90af1a93-efeb-4f25-82e8-71d3cc152b4d","remote_ip":"127.0.0.1","host":"127.0.0.1:43207","method":"GET","uri":"/api/analytics/reading-stats?end_date=not-a-date","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":79888,"latency_human":"79.888ยตs","bytes_in":0,"bytes_out":38} -=== RUN TestAnalyticsReadingStats/GetReadingStats_EmptyHistory -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'f9b1c878-9e17-4ebb-a159-8099bb54c131' -2026/02/10 16:47:50 [REQUEST] {"request_id":"80fbe3bf-cf17-4d8a-a422-c61fad4fba99","timestamp":"2026-02-10T16:47:50.895620049Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48344131,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:50.943981322Z","id":"80fbe3bf-cf17-4d8a-a422-c61fad4fba99","remote_ip":"127.0.0.1","host":"127.0.0.1:39027","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48359139,"latency_human":"48.359139ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:50.943989006Z","id":"80fbe3bf-cf17-4d8a-a422-c61fad4fba99","remote_ip":"127.0.0.1","host":"127.0.0.1:39027","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48367204,"latency_human":"48.367204ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:50 [REQUEST] {"request_id":"137a53bd-35c6-4908-87f6-b1a61621bdd6","timestamp":"2026-02-10T16:47:50.94421767Z","method":"GET","path":"/api/analytics/reading-stats","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzAsImlhdCI6MTc3MDc0MjA3MCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjgwNzA2NjMzLTNkYzAtNGVhZS05NDdlLWU4ZDIxYzc0YjIwNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.yADZ8EWe3GdijglqVIe6pz4V-xcJKs0PLQfdD_oaeyE","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2644786,"status_code":200,"response_size":218} -{"time":"2026-02-10T16:47:50.946897961Z","id":"137a53bd-35c6-4908-87f6-b1a61621bdd6","remote_ip":"127.0.0.1","host":"127.0.0.1:39027","method":"GET","uri":"/api/analytics/reading-stats","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2678638,"latency_human":"2.678638ms","bytes_in":0,"bytes_out":218} -{"time":"2026-02-10T16:47:50.94690805Z","id":"137a53bd-35c6-4908-87f6-b1a61621bdd6","remote_ip":"127.0.0.1","host":"127.0.0.1:39027","method":"GET","uri":"/api/analytics/reading-stats","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2690811,"latency_human":"2.690811ms","bytes_in":0,"bytes_out":218} ---- PASS: TestAnalyticsReadingStats (0.39s) - --- PASS: TestAnalyticsReadingStats/GetReadingStats_WithoutAuth (0.00s) - --- PASS: TestAnalyticsReadingStats/GetReadingStats_WithAuth_DefaultDates (0.10s) - --- PASS: TestAnalyticsReadingStats/GetReadingStats_WithCustomDateRange (0.07s) - --- PASS: TestAnalyticsReadingStats/GetReadingStats_InvalidStartDate (0.07s) - --- PASS: TestAnalyticsReadingStats/GetReadingStats_InvalidEndDate (0.07s) - --- PASS: TestAnalyticsReadingStats/GetReadingStats_EmptyHistory (0.07s) -=== RUN TestAnalyticsDeviceUsage -=== RUN TestAnalyticsDeviceUsage/GetDeviceUsage_WithoutAuth -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 [REQUEST] {"request_id":"d09c4daa-794b-4fd8-b94b-b4b485c9eb85","timestamp":"2026-02-10T16:47:50.948623312Z","method":"GET","path":"/api/analytics/device-usage","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4879,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:50.948656704Z","id":"d09c4daa-794b-4fd8-b94b-b4b485c9eb85","remote_ip":"127.0.0.1","host":"127.0.0.1:38869","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":31579,"latency_human":"31.579ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:50.948665009Z","id":"d09c4daa-794b-4fd8-b94b-b4b485c9eb85","remote_ip":"127.0.0.1","host":"127.0.0.1:38869","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":41577,"latency_human":"41.577ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestAnalyticsDeviceUsage/GetDeviceUsage_WithAuth_NoDevices -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:50 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '1d801a6a-0ab2-40fd-a04a-7dbd2a7606f0' -2026/02/10 16:47:51 [REQUEST] {"request_id":"93cde909-9539-4bb6-9f49-9062380af87c","timestamp":"2026-02-10T16:47:50.968082776Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49842480,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.017953308Z","id":"93cde909-9539-4bb6-9f49-9062380af87c","remote_ip":"127.0.0.1","host":"127.0.0.1:44897","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49866224,"latency_human":"49.866224ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.017962946Z","id":"93cde909-9539-4bb6-9f49-9062380af87c","remote_ip":"127.0.0.1","host":"127.0.0.1:44897","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49879488,"latency_human":"49.879488ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"9b2cba2a-6791-46ad-8e6d-8e6cb2a9fa1f","timestamp":"2026-02-10T16:47:51.018237174Z","method":"GET","path":"/api/analytics/device-usage","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImQzOGJhYTIyLTk0YzUtNDIyMy05NWY3LTkwZTYyMjUzZjAyYyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.IdQHTtlL2lnGmT7GPkl3LthJfjQejwzeemRB727BMcc","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4223875,"status_code":200,"response_size":15} -{"time":"2026-02-10T16:47:51.022480886Z","id":"9b2cba2a-6791-46ad-8e6d-8e6cb2a9fa1f","remote_ip":"127.0.0.1","host":"127.0.0.1:44897","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":4243591,"latency_human":"4.243591ms","bytes_in":0,"bytes_out":15} -{"time":"2026-02-10T16:47:51.022487989Z","id":"9b2cba2a-6791-46ad-8e6d-8e6cb2a9fa1f","remote_ip":"127.0.0.1","host":"127.0.0.1:44897","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":4251667,"latency_human":"4.251667ms","bytes_in":0,"bytes_out":15} -=== RUN TestAnalyticsDeviceUsage/GetDeviceUsage_WithAuth_WithDevices -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ba3b0597-c81b-469c-bf65-c8304ca061c0' -2026/02/10 16:47:51 [REQUEST] {"request_id":"687d5708-f51c-4e73-a0f4-719460070852","timestamp":"2026-02-10T16:47:51.043461261Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49080868,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.09255913Z","id":"687d5708-f51c-4e73-a0f4-719460070852","remote_ip":"127.0.0.1","host":"127.0.0.1:34855","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49096106,"latency_human":"49.096106ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.092565672Z","id":"687d5708-f51c-4e73-a0f4-719460070852","remote_ip":"127.0.0.1","host":"127.0.0.1:34855","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49104892,"latency_human":"49.104892ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"9e744791-905e-4412-9133-858c61fae93c","timestamp":"2026-02-10T16:47:51.092733033Z","method":"POST","path":"/api/devices/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjE1Zjk4YmJkLWM3MGQtNGVhMy1iYzA5LWRjOGM1ZjJmNzYyMSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.XDYMHqbaMLvF1I0vvas21VUljFpO9XBOV89HPi-4tlQ","Content-Length":"48","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"device_name":"Test Kobo","device_type":"kobo"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":151902,"status_code":400,"response_size":137} -{"time":"2026-02-10T16:47:51.092905382Z","id":"9e744791-905e-4412-9133-858c61fae93c","remote_ip":"127.0.0.1","host":"127.0.0.1:34855","method":"POST","uri":"/api/devices/register","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":172620,"latency_human":"172.62ยตs","bytes_in":48,"bytes_out":137} -{"time":"2026-02-10T16:47:51.092912536Z","id":"9e744791-905e-4412-9133-858c61fae93c","remote_ip":"127.0.0.1","host":"127.0.0.1:34855","method":"POST","uri":"/api/devices/register","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":180815,"latency_human":"180.815ยตs","bytes_in":48,"bytes_out":137} -2026/02/10 16:47:51 [REQUEST] {"request_id":"1898b2e8-3e31-40a8-8f4d-32fc79c1be47","timestamp":"2026-02-10T16:47:51.093220377Z","method":"GET","path":"/api/analytics/device-usage","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjE1Zjk4YmJkLWM3MGQtNGVhMy1iYzA5LWRjOGM1ZjJmNzYyMSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.XDYMHqbaMLvF1I0vvas21VUljFpO9XBOV89HPi-4tlQ","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2203377,"status_code":200,"response_size":15} -{"time":"2026-02-10T16:47:51.095433652Z","id":"1898b2e8-3e31-40a8-8f4d-32fc79c1be47","remote_ip":"127.0.0.1","host":"127.0.0.1:34855","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2213346,"latency_human":"2.213346ms","bytes_in":0,"bytes_out":15} -{"time":"2026-02-10T16:47:51.095436928Z","id":"1898b2e8-3e31-40a8-8f4d-32fc79c1be47","remote_ip":"127.0.0.1","host":"127.0.0.1:34855","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2217513,"latency_human":"2.217513ms","bytes_in":0,"bytes_out":15} -=== RUN TestAnalyticsDeviceUsage/GetDeviceUsage_ResponseStructure -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '698d9278-bdd1-4a66-aeae-ec379f8cbb07' -2026/02/10 16:47:51 [REQUEST] {"request_id":"c05baf78-e0a0-4d59-b627-b92dc2f08415","timestamp":"2026-02-10T16:47:51.114536435Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49078272,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.163667866Z","id":"c05baf78-e0a0-4d59-b627-b92dc2f08415","remote_ip":"127.0.0.1","host":"127.0.0.1:42377","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49125551,"latency_human":"49.125551ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.163688174Z","id":"c05baf78-e0a0-4d59-b627-b92dc2f08415","remote_ip":"127.0.0.1","host":"127.0.0.1:42377","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49148153,"latency_human":"49.148153ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"8f60fb22-29c6-4bc9-8461-7ed640e5e73a","timestamp":"2026-02-10T16:47:51.164054704Z","method":"GET","path":"/api/analytics/device-usage","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjE4NWE0YmQzLTgxNTMtNDBhMS05OTgyLWIxNThlNWFmMGZmZCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.8NXrqFsoBQ18aBh1-XsFkKmM3IK2N_2PwCNVbcOcp8c","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2387288,"status_code":200,"response_size":15} -{"time":"2026-02-10T16:47:51.166476335Z","id":"8f60fb22-29c6-4bc9-8461-7ed640e5e73a","remote_ip":"127.0.0.1","host":"127.0.0.1:42377","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2420960,"latency_human":"2.42096ms","bytes_in":0,"bytes_out":15} -{"time":"2026-02-10T16:47:51.166485412Z","id":"8f60fb22-29c6-4bc9-8461-7ed640e5e73a","remote_ip":"127.0.0.1","host":"127.0.0.1:42377","method":"GET","uri":"/api/analytics/device-usage","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2431039,"latency_human":"2.431039ms","bytes_in":0,"bytes_out":15} ---- PASS: TestAnalyticsDeviceUsage (0.22s) - --- PASS: TestAnalyticsDeviceUsage/GetDeviceUsage_WithoutAuth (0.00s) - --- PASS: TestAnalyticsDeviceUsage/GetDeviceUsage_WithAuth_NoDevices (0.07s) - --- PASS: TestAnalyticsDeviceUsage/GetDeviceUsage_WithAuth_WithDevices (0.07s) - --- PASS: TestAnalyticsDeviceUsage/GetDeviceUsage_ResponseStructure (0.07s) -=== RUN TestAnalyticsPopularBooks -=== RUN TestAnalyticsPopularBooks/GetPopularBooks_WithoutAuth -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 [REQUEST] {"request_id":"fc6297ad-40f4-4e2d-a23b-e01c8686d3fd","timestamp":"2026-02-10T16:47:51.167690637Z","method":"GET","path":"/api/analytics/popular-books","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2836,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:51.167710564Z","id":"fc6297ad-40f4-4e2d-a23b-e01c8686d3fd","remote_ip":"127.0.0.1","host":"127.0.0.1:42453","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":24074,"latency_human":"24.074ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:51.167716045Z","id":"fc6297ad-40f4-4e2d-a23b-e01c8686d3fd","remote_ip":"127.0.0.1","host":"127.0.0.1:42453","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":34263,"latency_human":"34.263ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestAnalyticsPopularBooks/GetPopularBooks_WithAuth_DefaultLimit -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'a43818bd-1a22-40a2-8e02-36e37e2db4e6' -2026/02/10 16:47:51 [REQUEST] {"request_id":"f0828138-057c-4773-9709-a4b225074f03","timestamp":"2026-02-10T16:47:51.187218809Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50165229,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.23740158Z","id":"f0828138-057c-4773-9709-a4b225074f03","remote_ip":"127.0.0.1","host":"127.0.0.1:45995","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50180978,"latency_human":"50.180978ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.23741211Z","id":"f0828138-057c-4773-9709-a4b225074f03","remote_ip":"127.0.0.1","host":"127.0.0.1:45995","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50189745,"latency_human":"50.189745ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"059251b2-c99a-4ebc-9d8a-32f367a58f34","timestamp":"2026-02-10T16:47:51.237606681Z","method":"GET","path":"/api/analytics/popular-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjIxYTk3OWFkLTI5YzMtNDhjNC05MTA2LWMxODcxZmVhMWRiYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.vbkUyDFugoK4_XP_1XgG2IHEfrxRNt7PyryRZmkIK_c","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3308146,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.240953218Z","id":"059251b2-c99a-4ebc-9d8a-32f367a58f34","remote_ip":"127.0.0.1","host":"127.0.0.1:45995","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3344824,"latency_human":"3.344824ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.240965391Z","id":"059251b2-c99a-4ebc-9d8a-32f367a58f34","remote_ip":"127.0.0.1","host":"127.0.0.1:45995","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3357588,"latency_human":"3.357588ms","bytes_in":0,"bytes_out":13} -=== RUN TestAnalyticsPopularBooks/GetPopularBooks_WithCustomLimit -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '13667e1f-e4cb-4af5-ba18-ba563629594a' -2026/02/10 16:47:51 [REQUEST] {"request_id":"3d61f327-3f99-40eb-90c2-55e5334416f4","timestamp":"2026-02-10T16:47:51.261912785Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48910481,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.310839146Z","id":"3d61f327-3f99-40eb-90c2-55e5334416f4","remote_ip":"127.0.0.1","host":"127.0.0.1:33297","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48924307,"latency_human":"48.924307ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.310845377Z","id":"3d61f327-3f99-40eb-90c2-55e5334416f4","remote_ip":"127.0.0.1","host":"127.0.0.1:33297","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48932893,"latency_human":"48.932893ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"699f0ce6-b086-41db-88dd-be4d698e18b8","timestamp":"2026-02-10T16:47:51.311036682Z","method":"GET","path":"/api/analytics/popular-books","query_params":{"limit":"5"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjFmZDkwMjI1LWY5YzItNGRmNi1iZDBiLTA3ZjUzN2RhODBmNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Zcz-NMA0yNBWBG9sMTvuHtI134PKvFfNYexrEa_bBdI","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1924300,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.312979285Z","id":"699f0ce6-b086-41db-88dd-be4d698e18b8","remote_ip":"127.0.0.1","host":"127.0.0.1:33297","method":"GET","uri":"/api/analytics/popular-books?limit=5","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1942163,"latency_human":"1.942163ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.312985026Z","id":"699f0ce6-b086-41db-88dd-be4d698e18b8","remote_ip":"127.0.0.1","host":"127.0.0.1:33297","method":"GET","uri":"/api/analytics/popular-books?limit=5","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1949105,"latency_human":"1.949105ms","bytes_in":0,"bytes_out":13} -=== RUN TestAnalyticsPopularBooks/GetPopularBooks_InvalidLimit -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '38fa6a57-3924-4069-b1f5-4d54756fa859' -2026/02/10 16:47:51 [REQUEST] {"request_id":"c546c0c5-914c-43d5-8dbb-7e98b15d83cd","timestamp":"2026-02-10T16:47:51.332609947Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":52243273,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.384911468Z","id":"c546c0c5-914c-43d5-8dbb-7e98b15d83cd","remote_ip":"127.0.0.1","host":"127.0.0.1:39003","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52295509,"latency_human":"52.295509ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.384933008Z","id":"c546c0c5-914c-43d5-8dbb-7e98b15d83cd","remote_ip":"127.0.0.1","host":"127.0.0.1:39003","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52316378,"latency_human":"52.316378ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"d74c5704-31fe-4fc9-a2cc-a23b0736f43c","timestamp":"2026-02-10T16:47:51.385193371Z","method":"GET","path":"/api/analytics/popular-books","query_params":{"limit":"invalid"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkxM2VlNWZmLTExYTgtNDg1NS1iODE1LWRhM2YxM2ExYTdiNiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.5YEua5f-e1jjJ7zU9fOO5XuULqb_YZh3WYcPhr6-X5g","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2285600,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.387503717Z","id":"d74c5704-31fe-4fc9-a2cc-a23b0736f43c","remote_ip":"127.0.0.1","host":"127.0.0.1:39003","method":"GET","uri":"/api/analytics/popular-books?limit=invalid","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2309274,"latency_human":"2.309274ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.38751077Z","id":"d74c5704-31fe-4fc9-a2cc-a23b0736f43c","remote_ip":"127.0.0.1","host":"127.0.0.1:39003","method":"GET","uri":"/api/analytics/popular-books?limit=invalid","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2317719,"latency_human":"2.317719ms","bytes_in":0,"bytes_out":13} -=== RUN TestAnalyticsPopularBooks/GetPopularBooks_ResponseStructure -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '9ce123ff-93ff-4137-82bf-4e37ae8c0add' -2026/02/10 16:47:51 [REQUEST] {"request_id":"51858316-c605-4630-8118-039481571605","timestamp":"2026-02-10T16:47:51.416299949Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50618289,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.4669406Z","id":"51858316-c605-4630-8118-039481571605","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50633276,"latency_human":"50.633276ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.466947482Z","id":"51858316-c605-4630-8118-039481571605","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50647663,"latency_human":"50.647663ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"211efd82-e082-4f28-9d7f-4a3afc3669f1","timestamp":"2026-02-10T16:47:51.467163703Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhmYWZhMTIwLWQ2Y2MtNDNkYy1hZDIxLTYxMTMzM2JiYmNlZSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.HAB24ilAW3dKmxqqI0oboCSU6RiGGD-mdGJmLwKndiU","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4496860,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:51.471688496Z","id":"211efd82-e082-4f28-9d7f-4a3afc3669f1","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":4524091,"latency_human":"4.524091ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:51.471695719Z","id":"211efd82-e082-4f28-9d7f-4a3afc3669f1","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":4532076,"latency_human":"4.532076ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:51 [REQUEST] {"request_id":"8a154e89-11e7-4b0d-b5a7-1310797e6eed","timestamp":"2026-02-10T16:47:51.47186324Z","method":"POST","path":"/api/libraries/2a2b392d-73c8-43c3-b853-57946850ce2d/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhmYWZhMTIwLWQ2Y2MtNDNkYy1hZDIxLTYxMTMzM2JiYmNlZSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.HAB24ilAW3dKmxqqI0oboCSU6RiGGD-mdGJmLwKndiU","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4017272,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:51.475896922Z","id":"8a154e89-11e7-4b0d-b5a7-1310797e6eed","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/libraries/2a2b392d-73c8-43c3-b853-57946850ce2d/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":4033733,"latency_human":"4.033733ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:51.475903084Z","id":"8a154e89-11e7-4b0d-b5a7-1310797e6eed","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/libraries/2a2b392d-73c8-43c3-b853-57946850ce2d/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":4040665,"latency_human":"4.040665ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:51 [REQUEST] {"request_id":"6443afd4-b1a2-428e-b674-5bdab3a3bb13","timestamp":"2026-02-10T16:47:51.476210424Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhmYWZhMTIwLWQ2Y2MtNDNkYy1hZDIxLTYxMTMzM2JiYmNlZSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.HAB24ilAW3dKmxqqI0oboCSU6RiGGD-mdGJmLwKndiU","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"2a2b392d-73c8-43c3-b853-57946850ce2d","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":9341627,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:51.485580524Z","id":"6443afd4-b1a2-428e-b674-5bdab3a3bb13","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":9367365,"latency_human":"9.367365ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:51.485587026Z","id":"6443afd4-b1a2-428e-b674-5bdab3a3bb13","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":9376792,"latency_human":"9.376792ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:51 [REQUEST] {"request_id":"66874e51-5917-47d7-8d68-4ca8d71bff8d","timestamp":"2026-02-10T16:47:51.485840596Z","method":"POST","path":"/api/media-items/68dd11a3-34bb-40ad-9364-55120dadc7f5/progress","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhmYWZhMTIwLWQ2Y2MtNDNkYy1hZDIxLTYxMTMzM2JiYmNlZSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.HAB24ilAW3dKmxqqI0oboCSU6RiGGD-mdGJmLwKndiU","Content-Length":"124","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"media_item_id":"68dd11a3-34bb-40ad-9364-55120dadc7f5","pages_read":100,"progress_percentage":50,"time_spent_seconds":1800},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":29675,"status_code":200,"response_size":0,"error":"code=404, message=Not Found"} -{"time":"2026-02-10T16:47:51.485885239Z","id":"66874e51-5917-47d7-8d68-4ca8d71bff8d","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/media-items/68dd11a3-34bb-40ad-9364-55120dadc7f5/progress","user_agent":"Go-http-client/1.1","status":404,"error":"code=404, message=Not Found","latency":44582,"latency_human":"44.582ยตs","bytes_in":124,"bytes_out":24} -{"time":"2026-02-10T16:47:51.485888415Z","id":"66874e51-5917-47d7-8d68-4ca8d71bff8d","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"POST","uri":"/api/media-items/68dd11a3-34bb-40ad-9364-55120dadc7f5/progress","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":48480,"latency_human":"48.48ยตs","bytes_in":124,"bytes_out":24} -2026/02/10 16:47:51 [REQUEST] {"request_id":"db30f4b9-5fdb-429d-ae28-53e810340c20","timestamp":"2026-02-10T16:47:51.486169286Z","method":"GET","path":"/api/analytics/popular-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhmYWZhMTIwLWQ2Y2MtNDNkYy1hZDIxLTYxMTMzM2JiYmNlZSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.HAB24ilAW3dKmxqqI0oboCSU6RiGGD-mdGJmLwKndiU","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1276478,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.487459659Z","id":"db30f4b9-5fdb-429d-ae28-53e810340c20","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1290624,"latency_human":"1.290624ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.487463006Z","id":"db30f4b9-5fdb-429d-ae28-53e810340c20","remote_ip":"127.0.0.1","host":"127.0.0.1:44629","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1294762,"latency_human":"1.294762ms","bytes_in":0,"bytes_out":13} -=== RUN TestAnalyticsPopularBooks/GetPopularBooks_NoReadingHistory -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ef902e24-d59e-43bf-86b1-bea71ee3c311' -2026/02/10 16:47:51 [REQUEST] {"request_id":"d57aed8c-851b-424c-b97f-151533cb8067","timestamp":"2026-02-10T16:47:51.510431639Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":54344931,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.564859985Z","id":"d57aed8c-851b-424c-b97f-151533cb8067","remote_ip":"127.0.0.1","host":"127.0.0.1:39005","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54424439,"latency_human":"54.424439ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.564870084Z","id":"d57aed8c-851b-424c-b97f-151533cb8067","remote_ip":"127.0.0.1","host":"127.0.0.1:39005","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54438315,"latency_human":"54.438315ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"209af783-f0c3-4f2c-90a7-1c0a54fe5d4f","timestamp":"2026-02-10T16:47:51.565074272Z","method":"GET","path":"/api/analytics/popular-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjVlNWEzMjhiLWIyNGItNGE5Yy1hZGQwLTA0MGZjZWIwYjEzYiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.98gemb4koWv_CEoOadp50Vohrnkg-wchZsW8ers7wiM","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1950578,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.567034418Z","id":"209af783-f0c3-4f2c-90a7-1c0a54fe5d4f","remote_ip":"127.0.0.1","host":"127.0.0.1:39005","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1960426,"latency_human":"1.960426ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.567037754Z","id":"209af783-f0c3-4f2c-90a7-1c0a54fe5d4f","remote_ip":"127.0.0.1","host":"127.0.0.1:39005","method":"GET","uri":"/api/analytics/popular-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1965546,"latency_human":"1.965546ms","bytes_in":0,"bytes_out":13} ---- PASS: TestAnalyticsPopularBooks (0.40s) - --- PASS: TestAnalyticsPopularBooks/GetPopularBooks_WithoutAuth (0.00s) - --- PASS: TestAnalyticsPopularBooks/GetPopularBooks_WithAuth_DefaultLimit (0.07s) - --- PASS: TestAnalyticsPopularBooks/GetPopularBooks_WithCustomLimit (0.07s) - --- PASS: TestAnalyticsPopularBooks/GetPopularBooks_InvalidLimit (0.07s) - --- PASS: TestAnalyticsPopularBooks/GetPopularBooks_ResponseStructure (0.10s) - --- PASS: TestAnalyticsPopularBooks/GetPopularBooks_NoReadingHistory (0.08s) -=== RUN TestAnalyticsEdgeCases -=== RUN TestAnalyticsEdgeCases/ReadingStats_FutureDateRange -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ea04d2d9-e1eb-4f32-b5f1-4c34f2ff365a' -2026/02/10 16:47:51 [REQUEST] {"request_id":"f9dc54a0-c803-4450-8a99-a765376ed231","timestamp":"2026-02-10T16:47:51.604116019Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":57655031,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.661795956Z","id":"f9dc54a0-c803-4450-8a99-a765376ed231","remote_ip":"127.0.0.1","host":"127.0.0.1:44707","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":57676761,"latency_human":"57.676761ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.661806465Z","id":"f9dc54a0-c803-4450-8a99-a765376ed231","remote_ip":"127.0.0.1","host":"127.0.0.1:44707","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":57690407,"latency_human":"57.690407ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"b1fa48ca-c316-4503-a4b6-76941d3a81e0","timestamp":"2026-02-10T16:47:51.662280144Z","method":"GET","path":"/api/analytics/reading-stats","query_params":{"end_date":"2026-02-24","start_date":"2026-02-17"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImJkODU4YmEyLTkxYmUtNDkxYS1iOTgwLTZmMGJkOGViMWM3YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.ahfcHfGWPPeDKa7TmxTNVKWLxdO1ao8sjsCLapqyj1s","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3070215,"status_code":200,"response_size":218} -{"time":"2026-02-10T16:47:51.665374153Z","id":"b1fa48ca-c316-4503-a4b6-76941d3a81e0","remote_ip":"127.0.0.1","host":"127.0.0.1:44707","method":"GET","uri":"/api/analytics/reading-stats?start_date=2026-02-17&end_date=2026-02-24","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3093488,"latency_human":"3.093488ms","bytes_in":0,"bytes_out":218} -{"time":"2026-02-10T16:47:51.665380114Z","id":"b1fa48ca-c316-4503-a4b6-76941d3a81e0","remote_ip":"127.0.0.1","host":"127.0.0.1:44707","method":"GET","uri":"/api/analytics/reading-stats?start_date=2026-02-17&end_date=2026-02-24","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3102775,"latency_human":"3.102775ms","bytes_in":0,"bytes_out":218} -=== RUN TestAnalyticsEdgeCases/PopularBooks_LimitZero -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '235d4701-7288-4b6b-be3e-90078d009e87' -2026/02/10 16:47:51 [REQUEST] {"request_id":"47dff906-d143-4ab9-ae38-e303bd5ffd4c","timestamp":"2026-02-10T16:47:51.700904286Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":56954421,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.757880257Z","id":"47dff906-d143-4ab9-ae38-e303bd5ffd4c","remote_ip":"127.0.0.1","host":"127.0.0.1:42529","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":56971714,"latency_human":"56.971714ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.757889534Z","id":"47dff906-d143-4ab9-ae38-e303bd5ffd4c","remote_ip":"127.0.0.1","host":"127.0.0.1:42529","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":56983666,"latency_human":"56.983666ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"41a35b1b-f089-49ca-8c19-7f873b688049","timestamp":"2026-02-10T16:47:51.758055232Z","method":"GET","path":"/api/analytics/popular-books","query_params":{"limit":"0"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjY0NWY3MjE3LTFkM2MtNDEyMS1iMTliLTIwZWNiZWNjMTViNSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.rUTdGlCHt47OmxHnspS7BBNFddT0jCdPaseS07Iuk0Q","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2860666,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.760930094Z","id":"41a35b1b-f089-49ca-8c19-7f873b688049","remote_ip":"127.0.0.1","host":"127.0.0.1:42529","method":"GET","uri":"/api/analytics/popular-books?limit=0","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2874673,"latency_human":"2.874673ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.76093339Z","id":"41a35b1b-f089-49ca-8c19-7f873b688049","remote_ip":"127.0.0.1","host":"127.0.0.1:42529","method":"GET","uri":"/api/analytics/popular-books?limit=0","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2878579,"latency_human":"2.878579ms","bytes_in":0,"bytes_out":13} -=== RUN TestAnalyticsEdgeCases/PopularBooks_VeryLargeLimit -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '36701103-7cef-4dea-bef9-eabb6b06fc1e' -2026/02/10 16:47:51 [REQUEST] {"request_id":"07aa0196-0c61-4895-bf94-8c6573018a93","timestamp":"2026-02-10T16:47:51.780894485Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":52840160,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.833754903Z","id":"07aa0196-0c61-4895-bf94-8c6573018a93","remote_ip":"127.0.0.1","host":"127.0.0.1:41607","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52856450,"latency_human":"52.85645ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.833763058Z","id":"07aa0196-0c61-4895-bf94-8c6573018a93","remote_ip":"127.0.0.1","host":"127.0.0.1:41607","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52868583,"latency_human":"52.868583ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"f96c0723-c8dd-4bd4-9fe0-8037633d0164","timestamp":"2026-02-10T16:47:51.833939626Z","method":"GET","path":"/api/analytics/popular-books","query_params":{"limit":"999999"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijg5MzZlYTc2LWIyOWMtNGJiYS04NGZhLWNkZTFlOGIyZTAyOCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Jq-nKYAeKBxx724CUKKKfhEZR-bXgugOz2rOgB8Wt6k","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1916394,"status_code":200,"response_size":13} -{"time":"2026-02-10T16:47:51.835868623Z","id":"f96c0723-c8dd-4bd4-9fe0-8037633d0164","remote_ip":"127.0.0.1","host":"127.0.0.1:41607","method":"GET","uri":"/api/analytics/popular-books?limit=999999","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1928517,"latency_human":"1.928517ms","bytes_in":0,"bytes_out":13} -{"time":"2026-02-10T16:47:51.835872611Z","id":"f96c0723-c8dd-4bd4-9fe0-8037633d0164","remote_ip":"127.0.0.1","host":"127.0.0.1:41607","method":"GET","uri":"/api/analytics/popular-books?limit=999999","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1933075,"latency_human":"1.933075ms","bytes_in":0,"bytes_out":13} ---- PASS: TestAnalyticsEdgeCases (0.27s) - --- PASS: TestAnalyticsEdgeCases/ReadingStats_FutureDateRange (0.10s) - --- PASS: TestAnalyticsEdgeCases/PopularBooks_LimitZero (0.10s) - --- PASS: TestAnalyticsEdgeCases/PopularBooks_VeryLargeLimit (0.07s) -=== RUN TestAuthMiddlewareAlt -=== RUN TestAuthMiddlewareAlt/Missing_JWT -=== RUN TestAuthMiddlewareAlt/Invalid_JWT_format -=== RUN TestAuthMiddlewareAlt/Valid_JWT_format ---- PASS: TestAuthMiddlewareAlt (0.00s) - --- PASS: TestAuthMiddlewareAlt/Missing_JWT (0.00s) - --- PASS: TestAuthMiddlewareAlt/Invalid_JWT_format (0.00s) - --- PASS: TestAuthMiddlewareAlt/Valid_JWT_format (0.00s) -=== RUN TestBookMatchingQueryBooks -=== RUN TestBookMatchingQueryBooks/QueryBooks_WithoutAuth -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 [REQUEST] {"request_id":"8666db54-b6b6-4583-84e5-fa313c4c243d","timestamp":"2026-02-10T16:47:51.837131326Z","method":"POST","path":"/api/sync/books/query","headers":{"Accept-Encoding":"gzip","Content-Length":"21","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"title":"Test Book"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":9598,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:51.837155911Z","id":"8666db54-b6b6-4583-84e5-fa313c4c243d","remote_ip":"127.0.0.1","host":"127.0.0.1:41129","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":23814,"latency_human":"23.814ยตs","bytes_in":21,"bytes_out":39} -{"time":"2026-02-10T16:47:51.837161732Z","id":"8666db54-b6b6-4583-84e5-fa313c4c243d","remote_ip":"127.0.0.1","host":"127.0.0.1:41129","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":30627,"latency_human":"30.627ยตs","bytes_in":21,"bytes_out":39} -=== RUN TestBookMatchingQueryBooks/QueryBooks_WithAuth_ByTitle -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'a3bb5f57-cdba-4ea7-884a-9848a0c4ca56' -2026/02/10 16:47:51 [REQUEST] {"request_id":"e7965f03-164a-4d9b-98db-4bf4d5866427","timestamp":"2026-02-10T16:47:51.869286449Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":56929164,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:51.926263011Z","id":"e7965f03-164a-4d9b-98db-4bf4d5866427","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":56967446,"latency_human":"56.967446ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:51.926282046Z","id":"e7965f03-164a-4d9b-98db-4bf4d5866427","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":56989727,"latency_human":"56.989727ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:51 [REQUEST] {"request_id":"2758d16d-de4e-4b36-af8a-b0cfc490c23e","timestamp":"2026-02-10T16:47:51.926637736Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk3ODZjOTM2LTQ3MGUtNDZlNC05NGFlLTFkZjkzYTMyNDE1NyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Zrl-aXhvb5FNsgkljeLhCf3xgxVTzXQGUhxBCTqhXAw","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":26658026,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:51.953339644Z","id":"2758d16d-de4e-4b36-af8a-b0cfc490c23e","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":26700996,"latency_human":"26.700996ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:51.953350183Z","id":"2758d16d-de4e-4b36-af8a-b0cfc490c23e","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":26712998,"latency_human":"26.712998ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:51 [REQUEST] {"request_id":"3f821f47-2b56-4edd-8036-7b0c4e4b4662","timestamp":"2026-02-10T16:47:51.953628099Z","method":"POST","path":"/api/libraries/15416d9d-2053-4754-bac4-64f923643a52/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk3ODZjOTM2LTQ3MGUtNDZlNC05NGFlLTFkZjkzYTMyNDE1NyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Zrl-aXhvb5FNsgkljeLhCf3xgxVTzXQGUhxBCTqhXAw","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":10890510,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:51.964559525Z","id":"3f821f47-2b56-4edd-8036-7b0c4e4b4662","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/libraries/15416d9d-2053-4754-bac4-64f923643a52/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":10929733,"latency_human":"10.929733ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:51.964571127Z","id":"3f821f47-2b56-4edd-8036-7b0c4e4b4662","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/libraries/15416d9d-2053-4754-bac4-64f923643a52/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":10942207,"latency_human":"10.942207ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:51 [REQUEST] {"request_id":"a793105c-c479-4dde-bad6-b0a1dabb63cd","timestamp":"2026-02-10T16:47:51.964993009Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk3ODZjOTM2LTQ3MGUtNDZlNC05NGFlLTFkZjkzYTMyNDE1NyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Zrl-aXhvb5FNsgkljeLhCf3xgxVTzXQGUhxBCTqhXAw","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"15416d9d-2053-4754-bac4-64f923643a52","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":10462356,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:51.975481544Z","id":"a793105c-c479-4dde-bad6-b0a1dabb63cd","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":10487884,"latency_human":"10.487884ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:51.975489909Z","id":"a793105c-c479-4dde-bad6-b0a1dabb63cd","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":10498153,"latency_human":"10.498153ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:51 [REQUEST] {"request_id":"73f00cd9-d479-498b-a598-416ce7955b0a","timestamp":"2026-02-10T16:47:51.975766342Z","method":"POST","path":"/api/sync/books/query","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzEsImlhdCI6MTc3MDc0MjA3MSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk3ODZjOTM2LTQ3MGUtNDZlNC05NGFlLTFkZjkzYTMyNDE1NyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Zrl-aXhvb5FNsgkljeLhCf3xgxVTzXQGUhxBCTqhXAw","Content-Length":"22","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"title":"Test Ebook"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":5746679,"status_code":200,"response_size":37} -{"time":"2026-02-10T16:47:51.981568564Z","id":"73f00cd9-d479-498b-a598-416ce7955b0a","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":5797403,"latency_human":"5.797403ms","bytes_in":22,"bytes_out":37} -{"time":"2026-02-10T16:47:51.981580917Z","id":"73f00cd9-d479-498b-a598-416ce7955b0a","remote_ip":"127.0.0.1","host":"127.0.0.1:39687","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":5811890,"latency_human":"5.81189ms","bytes_in":22,"bytes_out":37} -=== RUN TestBookMatchingQueryBooks/QueryBooks_InvalidRequestBody -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:51 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '4a31e12c-b250-446e-9bbc-a489c632fa96' -2026/02/10 16:47:52 [REQUEST] {"request_id":"7b14ac94-d3b4-4b51-a38d-8951fdcf9dd3","timestamp":"2026-02-10T16:47:52.042560665Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":60788543,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.103377821Z","id":"7b14ac94-d3b4-4b51-a38d-8951fdcf9dd3","remote_ip":"127.0.0.1","host":"127.0.0.1:37233","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":60814271,"latency_human":"60.814271ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.103388461Z","id":"7b14ac94-d3b4-4b51-a38d-8951fdcf9dd3","remote_ip":"127.0.0.1","host":"127.0.0.1:37233","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":60829029,"latency_human":"60.829029ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"9cfe8c59-2b2e-4e9e-8fee-a2f90eb51e7c","timestamp":"2026-02-10T16:47:52.103666637Z","method":"POST","path":"/api/sync/books/query","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjA0YjQ4ZWU2LTg3MTctNDRkYi05NGNlLTVmZmZjZDg2ZmIwNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.sCdATJvXlVWQAOpek4bgVgVqBD83vaEddApUGtw589o","Content-Length":"12","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":70190,"status_code":400,"response_size":33} -{"time":"2026-02-10T16:47:52.103764589Z","id":"9cfe8c59-2b2e-4e9e-8fee-a2f90eb51e7c","remote_ip":"127.0.0.1","host":"127.0.0.1:37233","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":97551,"latency_human":"97.551ยตs","bytes_in":12,"bytes_out":33} -{"time":"2026-02-10T16:47:52.103772944Z","id":"9cfe8c59-2b2e-4e9e-8fee-a2f90eb51e7c","remote_ip":"127.0.0.1","host":"127.0.0.1:37233","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":107479,"latency_human":"107.479ยตs","bytes_in":12,"bytes_out":33} -=== RUN TestBookMatchingQueryBooks/QueryBooks_NoResults -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '47d04941-13ab-43fe-b5b3-48b2d3daa397' -2026/02/10 16:47:52 [REQUEST] {"request_id":"d1208a1b-f46d-4a19-9c1f-c5c5da98ebd2","timestamp":"2026-02-10T16:47:52.140682666Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49386745,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.190086433Z","id":"d1208a1b-f46d-4a19-9c1f-c5c5da98ebd2","remote_ip":"127.0.0.1","host":"127.0.0.1:35433","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49401502,"latency_human":"49.401502ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.190094307Z","id":"d1208a1b-f46d-4a19-9c1f-c5c5da98ebd2","remote_ip":"127.0.0.1","host":"127.0.0.1:35433","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49410409,"latency_human":"49.410409ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"e8b4e62e-5230-4b29-a5a0-14ea3cf1b54d","timestamp":"2026-02-10T16:47:52.190339011Z","method":"POST","path":"/api/sync/books/query","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjAzMDQ4ZTUxLTM0OGMtNGUwOC04ODQyLTkzZWY1MTZhNzU0OSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.-93YqgQxBnlSYwZaNi7vJKlTZ6VOoAc-D41U7cbujCY","Content-Length":"57","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"title":"NonExistentBookTitleThatDoesNotExist123456789"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3939487,"status_code":200,"response_size":37} -{"time":"2026-02-10T16:47:52.194307572Z","id":"e8b4e62e-5230-4b29-a5a0-14ea3cf1b54d","remote_ip":"127.0.0.1","host":"127.0.0.1:35433","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3966668,"latency_human":"3.966668ms","bytes_in":57,"bytes_out":37} -{"time":"2026-02-10T16:47:52.1943171Z","id":"e8b4e62e-5230-4b29-a5a0-14ea3cf1b54d","remote_ip":"127.0.0.1","host":"127.0.0.1:35433","method":"POST","uri":"/api/sync/books/query","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3978009,"latency_human":"3.978009ms","bytes_in":57,"bytes_out":37} ---- PASS: TestBookMatchingQueryBooks (0.36s) - --- PASS: TestBookMatchingQueryBooks/QueryBooks_WithoutAuth (0.00s) - --- PASS: TestBookMatchingQueryBooks/QueryBooks_WithAuth_ByTitle (0.14s) - --- PASS: TestBookMatchingQueryBooks/QueryBooks_InvalidRequestBody (0.12s) - --- PASS: TestBookMatchingQueryBooks/QueryBooks_NoResults (0.09s) -=== RUN TestBookMatchingBulkLink -=== RUN TestBookMatchingBulkLink/BulkLinkBooks_WithoutAuth -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 [REQUEST] {"request_id":"180cb6c2-d60d-42ce-a22f-04558244dda2","timestamp":"2026-02-10T16:47:52.195103929Z","method":"POST","path":"/api/sync/bulk-link-books","headers":{"Accept-Encoding":"gzip","Content-Length":"149","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"links":[{"confidence_score":0.9,"media_item_id":"bab5b695-c863-4300-944b-1a85da501b2f","unlinked_book_id":"118f8527-5a51-40c9-8627-43a2bd0ce7f8"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":12253,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:52.195134646Z","id":"180cb6c2-d60d-42ce-a22f-04558244dda2","remote_ip":"127.0.0.1","host":"127.0.0.1:38683","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":28412,"latency_human":"28.412ยตs","bytes_in":149,"bytes_out":39} -{"time":"2026-02-10T16:47:52.195138874Z","id":"180cb6c2-d60d-42ce-a22f-04558244dda2","remote_ip":"127.0.0.1","host":"127.0.0.1:38683","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":35335,"latency_human":"35.335ยตs","bytes_in":149,"bytes_out":39} -=== RUN TestBookMatchingBulkLink/BulkLinkBooks_WithAuth_EmptyLinks -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '949e76bf-bf37-4659-901e-f8be6134e3a6' -2026/02/10 16:47:52 [REQUEST] {"request_id":"97ef824f-6d69-4547-9d04-8442b74e6e8c","timestamp":"2026-02-10T16:47:52.242641153Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50617618,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.293283066Z","id":"97ef824f-6d69-4547-9d04-8442b74e6e8c","remote_ip":"127.0.0.1","host":"127.0.0.1:42087","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50637375,"latency_human":"50.637375ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.293292263Z","id":"97ef824f-6d69-4547-9d04-8442b74e6e8c","remote_ip":"127.0.0.1","host":"127.0.0.1:42087","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50651160,"latency_human":"50.65116ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"810a5545-19fc-4a0e-a2ed-e6e1e27c2562","timestamp":"2026-02-10T16:47:52.293523963Z","method":"POST","path":"/api/sync/bulk-link-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjFmMmE1YTZhLTA3MmQtNDA2My1iN2E0LTJhNGE4OTgwNDY4MSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.1sm7RoOkAoM8WRpVRZZfAlZezpgPANatGTd8R_ZhvL8","Content-Length":"12","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"links":[]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":79477,"status_code":200,"response_size":51} -{"time":"2026-02-10T16:47:52.293626463Z","id":"810a5545-19fc-4a0e-a2ed-e6e1e27c2562","remote_ip":"127.0.0.1","host":"127.0.0.1:42087","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":101609,"latency_human":"101.609ยตs","bytes_in":12,"bytes_out":51} -{"time":"2026-02-10T16:47:52.293632103Z","id":"810a5545-19fc-4a0e-a2ed-e6e1e27c2562","remote_ip":"127.0.0.1","host":"127.0.0.1:42087","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":108872,"latency_human":"108.872ยตs","bytes_in":12,"bytes_out":51} -=== RUN TestBookMatchingBulkLink/BulkLinkBooks_InvalidUnlinkedBookID -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'c7248f4c-31d0-448c-89e1-ae663a25d495' -2026/02/10 16:47:52 [REQUEST] {"request_id":"230a49f3-213f-4d30-beaf-d45b5012f1b7","timestamp":"2026-02-10T16:47:52.329884907Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":54541776,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.384452261Z","id":"230a49f3-213f-4d30-beaf-d45b5012f1b7","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54558718,"latency_human":"54.558718ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.384459174Z","id":"230a49f3-213f-4d30-beaf-d45b5012f1b7","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54575358,"latency_human":"54.575358ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"ae993375-2a1a-4250-9021-c373a77a778e","timestamp":"2026-02-10T16:47:52.384623629Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3ZjYxMDk0LTgwYzMtNDQyNS05YjE5LWVhNzczNWE3ZDJiYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.24FA0xWq1rSWChcYQRrspZObU2Io3Kvm4Q_9znmr3sU","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3752260,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:52.388392379Z","id":"ae993375-2a1a-4250-9021-c373a77a778e","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3768720,"latency_human":"3.76872ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:52.388396827Z","id":"ae993375-2a1a-4250-9021-c373a77a778e","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3773810,"latency_human":"3.77381ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:52 [REQUEST] {"request_id":"4b1f1e79-4436-4360-a0ce-8e6428546620","timestamp":"2026-02-10T16:47:52.38857601Z","method":"POST","path":"/api/libraries/c3e54e6c-163c-4497-aefa-c725e54f9f30/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3ZjYxMDk0LTgwYzMtNDQyNS05YjE5LWVhNzczNWE3ZDJiYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.24FA0xWq1rSWChcYQRrspZObU2Io3Kvm4Q_9znmr3sU","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2802508,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:52.391431286Z","id":"4b1f1e79-4436-4360-a0ce-8e6428546620","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/libraries/c3e54e6c-163c-4497-aefa-c725e54f9f30/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2854705,"latency_human":"2.854705ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:52.391437297Z","id":"4b1f1e79-4436-4360-a0ce-8e6428546620","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/libraries/c3e54e6c-163c-4497-aefa-c725e54f9f30/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2862329,"latency_human":"2.862329ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:52 [REQUEST] {"request_id":"6345ebda-bd61-40e9-a61e-1b9ceaf6a423","timestamp":"2026-02-10T16:47:52.391671451Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3ZjYxMDk0LTgwYzMtNDQyNS05YjE5LWVhNzczNWE3ZDJiYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.24FA0xWq1rSWChcYQRrspZObU2Io3Kvm4Q_9znmr3sU","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"c3e54e6c-163c-4497-aefa-c725e54f9f30","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":40060867,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:52.431757895Z","id":"6345ebda-bd61-40e9-a61e-1b9ceaf6a423","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":40085603,"latency_human":"40.085603ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:52.431763626Z","id":"6345ebda-bd61-40e9-a61e-1b9ceaf6a423","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":40092395,"latency_human":"40.092395ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:52 [REQUEST] {"request_id":"1c4d918a-35f0-421b-812d-92dadafb6133","timestamp":"2026-02-10T16:47:52.431983584Z","method":"POST","path":"/api/sync/bulk-link-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3ZjYxMDk0LTgwYzMtNDQyNS05YjE5LWVhNzczNWE3ZDJiYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.24FA0xWq1rSWChcYQRrspZObU2Io3Kvm4Q_9znmr3sU","Content-Length":"149","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"links":[{"confidence_score":0.9,"media_item_id":"8a37aca6-a430-430b-aecf-b6b30b318c72","unlinked_book_id":"f2682bc0-f83b-4ba4-8203-d6370874998b"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":17174095,"status_code":200,"response_size":161} -{"time":"2026-02-10T16:47:52.449209455Z","id":"1c4d918a-35f0-421b-812d-92dadafb6133","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":17223457,"latency_human":"17.223457ms","bytes_in":149,"bytes_out":161} -{"time":"2026-02-10T16:47:52.449218512Z","id":"1c4d918a-35f0-421b-812d-92dadafb6133","remote_ip":"127.0.0.1","host":"127.0.0.1:34361","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":17235970,"latency_human":"17.23597ms","bytes_in":149,"bytes_out":161} -=== RUN TestBookMatchingBulkLink/BulkLinkBooks_MultipleLinks -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '5ca53b4d-6471-49db-97f5-75e450aba385' -2026/02/10 16:47:52 [REQUEST] {"request_id":"8e53cd2c-a8d5-4950-ab81-b0607a5dca39","timestamp":"2026-02-10T16:47:52.490122012Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":73864898,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.564042122Z","id":"8e53cd2c-a8d5-4950-ab81-b0607a5dca39","remote_ip":"127.0.0.1","host":"127.0.0.1:45141","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":73911785,"latency_human":"73.911785ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.564055096Z","id":"8e53cd2c-a8d5-4950-ab81-b0607a5dca39","remote_ip":"127.0.0.1","host":"127.0.0.1:45141","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":73932453,"latency_human":"73.932453ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"a5943c89-b641-468f-9d23-c4f2194c6128","timestamp":"2026-02-10T16:47:52.564476437Z","method":"POST","path":"/api/sync/bulk-link-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkxMGI3MjQ3LWYzY2UtNGUwNS05MDQ0LTEyY2RhZTkzN2ZkYiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.BR596oCNWee4uYtzenQXNxIbYsUAucTF2o2n4LIKvIs","Content-Length":"426","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"links":[{"confidence_score":0.9,"media_item_id":"26076f8b-de3c-428f-97ff-3afebc0c4659","unlinked_book_id":"f240cfca-c6a2-4ed2-b159-0b125c39ba8b"},{"confidence_score":0.8,"media_item_id":"bf3739b0-b996-4fc5-8351-9b40674864fe","unlinked_book_id":"a4acdf84-47fb-450b-bcb6-784c54e206e2"},{"confidence_score":0.95,"media_item_id":"75ed36ce-2521-4706-86e7-8b42c243c8a6","unlinked_book_id":"52d70771-0a4c-4b2f-ab29-35eaeb0c20b3"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1199515,"status_code":200,"response_size":383} -{"time":"2026-02-10T16:47:52.565703063Z","id":"a5943c89-b641-468f-9d23-c4f2194c6128","remote_ip":"127.0.0.1","host":"127.0.0.1:45141","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1225133,"latency_human":"1.225133ms","bytes_in":426,"bytes_out":383} -{"time":"2026-02-10T16:47:52.565709695Z","id":"a5943c89-b641-468f-9d23-c4f2194c6128","remote_ip":"127.0.0.1","host":"127.0.0.1:45141","method":"POST","uri":"/api/sync/bulk-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":1233939,"latency_human":"1.233939ms","bytes_in":426,"bytes_out":383} ---- PASS: TestBookMatchingBulkLink (0.37s) - --- PASS: TestBookMatchingBulkLink/BulkLinkBooks_WithoutAuth (0.00s) - --- PASS: TestBookMatchingBulkLink/BulkLinkBooks_WithAuth_EmptyLinks (0.10s) - --- PASS: TestBookMatchingBulkLink/BulkLinkBooks_InvalidUnlinkedBookID (0.16s) - --- PASS: TestBookMatchingBulkLink/BulkLinkBooks_MultipleLinks (0.12s) -=== RUN TestBookMatchingAutoLink -=== RUN TestBookMatchingAutoLink/AutoLinkBooks_WithoutAuth -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 [REQUEST] {"request_id":"813035e4-9c8a-4223-aee3-6870c414dfd8","timestamp":"2026-02-10T16:47:52.566564441Z","method":"POST","path":"/api/sync/auto-link-books","headers":{"Accept-Encoding":"gzip","Content-Length":"39","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"confidence_threshold":0.8,"limit":10},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":19756,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:52.566605867Z","id":"813035e4-9c8a-4223-aee3-6870c414dfd8","remote_ip":"127.0.0.1","host":"127.0.0.1:38717","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":40966,"latency_human":"40.966ยตs","bytes_in":39,"bytes_out":39} -{"time":"2026-02-10T16:47:52.566610155Z","id":"813035e4-9c8a-4223-aee3-6870c414dfd8","remote_ip":"127.0.0.1","host":"127.0.0.1:38717","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":46186,"latency_human":"46.186ยตs","bytes_in":39,"bytes_out":39} -=== RUN TestBookMatchingAutoLink/AutoLinkBooks_WithAuth_DefaultThreshold -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '4ab3b3ba-3121-42aa-be26-e17897f40811' -2026/02/10 16:47:52 [REQUEST] {"request_id":"7b955061-14a3-4bf1-9800-81a9698dd031","timestamp":"2026-02-10T16:47:52.611871428Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":59664528,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.67156485Z","id":"7b955061-14a3-4bf1-9800-81a9698dd031","remote_ip":"127.0.0.1","host":"127.0.0.1:34955","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":59690697,"latency_human":"59.690697ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.671574738Z","id":"7b955061-14a3-4bf1-9800-81a9698dd031","remote_ip":"127.0.0.1","host":"127.0.0.1:34955","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":59704252,"latency_human":"59.704252ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"f7d76047-08a0-49be-a83a-c8e85a0a384b","timestamp":"2026-02-10T16:47:52.671806538Z","method":"POST","path":"/api/sync/auto-link-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImY5YmUzY2ZkLTFkMDMtNDlkYi1hZWE0LWEzYmNjZGU1YzJhMyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.BgDVAbQ-FUTcqkMEJaAP6HSdQrEUhCxWv9dmiwLuhvI","Content-Length":"2","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2759118,"status_code":200,"response_size":31} -{"time":"2026-02-10T16:47:52.674596443Z","id":"f7d76047-08a0-49be-a83a-c8e85a0a384b","remote_ip":"127.0.0.1","host":"127.0.0.1:34955","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2788512,"latency_human":"2.788512ms","bytes_in":2,"bytes_out":31} -{"time":"2026-02-10T16:47:52.674646426Z","id":"f7d76047-08a0-49be-a83a-c8e85a0a384b","remote_ip":"127.0.0.1","host":"127.0.0.1:34955","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2839177,"latency_human":"2.839177ms","bytes_in":2,"bytes_out":31} -=== RUN TestBookMatchingAutoLink/AutoLinkBooks_CustomThreshold -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '42d0ab0c-a643-459c-932a-a7cdf089686e' -2026/02/10 16:47:52 [REQUEST] {"request_id":"f88ab06d-b579-40ce-8683-32dfe3044b5e","timestamp":"2026-02-10T16:47:52.694943995Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":57626929,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.752630194Z","id":"f88ab06d-b579-40ce-8683-32dfe3044b5e","remote_ip":"127.0.0.1","host":"127.0.0.1:33025","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":57682132,"latency_human":"57.682132ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.752642607Z","id":"f88ab06d-b579-40ce-8683-32dfe3044b5e","remote_ip":"127.0.0.1","host":"127.0.0.1:33025","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":57697790,"latency_human":"57.69779ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"870fb6b0-81c0-48d5-9659-c59bdbf17964","timestamp":"2026-02-10T16:47:52.752969313Z","method":"POST","path":"/api/sync/auto-link-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU3M2E4OTJlLTc1ZmMtNGFhNC1iMWU5LTI1MWRkMjE1NTUwYiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.icykQXSsHivFXijoaBlXdtRIl6sDyBCX6fM3Cce0LtQ","Content-Length":"40","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"confidence_threshold":0.95,"limit":20},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2512219,"status_code":200,"response_size":31} -{"time":"2026-02-10T16:47:52.755503022Z","id":"870fb6b0-81c0-48d5-9659-c59bdbf17964","remote_ip":"127.0.0.1","host":"127.0.0.1:33025","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2533238,"latency_human":"2.533238ms","bytes_in":40,"bytes_out":31} -{"time":"2026-02-10T16:47:52.755510576Z","id":"870fb6b0-81c0-48d5-9659-c59bdbf17964","remote_ip":"127.0.0.1","host":"127.0.0.1:33025","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2542446,"latency_human":"2.542446ms","bytes_in":40,"bytes_out":31} -=== RUN TestBookMatchingAutoLink/AutoLinkBooks_NoUnlinkedBooks -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '3ef76df9-03d1-4307-ba46-dc2c4a0360de' -2026/02/10 16:47:52 [REQUEST] {"request_id":"553a62ac-4260-4389-b3a9-8605894499f3","timestamp":"2026-02-10T16:47:52.776609481Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":57700906,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.83434434Z","id":"553a62ac-4260-4389-b3a9-8605894499f3","remote_ip":"127.0.0.1","host":"127.0.0.1:46133","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":57729199,"latency_human":"57.729199ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.834357485Z","id":"553a62ac-4260-4389-b3a9-8605894499f3","remote_ip":"127.0.0.1","host":"127.0.0.1:46133","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":57748063,"latency_human":"57.748063ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"804e0ca2-8b25-40e7-aade-5e8b37922dd1","timestamp":"2026-02-10T16:47:52.834638897Z","method":"POST","path":"/api/sync/auto-link-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjU0MjdhMjQ5LWU0MTUtNDA5NS1iZWJkLWQxOTJkNTVmOTQ4YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Q1ycPwOGjfwvUvqj52THR-D-JzEwSZw0b1xB_b0Ol6s","Content-Length":"11","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"limit":5},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2335572,"status_code":200,"response_size":31} -{"time":"2026-02-10T16:47:52.836995268Z","id":"804e0ca2-8b25-40e7-aade-5e8b37922dd1","remote_ip":"127.0.0.1","host":"127.0.0.1:46133","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2355609,"latency_human":"2.355609ms","bytes_in":11,"bytes_out":31} -{"time":"2026-02-10T16:47:52.837002872Z","id":"804e0ca2-8b25-40e7-aade-5e8b37922dd1","remote_ip":"127.0.0.1","host":"127.0.0.1:46133","method":"POST","uri":"/api/sync/auto-link-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2364677,"latency_human":"2.364677ms","bytes_in":11,"bytes_out":31} ---- PASS: TestBookMatchingAutoLink (0.27s) - --- PASS: TestBookMatchingAutoLink/AutoLinkBooks_WithoutAuth (0.00s) - --- PASS: TestBookMatchingAutoLink/AutoLinkBooks_WithAuth_DefaultThreshold (0.11s) - --- PASS: TestBookMatchingAutoLink/AutoLinkBooks_CustomThreshold (0.08s) - --- PASS: TestBookMatchingAutoLink/AutoLinkBooks_NoUnlinkedBooks (0.08s) -=== RUN TestBookMatchingSuggestions -=== RUN TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_WithoutAuth -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 [REQUEST] {"request_id":"0a468515-6950-4fb8-8fb6-1aaf3a6fd267","timestamp":"2026-02-10T16:47:52.838223686Z","method":"GET","path":"/api/sync/unlinked-books/45559ab8-3889-44ff-b488-d110751bc818/suggestions","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1884,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:52.838244675Z","id":"0a468515-6950-4fb8-8fb6-1aaf3a6fd267","remote_ip":"127.0.0.1","host":"127.0.0.1:44607","method":"GET","uri":"/api/sync/unlinked-books/45559ab8-3889-44ff-b488-d110751bc818/suggestions","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":26510,"latency_human":"26.51ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:52.838250286Z","id":"0a468515-6950-4fb8-8fb6-1aaf3a6fd267","remote_ip":"127.0.0.1","host":"127.0.0.1:44607","method":"GET","uri":"/api/sync/unlinked-books/45559ab8-3889-44ff-b488-d110751bc818/suggestions","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":32690,"latency_human":"32.69ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_InvalidUUID -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '9bca9a7a-5e80-4e75-8028-aaca1b042658' -2026/02/10 16:47:52 [REQUEST] {"request_id":"c3d37fc8-f1de-4934-b1c4-c550c7bce009","timestamp":"2026-02-10T16:47:52.915462542Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":61379820,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:52.976871286Z","id":"c3d37fc8-f1de-4934-b1c4-c550c7bce009","remote_ip":"127.0.0.1","host":"127.0.0.1:44501","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":61406028,"latency_human":"61.406028ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:52.976897765Z","id":"c3d37fc8-f1de-4934-b1c4-c550c7bce009","remote_ip":"127.0.0.1","host":"127.0.0.1:44501","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":61422899,"latency_human":"61.422899ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:52 [REQUEST] {"request_id":"748a8dc6-87c4-483e-9645-19bc0127f8bb","timestamp":"2026-02-10T16:47:52.977195888Z","method":"GET","path":"/api/sync/unlinked-books/invalid-uuid/suggestions","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzIsImlhdCI6MTc3MDc0MjA3MiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjA3MjVlMWRjLTcyMDItNDgzNi1hNGNhLTc4ODc3NGQwYjRjNSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Y9L1dQYkSmn2_wwPIFK-uAaPeudSIy77ZqACBrvnqew","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":38912,"status_code":400,"response_size":37} -{"time":"2026-02-10T16:47:52.977249217Z","id":"748a8dc6-87c4-483e-9645-19bc0127f8bb","remote_ip":"127.0.0.1","host":"127.0.0.1:44501","method":"GET","uri":"/api/sync/unlinked-books/invalid-uuid/suggestions","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":53509,"latency_human":"53.509ยตs","bytes_in":0,"bytes_out":37} -{"time":"2026-02-10T16:47:52.977255568Z","id":"748a8dc6-87c4-483e-9645-19bc0127f8bb","remote_ip":"127.0.0.1","host":"127.0.0.1:44501","method":"GET","uri":"/api/sync/unlinked-books/invalid-uuid/suggestions","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":60652,"latency_human":"60.652ยตs","bytes_in":0,"bytes_out":37} -=== RUN TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_BookNotFound -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:52 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '540ccd79-1e3f-4791-8be6-7826dd95262f' -2026/02/10 16:47:53 [REQUEST] {"request_id":"d02f5797-3d63-4132-baae-35f49b09d176","timestamp":"2026-02-10T16:47:53.029396412Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":54129632,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.0835457Z","id":"d02f5797-3d63-4132-baae-35f49b09d176","remote_ip":"127.0.0.1","host":"127.0.0.1:41271","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54147114,"latency_human":"54.147114ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.083554046Z","id":"d02f5797-3d63-4132-baae-35f49b09d176","remote_ip":"127.0.0.1","host":"127.0.0.1:41271","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54157945,"latency_human":"54.157945ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"95eb35f1-83f2-4eed-bac1-d9de5947c319","timestamp":"2026-02-10T16:47:53.08377198Z","method":"GET","path":"/api/sync/unlinked-books/c2f59119-14b5-4a71-af40-bab802e1c0b0/suggestions","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjBkOTE3ZjU1LTRmMTctNDlkZS04MDJhLWZiMmU0NTgyN2JhMiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.RDHLXblIC54X2R8uNyTKtHjoqsejZLTCgN6_WW_xMLs","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1066999,"status_code":404,"response_size":36} -{"time":"2026-02-10T16:47:53.084854488Z","id":"95eb35f1-83f2-4eed-bac1-d9de5947c319","remote_ip":"127.0.0.1","host":"127.0.0.1:41271","method":"GET","uri":"/api/sync/unlinked-books/c2f59119-14b5-4a71-af40-bab802e1c0b0/suggestions","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":1081456,"latency_human":"1.081456ms","bytes_in":0,"bytes_out":36} -{"time":"2026-02-10T16:47:53.084862593Z","id":"95eb35f1-83f2-4eed-bac1-d9de5947c319","remote_ip":"127.0.0.1","host":"127.0.0.1:41271","method":"GET","uri":"/api/sync/unlinked-books/c2f59119-14b5-4a71-af40-bab802e1c0b0/suggestions","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":1088138,"latency_human":"1.088138ms","bytes_in":0,"bytes_out":36} -=== RUN TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_ResponseStructure -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ad0a9b62-886c-43b1-a82c-431de69ce9be' -2026/02/10 16:47:53 [REQUEST] {"request_id":"413761fb-9a3d-4c1c-9dc8-0f90cabd0e82","timestamp":"2026-02-10T16:47:53.121170308Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":63564492,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.184752383Z","id":"413761fb-9a3d-4c1c-9dc8-0f90cabd0e82","remote_ip":"127.0.0.1","host":"127.0.0.1:37465","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":63580833,"latency_human":"63.580833ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.184758965Z","id":"413761fb-9a3d-4c1c-9dc8-0f90cabd0e82","remote_ip":"127.0.0.1","host":"127.0.0.1:37465","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":63590160,"latency_human":"63.59016ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"3a44f5f4-4c72-431f-8b3b-1ea83df0c8ff","timestamp":"2026-02-10T16:47:53.184991126Z","method":"GET","path":"/api/sync/unlinked-books/c17ce79d-ff89-414e-80d1-55ebf0992adc/suggestions","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImQ2NGM1YzU3LTc2ZTgtNDY0Yi04NzM0LTdmODM5MzA4ZDA4ZiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.-d7JXgVjHoFDvfP0_NBfnYIDMQGlT13yfzT1y2R49zo","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1110390,"status_code":404,"response_size":36} -{"time":"2026-02-10T16:47:53.186116012Z","id":"3a44f5f4-4c72-431f-8b3b-1ea83df0c8ff","remote_ip":"127.0.0.1","host":"127.0.0.1:37465","method":"GET","uri":"/api/sync/unlinked-books/c17ce79d-ff89-414e-80d1-55ebf0992adc/suggestions","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":1124085,"latency_human":"1.124085ms","bytes_in":0,"bytes_out":36} -{"time":"2026-02-10T16:47:53.186120931Z","id":"3a44f5f4-4c72-431f-8b3b-1ea83df0c8ff","remote_ip":"127.0.0.1","host":"127.0.0.1:37465","method":"GET","uri":"/api/sync/unlinked-books/c17ce79d-ff89-414e-80d1-55ebf0992adc/suggestions","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":1130447,"latency_human":"1.130447ms","bytes_in":0,"bytes_out":36} ---- PASS: TestBookMatchingSuggestions (0.35s) - --- PASS: TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_WithoutAuth (0.00s) - --- PASS: TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_InvalidUUID (0.14s) - --- PASS: TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_BookNotFound (0.11s) - --- PASS: TestBookMatchingSuggestions/GetUnlinkedBookSuggestions_ResponseStructure (0.10s) -=== RUN TestBookMatchingDeviceFileAliases -=== RUN TestBookMatchingDeviceFileAliases/GetDeviceFileAliases_WithoutAuth -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 [REQUEST] {"request_id":"4a00e0e4-0a86-4272-bf34-82410980028d","timestamp":"2026-02-10T16:47:53.186919713Z","method":"GET","path":"/api/devices/a6992f53-d5f2-43cc-8866-8fc45faba047/file-aliases","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4649,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:53.186944419Z","id":"4a00e0e4-0a86-4272-bf34-82410980028d","remote_ip":"127.0.0.1","host":"127.0.0.1:37541","method":"GET","uri":"/api/devices/a6992f53-d5f2-43cc-8866-8fc45faba047/file-aliases","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":24105,"latency_human":"24.105ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:53.186953606Z","id":"4a00e0e4-0a86-4272-bf34-82410980028d","remote_ip":"127.0.0.1","host":"127.0.0.1:37541","method":"GET","uri":"/api/devices/a6992f53-d5f2-43cc-8866-8fc45faba047/file-aliases","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":31528,"latency_human":"31.528ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestBookMatchingDeviceFileAliases/GetDeviceFileAliases_WithAuth -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ada5188c-9ebf-4c28-b407-2bd395968d42' -2026/02/10 16:47:53 [REQUEST] {"request_id":"05a8d518-3963-4b28-9c20-11a94d02d428","timestamp":"2026-02-10T16:47:53.208723315Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":58281624,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.267029213Z","id":"05a8d518-3963-4b28-9c20-11a94d02d428","remote_ip":"127.0.0.1","host":"127.0.0.1:41113","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":58318091,"latency_human":"58.318091ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.267045223Z","id":"05a8d518-3963-4b28-9c20-11a94d02d428","remote_ip":"127.0.0.1","host":"127.0.0.1:41113","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":58330394,"latency_human":"58.330394ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"22091d70-29a0-44fd-a8d9-724f3ef807d3","timestamp":"2026-02-10T16:47:53.267238532Z","method":"GET","path":"/api/devices/675b3ffa-cb30-47b8-88c8-f00e64bdba1c/file-aliases","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjZmODRiMTc3LTBkZGUtNGRmMi05MGEwLWE2YTc0ZjlhMjM4MiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.4xWYkv6Wq5fR_1go7_nnOSd4wxfmqJycdDm-3wOXNEI","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3330147,"status_code":200,"response_size":76} -{"time":"2026-02-10T16:47:53.270592954Z","id":"22091d70-29a0-44fd-a8d9-724f3ef807d3","remote_ip":"127.0.0.1","host":"127.0.0.1:41113","method":"GET","uri":"/api/devices/675b3ffa-cb30-47b8-88c8-f00e64bdba1c/file-aliases","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3353170,"latency_human":"3.35317ms","bytes_in":0,"bytes_out":76} -{"time":"2026-02-10T16:47:53.270601119Z","id":"22091d70-29a0-44fd-a8d9-724f3ef807d3","remote_ip":"127.0.0.1","host":"127.0.0.1:41113","method":"GET","uri":"/api/devices/675b3ffa-cb30-47b8-88c8-f00e64bdba1c/file-aliases","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":3363229,"latency_human":"3.363229ms","bytes_in":0,"bytes_out":76} -=== RUN TestBookMatchingDeviceFileAliases/CreateDeviceFileAlias_WithoutAuth -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 [REQUEST] {"request_id":"f034a1aa-499b-4019-a96e-1c6f6c8630d0","timestamp":"2026-02-10T16:47:53.271574424Z","method":"POST","path":"/api/devices/73cfa06e-6708-46b1-afbe-8f48454f715a/file-aliases","headers":{"Accept-Encoding":"gzip","Content-Length":"128","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"confidence_score":0.9,"file_path":"/mnt/sd/test.epub","file_sha256":"","media_item_id":"fe97fc23-0da1-49a8-a5fc-99ee57bff027"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":11502,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:53.271606253Z","id":"f034a1aa-499b-4019-a96e-1c6f6c8630d0","remote_ip":"127.0.0.1","host":"127.0.0.1:37945","method":"POST","uri":"/api/devices/73cfa06e-6708-46b1-afbe-8f48454f715a/file-aliases","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":31328,"latency_human":"31.328ยตs","bytes_in":128,"bytes_out":39} -{"time":"2026-02-10T16:47:53.271611223Z","id":"f034a1aa-499b-4019-a96e-1c6f6c8630d0","remote_ip":"127.0.0.1","host":"127.0.0.1:37945","method":"POST","uri":"/api/devices/73cfa06e-6708-46b1-afbe-8f48454f715a/file-aliases","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":36989,"latency_human":"36.989ยตs","bytes_in":128,"bytes_out":39} -=== RUN TestBookMatchingDeviceFileAliases/CreateDeviceFileAlias_InvalidDeviceID -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'd9a29946-0bd5-4812-9833-27cc2c197b7a' -2026/02/10 16:47:53 [REQUEST] {"request_id":"f0ec4f30-2963-4156-acb0-e799f8c046e1","timestamp":"2026-02-10T16:47:53.305352097Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":53239671,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.358622605Z","id":"f0ec4f30-2963-4156-acb0-e799f8c046e1","remote_ip":"127.0.0.1","host":"127.0.0.1:46291","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":53260310,"latency_human":"53.26031ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.358632694Z","id":"f0ec4f30-2963-4156-acb0-e799f8c046e1","remote_ip":"127.0.0.1","host":"127.0.0.1:46291","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":53281008,"latency_human":"53.281008ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"3f8b17e7-4ba8-4dad-b6ac-fdda43947397","timestamp":"2026-02-10T16:47:53.358902244Z","method":"POST","path":"/api/devices/invalid-uuid/file-aliases","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjM0YzM0ZjU5LWIzZjQtNGY2ZC05YmQ3LTM2NDI2Yzg5N2E3YiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.95TfZznQOKsvLZPWNDwXycHaHbHeWe7alZR_oBMO-jA","Content-Length":"128","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"confidence_score":0.9,"file_path":"/mnt/sd/test.epub","file_sha256":"","media_item_id":"e1997388-a9e6-4f4e-8943-1d6e35f7930d"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":61494,"status_code":400,"response_size":30} -{"time":"2026-02-10T16:47:53.358984286Z","id":"3f8b17e7-4ba8-4dad-b6ac-fdda43947397","remote_ip":"127.0.0.1","host":"127.0.0.1:46291","method":"POST","uri":"/api/devices/invalid-uuid/file-aliases","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":81231,"latency_human":"81.231ยตs","bytes_in":128,"bytes_out":30} -{"time":"2026-02-10T16:47:53.358991479Z","id":"3f8b17e7-4ba8-4dad-b6ac-fdda43947397","remote_ip":"127.0.0.1","host":"127.0.0.1:46291","method":"POST","uri":"/api/devices/invalid-uuid/file-aliases","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":89846,"latency_human":"89.846ยตs","bytes_in":128,"bytes_out":30} -=== RUN TestBookMatchingDeviceFileAliases/CreateDeviceFileAlias_InvalidMediaItemID -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '55e9d80f-b633-49af-ab97-3310a45b5cbc' -2026/02/10 16:47:53 [REQUEST] {"request_id":"b7645ecb-9962-45f7-a3d6-6a4f92897e35","timestamp":"2026-02-10T16:47:53.397327266Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":62737248,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.46011611Z","id":"b7645ecb-9962-45f7-a3d6-6a4f92897e35","remote_ip":"127.0.0.1","host":"127.0.0.1:37793","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":62782571,"latency_human":"62.782571ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.460135686Z","id":"b7645ecb-9962-45f7-a3d6-6a4f92897e35","remote_ip":"127.0.0.1","host":"127.0.0.1:37793","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":62809862,"latency_human":"62.809862ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"0c51d364-90dc-4281-9725-eced68a2ac26","timestamp":"2026-02-10T16:47:53.460521522Z","method":"POST","path":"/api/devices/f27c24cd-e7e8-4378-a25d-27bc7a7fb41d/file-aliases","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjFhMzM3NjQ1LTg0M2QtNDY3NS05NDgwLWY0OThjZWQxYmVlYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.9OFm_rQ8NS5fcl-UtsxfTxsgsMBwfvy8NPlB_LS_INU","Content-Length":"104","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"confidence_score":0.9,"file_path":"/mnt/sd/test.epub","file_sha256":"","media_item_id":"invalid-uuid"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":125092,"status_code":400,"response_size":34} -{"time":"2026-02-10T16:47:53.460686999Z","id":"0c51d364-90dc-4281-9725-eced68a2ac26","remote_ip":"127.0.0.1","host":"127.0.0.1:37793","method":"POST","uri":"/api/devices/f27c24cd-e7e8-4378-a25d-27bc7a7fb41d/file-aliases","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":164846,"latency_human":"164.846ยตs","bytes_in":104,"bytes_out":34} -{"time":"2026-02-10T16:47:53.460697598Z","id":"0c51d364-90dc-4281-9725-eced68a2ac26","remote_ip":"127.0.0.1","host":"127.0.0.1:37793","method":"POST","uri":"/api/devices/f27c24cd-e7e8-4378-a25d-27bc7a7fb41d/file-aliases","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":177269,"latency_human":"177.269ยตs","bytes_in":104,"bytes_out":34} -=== RUN TestBookMatchingDeviceFileAliases/UpdateDeviceFileAlias_InvalidAliasID -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ec8e9799-a691-4b41-ae13-f2291ccaa93b' -2026/02/10 16:47:53 [REQUEST] {"request_id":"4bfc201b-aa48-43be-bf9c-eb2d50b88e24","timestamp":"2026-02-10T16:47:53.482387049Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":65522204,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.547954677Z","id":"4bfc201b-aa48-43be-bf9c-eb2d50b88e24","remote_ip":"127.0.0.1","host":"127.0.0.1:34075","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":65557458,"latency_human":"65.557458ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.547977489Z","id":"4bfc201b-aa48-43be-bf9c-eb2d50b88e24","remote_ip":"127.0.0.1","host":"127.0.0.1:34075","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":65588517,"latency_human":"65.588517ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"b37ac936-aac0-4548-9d3e-9338662f13af","timestamp":"2026-02-10T16:47:53.548292303Z","method":"PUT","path":"/api/devices/06518f1d-2cb3-4a43-aca4-cb16b75c28b5/file-aliases/invalid-uuid","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImZlNmRlM2I1LTFmZjItNDIzZC04NDVkLTA4NTQ4NjBjZGMxOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Od74tDW3Yjt6JZcxCGzqZzs-OqbQ1t5y8TD99w1gZoE","Content-Length":"25","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"confidence_score":0.95},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":47168,"status_code":400,"response_size":29} -{"time":"2026-02-10T16:47:53.548353747Z","id":"b37ac936-aac0-4548-9d3e-9338662f13af","remote_ip":"127.0.0.1","host":"127.0.0.1:34075","method":"PUT","uri":"/api/devices/06518f1d-2cb3-4a43-aca4-cb16b75c28b5/file-aliases/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":61394,"latency_human":"61.394ยตs","bytes_in":25,"bytes_out":29} -{"time":"2026-02-10T16:47:53.548357314Z","id":"b37ac936-aac0-4548-9d3e-9338662f13af","remote_ip":"127.0.0.1","host":"127.0.0.1:34075","method":"PUT","uri":"/api/devices/06518f1d-2cb3-4a43-aca4-cb16b75c28b5/file-aliases/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":65752,"latency_human":"65.752ยตs","bytes_in":25,"bytes_out":29} -=== RUN TestBookMatchingDeviceFileAliases/DeleteDeviceFileAlias_InvalidAliasID -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '04b1ec83-5d40-4e81-9881-a8c60c68fbb1' -2026/02/10 16:47:53 [REQUEST] {"request_id":"79bf334d-67b9-46bb-8edc-a832e77bd6f2","timestamp":"2026-02-10T16:47:53.578208994Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49071710,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.627300481Z","id":"79bf334d-67b9-46bb-8edc-a832e77bd6f2","remote_ip":"127.0.0.1","host":"127.0.0.1:36623","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49088852,"latency_human":"49.088852ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.627307925Z","id":"79bf334d-67b9-46bb-8edc-a832e77bd6f2","remote_ip":"127.0.0.1","host":"127.0.0.1:36623","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49098911,"latency_human":"49.098911ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"dd6305fe-e610-4728-9665-025eb4b60c68","timestamp":"2026-02-10T16:47:53.627473061Z","method":"DELETE","path":"/api/devices/6b6d8842-1cc7-4596-8aae-0015707a27c9/file-aliases/invalid-uuid","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjQ5NzFhYjgzLTMyNWMtNDE1My04ZDliLTMxNTA0YmUwN2Q3OSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Ff_pGWh6kMaU9RMxa55ymhxE-KRwX02fWdyx_6i57uM","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":30276,"status_code":400,"response_size":29} -{"time":"2026-02-10T16:47:53.627512314Z","id":"dd6305fe-e610-4728-9665-025eb4b60c68","remote_ip":"127.0.0.1","host":"127.0.0.1:36623","method":"DELETE","uri":"/api/devices/6b6d8842-1cc7-4596-8aae-0015707a27c9/file-aliases/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":39303,"latency_human":"39.303ยตs","bytes_in":0,"bytes_out":29} -{"time":"2026-02-10T16:47:53.62751561Z","id":"dd6305fe-e610-4728-9665-025eb4b60c68","remote_ip":"127.0.0.1","host":"127.0.0.1:36623","method":"DELETE","uri":"/api/devices/6b6d8842-1cc7-4596-8aae-0015707a27c9/file-aliases/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":44343,"latency_human":"44.343ยตs","bytes_in":0,"bytes_out":29} ---- PASS: TestBookMatchingDeviceFileAliases (0.44s) - --- PASS: TestBookMatchingDeviceFileAliases/GetDeviceFileAliases_WithoutAuth (0.00s) - --- PASS: TestBookMatchingDeviceFileAliases/GetDeviceFileAliases_WithAuth (0.08s) - --- PASS: TestBookMatchingDeviceFileAliases/CreateDeviceFileAlias_WithoutAuth (0.00s) - --- PASS: TestBookMatchingDeviceFileAliases/CreateDeviceFileAlias_InvalidDeviceID (0.09s) - --- PASS: TestBookMatchingDeviceFileAliases/CreateDeviceFileAlias_InvalidMediaItemID (0.10s) - --- PASS: TestBookMatchingDeviceFileAliases/UpdateDeviceFileAlias_InvalidAliasID (0.09s) - --- PASS: TestBookMatchingDeviceFileAliases/DeleteDeviceFileAlias_InvalidAliasID (0.08s) -=== RUN TestBookMatchingGetBookMatches -=== RUN TestBookMatchingGetBookMatches/GetBookMatches_WithoutAuth -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 [REQUEST] {"request_id":"858b771a-2235-4481-bb2d-fa7f41afa3c4","timestamp":"2026-02-10T16:47:53.628557312Z","method":"GET","path":"/api/books/match","query_params":{"title":"Test"},"headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":5961,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:53.628578892Z","id":"858b771a-2235-4481-bb2d-fa7f41afa3c4","remote_ip":"127.0.0.1","host":"127.0.0.1:40745","method":"GET","uri":"/api/books/match?title=Test","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":20979,"latency_human":"20.979ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:53.628583591Z","id":"858b771a-2235-4481-bb2d-fa7f41afa3c4","remote_ip":"127.0.0.1","host":"127.0.0.1:40745","method":"GET","uri":"/api/books/match?title=Test","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":25768,"latency_human":"25.768ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestBookMatchingGetBookMatches/GetBookMatches_WithAuth_ByTitle -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '646ee7c7-09af-4123-9fe5-8a7733feb3ed' -2026/02/10 16:47:53 [REQUEST] {"request_id":"fd0de0a0-642d-4aab-81cf-8c265607898d","timestamp":"2026-02-10T16:47:53.648358951Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50268921,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.698648109Z","id":"fd0de0a0-642d-4aab-81cf-8c265607898d","remote_ip":"127.0.0.1","host":"127.0.0.1:39705","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50285492,"latency_human":"50.285492ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.698660583Z","id":"fd0de0a0-642d-4aab-81cf-8c265607898d","remote_ip":"127.0.0.1","host":"127.0.0.1:39705","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50301561,"latency_human":"50.301561ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"2a18cc70-bed7-40d7-a4c2-84b69a71d9ab","timestamp":"2026-02-10T16:47:53.698932818Z","method":"GET","path":"/api/books/match","query_params":{"title":"Test"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImZmMTZlMzA2LTYyMWYtNDk3Yy04YTY0LWJkMzdmMTc2MmNlOCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.91f28l5GbuBcWx2wXuSPQiItGm3U0MZpLjIfnu9k4wE","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4365427,"status_code":200,"response_size":37} -{"time":"2026-02-10T16:47:53.703315397Z","id":"2a18cc70-bed7-40d7-a4c2-84b69a71d9ab","remote_ip":"127.0.0.1","host":"127.0.0.1:39705","method":"GET","uri":"/api/books/match?title=Test","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":4381257,"latency_human":"4.381257ms","bytes_in":0,"bytes_out":37} -{"time":"2026-02-10T16:47:53.703320977Z","id":"2a18cc70-bed7-40d7-a4c2-84b69a71d9ab","remote_ip":"127.0.0.1","host":"127.0.0.1:39705","method":"GET","uri":"/api/books/match?title=Test","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":4388050,"latency_human":"4.38805ms","bytes_in":0,"bytes_out":37} -=== RUN TestBookMatchingGetBookMatches/GetBookMatches_InvalidFileSize -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '980c7a14-5391-4378-813c-aafe03add1c0' -2026/02/10 16:47:53 [REQUEST] {"request_id":"0436bcb0-cca5-40c6-bf21-d0c0ec40831c","timestamp":"2026-02-10T16:47:53.722988277Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50561544,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.773569397Z","id":"0436bcb0-cca5-40c6-bf21-d0c0ec40831c","remote_ip":"127.0.0.1","host":"127.0.0.1:39017","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50577914,"latency_human":"50.577914ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.773575338Z","id":"0436bcb0-cca5-40c6-bf21-d0c0ec40831c","remote_ip":"127.0.0.1","host":"127.0.0.1:39017","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50587752,"latency_human":"50.587752ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"2841c36f-630b-4a67-802e-78437119f1c4","timestamp":"2026-02-10T16:47:53.773736777Z","method":"GET","path":"/api/books/match","query_params":{"file_size":"invalid","title":"Test"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkyYmIzM2I0LTU5MDAtNGUwMS1iZDAzLWJjNzU1MDljNTJmNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.qBNuW712q4gpHDDqtEqgXMBMCafi6Y86ovDbJkXP9Ss","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":38031,"status_code":400,"response_size":40} -{"time":"2026-02-10T16:47:53.773784356Z","id":"2841c36f-630b-4a67-802e-78437119f1c4","remote_ip":"127.0.0.1","host":"127.0.0.1:39017","method":"GET","uri":"/api/books/match?title=Test&file_size=invalid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":47258,"latency_human":"47.258ยตs","bytes_in":0,"bytes_out":40} -{"time":"2026-02-10T16:47:53.773787361Z","id":"2841c36f-630b-4a67-802e-78437119f1c4","remote_ip":"127.0.0.1","host":"127.0.0.1:39017","method":"GET","uri":"/api/books/match?title=Test&file_size=invalid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":51105,"latency_human":"51.105ยตs","bytes_in":0,"bytes_out":40} -=== RUN TestBookMatchingGetBookMatches/GetBookMatches_MultipleIdentifiers -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '4cf9a7e6-a77b-4584-87f3-4da6156c00f5' -2026/02/10 16:47:53 [REQUEST] {"request_id":"7e627f4a-901a-45e4-887f-85a50289f270","timestamp":"2026-02-10T16:47:53.794013898Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50493127,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.844537882Z","id":"7e627f4a-901a-45e4-887f-85a50289f270","remote_ip":"127.0.0.1","host":"127.0.0.1:45333","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50520147,"latency_human":"50.520147ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.844546298Z","id":"7e627f4a-901a-45e4-887f-85a50289f270","remote_ip":"127.0.0.1","host":"127.0.0.1:45333","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50532290,"latency_human":"50.53229ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"0be76ff9-b913-4175-9020-229d91f308df","timestamp":"2026-02-10T16:47:53.844807202Z","method":"GET","path":"/api/books/match","query_params":{"identifier":"id1","title":"Test"},"headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjQyOTU1NjI5LWQyZmYtNGYzMy05YjhkLWNmYmU2ODYyYjg0YiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.iH1_KIYFwe0PuYsgOv8HgqX4JFPKcXzc2QEGx0Mq6Uk","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":4615541,"status_code":200,"response_size":37} -{"time":"2026-02-10T16:47:53.849447208Z","id":"0be76ff9-b913-4175-9020-229d91f308df","remote_ip":"127.0.0.1","host":"127.0.0.1:45333","method":"GET","uri":"/api/books/match?identifier=id1&identifier=id2&title=Test","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":4638654,"latency_human":"4.638654ms","bytes_in":0,"bytes_out":37} -{"time":"2026-02-10T16:47:53.849456626Z","id":"0be76ff9-b913-4175-9020-229d91f308df","remote_ip":"127.0.0.1","host":"127.0.0.1:45333","method":"GET","uri":"/api/books/match?identifier=id1&identifier=id2&title=Test","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":4650907,"latency_human":"4.650907ms","bytes_in":0,"bytes_out":37} ---- PASS: TestBookMatchingGetBookMatches (0.22s) - --- PASS: TestBookMatchingGetBookMatches/GetBookMatches_WithoutAuth (0.00s) - --- PASS: TestBookMatchingGetBookMatches/GetBookMatches_WithAuth_ByTitle (0.07s) - --- PASS: TestBookMatchingGetBookMatches/GetBookMatches_InvalidFileSize (0.07s) - --- PASS: TestBookMatchingGetBookMatches/GetBookMatches_MultipleIdentifiers (0.08s) -=== RUN TestCollectionsBulkOperations -=== RUN TestCollectionsBulkOperations/BulkAddBooks_WithoutAuth -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 [REQUEST] {"request_id":"87e1a82d-ada3-41c6-a6d3-eab0cef33ddf","timestamp":"2026-02-10T16:47:53.850347919Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Content-Length":"125","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["cccada95-160d-473e-be7b-6121d04e42e0"],"collection_id":"eacd8bb9-469f-403b-9461-9c111a569925"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":10770,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:53.850392562Z","id":"87e1a82d-ada3-41c6-a6d3-eab0cef33ddf","remote_ip":"127.0.0.1","host":"127.0.0.1:38789","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":49171,"latency_human":"49.171ยตs","bytes_in":125,"bytes_out":39} -{"time":"2026-02-10T16:47:53.850396699Z","id":"87e1a82d-ada3-41c6-a6d3-eab0cef33ddf","remote_ip":"127.0.0.1","host":"127.0.0.1:38789","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":55794,"latency_human":"55.794ยตs","bytes_in":125,"bytes_out":39} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_EmptyOperations -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '6f8daf00-6535-4afb-90a9-0aae2916b6c2' -2026/02/10 16:47:53 [REQUEST] {"request_id":"4991d0ec-0531-42fc-af4f-f478d57f9402","timestamp":"2026-02-10T16:47:53.870958068Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":54096961,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.925096977Z","id":"4991d0ec-0531-42fc-af4f-f478d57f9402","remote_ip":"127.0.0.1","host":"127.0.0.1:44075","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54134360,"latency_human":"54.13436ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.925126241Z","id":"4991d0ec-0531-42fc-af4f-f478d57f9402","remote_ip":"127.0.0.1","host":"127.0.0.1:44075","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":54158175,"latency_human":"54.158175ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:53 [REQUEST] {"request_id":"366afdf9-b814-41d0-92c7-8bcbfe100540","timestamp":"2026-02-10T16:47:53.925444361Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImJlNWU4NmQ0LTE4M2UtNDQ3ZC04MmRkLTI1MzgzMTdjZGIxOCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.BanKfNuHBjfP9zIki336bCDIwcEEaIIxPEhrd-H_0fM","Content-Length":"17","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":85018,"status_code":400,"response_size":32} -{"time":"2026-02-10T16:47:53.925553774Z","id":"366afdf9-b814-41d0-92c7-8bcbfe100540","remote_ip":"127.0.0.1","host":"127.0.0.1:44075","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":109323,"latency_human":"109.323ยตs","bytes_in":17,"bytes_out":32} -{"time":"2026-02-10T16:47:53.925560757Z","id":"366afdf9-b814-41d0-92c7-8bcbfe100540","remote_ip":"127.0.0.1","host":"127.0.0.1:44075","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":117228,"latency_human":"117.228ยตs","bytes_in":17,"bytes_out":32} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_InvalidCollectionID -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:53 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '15c349f1-2f5c-4e2b-aa0c-4f183475c8d2' -2026/02/10 16:47:53 [REQUEST] {"request_id":"c546b450-ada1-4f01-a6bc-fcadd178887c","timestamp":"2026-02-10T16:47:53.944040354Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":52038483,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:53.996126626Z","id":"c546b450-ada1-4f01-a6bc-fcadd178887c","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52078818,"latency_human":"52.078818ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:53.996159196Z","id":"c546b450-ada1-4f01-a6bc-fcadd178887c","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52118121,"latency_human":"52.118121ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"180c2168-8be0-49ac-8972-a1533fa800ff","timestamp":"2026-02-10T16:47:53.99672736Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjcwODljOGQ0LWZlODEtNGU3NC1iY2EwLTNhYjI1MGNkZWY1NCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.n8hsPRNlXueuUuz5llCTKNFMQ2jaXCtnpa5Wmv7Fo_I","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3670568,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.00043123Z","id":"180c2168-8be0-49ac-8972-a1533fa800ff","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3691968,"latency_human":"3.691968ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.000440557Z","id":"180c2168-8be0-49ac-8972-a1533fa800ff","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3712957,"latency_human":"3.712957ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"20338c3e-f147-4a71-bd44-e983c0d3d4e1","timestamp":"2026-02-10T16:47:54.000718092Z","method":"POST","path":"/api/libraries/8ed64b45-a818-45a3-aebc-4af3c25f3797/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjcwODljOGQ0LWZlODEtNGU3NC1iY2EwLTNhYjI1MGNkZWY1NCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.n8hsPRNlXueuUuz5llCTKNFMQ2jaXCtnpa5Wmv7Fo_I","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3055288,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.003790892Z","id":"20338c3e-f147-4a71-bd44-e983c0d3d4e1","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/libraries/8ed64b45-a818-45a3-aebc-4af3c25f3797/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3072449,"latency_human":"3.072449ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.003798436Z","id":"20338c3e-f147-4a71-bd44-e983c0d3d4e1","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/libraries/8ed64b45-a818-45a3-aebc-4af3c25f3797/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3081085,"latency_human":"3.081085ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"99f05bda-8aa7-408c-ac68-db02efa688fc","timestamp":"2026-02-10T16:47:54.004066032Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjcwODljOGQ0LWZlODEtNGU3NC1iY2EwLTNhYjI1MGNkZWY1NCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.n8hsPRNlXueuUuz5llCTKNFMQ2jaXCtnpa5Wmv7Fo_I","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"8ed64b45-a818-45a3-aebc-4af3c25f3797","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":6398889,"status_code":201,"response_size":1043} -{"time":"2026-02-10T16:47:54.010488866Z","id":"99f05bda-8aa7-408c-ac68-db02efa688fc","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6421842,"latency_human":"6.421842ms","bytes_in":183,"bytes_out":1043} -{"time":"2026-02-10T16:47:54.010495989Z","id":"99f05bda-8aa7-408c-ac68-db02efa688fc","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6430557,"latency_human":"6.430557ms","bytes_in":183,"bytes_out":1043} -2026/02/10 16:47:54 [REQUEST] {"request_id":"d8ef9d50-d374-4e55-9ab3-e398123154a7","timestamp":"2026-02-10T16:47:54.010773984Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzMsImlhdCI6MTc3MDc0MjA3MywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjcwODljOGQ0LWZlODEtNGU3NC1iY2EwLTNhYjI1MGNkZWY1NCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.n8hsPRNlXueuUuz5llCTKNFMQ2jaXCtnpa5Wmv7Fo_I","Content-Length":"101","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["40d1934d-27e4-44a3-acc6-a4b36e3cca76"],"collection_id":"invalid-uuid"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":68057,"status_code":200,"response_size":131} -{"time":"2026-02-10T16:47:54.010860074Z","id":"d8ef9d50-d374-4e55-9ab3-e398123154a7","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":86129,"latency_human":"86.129ยตs","bytes_in":101,"bytes_out":131} -{"time":"2026-02-10T16:47:54.010866316Z","id":"d8ef9d50-d374-4e55-9ab3-e398123154a7","remote_ip":"127.0.0.1","host":"127.0.0.1:37375","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":94235,"latency_human":"94.235ยตs","bytes_in":101,"bytes_out":131} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_InvalidBookID -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'e6b5cf68-c914-4ded-92e4-e6842aec8452' -2026/02/10 16:47:54 [REQUEST] {"request_id":"d1c3d344-4c58-466d-a047-327bc259a1ac","timestamp":"2026-02-10T16:47:54.033051075Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48712544,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.081800157Z","id":"d1c3d344-4c58-466d-a047-327bc259a1ac","remote_ip":"127.0.0.1","host":"127.0.0.1:42927","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48741759,"latency_human":"48.741759ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.081820375Z","id":"d1c3d344-4c58-466d-a047-327bc259a1ac","remote_ip":"127.0.0.1","host":"127.0.0.1:42927","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48766243,"latency_human":"48.766243ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"c78b3f66-705c-4e1c-8354-228a4559a73c","timestamp":"2026-02-10T16:47:54.082071641Z","method":"POST","path":"/api/collections","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk0MjE3ZmFhLTZkMjktNDAzOS04MTYyLTRkNmRhMTBhZWY1MSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.gOQWCnOEpg8zMmisNqdtHZPZgR70DXTnssDxVXT-fJI","Content-Length":"60","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test collection","name":"Test Collection"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2907122,"status_code":201,"response_size":288} -{"time":"2026-02-10T16:47:54.08499871Z","id":"c78b3f66-705c-4e1c-8354-228a4559a73c","remote_ip":"127.0.0.1","host":"127.0.0.1:42927","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2925667,"latency_human":"2.925667ms","bytes_in":60,"bytes_out":288} -{"time":"2026-02-10T16:47:54.085006384Z","id":"c78b3f66-705c-4e1c-8354-228a4559a73c","remote_ip":"127.0.0.1","host":"127.0.0.1:42927","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2934824,"latency_human":"2.934824ms","bytes_in":60,"bytes_out":288} -2026/02/10 16:47:54 [REQUEST] {"request_id":"c7b65948-ec44-466c-abb2-92f06549aa31","timestamp":"2026-02-10T16:47:54.08517123Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk0MjE3ZmFhLTZkMjktNDAzOS04MTYyLTRkNmRhMTBhZWY1MSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.gOQWCnOEpg8zMmisNqdtHZPZgR70DXTnssDxVXT-fJI","Content-Length":"101","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["invalid-uuid"],"collection_id":"e6f95f65-717a-4313-9df7-92205c8a55ad"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":66834,"status_code":200,"response_size":174} -{"time":"2026-02-10T16:47:54.085254595Z","id":"c7b65948-ec44-466c-abb2-92f06549aa31","remote_ip":"127.0.0.1","host":"127.0.0.1:42927","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":83455,"latency_human":"83.455ยตs","bytes_in":101,"bytes_out":174} -{"time":"2026-02-10T16:47:54.085259243Z","id":"c7b65948-ec44-466c-abb2-92f06549aa31","remote_ip":"127.0.0.1","host":"127.0.0.1:42927","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":88955,"latency_human":"88.955ยตs","bytes_in":101,"bytes_out":174} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_SingleOperation -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '8ca861b7-b493-4df3-8156-f78cc1296831' -2026/02/10 16:47:54 [REQUEST] {"request_id":"795aabdf-57bb-49c3-816c-7d9bf8b83f91","timestamp":"2026-02-10T16:47:54.105851819Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49182626,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.155053651Z","id":"795aabdf-57bb-49c3-816c-7d9bf8b83f91","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49197263,"latency_human":"49.197263ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.155062788Z","id":"795aabdf-57bb-49c3-816c-7d9bf8b83f91","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49209115,"latency_human":"49.209115ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"02881221-4f65-41d5-bad5-c0b6cf16fcdd","timestamp":"2026-02-10T16:47:54.155276354Z","method":"POST","path":"/api/collections","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3NGY2MjhkLWY5NzQtNDMzOC05ZjlhLTk5NmE0Zjk5YzY4MyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.0cKiLnY5lFC6xrAJ4r0GON_YHOaJQE47xd2oO6SVaRw","Content-Length":"60","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test collection","name":"Test Collection"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2874672,"status_code":201,"response_size":288} -{"time":"2026-02-10T16:47:54.158163569Z","id":"02881221-4f65-41d5-bad5-c0b6cf16fcdd","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2887195,"latency_human":"2.887195ms","bytes_in":60,"bytes_out":288} -{"time":"2026-02-10T16:47:54.158167897Z","id":"02881221-4f65-41d5-bad5-c0b6cf16fcdd","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2893107,"latency_human":"2.893107ms","bytes_in":60,"bytes_out":288} -2026/02/10 16:47:54 [REQUEST] {"request_id":"a0bc41c3-e0e9-4594-8336-348349950a7d","timestamp":"2026-02-10T16:47:54.158330188Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3NGY2MjhkLWY5NzQtNDMzOC05ZjlhLTk5NmE0Zjk5YzY4MyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.0cKiLnY5lFC6xrAJ4r0GON_YHOaJQE47xd2oO6SVaRw","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2703104,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.161050153Z","id":"a0bc41c3-e0e9-4594-8336-348349950a7d","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2718973,"latency_human":"2.718973ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.161056295Z","id":"a0bc41c3-e0e9-4594-8336-348349950a7d","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2726006,"latency_human":"2.726006ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"8bb705de-724f-4a9f-8ab6-6f6ef2b6bb0a","timestamp":"2026-02-10T16:47:54.161275261Z","method":"POST","path":"/api/libraries/f5c43a17-f704-4fb3-9bf9-8e0fd37386fc/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3NGY2MjhkLWY5NzQtNDMzOC05ZjlhLTk5NmE0Zjk5YzY4MyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.0cKiLnY5lFC6xrAJ4r0GON_YHOaJQE47xd2oO6SVaRw","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3186200,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.164526171Z","id":"8bb705de-724f-4a9f-8ab6-6f6ef2b6bb0a","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/libraries/f5c43a17-f704-4fb3-9bf9-8e0fd37386fc/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3250079,"latency_human":"3.250079ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.164535919Z","id":"8bb705de-724f-4a9f-8ab6-6f6ef2b6bb0a","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/libraries/f5c43a17-f704-4fb3-9bf9-8e0fd37386fc/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3260478,"latency_human":"3.260478ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"a2e5b8bf-34ab-40dc-98a0-eba6fc64006a","timestamp":"2026-02-10T16:47:54.165115504Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3NGY2MjhkLWY5NzQtNDMzOC05ZjlhLTk5NmE0Zjk5YzY4MyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.0cKiLnY5lFC6xrAJ4r0GON_YHOaJQE47xd2oO6SVaRw","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"f5c43a17-f704-4fb3-9bf9-8e0fd37386fc","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":6425278,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:54.171560629Z","id":"a2e5b8bf-34ab-40dc-98a0-eba6fc64006a","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6445356,"latency_human":"6.445356ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:54.171566239Z","id":"a2e5b8bf-34ab-40dc-98a0-eba6fc64006a","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6453180,"latency_human":"6.45318ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:54 [REQUEST] {"request_id":"48c7b4e8-1598-4091-b5fc-c87cf4c527ff","timestamp":"2026-02-10T16:47:54.171731175Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3NGY2MjhkLWY5NzQtNDMzOC05ZjlhLTk5NmE0Zjk5YzY4MyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.0cKiLnY5lFC6xrAJ4r0GON_YHOaJQE47xd2oO6SVaRw","Content-Length":"125","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["ec5c7839-678b-4654-902a-4703fcb03b27"],"collection_id":"7df5b457-caa2-4f10-9925-65adc5cf6f8b"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2616002,"status_code":200,"response_size":172} -{"time":"2026-02-10T16:47:54.17437579Z","id":"48c7b4e8-1598-4091-b5fc-c87cf4c527ff","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2644325,"latency_human":"2.644325ms","bytes_in":125,"bytes_out":172} -{"time":"2026-02-10T16:47:54.174382162Z","id":"48c7b4e8-1598-4091-b5fc-c87cf4c527ff","remote_ip":"127.0.0.1","host":"127.0.0.1:37409","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2651368,"latency_human":"2.651368ms","bytes_in":125,"bytes_out":172} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_MultipleBooksSingleCollection -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'c93bfc81-da8b-4bb4-8c46-31f45e7fc41c' -2026/02/10 16:47:54 [REQUEST] {"request_id":"2073dc80-70f5-4486-8e26-7382932d926b","timestamp":"2026-02-10T16:47:54.198385705Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50047731,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.248477357Z","id":"2073dc80-70f5-4486-8e26-7382932d926b","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50084660,"latency_human":"50.08466ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.248504147Z","id":"2073dc80-70f5-4486-8e26-7382932d926b","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50105619,"latency_human":"50.105619ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"892687f5-b165-4320-9b2b-c3dc35df3ec3","timestamp":"2026-02-10T16:47:54.248796579Z","method":"POST","path":"/api/collections","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"60","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test collection","name":"Test Collection"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2426721,"status_code":201,"response_size":288} -{"time":"2026-02-10T16:47:54.251237176Z","id":"892687f5-b165-4320-9b2b-c3dc35df3ec3","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2440547,"latency_human":"2.440547ms","bytes_in":60,"bytes_out":288} -{"time":"2026-02-10T16:47:54.251240763Z","id":"892687f5-b165-4320-9b2b-c3dc35df3ec3","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2444725,"latency_human":"2.444725ms","bytes_in":60,"bytes_out":288} -2026/02/10 16:47:54 [REQUEST] {"request_id":"5bcb7240-68b2-48c9-b3a5-f05d0cd720f7","timestamp":"2026-02-10T16:47:54.251348673Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3599006,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.254959801Z","id":"5bcb7240-68b2-48c9-b3a5-f05d0cd720f7","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3610366,"latency_human":"3.610366ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.254964099Z","id":"5bcb7240-68b2-48c9-b3a5-f05d0cd720f7","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3615526,"latency_human":"3.615526ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"ba778870-dd3a-42e4-981d-df59169426d2","timestamp":"2026-02-10T16:47:54.255132792Z","method":"POST","path":"/api/libraries/930fe55d-ea60-4a4b-9eae-9f97bd31350c/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2976351,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.258123299Z","id":"ba778870-dd3a-42e4-981d-df59169426d2","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries/930fe55d-ea60-4a4b-9eae-9f97bd31350c/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2990417,"latency_human":"2.990417ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.258128579Z","id":"ba778870-dd3a-42e4-981d-df59169426d2","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries/930fe55d-ea60-4a4b-9eae-9f97bd31350c/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2996628,"latency_human":"2.996628ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"82fe7b08-8ce1-4bfd-ac4c-503864234e58","timestamp":"2026-02-10T16:47:54.258440728Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"930fe55d-ea60-4a4b-9eae-9f97bd31350c","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":6932869,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:54.265399495Z","id":"82fe7b08-8ce1-4bfd-ac4c-503864234e58","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6958597,"latency_human":"6.958597ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:54.265404775Z","id":"82fe7b08-8ce1-4bfd-ac4c-503864234e58","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6964548,"latency_human":"6.964548ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:54 [REQUEST] {"request_id":"a89b7ec8-c210-47ed-9816-a03333d471d4","timestamp":"2026-02-10T16:47:54.265598785Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2335883,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.267948934Z","id":"a89b7ec8-c210-47ed-9816-a03333d471d4","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2349007,"latency_human":"2.349007ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.267953482Z","id":"a89b7ec8-c210-47ed-9816-a03333d471d4","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2354848,"latency_human":"2.354848ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"f5609350-725e-4e26-8b9e-7dd2a06cbb12","timestamp":"2026-02-10T16:47:54.268071181Z","method":"POST","path":"/api/libraries/39ced777-4cb9-4f2f-90d0-cd6ff45ec902/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2379724,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.270472685Z","id":"f5609350-725e-4e26-8b9e-7dd2a06cbb12","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries/39ced777-4cb9-4f2f-90d0-cd6ff45ec902/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2401394,"latency_human":"2.401394ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.270476282Z","id":"f5609350-725e-4e26-8b9e-7dd2a06cbb12","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries/39ced777-4cb9-4f2f-90d0-cd6ff45ec902/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2405672,"latency_human":"2.405672ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"f3cdb694-6e77-4c57-81a7-fb8e182d9f50","timestamp":"2026-02-10T16:47:54.270662778Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"39ced777-4cb9-4f2f-90d0-cd6ff45ec902","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3491416,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:54.274178339Z","id":"f3cdb694-6e77-4c57-81a7-fb8e182d9f50","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3515050,"latency_human":"3.51505ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:54.274193096Z","id":"f3cdb694-6e77-4c57-81a7-fb8e182d9f50","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3530819,"latency_human":"3.530819ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:54 [REQUEST] {"request_id":"13ec6742-9239-43f3-bb14-ec65e44f395a","timestamp":"2026-02-10T16:47:54.274446065Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2507501,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.27697159Z","id":"13ec6742-9239-43f3-bb14-ec65e44f395a","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2525113,"latency_human":"2.525113ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.276985115Z","id":"13ec6742-9239-43f3-bb14-ec65e44f395a","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2539430,"latency_human":"2.53943ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"1a440c50-a8fb-4366-ad3f-d54e19bd5f03","timestamp":"2026-02-10T16:47:54.277160149Z","method":"POST","path":"/api/libraries/74bc96ea-6a5e-4035-ae9c-56e839be60d8/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2136964,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.279472528Z","id":"1a440c50-a8fb-4366-ad3f-d54e19bd5f03","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries/74bc96ea-6a5e-4035-ae9c-56e839be60d8/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2311828,"latency_human":"2.311828ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.279479902Z","id":"1a440c50-a8fb-4366-ad3f-d54e19bd5f03","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/libraries/74bc96ea-6a5e-4035-ae9c-56e839be60d8/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2319993,"latency_human":"2.319993ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"0754b7fc-90a6-4887-a847-e6b00b4080f0","timestamp":"2026-02-10T16:47:54.280031726Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"74bc96ea-6a5e-4035-ae9c-56e839be60d8","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3683292,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:54.283739924Z","id":"0754b7fc-90a6-4887-a847-e6b00b4080f0","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3707025,"latency_human":"3.707025ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:54.283746376Z","id":"0754b7fc-90a6-4887-a847-e6b00b4080f0","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3715531,"latency_human":"3.715531ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:54 [REQUEST] {"request_id":"ac463606-d88c-42c4-83d6-785e4efbfa6d","timestamp":"2026-02-10T16:47:54.284060538Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU1NGYwOTU3LTBmODItNDIxMi05Mjk2LWMzNmQyOTZjYWNiOSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.AwDzC-TsyFFZSK95otJZtH36jxJZMYyl1ZeVhzAWFTA","Content-Length":"203","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["ad16f885-0e28-406f-b127-50195afc3623","9d051459-f626-43bf-ba3d-58dda0fb766e","baad5825-08ee-4a42-91ed-4a408a9f0064"],"collection_id":"0b18fbf2-d0a0-4d54-b725-5dc0473256e7"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":7247133,"status_code":200,"response_size":422} -{"time":"2026-02-10T16:47:54.291337416Z","id":"ac463606-d88c-42c4-83d6-785e4efbfa6d","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":7276207,"latency_human":"7.276207ms","bytes_in":203,"bytes_out":422} -{"time":"2026-02-10T16:47:54.291345281Z","id":"ac463606-d88c-42c4-83d6-785e4efbfa6d","remote_ip":"127.0.0.1","host":"127.0.0.1:39553","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":7285674,"latency_human":"7.285674ms","bytes_in":203,"bytes_out":422} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_MultipleCollections -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ae2d5706-f117-498c-835b-7d225567b876' -2026/02/10 16:47:54 [REQUEST] {"request_id":"7524c3f5-8fd1-4307-99dc-ee270fa27758","timestamp":"2026-02-10T16:47:54.31344909Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49072452,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.36254758Z","id":"7524c3f5-8fd1-4307-99dc-ee270fa27758","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49096056,"latency_human":"49.096056ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.362559352Z","id":"7524c3f5-8fd1-4307-99dc-ee270fa27758","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49106675,"latency_human":"49.106675ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"b8af714c-bfd5-4cc5-8e1c-3ee6a735777e","timestamp":"2026-02-10T16:47:54.36309196Z","method":"POST","path":"/api/collections","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"66","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"First test collection","name":"Test Collection 1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3212398,"status_code":201,"response_size":294} -{"time":"2026-02-10T16:47:54.36633194Z","id":"b8af714c-bfd5-4cc5-8e1c-3ee6a735777e","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3239108,"latency_human":"3.239108ms","bytes_in":66,"bytes_out":294} -{"time":"2026-02-10T16:47:54.366339965Z","id":"b8af714c-bfd5-4cc5-8e1c-3ee6a735777e","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3248275,"latency_human":"3.248275ms","bytes_in":66,"bytes_out":294} -2026/02/10 16:47:54 [REQUEST] {"request_id":"771ea115-a960-48d1-b21c-a365c4860aaa","timestamp":"2026-02-10T16:47:54.366555725Z","method":"POST","path":"/api/collections","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"67","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"Second test collection","name":"Test Collection 2"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2411523,"status_code":201,"response_size":294} -{"time":"2026-02-10T16:47:54.368982085Z","id":"771ea115-a960-48d1-b21c-a365c4860aaa","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2425369,"latency_human":"2.425369ms","bytes_in":67,"bytes_out":294} -{"time":"2026-02-10T16:47:54.368986764Z","id":"771ea115-a960-48d1-b21c-a365c4860aaa","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2431480,"latency_human":"2.43148ms","bytes_in":67,"bytes_out":294} -2026/02/10 16:47:54 [REQUEST] {"request_id":"3354a662-dc60-4563-833f-0db11f784926","timestamp":"2026-02-10T16:47:54.369082341Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3303568,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.372412608Z","id":"3354a662-dc60-4563-833f-0db11f784926","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3329566,"latency_human":"3.329566ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.372420373Z","id":"3354a662-dc60-4563-833f-0db11f784926","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3338092,"latency_human":"3.338092ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"67774316-ae7e-4667-9ba3-f4837b588f2a","timestamp":"2026-02-10T16:47:54.37260776Z","method":"POST","path":"/api/libraries/1a501b0f-9b3b-4f4a-87c0-d8d710aee990/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2941607,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.375565456Z","id":"67774316-ae7e-4667-9ba3-f4837b588f2a","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries/1a501b0f-9b3b-4f4a-87c0-d8d710aee990/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2957316,"latency_human":"2.957316ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.375575505Z","id":"67774316-ae7e-4667-9ba3-f4837b588f2a","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries/1a501b0f-9b3b-4f4a-87c0-d8d710aee990/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2980177,"latency_human":"2.980177ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"dcf38384-5c83-4e45-ac44-4b148664a2d7","timestamp":"2026-02-10T16:47:54.375839354Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"1a501b0f-9b3b-4f4a-87c0-d8d710aee990","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":5792695,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:54.381660592Z","id":"dcf38384-5c83-4e45-ac44-4b148664a2d7","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":5819694,"latency_human":"5.819694ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:54.381669148Z","id":"dcf38384-5c83-4e45-ac44-4b148664a2d7","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":5829644,"latency_human":"5.829644ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:54 [REQUEST] {"request_id":"cea58837-438c-4115-b88c-29e3b7ae75b6","timestamp":"2026-02-10T16:47:54.381898864Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2450676,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.384372232Z","id":"cea58837-438c-4115-b88c-29e3b7ae75b6","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2472677,"latency_human":"2.472677ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.384377241Z","id":"cea58837-438c-4115-b88c-29e3b7ae75b6","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2478708,"latency_human":"2.478708ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"11685c43-1663-4be1-9ba7-0069d685ed7e","timestamp":"2026-02-10T16:47:54.38450589Z","method":"POST","path":"/api/libraries/09bd14a3-ffd4-4609-8200-eb42fe251116/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2211241,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.386735495Z","id":"11685c43-1663-4be1-9ba7-0069d685ed7e","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries/09bd14a3-ffd4-4609-8200-eb42fe251116/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2229165,"latency_human":"2.229165ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.386742729Z","id":"11685c43-1663-4be1-9ba7-0069d685ed7e","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/libraries/09bd14a3-ffd4-4609-8200-eb42fe251116/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2236929,"latency_human":"2.236929ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"e9c1f4fd-92a0-4adf-b719-1ea5f4f97959","timestamp":"2026-02-10T16:47:54.386979979Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"09bd14a3-ffd4-4609-8200-eb42fe251116","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3394456,"status_code":201,"response_size":1045} -{"time":"2026-02-10T16:47:54.390389813Z","id":"e9c1f4fd-92a0-4adf-b719-1ea5f4f97959","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3409985,"latency_human":"3.409985ms","bytes_in":183,"bytes_out":1045} -{"time":"2026-02-10T16:47:54.390394371Z","id":"e9c1f4fd-92a0-4adf-b719-1ea5f4f97959","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3429110,"latency_human":"3.42911ms","bytes_in":183,"bytes_out":1045} -2026/02/10 16:47:54 [REQUEST] {"request_id":"869c0233-0612-4315-9132-34bf273643d3","timestamp":"2026-02-10T16:47:54.390644676Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjkwNDc0YWJmLTFmNTctNGI4Ny04NThkLTU0YzVlYzBkNDk5YSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.GF4yjyrXt4IKHMLnPBcCVs1JV75NJyvLLcYZBBFyMZc","Content-Length":"273","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["d908d677-7881-4e78-8eab-7a84503c31f5"],"collection_id":"e6e6be88-2813-492d-a83f-a5d358de6aa9"},{"book_ids":["d908d677-7881-4e78-8eab-7a84503c31f5","6120d90b-7b00-4985-9b88-fbe24f41f6a1"],"collection_id":"e6d0ed3b-3478-4478-8464-267ed3e1c0a6"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":7360423,"status_code":200,"response_size":422} -{"time":"2026-02-10T16:47:54.398078915Z","id":"869c0233-0612-4315-9132-34bf273643d3","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":7433458,"latency_human":"7.433458ms","bytes_in":273,"bytes_out":422} -{"time":"2026-02-10T16:47:54.398089455Z","id":"869c0233-0612-4315-9132-34bf273643d3","remote_ip":"127.0.0.1","host":"127.0.0.1:37893","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":7445310,"latency_human":"7.44531ms","bytes_in":273,"bytes_out":422} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_DuplicateBooks -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'fe078d60-7d6f-4a9f-80cf-7c9b6c31d1f6' -2026/02/10 16:47:54 [REQUEST] {"request_id":"9fea5e97-4a58-49ad-bfba-ca144e6b3fce","timestamp":"2026-02-10T16:47:54.418674196Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":51238169,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.46993661Z","id":"9fea5e97-4a58-49ad-bfba-ca144e6b3fce","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51257825,"latency_human":"51.257825ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.469944224Z","id":"9fea5e97-4a58-49ad-bfba-ca144e6b3fce","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51270078,"latency_human":"51.270078ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"f2ce13f8-da5a-425b-a6d4-bbcd66aa4ab9","timestamp":"2026-02-10T16:47:54.470121233Z","method":"POST","path":"/api/collections","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhlZDhlZDUyLWY0YTYtNGQ2My1hMGU4LTQzZDU0YmFiOTIxNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Fj5jKBeg-D2PewWpDmF_XmOPkiTOHMPBfk3j0wQygYo","Content-Length":"60","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test collection","name":"Test Collection"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2518000,"status_code":201,"response_size":288} -{"time":"2026-02-10T16:47:54.472654672Z","id":"f2ce13f8-da5a-425b-a6d4-bbcd66aa4ab9","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2533299,"latency_human":"2.533299ms","bytes_in":60,"bytes_out":288} -{"time":"2026-02-10T16:47:54.472660433Z","id":"f2ce13f8-da5a-425b-a6d4-bbcd66aa4ab9","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/collections","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2540051,"latency_human":"2.540051ms","bytes_in":60,"bytes_out":288} -2026/02/10 16:47:54 [REQUEST] {"request_id":"b13329e7-bb8e-4e88-b0ff-c27d03f11c19","timestamp":"2026-02-10T16:47:54.472790654Z","method":"POST","path":"/api/libraries","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhlZDhlZDUyLWY0YTYtNGQ2My1hMGU4LTQzZDU0YmFiOTIxNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Fj5jKBeg-D2PewWpDmF_XmOPkiTOHMPBfk3j0wQygYo","Content-Length":"86","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"description":"A test library for media items","name":"Test Library","type":"ebooks"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":3094861,"status_code":201,"response_size":319} -{"time":"2026-02-10T16:47:54.475907926Z","id":"b13329e7-bb8e-4e88-b0ff-c27d03f11c19","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3116271,"latency_human":"3.116271ms","bytes_in":86,"bytes_out":319} -{"time":"2026-02-10T16:47:54.475913446Z","id":"b13329e7-bb8e-4e88-b0ff-c27d03f11c19","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/libraries","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":3123303,"latency_human":"3.123303ms","bytes_in":86,"bytes_out":319} -2026/02/10 16:47:54 [REQUEST] {"request_id":"b8d1d802-3ff3-4e73-bb0e-fabd42ec1d8a","timestamp":"2026-02-10T16:47:54.476059988Z","method":"POST","path":"/api/libraries/541c1857-86c4-4d34-969a-6e5ac32d2cd4/folders","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhlZDhlZDUyLWY0YTYtNGQ2My1hMGU4LTQzZDU0YmFiOTIxNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Fj5jKBeg-D2PewWpDmF_XmOPkiTOHMPBfk3j0wQygYo","Content-Length":"30","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"folder_path":"/app/uploads"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2613298,"status_code":201,"response_size":170} -{"time":"2026-02-10T16:47:54.47869705Z","id":"b8d1d802-3ff3-4e73-bb0e-fabd42ec1d8a","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/libraries/541c1857-86c4-4d34-969a-6e5ac32d2cd4/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2636039,"latency_human":"2.636039ms","bytes_in":30,"bytes_out":170} -{"time":"2026-02-10T16:47:54.478704463Z","id":"b8d1d802-3ff3-4e73-bb0e-fabd42ec1d8a","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/libraries/541c1857-86c4-4d34-969a-6e5ac32d2cd4/folders","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":2644966,"latency_human":"2.644966ms","bytes_in":30,"bytes_out":170} -2026/02/10 16:47:54 [REQUEST] {"request_id":"7f73b616-a135-445e-a471-fff998d85d7c","timestamp":"2026-02-10T16:47:54.478962682Z","method":"POST","path":"/api/media-items","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhlZDhlZDUyLWY0YTYtNGQ2My1hMGU4LTQzZDU0YmFiOTIxNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Fj5jKBeg-D2PewWpDmF_XmOPkiTOHMPBfk3j0wQygYo","Content-Length":"183","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"author":"Test Author","file_path":"/tmp/test.epub","file_size":1024,"library_id":"541c1857-86c4-4d34-969a-6e5ac32d2cd4","mime_type":"application/epub+zip","title":"Test Media Item"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":6949891,"status_code":201,"response_size":1043} -{"time":"2026-02-10T16:47:54.485942198Z","id":"7f73b616-a135-445e-a471-fff998d85d7c","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6978383,"latency_human":"6.978383ms","bytes_in":183,"bytes_out":1043} -{"time":"2026-02-10T16:47:54.485952898Z","id":"7f73b616-a135-445e-a471-fff998d85d7c","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/media-items","user_agent":"Go-http-client/1.1","status":201,"error":"","latency":6990536,"latency_human":"6.990536ms","bytes_in":183,"bytes_out":1043} -2026/02/10 16:47:54 [REQUEST] {"request_id":"bff606d9-d532-4acb-9f62-25b868ec66cd","timestamp":"2026-02-10T16:47:54.486286707Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhlZDhlZDUyLWY0YTYtNGQ2My1hMGU4LTQzZDU0YmFiOTIxNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Fj5jKBeg-D2PewWpDmF_XmOPkiTOHMPBfk3j0wQygYo","Content-Length":"125","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["b1086fb2-844f-4923-84bc-3627b4dda9a9"],"collection_id":"bdb2f427-b4f8-4dcb-861d-41bcedb3ca03"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2530363,"status_code":200,"response_size":172} -{"time":"2026-02-10T16:47:54.48883864Z","id":"bff606d9-d532-4acb-9f62-25b868ec66cd","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2551122,"latency_human":"2.551122ms","bytes_in":125,"bytes_out":172} -{"time":"2026-02-10T16:47:54.48884388Z","id":"bff606d9-d532-4acb-9f62-25b868ec66cd","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2557944,"latency_human":"2.557944ms","bytes_in":125,"bytes_out":172} -2026/02/10 16:47:54 [REQUEST] {"request_id":"a166a95d-a677-493b-9c49-1b46b040e960","timestamp":"2026-02-10T16:47:54.489069579Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhlZDhlZDUyLWY0YTYtNGQ2My1hMGU4LTQzZDU0YmFiOTIxNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Fj5jKBeg-D2PewWpDmF_XmOPkiTOHMPBfk3j0wQygYo","Content-Length":"125","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"operations":[{"book_ids":["b1086fb2-844f-4923-84bc-3627b4dda9a9"],"collection_id":"bdb2f427-b4f8-4dcb-861d-41bcedb3ca03"}]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":689880,"status_code":200,"response_size":202} -{"time":"2026-02-10T16:47:54.489854735Z","id":"a166a95d-a677-493b-9c49-1b46b040e960","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":779406,"latency_human":"779.406ยตs","bytes_in":125,"bytes_out":202} -{"time":"2026-02-10T16:47:54.48987345Z","id":"a166a95d-a677-493b-9c49-1b46b040e960","remote_ip":"127.0.0.1","host":"127.0.0.1:42173","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":799182,"latency_human":"799.182ยตs","bytes_in":125,"bytes_out":202} -=== RUN TestCollectionsBulkOperations/BulkAddBooks_InvalidRequestBody -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '1bae6edc-2c77-4e93-817e-67f44aa7f2ab' -2026/02/10 16:47:54 [REQUEST] {"request_id":"84761cad-bc96-497d-9c15-46c50c013aa4","timestamp":"2026-02-10T16:47:54.512308754Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49089363,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.561437771Z","id":"84761cad-bc96-497d-9c15-46c50c013aa4","remote_ip":"127.0.0.1","host":"127.0.0.1:39483","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49123066,"latency_human":"49.123066ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.561459501Z","id":"84761cad-bc96-497d-9c15-46c50c013aa4","remote_ip":"127.0.0.1","host":"127.0.0.1:39483","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49141841,"latency_human":"49.141841ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"d4cfee1e-7037-4eff-9f5b-f91efeb2a27f","timestamp":"2026-02-10T16:47:54.561768174Z","method":"POST","path":"/api/collections/bulk-add-books","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImMxMWJlYzkzLTZmMzQtNGFjMy1hMGQzLTFiNTJhYTJjNjUyMyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.gi-b6B6Yb52Qmq8iMz3fGF1OGEQei24zbD3olU2UzBg","Content-Length":"12","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":71723,"status_code":400,"response_size":28} -{"time":"2026-02-10T16:47:54.561871235Z","id":"d4cfee1e-7037-4eff-9f5b-f91efeb2a27f","remote_ip":"127.0.0.1","host":"127.0.0.1:39483","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":103091,"latency_human":"103.091ยตs","bytes_in":12,"bytes_out":28} -{"time":"2026-02-10T16:47:54.561878328Z","id":"d4cfee1e-7037-4eff-9f5b-f91efeb2a27f","remote_ip":"127.0.0.1","host":"127.0.0.1:39483","method":"POST","uri":"/api/collections/bulk-add-books","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":111416,"latency_human":"111.416ยตs","bytes_in":12,"bytes_out":28} ---- PASS: TestCollectionsBulkOperations (0.71s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_WithoutAuth (0.00s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_EmptyOperations (0.08s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_InvalidCollectionID (0.09s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_InvalidBookID (0.07s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_SingleOperation (0.09s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_MultipleBooksSingleCollection (0.12s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_MultipleCollections (0.11s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_DuplicateBooks (0.09s) - --- PASS: TestCollectionsBulkOperations/BulkAddBooks_InvalidRequestBody (0.07s) -=== RUN TestConflictDetection_TriggeringConditions -=== RUN TestConflictDetection_TriggeringConditions/conflict_detected_when_different_devices_sync_within_5_minutes -=== RUN TestConflictDetection_TriggeringConditions/no_conflict_when_progress_difference_is_less_than_1% -=== RUN TestConflictDetection_TriggeringConditions/no_conflict_when_sync_timestamps_are_more_than_5_minutes_apart ---- PASS: TestConflictDetection_TriggeringConditions (0.00s) - --- PASS: TestConflictDetection_TriggeringConditions/conflict_detected_when_different_devices_sync_within_5_minutes (0.00s) - --- PASS: TestConflictDetection_TriggeringConditions/no_conflict_when_progress_difference_is_less_than_1% (0.00s) - --- PASS: TestConflictDetection_TriggeringConditions/no_conflict_when_sync_timestamps_are_more_than_5_minutes_apart (0.00s) -=== RUN TestConflictResolution_ChoosingWinner -=== RUN TestConflictResolution_ChoosingWinner/resolve_conflict_by_choosing_koreader_source -=== RUN TestConflictResolution_ChoosingWinner/resolve_conflict_with_manual_merge_data -=== RUN TestConflictResolution_ChoosingWinner/error_when_winner_is_manual_but_no_manual_data_provided ---- PASS: TestConflictResolution_ChoosingWinner (0.00s) - --- PASS: TestConflictResolution_ChoosingWinner/resolve_conflict_by_choosing_koreader_source (0.00s) - --- PASS: TestConflictResolution_ChoosingWinner/resolve_conflict_with_manual_merge_data (0.00s) - --- PASS: TestConflictResolution_ChoosingWinner/error_when_winner_is_manual_but_no_manual_data_provided (0.00s) -=== RUN TestConflictListing_Filtering -=== RUN TestConflictListing_Filtering/list_only_unresolved_conflicts -=== RUN TestConflictListing_Filtering/list_all_conflicts_regardless_of_status -=== RUN TestConflictListing_Filtering/list_only_resolved_conflicts ---- PASS: TestConflictListing_Filtering (0.00s) - --- PASS: TestConflictListing_Filtering/list_only_unresolved_conflicts (0.00s) - --- PASS: TestConflictListing_Filtering/list_all_conflicts_regardless_of_status (0.00s) - --- PASS: TestConflictListing_Filtering/list_only_resolved_conflicts (0.00s) -=== RUN TestConflictResponse_Structure -=== RUN TestConflictResponse_Structure/conflict_detail_response_includes_all_required_fields -=== RUN TestConflictResponse_Structure/conflict_list_response_includes_summary_counts ---- PASS: TestConflictResponse_Structure (0.00s) - --- PASS: TestConflictResponse_Structure/conflict_detail_response_includes_all_required_fields (0.00s) - --- PASS: TestConflictResponse_Structure/conflict_list_response_includes_summary_counts (0.00s) -=== RUN TestConflictDeletion -=== RUN TestConflictDeletion/delete_single_conflict_by_ID -=== RUN TestConflictDeletion/dismiss_all_resolved_conflicts ---- PASS: TestConflictDeletion (0.00s) - --- PASS: TestConflictDeletion/delete_single_conflict_by_ID (0.00s) - --- PASS: TestConflictDeletion/dismiss_all_resolved_conflicts (0.00s) -=== RUN TestConflictNotification_WebSocketBroadcast -=== RUN TestConflictNotification_WebSocketBroadcast/conflict_detection_notification -=== RUN TestConflictNotification_WebSocketBroadcast/conflict_resolved_notification ---- PASS: TestConflictNotification_WebSocketBroadcast (0.00s) - --- PASS: TestConflictNotification_WebSocketBroadcast/conflict_detection_notification (0.00s) - --- PASS: TestConflictNotification_WebSocketBroadcast/conflict_resolved_notification (0.00s) -=== RUN TestConflictsBulkOperations -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_WithoutAuth -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 [REQUEST] {"request_id":"fce61493-6309-4e31-86ee-9d59cb637748","timestamp":"2026-02-10T16:47:54.563662938Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Content-Length":"82","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["b463802b-887e-437b-89db-d2ccd4be5c24"],"strategy":"most_recent"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":14016,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:54.563707711Z","id":"fce61493-6309-4e31-86ee-9d59cb637748","remote_ip":"127.0.0.1","host":"127.0.0.1:45123","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":38762,"latency_human":"38.762ยตs","bytes_in":82,"bytes_out":39} -{"time":"2026-02-10T16:47:54.563713912Z","id":"fce61493-6309-4e31-86ee-9d59cb637748","remote_ip":"127.0.0.1","host":"127.0.0.1:45123","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":51405,"latency_human":"51.405ยตs","bytes_in":82,"bytes_out":39} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_EmptyConflictIDs -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'faea0af8-a335-448d-9eb1-98783d91d71d' -2026/02/10 16:47:54 [REQUEST] {"request_id":"1e8c2d49-668a-4782-bc05-391bf53fd8eb","timestamp":"2026-02-10T16:47:54.584166379Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50454164,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.634648285Z","id":"1e8c2d49-668a-4782-bc05-391bf53fd8eb","remote_ip":"127.0.0.1","host":"127.0.0.1:40537","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50478490,"latency_human":"50.47849ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.634657462Z","id":"1e8c2d49-668a-4782-bc05-391bf53fd8eb","remote_ip":"127.0.0.1","host":"127.0.0.1:40537","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50492436,"latency_human":"50.492436ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"a83607f3-e38e-4097-98ca-f7038cbc56e8","timestamp":"2026-02-10T16:47:54.634954984Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImZiZTViNDFkLTliNDgtNGFlOC1hMWUwLWNlZWRmMmJlOWY4YyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.WmY69bl51JY1cbIZiew-AzTHu9wW5ovssjuwXqd2a8M","Content-Length":"44","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":[],"strategy":"most_recent"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":63918,"status_code":200,"response_size":0,"error":"code=400, message=conflict_ids required"} -{"time":"2026-02-10T16:47:54.63503948Z","id":"a83607f3-e38e-4097-98ca-f7038cbc56e8","remote_ip":"127.0.0.1","host":"127.0.0.1:40537","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":400,"error":"code=400, message=conflict_ids required","latency":84276,"latency_human":"84.276ยตs","bytes_in":44,"bytes_out":36} -{"time":"2026-02-10T16:47:54.635045862Z","id":"a83607f3-e38e-4097-98ca-f7038cbc56e8","remote_ip":"127.0.0.1","host":"127.0.0.1:40537","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":91349,"latency_human":"91.349ยตs","bytes_in":44,"bytes_out":36} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_InvalidConflictID -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '0c820bdb-a001-4034-9160-49b3a3bf7cdc' -2026/02/10 16:47:54 [REQUEST] {"request_id":"bcc33c92-24af-4733-a081-6f2dbbeaeb6d","timestamp":"2026-02-10T16:47:54.657245389Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49908883,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.707194487Z","id":"bcc33c92-24af-4733-a081-6f2dbbeaeb6d","remote_ip":"127.0.0.1","host":"127.0.0.1:46713","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49939220,"latency_human":"49.93922ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.707204846Z","id":"bcc33c92-24af-4733-a081-6f2dbbeaeb6d","remote_ip":"127.0.0.1","host":"127.0.0.1:46713","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49959307,"latency_human":"49.959307ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"8d2ea509-4b35-4e32-a2bb-93a9ff668461","timestamp":"2026-02-10T16:47:54.707445853Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImVkNmQyZTJhLWQwOWMtNGM2OS1iNGFmLTUwMDJlZTRmMGJhNSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.X8mgY3dKczDeGIy2y7nqpj6JcuHh9POBVRyMLJDxe18","Content-Length":"58","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["invalid-uuid"],"strategy":"most_recent"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":87031,"status_code":200,"response_size":125} -{"time":"2026-02-10T16:47:54.707544386Z","id":"8d2ea509-4b35-4e32-a2bb-93a9ff668461","remote_ip":"127.0.0.1","host":"127.0.0.1:46713","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":98322,"latency_human":"98.322ยตs","bytes_in":58,"bytes_out":125} -{"time":"2026-02-10T16:47:54.707550727Z","id":"8d2ea509-4b35-4e32-a2bb-93a9ff668461","remote_ip":"127.0.0.1","host":"127.0.0.1:46713","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":105977,"latency_human":"105.977ยตs","bytes_in":58,"bytes_out":125} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_InvalidStrategy -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '7b0436a4-8bc0-41aa-b6e9-18be03e848ce' -2026/02/10 16:47:54 [REQUEST] {"request_id":"66c4451a-22b9-45fa-b945-7b098371f344","timestamp":"2026-02-10T16:47:54.72909555Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49721626,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.778849626Z","id":"66c4451a-22b9-45fa-b945-7b098371f344","remote_ip":"127.0.0.1","host":"127.0.0.1:36589","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49742374,"latency_human":"49.742374ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.778859684Z","id":"66c4451a-22b9-45fa-b945-7b098371f344","remote_ip":"127.0.0.1","host":"127.0.0.1:36589","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49764335,"latency_human":"49.764335ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"884150f2-b6f8-44b8-ba75-b78060dfa7b4","timestamp":"2026-02-10T16:47:54.77913207Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjFiMWVhZjBiLWRlMGMtNGM3Zi04MmM0LWZlMGQ0ODhhYjAyYyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.ZxQudyqWkpCmGXUR-L5NCfXiNNnnk0JEvzehGKf8ihM","Content-Length":"87","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["2705df08-ea4e-4e33-b18a-bbfe5e2a8e68"],"strategy":"invalid_strategy"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":336544,"status_code":200,"response_size":148} -{"time":"2026-02-10T16:47:54.779490014Z","id":"884150f2-b6f8-44b8-ba75-b78060dfa7b4","remote_ip":"127.0.0.1","host":"127.0.0.1:36589","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":357513,"latency_human":"357.513ยตs","bytes_in":87,"bytes_out":148} -{"time":"2026-02-10T16:47:54.779497658Z","id":"884150f2-b6f8-44b8-ba75-b78060dfa7b4","remote_ip":"127.0.0.1","host":"127.0.0.1:36589","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":366179,"latency_human":"366.179ยตs","bytes_in":87,"bytes_out":148} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_MostRecentStrategy -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ffb0095f-d8cb-48b7-a1de-aeb6aeb26b27' -2026/02/10 16:47:54 [REQUEST] {"request_id":"6fabf8e6-14d4-4172-926f-8533a068a1fd","timestamp":"2026-02-10T16:47:54.798622662Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48927743,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.847642165Z","id":"6fabf8e6-14d4-4172-926f-8533a068a1fd","remote_ip":"127.0.0.1","host":"127.0.0.1:44115","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48965383,"latency_human":"48.965383ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.847675958Z","id":"6fabf8e6-14d4-4172-926f-8533a068a1fd","remote_ip":"127.0.0.1","host":"127.0.0.1:44115","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49036174,"latency_human":"49.036174ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"1612a3df-3602-431e-9d20-8268519eaf84","timestamp":"2026-02-10T16:47:54.84794118Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjVlZDlkYmYyLWExMzEtNGEyYy05NGUxLTAyNDIzYjM0MDliYyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.O6t9oqMelWl09_NCHK1S9doQh-uMD1tCTyiRuwZhuCk","Content-Length":"121","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["f968053b-4fe4-4255-994f-c14f389bae40","45058dc1-9897-437c-9d62-76808d0cd6fd"],"strategy":"most_recent"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":551082,"status_code":200,"response_size":249} -{"time":"2026-02-10T16:47:54.848506468Z","id":"1612a3df-3602-431e-9d20-8268519eaf84","remote_ip":"127.0.0.1","host":"127.0.0.1:44115","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":564578,"latency_human":"564.578ยตs","bytes_in":121,"bytes_out":249} -{"time":"2026-02-10T16:47:54.848511067Z","id":"1612a3df-3602-431e-9d20-8268519eaf84","remote_ip":"127.0.0.1","host":"127.0.0.1:44115","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":569887,"latency_human":"569.887ยตs","bytes_in":121,"bytes_out":249} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_HighestProgressStrategy -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '7f651822-f714-4ae3-99a4-33ce83cc2064' -2026/02/10 16:47:54 [REQUEST] {"request_id":"5ae8f95c-fbf5-402b-8da8-c8261e26acbf","timestamp":"2026-02-10T16:47:54.86802901Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49320902,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.917368557Z","id":"5ae8f95c-fbf5-402b-8da8-c8261e26acbf","remote_ip":"127.0.0.1","host":"127.0.0.1:45591","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49336943,"latency_human":"49.336943ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.917374087Z","id":"5ae8f95c-fbf5-402b-8da8-c8261e26acbf","remote_ip":"127.0.0.1","host":"127.0.0.1:45591","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49344827,"latency_human":"49.344827ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"25be465f-4ea1-4b17-8e21-4d44444ff00d","timestamp":"2026-02-10T16:47:54.91761305Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjUyN2U3MmEwLTY0YjctNDNhMS1iNWZlLWJiMjYzMjBmYThmMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.MDgW0pL4X4ylaAY_6tsxuNmhwetRqRS1HIwhR_7K3Ak","Content-Length":"126","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["15b26029-3149-44c6-be4e-07ea8c80276c","62c991c9-cf56-4c9f-befc-a6d13004559c"],"strategy":"highest_progress"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":557475,"status_code":200,"response_size":249} -{"time":"2026-02-10T16:47:54.918203125Z","id":"25be465f-4ea1-4b17-8e21-4d44444ff00d","remote_ip":"127.0.0.1","host":"127.0.0.1:45591","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":589935,"latency_human":"589.935ยตs","bytes_in":126,"bytes_out":249} -{"time":"2026-02-10T16:47:54.918209737Z","id":"25be465f-4ea1-4b17-8e21-4d44444ff00d","remote_ip":"127.0.0.1","host":"127.0.0.1:45591","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":597609,"latency_human":"597.609ยตs","bytes_in":126,"bytes_out":249} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_ManualStrategy_WithoutWinner -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '98d6d5b7-63df-4c7d-955b-0bfe88086631' -2026/02/10 16:47:54 [REQUEST] {"request_id":"cde0bc45-1d11-41a0-a5f1-a0958a87aeda","timestamp":"2026-02-10T16:47:54.938680307Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50119343,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:54.988825598Z","id":"cde0bc45-1d11-41a0-a5f1-a0958a87aeda","remote_ip":"127.0.0.1","host":"127.0.0.1:32847","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50140372,"latency_human":"50.140372ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:54.988836298Z","id":"cde0bc45-1d11-41a0-a5f1-a0958a87aeda","remote_ip":"127.0.0.1","host":"127.0.0.1:32847","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50156452,"latency_human":"50.156452ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:54 [REQUEST] {"request_id":"a6d56f0e-cc48-48d3-afad-ecd895c50ea0","timestamp":"2026-02-10T16:47:54.989115886Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzQsImlhdCI6MTc3MDc0MjA3NCwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImJjNzc0OGI1LTZkMTYtNGMyZC1iM2Y2LWU4ODU5MGRjMzdjNyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.N6rXjpxPytoaa8AR2V3P4H0imHuIHt2vusBfWG7Z8pI","Content-Length":"77","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["0fdeb10e-6b22-4075-940a-71bb5dee6f22"],"strategy":"manual"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":329681,"status_code":200,"response_size":148} -{"time":"2026-02-10T16:47:54.989465024Z","id":"a6d56f0e-cc48-48d3-afad-ecd895c50ea0","remote_ip":"127.0.0.1","host":"127.0.0.1:32847","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":348256,"latency_human":"348.256ยตs","bytes_in":77,"bytes_out":148} -{"time":"2026-02-10T16:47:54.989472398Z","id":"a6d56f0e-cc48-48d3-afad-ecd895c50ea0","remote_ip":"127.0.0.1","host":"127.0.0.1:32847","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":357023,"latency_human":"357.023ยตs","bytes_in":77,"bytes_out":148} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_ManualStrategy_WithWinner -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:54 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '03e0c2e0-5b0e-476f-a5ff-1b5faaaafaeb' -2026/02/10 16:47:55 [REQUEST] {"request_id":"9d345629-25d6-4d55-a0c7-7ac10fca9610","timestamp":"2026-02-10T16:47:55.01030528Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49134497,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.059463861Z","id":"9d345629-25d6-4d55-a0c7-7ac10fca9610","remote_ip":"127.0.0.1","host":"127.0.0.1:34037","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49153683,"latency_human":"49.153683ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.059472547Z","id":"9d345629-25d6-4d55-a0c7-7ac10fca9610","remote_ip":"127.0.0.1","host":"127.0.0.1:34037","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49166756,"latency_human":"49.166756ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"8e52dee2-36dd-4e34-b358-fd0c6933a1cf","timestamp":"2026-02-10T16:47:55.059722661Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjQxMzA2MjRkLWEzZTYtNGE3MS05N2I3LWM5NmQ5ZDBmM2FkNiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.1MvzRmjp65pQy8OYt4mVzwbjvATgojERmS0pf5tS7L4","Content-Length":"103","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["113dafb2-aa7c-42d6-afad-ba3a0df1714e"],"strategy":"manual","winning_source":"device"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":399912,"status_code":200,"response_size":148} -{"time":"2026-02-10T16:47:55.060139123Z","id":"8e52dee2-36dd-4e34-b358-fd0c6933a1cf","remote_ip":"127.0.0.1","host":"127.0.0.1:34037","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":416513,"latency_human":"416.513ยตs","bytes_in":103,"bytes_out":148} -{"time":"2026-02-10T16:47:55.060145205Z","id":"8e52dee2-36dd-4e34-b358-fd0c6933a1cf","remote_ip":"127.0.0.1","host":"127.0.0.1:34037","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":424086,"latency_human":"424.086ยตs","bytes_in":103,"bytes_out":148} -=== RUN TestConflictsBulkOperations/BulkResolveConflicts_InvalidRequestBody -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'a2f44f61-be36-462f-b4d4-13c21ab098dc' -2026/02/10 16:47:55 [REQUEST] {"request_id":"c328805c-6ce7-4467-8c33-8a4ca1841e6c","timestamp":"2026-02-10T16:47:55.080102903Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48592822,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.128722084Z","id":"c328805c-6ce7-4467-8c33-8a4ca1841e6c","remote_ip":"127.0.0.1","host":"127.0.0.1:43159","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48614613,"latency_human":"48.614613ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.128739166Z","id":"c328805c-6ce7-4467-8c33-8a4ca1841e6c","remote_ip":"127.0.0.1","host":"127.0.0.1:43159","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48635682,"latency_human":"48.635682ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"75951aaf-e926-477a-a995-6ca7aafc85b7","timestamp":"2026-02-10T16:47:55.129137966Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjhjNjE2NGQ1LTlmZDAtNDY3Zi1iZTBjLTMwNTUzZDhlMjZhYiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.sxIE7AzpDAmP9QEDuunk2jhDoSoOkHRVtPFoISUa3t8","Content-Length":"12","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":88724,"status_code":200,"response_size":0,"error":"code=400, message=invalid request body"} -{"time":"2026-02-10T16:47:55.129255203Z","id":"75951aaf-e926-477a-a995-6ca7aafc85b7","remote_ip":"127.0.0.1","host":"127.0.0.1:43159","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":400,"error":"code=400, message=invalid request body","latency":116376,"latency_human":"116.376ยตs","bytes_in":12,"bytes_out":35} -{"time":"2026-02-10T16:47:55.129264039Z","id":"75951aaf-e926-477a-a995-6ca7aafc85b7","remote_ip":"127.0.0.1","host":"127.0.0.1:43159","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":126775,"latency_human":"126.775ยตs","bytes_in":12,"bytes_out":35} ---- PASS: TestConflictsBulkOperations (0.57s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_WithoutAuth (0.00s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_EmptyConflictIDs (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_InvalidConflictID (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_InvalidStrategy (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_MostRecentStrategy (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_HighestProgressStrategy (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_ManualStrategy_WithoutWinner (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_ManualStrategy_WithWinner (0.07s) - --- PASS: TestConflictsBulkOperations/BulkResolveConflicts_InvalidRequestBody (0.07s) -=== RUN TestConflictsBulkDismiss -=== RUN TestConflictsBulkDismiss/BulkDismissConflicts_WithoutAuth -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 [REQUEST] {"request_id":"81d123d4-5281-4db9-9137-c6f50f56192a","timestamp":"2026-02-10T16:47:55.137333818Z","method":"POST","path":"/api/conflicts/bulk-dismiss","headers":{"Accept-Encoding":"gzip","Content-Length":"57","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["dddc1f6c-8644-473e-be58-cab611749b0c"]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":16501,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:55.137381607Z","id":"81d123d4-5281-4db9-9137-c6f50f56192a","remote_ip":"127.0.0.1","host":"127.0.0.1:35537","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":46717,"latency_human":"46.717ยตs","bytes_in":57,"bytes_out":39} -{"time":"2026-02-10T16:47:55.137390964Z","id":"81d123d4-5281-4db9-9137-c6f50f56192a","remote_ip":"127.0.0.1","host":"127.0.0.1:35537","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":57407,"latency_human":"57.407ยตs","bytes_in":57,"bytes_out":39} -=== RUN TestConflictsBulkDismiss/BulkDismissConflicts_EmptyConflictIDs -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'f66d31c7-012a-46b9-9d72-17dc670d13dd' -2026/02/10 16:47:55 [REQUEST] {"request_id":"6186c7b9-a542-4a3a-b065-a9c1ec18b8cd","timestamp":"2026-02-10T16:47:55.156500679Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":56049032,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.212586981Z","id":"6186c7b9-a542-4a3a-b065-a9c1ec18b8cd","remote_ip":"127.0.0.1","host":"127.0.0.1:36647","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":56082073,"latency_human":"56.082073ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.212600886Z","id":"6186c7b9-a542-4a3a-b065-a9c1ec18b8cd","remote_ip":"127.0.0.1","host":"127.0.0.1:36647","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":56097983,"latency_human":"56.097983ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"a8fac855-2e8c-4aa0-a859-fbb3272988c5","timestamp":"2026-02-10T16:47:55.213128695Z","method":"POST","path":"/api/conflicts/bulk-dismiss","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjRmYmYxMDRiLTQ3M2QtNGY0YS1iMWE5LWE1ODZkODgzNTVhNSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.ESKqYKMbqUCn_NgPratt1r_RnWY4WnvUH2D3cv3TRaE","Content-Length":"19","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":[]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":181788,"status_code":200,"response_size":0,"error":"code=400, message=conflict_ids required"} -{"time":"2026-02-10T16:47:55.213378318Z","id":"a8fac855-2e8c-4aa0-a859-fbb3272988c5","remote_ip":"127.0.0.1","host":"127.0.0.1:36647","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":400,"error":"code=400, message=conflict_ids required","latency":247038,"latency_human":"247.038ยตs","bytes_in":19,"bytes_out":36} -{"time":"2026-02-10T16:47:55.213391924Z","id":"a8fac855-2e8c-4aa0-a859-fbb3272988c5","remote_ip":"127.0.0.1","host":"127.0.0.1:36647","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":263579,"latency_human":"263.579ยตs","bytes_in":19,"bytes_out":36} -=== RUN TestConflictsBulkDismiss/BulkDismissConflicts_InvalidConflictID -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '97cd0574-0432-45be-ae26-a75343fc465d' -2026/02/10 16:47:55 [REQUEST] {"request_id":"5d112719-aac9-478e-a455-39a5ee6ee5fa","timestamp":"2026-02-10T16:47:55.235798594Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":51365585,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.287200988Z","id":"5d112719-aac9-478e-a455-39a5ee6ee5fa","remote_ip":"127.0.0.1","host":"127.0.0.1:35879","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51398205,"latency_human":"51.398205ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.287219773Z","id":"5d112719-aac9-478e-a455-39a5ee6ee5fa","remote_ip":"127.0.0.1","host":"127.0.0.1:35879","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51417492,"latency_human":"51.417492ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"c26286f6-bd12-426c-9031-b2c4f9291033","timestamp":"2026-02-10T16:47:55.287499241Z","method":"POST","path":"/api/conflicts/bulk-dismiss","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjFkOWUwMjNhLTQyOTktNDQ3Yi1hMDQ2LWRkNTY4ZjMzOTFkMyIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.TvoDSd80e7US2XB0xKVBvoobFkQ-7oEIMHcn_C_coyg","Content-Length":"72","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["invalid-uuid","9abc9cc8-bfc8-4e63-b721-f4f32bee2345"]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":427002,"status_code":200,"response_size":226} -{"time":"2026-02-10T16:47:55.287950849Z","id":"c26286f6-bd12-426c-9031-b2c4f9291033","remote_ip":"127.0.0.1","host":"127.0.0.1:35879","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":451388,"latency_human":"451.388ยตs","bytes_in":72,"bytes_out":226} -{"time":"2026-02-10T16:47:55.287958022Z","id":"c26286f6-bd12-426c-9031-b2c4f9291033","remote_ip":"127.0.0.1","host":"127.0.0.1:35879","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":459432,"latency_human":"459.432ยตs","bytes_in":72,"bytes_out":226} -=== RUN TestConflictsBulkDismiss/BulkDismissConflicts_MultipleConflicts -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '573babe7-6045-4650-9948-34f9a4ac294d' -2026/02/10 16:47:55 [REQUEST] {"request_id":"d64cfad9-0c76-40c5-9ca4-bdcacee6c5fd","timestamp":"2026-02-10T16:47:55.30694525Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49894337,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.356868611Z","id":"d64cfad9-0c76-40c5-9ca4-bdcacee6c5fd","remote_ip":"127.0.0.1","host":"127.0.0.1:35119","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49918782,"latency_human":"49.918782ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.356878429Z","id":"d64cfad9-0c76-40c5-9ca4-bdcacee6c5fd","remote_ip":"127.0.0.1","host":"127.0.0.1:35119","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49934441,"latency_human":"49.934441ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"c9a2c7d5-0fee-444f-8520-c60bf7cc816c","timestamp":"2026-02-10T16:47:55.357070826Z","method":"POST","path":"/api/conflicts/bulk-dismiss","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjAwYmVlNzY4LWFjMTgtNGY3Ni1hZDY3LWE0YjI0YmFmNzVlZiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.v3yGqolSuZZq9gz3H5W9x9fID3Rj69YacgReiqzElEY","Content-Length":"135","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["ac9a753f-ea8b-41c4-bf68-0b3f594e93c1","f1fac0fd-b864-42e3-b3f6-4b43b41465a3","6152eb5c-2eb4-49b0-b5be-2999c57f9f43"]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":694859,"status_code":200,"response_size":350} -{"time":"2026-02-10T16:47:55.357785341Z","id":"c9a2c7d5-0fee-444f-8520-c60bf7cc816c","remote_ip":"127.0.0.1","host":"127.0.0.1:35119","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":713794,"latency_human":"713.794ยตs","bytes_in":135,"bytes_out":350} -{"time":"2026-02-10T16:47:55.357791893Z","id":"c9a2c7d5-0fee-444f-8520-c60bf7cc816c","remote_ip":"127.0.0.1","host":"127.0.0.1:35119","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":721658,"latency_human":"721.658ยตs","bytes_in":135,"bytes_out":350} -=== RUN TestConflictsBulkDismiss/BulkDismissConflicts_InvalidRequestBody -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '332a8b40-c0f9-476a-bb7f-c4e7aff508d0' -2026/02/10 16:47:55 [REQUEST] {"request_id":"aa4d1abe-6aa5-4b65-a2b3-26ed921957d7","timestamp":"2026-02-10T16:47:55.37814206Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50015100,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.428195481Z","id":"aa4d1abe-6aa5-4b65-a2b3-26ed921957d7","remote_ip":"127.0.0.1","host":"127.0.0.1:41305","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50050095,"latency_human":"50.050095ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.428214005Z","id":"aa4d1abe-6aa5-4b65-a2b3-26ed921957d7","remote_ip":"127.0.0.1","host":"127.0.0.1:41305","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50067807,"latency_human":"50.067807ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"17f82faa-f6bd-4a6a-b49b-5f9a7b12f021","timestamp":"2026-02-10T16:47:55.428406713Z","method":"POST","path":"/api/conflicts/bulk-dismiss","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImRlYmExNjFhLTRmODUtNGE3My1iZTBhLWE4ODI2YTNhZDllYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.2nz8nvMo263c1lRlvxF5604JOVLzk6eLORn-bNZwomI","Content-Length":"12","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48590,"status_code":200,"response_size":0,"error":"code=400, message=invalid request body"} -{"time":"2026-02-10T16:47:55.428473707Z","id":"17f82faa-f6bd-4a6a-b49b-5f9a7b12f021","remote_ip":"127.0.0.1","host":"127.0.0.1:41305","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":400,"error":"code=400, message=invalid request body","latency":66744,"latency_human":"66.744ยตs","bytes_in":12,"bytes_out":35} -{"time":"2026-02-10T16:47:55.428483375Z","id":"17f82faa-f6bd-4a6a-b49b-5f9a7b12f021","remote_ip":"127.0.0.1","host":"127.0.0.1:41305","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":77153,"latency_human":"77.153ยตs","bytes_in":12,"bytes_out":35} ---- PASS: TestConflictsBulkDismiss (0.30s) - --- PASS: TestConflictsBulkDismiss/BulkDismissConflicts_WithoutAuth (0.01s) - --- PASS: TestConflictsBulkDismiss/BulkDismissConflicts_EmptyConflictIDs (0.08s) - --- PASS: TestConflictsBulkDismiss/BulkDismissConflicts_InvalidConflictID (0.07s) - --- PASS: TestConflictsBulkDismiss/BulkDismissConflicts_MultipleConflicts (0.07s) - --- PASS: TestConflictsBulkDismiss/BulkDismissConflicts_InvalidRequestBody (0.07s) -=== RUN TestConflictsBulkEdgeCases -=== RUN TestConflictsBulkEdgeCases/BulkResolve_NonExistentConflicts -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'eef5a546-0717-4ef0-ae94-090c606527fb' -2026/02/10 16:47:55 [REQUEST] {"request_id":"4edda437-2703-4551-b422-00d7df67896e","timestamp":"2026-02-10T16:47:55.448786504Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":51168540,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.499972185Z","id":"4edda437-2703-4551-b422-00d7df67896e","remote_ip":"127.0.0.1","host":"127.0.0.1:46045","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51183427,"latency_human":"51.183427ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.499980641Z","id":"4edda437-2703-4551-b422-00d7df67896e","remote_ip":"127.0.0.1","host":"127.0.0.1:46045","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51192364,"latency_human":"51.192364ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"69ae367e-5f1f-4884-885c-8d1e767b3532","timestamp":"2026-02-10T16:47:55.500185892Z","method":"POST","path":"/api/conflicts/bulk-resolve","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImE3YTE0ZWIzLTM0YzktNDU0OS1iNzhjLTU1ZjEwZDNlZDU5YiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.tSNrkPKPz8u6SnE1QRtudOrvc0vJPN5XGVPmZUDx_dY","Content-Length":"160","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["31d95760-82bf-40a8-a4f4-b56bc52aad33","82d4a18f-5e96-48cd-b695-727a8eab74fb","81d50d3a-0b8b-431c-8b38-de1b4b7337e6"],"strategy":"most_recent"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":406434,"status_code":200,"response_size":350} -{"time":"2026-02-10T16:47:55.500603727Z","id":"69ae367e-5f1f-4884-885c-8d1e767b3532","remote_ip":"127.0.0.1","host":"127.0.0.1:46045","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":417244,"latency_human":"417.244ยตs","bytes_in":160,"bytes_out":350} -{"time":"2026-02-10T16:47:55.500607193Z","id":"69ae367e-5f1f-4884-885c-8d1e767b3532","remote_ip":"127.0.0.1","host":"127.0.0.1:46045","method":"POST","uri":"/api/conflicts/bulk-resolve","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":422885,"latency_human":"422.885ยตs","bytes_in":160,"bytes_out":350} -=== RUN TestConflictsBulkEdgeCases/BulkDismiss_MixedValidInvalid -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '4111c05f-7227-405f-bb21-36d7bfec7a83' -2026/02/10 16:47:55 [REQUEST] {"request_id":"fd0edc22-e83d-43cf-88bd-a55dc6b0ed85","timestamp":"2026-02-10T16:47:55.519152673Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49877124,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.56904738Z","id":"fd0edc22-e83d-43cf-88bd-a55dc6b0ed85","remote_ip":"127.0.0.1","host":"127.0.0.1:35989","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49892763,"latency_human":"49.892763ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.569055174Z","id":"fd0edc22-e83d-43cf-88bd-a55dc6b0ed85","remote_ip":"127.0.0.1","host":"127.0.0.1:35989","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49901640,"latency_human":"49.90164ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"a9c1e8de-ba33-4c5d-a789-3b9c3c5b742b","timestamp":"2026-02-10T16:47:55.569294408Z","method":"POST","path":"/api/conflicts/bulk-dismiss","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjA1ZmMyOThiLTU0ODMtNDQxMC1hNjQ0LTVhMGU1NTJjODE1MSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.t_4vPLHEaHlP3fYI_V5q_I0jNdghW8vBvMx7LzAGhfE","Content-Length":"91","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"conflict_ids":["invalid-uuid-1","invalid-uuid-2","5525652f-2d04-4e08-b760-70f932d52db2"]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":432943,"status_code":200,"response_size":308} -{"time":"2026-02-10T16:47:55.569745755Z","id":"a9c1e8de-ba33-4c5d-a789-3b9c3c5b742b","remote_ip":"127.0.0.1","host":"127.0.0.1:35989","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":450666,"latency_human":"450.666ยตs","bytes_in":91,"bytes_out":308} -{"time":"2026-02-10T16:47:55.569754571Z","id":"a9c1e8de-ba33-4c5d-a789-3b9c3c5b742b","remote_ip":"127.0.0.1","host":"127.0.0.1:35989","method":"POST","uri":"/api/conflicts/bulk-dismiss","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":460594,"latency_human":"460.594ยตs","bytes_in":91,"bytes_out":308} ---- PASS: TestConflictsBulkEdgeCases (0.14s) - --- PASS: TestConflictsBulkEdgeCases/BulkResolve_NonExistentConflicts (0.07s) - --- PASS: TestConflictsBulkEdgeCases/BulkDismiss_MixedValidInvalid (0.07s) -=== RUN TestUpdateUserMaxDevices -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'de7fa692-2f9c-4e47-a999-f6fd0990d734' -2026/02/10 16:47:55 [REQUEST] {"request_id":"8d57e57d-0a23-4f1c-9f30-367ddd0fa929","timestamp":"2026-02-10T16:47:55.590685916Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":52479601,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.643216682Z","id":"8d57e57d-0a23-4f1c-9f30-367ddd0fa929","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52526970,"latency_human":"52.52697ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.643228043Z","id":"8d57e57d-0a23-4f1c-9f30-367ddd0fa929","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52541587,"latency_human":"52.541587ms","bytes_in":59,"bytes_out":579} -DEBUG: refreshToken generated: 'f824447d-0af5-4e37-8595-270fd6d08631' -2026/02/10 16:47:55 [REQUEST] {"request_id":"7466ec30-c3b4-4c0e-9fb0-ea9bc6148207","timestamp":"2026-02-10T16:47:55.649227101Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":52318423,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.701561594Z","id":"7466ec30-c3b4-4c0e-9fb0-ea9bc6148207","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52334793,"latency_human":"52.334793ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.701567114Z","id":"7466ec30-c3b4-4c0e-9fb0-ea9bc6148207","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":52341776,"latency_human":"52.341776ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"e5121a4f-2f4f-48d8-88f0-3c30b80a22f9","timestamp":"2026-02-10T16:47:55.701735436Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk4YjdiNDM0LWVjMDktNGJjMy04NTVlLTg3YzhmMTNjZjJjMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.3Lu6Fk1yRj2WmsWruoGryrl5AB4gqxcR7jwneN7uaBQ","Content-Length":"128","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"maxdevices@example.com","first_name":"Test","last_name":"User","password":"Test@Pass123!","username":"maxdevicesuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":410913,"status_code":409,"response_size":33} -{"time":"2026-02-10T16:47:55.702169021Z","id":"e5121a4f-2f4f-48d8-88f0-3c30b80a22f9","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":433444,"latency_human":"433.444ยตs","bytes_in":128,"bytes_out":33} -{"time":"2026-02-10T16:47:55.702192534Z","id":"e5121a4f-2f4f-48d8-88f0-3c30b80a22f9","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":458120,"latency_human":"458.12ยตs","bytes_in":128,"bytes_out":33} - device_cap_test.go:345: User maxdevices@example.com already exists, logging in to get ID -DEBUG: refreshToken generated: 'b441ce33-4045-40ed-ad64-f8b9f551f477' -2026/02/10 16:47:55 [REQUEST] {"request_id":"f7594371-19f4-4c14-8cec-1901f961fbad","timestamp":"2026-02-10T16:47:55.702916237Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"61","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"maxdevices@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49028220,"status_code":200,"response_size":595} -{"time":"2026-02-10T16:47:55.751961018Z","id":"f7594371-19f4-4c14-8cec-1901f961fbad","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49043869,"latency_human":"49.043869ms","bytes_in":61,"bytes_out":595} -{"time":"2026-02-10T16:47:55.751967229Z","id":"f7594371-19f4-4c14-8cec-1901f961fbad","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49051193,"latency_human":"49.051193ms","bytes_in":61,"bytes_out":595} - device_cap_test.go:379: JWT claims: map[exp:1.770745675e+09 iat:1.770742075e+09 user_email:maxdevices@example.com user_id:5376cadf-3c0e-4629-9b00-eba8d0e176f9 user_role:user user_username:maxdevicesuser] - device_cap_test.go:382: Extracted userID from JWT: 5376cadf-3c0e-4629-9b00-eba8d0e176f9 -=== RUN TestUpdateUserMaxDevices/Update_to_5_devices -2026/02/10 16:47:55 [REQUEST] {"request_id":"88bd41ff-aa3c-4efb-97e3-3d54382eab8b","timestamp":"2026-02-10T16:47:55.752294326Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk4YjdiNDM0LWVjMDktNGJjMy04NTVlLTg3YzhmMTNjZjJjMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.3Lu6Fk1yRj2WmsWruoGryrl5AB4gqxcR7jwneN7uaBQ","Content-Length":"17","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":5},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2360448,"status_code":200,"response_size":34} -{"time":"2026-02-10T16:47:55.754682445Z","id":"88bd41ff-aa3c-4efb-97e3-3d54382eab8b","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2386417,"latency_human":"2.386417ms","bytes_in":17,"bytes_out":34} -{"time":"2026-02-10T16:47:55.754692224Z","id":"88bd41ff-aa3c-4efb-97e3-3d54382eab8b","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2398188,"latency_human":"2.398188ms","bytes_in":17,"bytes_out":34} -=== RUN TestUpdateUserMaxDevices/Update_to_10_devices_(default) -2026/02/10 16:47:55 [REQUEST] {"request_id":"c9211265-384b-4096-8d7a-7eed86c84761","timestamp":"2026-02-10T16:47:55.754987782Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk4YjdiNDM0LWVjMDktNGJjMy04NTVlLTg3YzhmMTNjZjJjMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.3Lu6Fk1yRj2WmsWruoGryrl5AB4gqxcR7jwneN7uaBQ","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":10},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2107649,"status_code":200,"response_size":34} -{"time":"2026-02-10T16:47:55.757107924Z","id":"c9211265-384b-4096-8d7a-7eed86c84761","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2119221,"latency_human":"2.119221ms","bytes_in":18,"bytes_out":34} -{"time":"2026-02-10T16:47:55.757111912Z","id":"c9211265-384b-4096-8d7a-7eed86c84761","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2124450,"latency_human":"2.12445ms","bytes_in":18,"bytes_out":34} -=== RUN TestUpdateUserMaxDevices/Update_to_50_devices -2026/02/10 16:47:55 [REQUEST] {"request_id":"b1b2a921-fe90-4d71-9ae0-da035dfbe8ae","timestamp":"2026-02-10T16:47:55.757260227Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk4YjdiNDM0LWVjMDktNGJjMy04NTVlLTg3YzhmMTNjZjJjMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.3Lu6Fk1yRj2WmsWruoGryrl5AB4gqxcR7jwneN7uaBQ","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":50},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2052336,"status_code":200,"response_size":34} -{"time":"2026-02-10T16:47:55.759336768Z","id":"b1b2a921-fe90-4d71-9ae0-da035dfbe8ae","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2076241,"latency_human":"2.076241ms","bytes_in":18,"bytes_out":34} -{"time":"2026-02-10T16:47:55.759341788Z","id":"b1b2a921-fe90-4d71-9ae0-da035dfbe8ae","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2081841,"latency_human":"2.081841ms","bytes_in":18,"bytes_out":34} -=== RUN TestUpdateUserMaxDevices/Update_to_100_devices_(maximum) -2026/02/10 16:47:55 [REQUEST] {"request_id":"48503d95-5141-43fc-84de-53454bf7cc99","timestamp":"2026-02-10T16:47:55.75945607Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk4YjdiNDM0LWVjMDktNGJjMy04NTVlLTg3YzhmMTNjZjJjMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.3Lu6Fk1yRj2WmsWruoGryrl5AB4gqxcR7jwneN7uaBQ","Content-Length":"19","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":100},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":2154596,"status_code":200,"response_size":34} -{"time":"2026-02-10T16:47:55.761626996Z","id":"48503d95-5141-43fc-84de-53454bf7cc99","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2171097,"latency_human":"2.171097ms","bytes_in":19,"bytes_out":34} -{"time":"2026-02-10T16:47:55.761631775Z","id":"48503d95-5141-43fc-84de-53454bf7cc99","remote_ip":"127.0.0.1","host":"127.0.0.1:39819","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":2176698,"latency_human":"2.176698ms","bytes_in":19,"bytes_out":34} ---- PASS: TestUpdateUserMaxDevices (0.19s) - --- PASS: TestUpdateUserMaxDevices/Update_to_5_devices (0.00s) - --- PASS: TestUpdateUserMaxDevices/Update_to_10_devices_(default) (0.00s) - --- PASS: TestUpdateUserMaxDevices/Update_to_50_devices (0.00s) - --- PASS: TestUpdateUserMaxDevices/Update_to_100_devices_(maximum) (0.00s) -=== RUN TestUpdateUserMaxDevicesValidation -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'aed5da12-01db-422d-abef-9f87d472e25a' -2026/02/10 16:47:55 [REQUEST] {"request_id":"a98d8b4b-d843-4292-9726-81cbe80992fa","timestamp":"2026-02-10T16:47:55.784150714Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49778340,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.833949442Z","id":"a98d8b4b-d843-4292-9726-81cbe80992fa","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49794430,"latency_human":"49.79443ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.833962827Z","id":"a98d8b4b-d843-4292-9726-81cbe80992fa","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49805701,"latency_human":"49.805701ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"ccc045d2-052a-4beb-9cfe-a6995a394109","timestamp":"2026-02-10T16:47:55.834150375Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImIwNThlZWUwLTk3MDYtNDRjNS1hZGNiLWUxZWFjZjM4NWY1MSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.q_W3BfwiTp3KtSjcgyQBXXOM1M_f8xh5c4lCT7UMgrE","Content-Length":"135","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"admin@example.com","first_name":"Admin","last_name":"User","password":"Admin@Pass123!","role":"admin","username":"adminuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48820584,"status_code":403,"response_size":58} -{"time":"2026-02-10T16:47:55.882994783Z","id":"ccc045d2-052a-4beb-9cfe-a6995a394109","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":48843427,"latency_human":"48.843427ms","bytes_in":135,"bytes_out":58} -{"time":"2026-02-10T16:47:55.883000684Z","id":"ccc045d2-052a-4beb-9cfe-a6995a394109","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":48850640,"latency_human":"48.85064ms","bytes_in":135,"bytes_out":58} -DEBUG: refreshToken generated: '9791a725-7418-4238-9aff-4c90005a0b3f' -2026/02/10 16:47:55 [REQUEST] {"request_id":"2c6935b8-5624-4262-9a5a-d7fd525c4051","timestamp":"2026-02-10T16:47:55.888427129Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48576861,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:55.937029427Z","id":"2c6935b8-5624-4262-9a5a-d7fd525c4051","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48600535,"latency_human":"48.600535ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:55.937039135Z","id":"2c6935b8-5624-4262-9a5a-d7fd525c4051","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48612226,"latency_human":"48.612226ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:55 [REQUEST] {"request_id":"33970c75-bf0b-49a7-8214-1599cbac8693","timestamp":"2026-02-10T16:47:55.937270895Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjY0ZDliZTY4LWQ3ZjQtNGJjOS1hYTgwLTJmOGU5OWVlNDJhNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.KDEBWcNUSM5weTFQ4VIx_Ghv_5mLgRJ8AqIqebUdMJI","Content-Length":"128","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"maxdevices@example.com","first_name":"Test","last_name":"User","password":"Test@Pass123!","username":"maxdevicesuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":339590,"status_code":409,"response_size":33} -{"time":"2026-02-10T16:47:55.937632306Z","id":"33970c75-bf0b-49a7-8214-1599cbac8693","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":360819,"latency_human":"360.819ยตs","bytes_in":128,"bytes_out":33} -{"time":"2026-02-10T16:47:55.937640721Z","id":"33970c75-bf0b-49a7-8214-1599cbac8693","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":370848,"latency_human":"370.848ยตs","bytes_in":128,"bytes_out":33} - device_cap_test.go:345: User maxdevices@example.com already exists, logging in to get ID -DEBUG: refreshToken generated: '3c4cd4fd-a1cf-42ea-a737-33d95743cb5a' -2026/02/10 16:47:55 [REQUEST] {"request_id":"1ae3a91d-6e35-4643-867e-836bcc491d30","timestamp":"2026-02-10T16:47:55.938028821Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"61","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"maxdevices@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":51532043,"status_code":200,"response_size":595} -{"time":"2026-02-10T16:47:55.989599927Z","id":"1ae3a91d-6e35-4643-867e-836bcc491d30","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51570374,"latency_human":"51.570374ms","bytes_in":61,"bytes_out":595} -{"time":"2026-02-10T16:47:55.989609044Z","id":"1ae3a91d-6e35-4643-867e-836bcc491d30","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51580843,"latency_human":"51.580843ms","bytes_in":61,"bytes_out":595} - device_cap_test.go:379: JWT claims: map[exp:1.770745675e+09 iat:1.770742075e+09 user_email:maxdevices@example.com user_id:5376cadf-3c0e-4629-9b00-eba8d0e176f9 user_role:user user_username:maxdevicesuser] - device_cap_test.go:382: Extracted userID from JWT: 5376cadf-3c0e-4629-9b00-eba8d0e176f9 -=== RUN TestUpdateUserMaxDevicesValidation/Zero_devices_(below_minimum) -2026/02/10 16:47:55 [REQUEST] {"request_id":"7665dd04-eea6-4ccb-8aaa-f09804514c4b","timestamp":"2026-02-10T16:47:55.989964383Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjY0ZDliZTY4LWQ3ZjQtNGJjOS1hYTgwLTJmOGU5OWVlNDJhNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.KDEBWcNUSM5weTFQ4VIx_Ghv_5mLgRJ8AqIqebUdMJI","Content-Length":"17","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":0},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":44452,"status_code":400,"response_size":127} -{"time":"2026-02-10T16:47:55.990018393Z","id":"7665dd04-eea6-4ccb-8aaa-f09804514c4b","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":54020,"latency_human":"54.02ยตs","bytes_in":17,"bytes_out":127} -{"time":"2026-02-10T16:47:55.990021949Z","id":"7665dd04-eea6-4ccb-8aaa-f09804514c4b","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":57928,"latency_human":"57.928ยตs","bytes_in":17,"bytes_out":127} -=== RUN TestUpdateUserMaxDevicesValidation/Negative_devices -2026/02/10 16:47:55 [REQUEST] {"request_id":"c1b2205f-c1d8-4b5b-a1df-a703521785ac","timestamp":"2026-02-10T16:47:55.990324601Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjY0ZDliZTY4LWQ3ZjQtNGJjOS1hYTgwLTJmOGU5OWVlNDJhNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.KDEBWcNUSM5weTFQ4VIx_Ghv_5mLgRJ8AqIqebUdMJI","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":-1},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":29074,"status_code":400,"response_size":122} -{"time":"2026-02-10T16:47:55.990362621Z","id":"c1b2205f-c1d8-4b5b-a1df-a703521785ac","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":37370,"latency_human":"37.37ยตs","bytes_in":18,"bytes_out":122} -{"time":"2026-02-10T16:47:55.990370376Z","id":"c1b2205f-c1d8-4b5b-a1df-a703521785ac","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":45805,"latency_human":"45.805ยตs","bytes_in":18,"bytes_out":122} -=== RUN TestUpdateUserMaxDevicesValidation/101_devices_(above_maximum) -2026/02/10 16:47:55 [REQUEST] {"request_id":"6c830db7-b927-41ca-9df2-957b5bdba982","timestamp":"2026-02-10T16:47:55.99053966Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjY0ZDliZTY4LWQ3ZjQtNGJjOS1hYTgwLTJmOGU5OWVlNDJhNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.KDEBWcNUSM5weTFQ4VIx_Ghv_5mLgRJ8AqIqebUdMJI","Content-Length":"19","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":101},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":23052,"status_code":400,"response_size":122} -{"time":"2026-02-10T16:47:55.990574434Z","id":"6c830db7-b927-41ca-9df2-957b5bdba982","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":34735,"latency_human":"34.735ยตs","bytes_in":19,"bytes_out":122} -{"time":"2026-02-10T16:47:55.99057745Z","id":"6c830db7-b927-41ca-9df2-957b5bdba982","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":38130,"latency_human":"38.13ยตs","bytes_in":19,"bytes_out":122} -=== RUN TestUpdateUserMaxDevicesValidation/1000_devices_(far_above_maximum) -2026/02/10 16:47:55 [REQUEST] {"request_id":"b485b967-913b-4ad7-9674-a50f899a7b5e","timestamp":"2026-02-10T16:47:55.990902152Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzUsImlhdCI6MTc3MDc0MjA3NSwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjY0ZDliZTY4LWQ3ZjQtNGJjOS1hYTgwLTJmOGU5OWVlNDJhNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.KDEBWcNUSM5weTFQ4VIx_Ghv_5mLgRJ8AqIqebUdMJI","Content-Length":"20","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":1000},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":34975,"status_code":400,"response_size":122} -{"time":"2026-02-10T16:47:55.990950722Z","id":"b485b967-913b-4ad7-9674-a50f899a7b5e","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":48369,"latency_human":"48.369ยตs","bytes_in":20,"bytes_out":122} -{"time":"2026-02-10T16:47:55.990953798Z","id":"b485b967-913b-4ad7-9674-a50f899a7b5e","remote_ip":"127.0.0.1","host":"127.0.0.1:37475","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":51936,"latency_human":"51.936ยตs","bytes_in":20,"bytes_out":122} ---- PASS: TestUpdateUserMaxDevicesValidation (0.23s) - --- PASS: TestUpdateUserMaxDevicesValidation/Zero_devices_(below_minimum) (0.00s) - --- PASS: TestUpdateUserMaxDevicesValidation/Negative_devices (0.00s) - --- PASS: TestUpdateUserMaxDevicesValidation/101_devices_(above_maximum) (0.00s) - --- PASS: TestUpdateUserMaxDevicesValidation/1000_devices_(far_above_maximum) (0.00s) -=== RUN TestUpdateUserMaxDevicesAuth -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:55 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '17c9c7da-4195-4ab6-b937-a4385c4f8d6d' -2026/02/10 16:47:56 [REQUEST] {"request_id":"00ad1082-155e-4940-b516-4c500d076dd9","timestamp":"2026-02-10T16:47:56.011004479Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49841378,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.060892383Z","id":"00ad1082-155e-4940-b516-4c500d076dd9","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49877244,"latency_human":"49.877244ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.060903434Z","id":"00ad1082-155e-4940-b516-4c500d076dd9","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49897703,"latency_human":"49.897703ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"8e454740-a834-421d-8e72-b7ac2380f11c","timestamp":"2026-02-10T16:47:56.061169257Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjU3MmQzZmRhLTY0OGEtNDAzNC1hZjI2LWZkNmU2NTZiODExMiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.XoDd_ZyqZB7c9zPcgXX5sfB_uqDrilGTpinxafJJ-8A","Content-Length":"135","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"admin@example.com","first_name":"Admin","last_name":"User","password":"Admin@Pass123!","role":"admin","username":"adminuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48419711,"status_code":403,"response_size":58} -{"time":"2026-02-10T16:47:56.109618392Z","id":"8e454740-a834-421d-8e72-b7ac2380f11c","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":48447313,"latency_human":"48.447313ms","bytes_in":135,"bytes_out":58} -{"time":"2026-02-10T16:47:56.109626147Z","id":"8e454740-a834-421d-8e72-b7ac2380f11c","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":48457050,"latency_human":"48.45705ms","bytes_in":135,"bytes_out":58} -DEBUG: refreshToken generated: '00679ec0-0a06-4d09-a939-bdf00708b437' -2026/02/10 16:47:56 [REQUEST] {"request_id":"17d8d12e-8631-4366-bc2c-801cfe10cf04","timestamp":"2026-02-10T16:47:56.115391631Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50497014,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.165911918Z","id":"17d8d12e-8631-4366-bc2c-801cfe10cf04","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50518684,"latency_human":"50.518684ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.165919913Z","id":"17d8d12e-8631-4366-bc2c-801cfe10cf04","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50528252,"latency_human":"50.528252ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"905602c2-ec6c-444d-a66e-b1947932cd6a","timestamp":"2026-02-10T16:47:56.166211413Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjljYTA2Nzk4LTA0ZmEtNDE5MC04ZjVkLTAyNzE4N2Q0MWQwYiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.wVf5PT6Wty464MCwFFPhTJBw8CjTtIy2j5ggW7_MgBA","Content-Length":"128","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"maxdevices@example.com","first_name":"Test","last_name":"User","password":"Test@Pass123!","username":"maxdevicesuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":319804,"status_code":409,"response_size":33} -{"time":"2026-02-10T16:47:56.166552095Z","id":"905602c2-ec6c-444d-a66e-b1947932cd6a","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":339901,"latency_human":"339.901ยตs","bytes_in":128,"bytes_out":33} -{"time":"2026-02-10T16:47:56.166557125Z","id":"905602c2-ec6c-444d-a66e-b1947932cd6a","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":346543,"latency_human":"346.543ยตs","bytes_in":128,"bytes_out":33} - device_cap_test.go:345: User maxdevices@example.com already exists, logging in to get ID -DEBUG: refreshToken generated: '705410a9-9533-4d2a-91bf-5ba2cd26418e' -2026/02/10 16:47:56 [REQUEST] {"request_id":"d8306d27-b64e-4842-99bf-b9f686743571","timestamp":"2026-02-10T16:47:56.166763047Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"61","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"maxdevices@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50247039,"status_code":200,"response_size":595} -{"time":"2026-02-10T16:47:56.217032859Z","id":"d8306d27-b64e-4842-99bf-b9f686743571","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50267317,"latency_human":"50.267317ms","bytes_in":61,"bytes_out":595} -{"time":"2026-02-10T16:47:56.217041424Z","id":"d8306d27-b64e-4842-99bf-b9f686743571","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50285691,"latency_human":"50.285691ms","bytes_in":61,"bytes_out":595} - device_cap_test.go:379: JWT claims: map[exp:1.770745676e+09 iat:1.770742076e+09 user_email:maxdevices@example.com user_id:5376cadf-3c0e-4629-9b00-eba8d0e176f9 user_role:user user_username:maxdevicesuser] - device_cap_test.go:382: Extracted userID from JWT: 5376cadf-3c0e-4629-9b00-eba8d0e176f9 -=== RUN TestUpdateUserMaxDevicesAuth/No_authorization -2026/02/10 16:47:56 [REQUEST] {"request_id":"cd0c21a0-9325-4006-8319-09dfa6c074a7","timestamp":"2026-02-10T16:47:56.217447628Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":10},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":17152,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:56.217503041Z","id":"cd0c21a0-9325-4006-8319-09dfa6c074a7","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":55363,"latency_human":"55.363ยตs","bytes_in":18,"bytes_out":39} -{"time":"2026-02-10T16:47:56.217510535Z","id":"cd0c21a0-9325-4006-8319-09dfa6c074a7","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":70821,"latency_human":"70.821ยตs","bytes_in":18,"bytes_out":39} -=== RUN TestUpdateUserMaxDevicesAuth/Non-admin_user -2026/02/10 16:47:56 [REQUEST] {"request_id":"ebbc1082-5a55-4ec5-a64e-f31de76c6fb1","timestamp":"2026-02-10T16:47:56.217839074Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjljYTA2Nzk4LTA0ZmEtNDE5MC04ZjVkLTAyNzE4N2Q0MWQwYiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.wVf5PT6Wty464MCwFFPhTJBw8CjTtIy2j5ggW7_MgBA","Content-Length":"128","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"maxdevices@example.com","first_name":"Test","last_name":"User","password":"Test@Pass123!","username":"maxdevicesuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":294897,"status_code":409,"response_size":33} -{"time":"2026-02-10T16:47:56.218169888Z","id":"ebbc1082-5a55-4ec5-a64e-f31de76c6fb1","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":330764,"latency_human":"330.764ยตs","bytes_in":128,"bytes_out":33} -{"time":"2026-02-10T16:47:56.218185276Z","id":"ebbc1082-5a55-4ec5-a64e-f31de76c6fb1","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":409,"error":"","latency":347224,"latency_human":"347.224ยตs","bytes_in":128,"bytes_out":33} - device_cap_test.go:345: User maxdevices@example.com already exists, logging in to get ID -DEBUG: refreshToken generated: '172ce389-f307-4ab0-9b07-98a4e8341659' -2026/02/10 16:47:56 [REQUEST] {"request_id":"6dc8df0e-5a92-4c6c-b84b-04cc6e0ae038","timestamp":"2026-02-10T16:47:56.218390737Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"61","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"maxdevices@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50716130,"status_code":200,"response_size":595} -{"time":"2026-02-10T16:47:56.269121835Z","id":"6dc8df0e-5a92-4c6c-b84b-04cc6e0ae038","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50730316,"latency_human":"50.730316ms","bytes_in":61,"bytes_out":595} -{"time":"2026-02-10T16:47:56.269127335Z","id":"6dc8df0e-5a92-4c6c-b84b-04cc6e0ae038","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50736728,"latency_human":"50.736728ms","bytes_in":61,"bytes_out":595} - device_cap_test.go:379: JWT claims: map[exp:1.770745676e+09 iat:1.770742076e+09 user_email:maxdevices@example.com user_id:5376cadf-3c0e-4629-9b00-eba8d0e176f9 user_role:user user_username:maxdevicesuser] - device_cap_test.go:382: Extracted userID from JWT: 5376cadf-3c0e-4629-9b00-eba8d0e176f9 -DEBUG: refreshToken generated: '23cf5337-a596-4c10-9970-e683fa9d839e' -2026/02/10 16:47:56 [REQUEST] {"request_id":"2addc3d4-bcc9-4364-80e8-018eafc3303c","timestamp":"2026-02-10T16:47:56.269365517Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"61","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"maxdevices@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48303676,"status_code":200,"response_size":595} -{"time":"2026-02-10T16:47:56.317693178Z","id":"2addc3d4-bcc9-4364-80e8-018eafc3303c","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48326478,"latency_human":"48.326478ms","bytes_in":61,"bytes_out":595} -{"time":"2026-02-10T16:47:56.31769969Z","id":"2addc3d4-bcc9-4364-80e8-018eafc3303c","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48337780,"latency_human":"48.33778ms","bytes_in":61,"bytes_out":595} -2026/02/10 16:47:56 [REQUEST] {"request_id":"5dff3430-175e-46a9-a6bb-2aadfc558ea2","timestamp":"2026-02-10T16:47:56.317868112Z","method":"PUT","path":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6Im1heGRldmljZXNAZXhhbXBsZS5jb20iLCJ1c2VyX2lkIjoiNTM3NmNhZGYtM2MwZS00NjI5LTliMDAtZWJhOGQwZTE3NmY5IiwidXNlcl9yb2xlIjoidXNlciIsInVzZXJfdXNlcm5hbWUiOiJtYXhkZXZpY2VzdXNlciJ9.hDnfTBbp7zszcmK3FvB3z0WYnRET7ZfizPaNWfqbgQE","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":10},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":60002,"status_code":403,"response_size":34} -{"time":"2026-02-10T16:47:56.317939024Z","id":"5dff3430-175e-46a9-a6bb-2aadfc558ea2","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":70951,"latency_human":"70.951ยตs","bytes_in":18,"bytes_out":34} -{"time":"2026-02-10T16:47:56.317941959Z","id":"5dff3430-175e-46a9-a6bb-2aadfc558ea2","remote_ip":"127.0.0.1","host":"127.0.0.1:44069","method":"PUT","uri":"/api/auth/users/5376cadf-3c0e-4629-9b00-eba8d0e176f9/max-devices","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":74799,"latency_human":"74.799ยตs","bytes_in":18,"bytes_out":34} ---- PASS: TestUpdateUserMaxDevicesAuth (0.33s) - --- PASS: TestUpdateUserMaxDevicesAuth/No_authorization (0.00s) - --- PASS: TestUpdateUserMaxDevicesAuth/Non-admin_user (0.10s) -=== RUN TestUpdateUserMaxDevicesNonExistentUser -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '3af8f5ed-2489-41c5-a20b-67eb63063fd4' -2026/02/10 16:47:56 [REQUEST] {"request_id":"d44774ae-ee28-4406-9dc4-bc82fd94d21d","timestamp":"2026-02-10T16:47:56.337516918Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":51894225,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.389435128Z","id":"d44774ae-ee28-4406-9dc4-bc82fd94d21d","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51915144,"latency_human":"51.915144ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.389445517Z","id":"d44774ae-ee28-4406-9dc4-bc82fd94d21d","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":51928730,"latency_human":"51.92873ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"3fdfdc5e-1c3d-48e3-af45-fc0a8ccecdcb","timestamp":"2026-02-10T16:47:56.389681144Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjJkNGI5OWMwLTk1OGMtNDBmNS1hYjhhLWJmZWJiYWI3Y2RjYSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.ZMEcItWC-Lgx6bCnIrvs09lBe8w_3BWl7zG6pUg9bcY","Content-Length":"135","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"admin@example.com","first_name":"Admin","last_name":"User","password":"Admin@Pass123!","role":"admin","username":"adminuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49640624,"status_code":403,"response_size":58} -{"time":"2026-02-10T16:47:56.439356462Z","id":"3fdfdc5e-1c3d-48e3-af45-fc0a8ccecdcb","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":49674507,"latency_human":"49.674507ms","bytes_in":135,"bytes_out":58} -{"time":"2026-02-10T16:47:56.439363976Z","id":"3fdfdc5e-1c3d-48e3-af45-fc0a8ccecdcb","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":49683794,"latency_human":"49.683794ms","bytes_in":135,"bytes_out":58} -DEBUG: refreshToken generated: 'a18b973e-339f-4a30-b2c3-088698269f3b' -2026/02/10 16:47:56 [REQUEST] {"request_id":"29ab3c8b-ec6c-4897-a884-bc771b84fb26","timestamp":"2026-02-10T16:47:56.445004168Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50254444,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.495283458Z","id":"29ab3c8b-ec6c-4897-a884-bc771b84fb26","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50277857,"latency_human":"50.277857ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.495292635Z","id":"29ab3c8b-ec6c-4897-a884-bc771b84fb26","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50288797,"latency_human":"50.288797ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"82d063e5-7456-42f5-b702-579159fc8ae6","timestamp":"2026-02-10T16:47:56.495496403Z","method":"PUT","path":"/api/auth/users/a34fd9d0-fa61-41a2-ab54-3b6614c67d98/max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjdkZGQxYjdkLTUwZDAtNGFjMC1hODg2LWI3ZTYzZjhlNGFjNiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.Nc8P-7r8TM2seOlJEUvIQ4u9PlRLJU1d_reEafOu6-I","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":10},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":1018249,"status_code":404,"response_size":27} -{"time":"2026-02-10T16:47:56.49653549Z","id":"82d063e5-7456-42f5-b702-579159fc8ae6","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"PUT","uri":"/api/auth/users/a34fd9d0-fa61-41a2-ab54-3b6614c67d98/max-devices","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":1038146,"latency_human":"1.038146ms","bytes_in":18,"bytes_out":27} -{"time":"2026-02-10T16:47:56.496559374Z","id":"82d063e5-7456-42f5-b702-579159fc8ae6","remote_ip":"127.0.0.1","host":"127.0.0.1:43633","method":"PUT","uri":"/api/auth/users/a34fd9d0-fa61-41a2-ab54-3b6614c67d98/max-devices","user_agent":"Go-http-client/1.1","status":404,"error":"","latency":1046301,"latency_human":"1.046301ms","bytes_in":18,"bytes_out":27} ---- PASS: TestUpdateUserMaxDevicesNonExistentUser (0.18s) -=== RUN TestUpdateUserMaxDevicesMissingUserID -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '885a742e-a5e1-4f14-b562-380b731394c4' -2026/02/10 16:47:56 [REQUEST] {"request_id":"a676484c-04be-42fc-8919-964b34afe126","timestamp":"2026-02-10T16:47:56.516619292Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49508780,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.566159983Z","id":"a676484c-04be-42fc-8919-964b34afe126","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49536262,"latency_human":"49.536262ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.566176193Z","id":"a676484c-04be-42fc-8919-964b34afe126","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49551320,"latency_human":"49.55132ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"e8cb3840-1e2e-4326-be32-34b2c5ddc7ab","timestamp":"2026-02-10T16:47:56.566529308Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjEyOWQyNjMyLTViZTgtNGVjMi1iNmI5LTY4YTlkNDYzZDRmNSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.rVRLlfLOeG2Rlc4sOOYUWUCFJZ7liZ-11D3uApNZtNk","Content-Length":"135","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"admin@example.com","first_name":"Admin","last_name":"User","password":"Admin@Pass123!","role":"admin","username":"adminuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":46318614,"status_code":403,"response_size":58} -{"time":"2026-02-10T16:47:56.61287945Z","id":"e8cb3840-1e2e-4326-be32-34b2c5ddc7ab","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":46349200,"latency_human":"46.3492ms","bytes_in":135,"bytes_out":58} -{"time":"2026-02-10T16:47:56.612886263Z","id":"e8cb3840-1e2e-4326-be32-34b2c5ddc7ab","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":46357446,"latency_human":"46.357446ms","bytes_in":135,"bytes_out":58} -DEBUG: refreshToken generated: '89d62519-5fd5-4ddb-b2d4-629b70e084dc' -2026/02/10 16:47:56 [REQUEST] {"request_id":"0b709c36-a782-4400-ad0a-02d07d274e61","timestamp":"2026-02-10T16:47:56.619131817Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50611156,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.669766587Z","id":"0b709c36-a782-4400-ad0a-02d07d274e61","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50632806,"latency_human":"50.632806ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.669776115Z","id":"0b709c36-a782-4400-ad0a-02d07d274e61","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50644418,"latency_human":"50.644418ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"d64a5aad-99d8-41af-95d7-60fbd864539f","timestamp":"2026-02-10T16:47:56.669952882Z","method":"PUT","path":"/api/auth/users//max-devices","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6ImU4YmVjZjdmLWQ5MDUtNGFkNC1hZDM4LWI3MmJmZGQ3MTEyZiIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.I2fWzFm_xNE9Zp8UbWieXV3_GrlXhTf7RVVoKMcCAdk","Content-Length":"18","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"max_devices":10},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":62435,"status_code":400,"response_size":29} -{"time":"2026-02-10T16:47:56.670043751Z","id":"d64a5aad-99d8-41af-95d7-60fbd864539f","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"PUT","uri":"/api/auth/users//max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":90427,"latency_human":"90.427ยตs","bytes_in":18,"bytes_out":29} -{"time":"2026-02-10T16:47:56.670049952Z","id":"d64a5aad-99d8-41af-95d7-60fbd864539f","remote_ip":"127.0.0.1","host":"127.0.0.1:34165","method":"PUT","uri":"/api/auth/users//max-devices","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":97862,"latency_human":"97.862ยตs","bytes_in":18,"bytes_out":29} ---- PASS: TestUpdateUserMaxDevicesMissingUserID (0.17s) -=== RUN TestListUsersIncludesMaxDevices -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ca5c2474-bffe-4d8b-95df-741bdb9ecea2' -2026/02/10 16:47:56 [REQUEST] {"request_id":"eea732e7-06a4-4eb9-a31a-78b8eb759ae6","timestamp":"2026-02-10T16:47:56.689274211Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50886085,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.740225317Z","id":"eea732e7-06a4-4eb9-a31a-78b8eb759ae6","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50942731,"latency_human":"50.942731ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.740240926Z","id":"eea732e7-06a4-4eb9-a31a-78b8eb759ae6","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50965613,"latency_human":"50.965613ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"01e271b4-9312-4467-9bee-a6dc3564cab8","timestamp":"2026-02-10T16:47:56.740621472Z","method":"POST","path":"/api/auth/register","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjNiODM0NjgzLWJjZjktNDY1NS04NTM2LTEwNjQzZGM0NjJhNSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.yecrQ3hU-M6cseY95jwe1YY20mFDFAGiHKHJMwSvvIM","Content-Length":"135","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"email":"admin@example.com","first_name":"Admin","last_name":"User","password":"Admin@Pass123!","role":"admin","username":"adminuser"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50438856,"status_code":403,"response_size":58} -{"time":"2026-02-10T16:47:56.791092568Z","id":"01e271b4-9312-4467-9bee-a6dc3564cab8","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":50470635,"latency_human":"50.470635ms","bytes_in":135,"bytes_out":58} -{"time":"2026-02-10T16:47:56.791098198Z","id":"01e271b4-9312-4467-9bee-a6dc3564cab8","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"POST","uri":"/api/auth/register","user_agent":"Go-http-client/1.1","status":403,"error":"","latency":50478048,"latency_human":"50.478048ms","bytes_in":135,"bytes_out":58} -DEBUG: refreshToken generated: '1f96750e-a35a-4f52-8e34-d1362680b15e' -2026/02/10 16:47:56 [REQUEST] {"request_id":"10f09196-330c-46f3-bb81-397019a3f8bb","timestamp":"2026-02-10T16:47:56.796898397Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":50437904,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.847395972Z","id":"10f09196-330c-46f3-bb81-397019a3f8bb","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50495041,"latency_human":"50.495041ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:56.847408495Z","id":"10f09196-330c-46f3-bb81-397019a3f8bb","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":50510029,"latency_human":"50.510029ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"88485622-e47e-4817-8cff-66beb5670444","timestamp":"2026-02-10T16:47:56.847627792Z","method":"GET","path":"/api/auth/users","headers":{"Accept-Encoding":"gzip","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjgwYjNmZmMwLWM5NGUtNDhmYi05OGFjLWYwYWFiNzY2NTFjOCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0._j6Bh84lYINfaD4X3wGF6fI0R456hrhl_KznxCt-obw","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":580096,"status_code":200,"response_size":3792} -{"time":"2026-02-10T16:47:56.848237673Z","id":"88485622-e47e-4817-8cff-66beb5670444","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"GET","uri":"/api/auth/users","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":608308,"latency_human":"608.308ยตs","bytes_in":0,"bytes_out":3792} -{"time":"2026-02-10T16:47:56.848254825Z","id":"88485622-e47e-4817-8cff-66beb5670444","remote_ip":"127.0.0.1","host":"127.0.0.1:42089","method":"GET","uri":"/api/auth/users","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":627965,"latency_human":"627.965ยตs","bytes_in":0,"bytes_out":3792} ---- PASS: TestListUsersIncludesMaxDevices (0.18s) -=== RUN TestDeviceRegistrationFlow -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:56 [REQUEST] {"request_id":"4dacf355-2b64-4381-b85b-25fac2cbd2a1","timestamp":"2026-02-10T16:47:56.84905032Z","method":"POST","path":"/api/devices/register","headers":{"Content-Type":"application/json"},"body":{"device_identifier":"kindle-test-hw-id-12345","device_name":"Test Kindle Paperwhite","device_type":"koreader"},"remote_addr":"192.0.2.1","duration":1441314,"status_code":201,"response_size":1239} -{"time":"2026-02-10T16:47:56.850506211Z","id":"4dacf355-2b64-4381-b85b-25fac2cbd2a1","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/devices/register","user_agent":"","status":201,"error":"","latency":1452785,"latency_human":"1.452785ms","bytes_in":0,"bytes_out":1239} -{"time":"2026-02-10T16:47:56.850512362Z","id":"4dacf355-2b64-4381-b85b-25fac2cbd2a1","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/devices/register","user_agent":"","status":201,"error":"","latency":1460449,"latency_human":"1.460449ms","bytes_in":0,"bytes_out":1239} -2026/02/10 16:47:56 [REQUEST] {"request_id":"13995485-207c-424a-ba1d-52eea415e0cc","timestamp":"2026-02-10T16:47:56.850542949Z","method":"POST","path":"/api/devices/register/status","headers":{"Content-Type":"application/json"},"body":{"registration_id":"3e9df04d-4ac1-4f7a-b913-fbccfebb562d"},"remote_addr":"192.0.2.1","duration":18575,"status_code":200,"response_size":124} -{"time":"2026-02-10T16:47:56.850566583Z","id":"13995485-207c-424a-ba1d-52eea415e0cc","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/devices/register/status","user_agent":"","status":200,"error":"","latency":23704,"latency_human":"23.704ยตs","bytes_in":0,"bytes_out":124} -{"time":"2026-02-10T16:47:56.850568757Z","id":"13995485-207c-424a-ba1d-52eea415e0cc","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/devices/register/status","user_agent":"","status":200,"error":"","latency":26149,"latency_human":"26.149ยตs","bytes_in":0,"bytes_out":124} -DEBUG: refreshToken generated: '69333176-b3ab-4e87-8f8a-bb58fc291da4' -2026/02/10 16:47:56 [REQUEST] {"request_id":"ee38fbe7-cc7d-4b38-89fb-c48052c46500","timestamp":"2026-02-10T16:47:56.850585428Z","method":"POST","path":"/api/auth/login","headers":{"Content-Type":"application/json"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"192.0.2.1","duration":54524474,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:56.905138115Z","id":"ee38fbe7-cc7d-4b38-89fb-c48052c46500","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/auth/login","user_agent":"","status":200,"error":"","latency":54549220,"latency_human":"54.54922ms","bytes_in":0,"bytes_out":579} -{"time":"2026-02-10T16:47:56.905144146Z","id":"ee38fbe7-cc7d-4b38-89fb-c48052c46500","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/auth/login","user_agent":"","status":200,"error":"","latency":54558438,"latency_human":"54.558438ms","bytes_in":0,"bytes_out":579} -2026/02/10 16:47:56 [REQUEST] {"request_id":"e2d56846-def4-466a-8420-067bca79b9c9","timestamp":"2026-02-10T16:47:56.905190492Z","method":"GET","path":"/api/devices/approve/3e9df04d-4ac1-4f7a-b913-fbccfebb562d","headers":{"Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzYsImlhdCI6MTc3MDc0MjA3NiwidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjgwYjNmZmMwLWM5NGUtNDhmYi05OGFjLWYwYWFiNzY2NTFjOCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0._j6Bh84lYINfaD4X3wGF6fI0R456hrhl_KznxCt-obw","Content-Type":"application/json"},"remote_addr":"192.0.2.1","duration":22151,"status_code":200,"response_size":180} -{"time":"2026-02-10T16:47:56.905217031Z","id":"e2d56846-def4-466a-8420-067bca79b9c9","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/devices/approve/3e9df04d-4ac1-4f7a-b913-fbccfebb562d","user_agent":"","status":200,"error":"","latency":26619,"latency_human":"26.619ยตs","bytes_in":0,"bytes_out":180} -{"time":"2026-02-10T16:47:56.905218925Z","id":"e2d56846-def4-466a-8420-067bca79b9c9","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/devices/approve/3e9df04d-4ac1-4f7a-b913-fbccfebb562d","user_agent":"","status":200,"error":"","latency":29054,"latency_human":"29.054ยตs","bytes_in":0,"bytes_out":180} -2026/02/10 16:47:56 [REQUEST] {"request_id":"8774211d-df18-4c1b-a1b1-2185d113d2dc","timestamp":"2026-02-10T16:47:56.905227631Z","method":"POST","path":"/api/devices/register/status","headers":{"Content-Type":"application/json"},"body":{"registration_id":"3e9df04d-4ac1-4f7a-b913-fbccfebb562d"},"remote_addr":"192.0.2.1","duration":49028039,"status_code":200,"response_size":365} -{"time":"2026-02-10T16:47:56.954278352Z","id":"8774211d-df18-4c1b-a1b1-2185d113d2dc","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/devices/register/status","user_agent":"","status":200,"error":"","latency":49049659,"latency_human":"49.049659ms","bytes_in":0,"bytes_out":365} -{"time":"2026-02-10T16:47:56.954286307Z","id":"8774211d-df18-4c1b-a1b1-2185d113d2dc","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/devices/register/status","user_agent":"","status":200,"error":"","latency":49058346,"latency_human":"49.058346ms","bytes_in":0,"bytes_out":365} ---- PASS: TestDeviceRegistrationFlow (0.11s) -=== RUN TestListDevices -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:56 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: 'ac26bd4e-23e1-4eab-b76f-a143abb3341d' -2026/02/10 16:47:57 [REQUEST] {"request_id":"4f0dff82-483f-4938-8680-6fa685378255","timestamp":"2026-02-10T16:47:56.980619371Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":49758865,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:57.030427777Z","id":"4f0dff82-483f-4938-8680-6fa685378255","remote_ip":"127.0.0.1","host":"127.0.0.1:34849","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49797056,"latency_human":"49.797056ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:57.030446893Z","id":"4f0dff82-483f-4938-8680-6fa685378255","remote_ip":"127.0.0.1","host":"127.0.0.1:34849","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49827012,"latency_human":"49.827012ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:57 [REQUEST] {"request_id":"1cb208de-1c6b-42fb-b50d-e256d096b03a","timestamp":"2026-02-10T16:47:57.033703834Z","method":"GET","path":"/api/devices","headers":{"Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzcsImlhdCI6MTc3MDc0MjA3NywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6Ijk3ODVkNTNmLThiYWMtNDA3Ni1hZDJkLWRmYWU3N2VjNzRhNCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.ATazFbFSO4EY-xiMUOcH-jNIJRhg4NYWmtoa1FIVywY"},"remote_addr":"192.0.2.1","duration":1232676,"status_code":200,"response_size":321} -{"time":"2026-02-10T16:47:57.034958191Z","id":"1cb208de-1c6b-42fb-b50d-e256d096b03a","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/devices","user_agent":"","status":200,"error":"","latency":1253234,"latency_human":"1.253234ms","bytes_in":0,"bytes_out":321} -{"time":"2026-02-10T16:47:57.03496323Z","id":"1cb208de-1c6b-42fb-b50d-e256d096b03a","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/devices","user_agent":"","status":200,"error":"","latency":1259897,"latency_human":"1.259897ms","bytes_in":0,"bytes_out":321} ---- PASS: TestListDevices (0.08s) -=== RUN TestUpdateDevice -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '1d1da3af-f596-4db4-b7be-b8663a4e17e0' -2026/02/10 16:47:57 [REQUEST] {"request_id":"adb7c2e8-2569-4779-a6b2-831ddfdcd2d4","timestamp":"2026-02-10T16:47:57.057487449Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48836013,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:57.1063763Z","id":"adb7c2e8-2569-4779-a6b2-831ddfdcd2d4","remote_ip":"127.0.0.1","host":"127.0.0.1:35575","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48878883,"latency_human":"48.878883ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:57.106393552Z","id":"adb7c2e8-2569-4779-a6b2-831ddfdcd2d4","remote_ip":"127.0.0.1","host":"127.0.0.1:35575","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":48901946,"latency_human":"48.901946ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:57 [REQUEST] {"request_id":"d8011d2f-6980-4796-9a7b-8cb54d0b2f86","timestamp":"2026-02-10T16:47:57.109297919Z","method":"PUT","path":"/api/devices/af8df2a6-1abc-45f9-a4d2-fd633855d751","headers":{"Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzcsImlhdCI6MTc3MDc0MjA3NywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjQ5MTE2MDlhLTgxYzktNDMxNy04ZmQ1LTk3YTlmZjMzOTQzMCIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.F0kN3As7_A3Vrth-vlYXMWcL4Eyke7XuRRMkjjFISoE","Content-Type":"application/json"},"body":{"device_name":"Updated Device Name","sync_enabled":false,"sync_frequency_minutes":10},"remote_addr":"192.0.2.1","duration":5014562,"status_code":200,"response_size":340} -{"time":"2026-02-10T16:47:57.114361391Z","id":"d8011d2f-6980-4796-9a7b-8cb54d0b2f86","remote_ip":"192.0.2.1","host":"example.com","method":"PUT","uri":"/api/devices/af8df2a6-1abc-45f9-a4d2-fd633855d751","user_agent":"","status":200,"error":"","latency":5060376,"latency_human":"5.060376ms","bytes_in":0,"bytes_out":340} -{"time":"2026-02-10T16:47:57.114382631Z","id":"d8011d2f-6980-4796-9a7b-8cb54d0b2f86","remote_ip":"192.0.2.1","host":"example.com","method":"PUT","uri":"/api/devices/af8df2a6-1abc-45f9-a4d2-fd633855d751","user_agent":"","status":200,"error":"","latency":5076716,"latency_human":"5.076716ms","bytes_in":0,"bytes_out":340} ---- PASS: TestUpdateDevice (0.08s) -=== RUN TestDeleteDevice -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -DEBUG: refreshToken generated: '6f4d2a7e-7967-4303-8270-8b403ddd1c8f' -2026/02/10 16:47:57 [REQUEST] {"request_id":"9ca200ea-e8ef-4006-9d24-e7332c7fbc61","timestamp":"2026-02-10T16:47:57.13987758Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":48996190,"status_code":200,"response_size":579} -{"time":"2026-02-10T16:47:57.188903545Z","id":"9ca200ea-e8ef-4006-9d24-e7332c7fbc61","remote_ip":"127.0.0.1","host":"127.0.0.1:35689","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49017731,"latency_human":"49.017731ms","bytes_in":59,"bytes_out":579} -{"time":"2026-02-10T16:47:57.188914826Z","id":"9ca200ea-e8ef-4006-9d24-e7332c7fbc61","remote_ip":"127.0.0.1","host":"127.0.0.1:35689","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":200,"error":"","latency":49035042,"latency_human":"49.035042ms","bytes_in":59,"bytes_out":579} -2026/02/10 16:47:57 [REQUEST] {"request_id":"92e8c350-f0ce-49ac-8f63-664f40f6a560","timestamp":"2026-02-10T16:47:57.191958772Z","method":"DELETE","path":"/api/devices/ec7a2870-9c81-4a9f-a488-71a8fe1eec02","headers":{"Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA3NDU2NzcsImlhdCI6MTc3MDc0MjA3NywidXNlcl9lbWFpbCI6InRlc3R1c2VyQGV4YW1wbGUuY29tIiwidXNlcl9pZCI6IjU1ZDFmZTM1LTA4MjktNGYyYy1hNWM4LWU4MjExZGJkNGUxZSIsInVzZXJfcm9sZSI6ImFkbWluIiwidXNlcl91c2VybmFtZSI6InRlc3R1c2VyIn0.heeFlfq9xWkd9FMHjGIzN8y5UsAS02hKitQtRE1Fx8c"},"remote_addr":"192.0.2.1","duration":3324928,"status_code":204,"response_size":0} -{"time":"2026-02-10T16:47:57.195322041Z","id":"92e8c350-f0ce-49ac-8f63-664f40f6a560","remote_ip":"192.0.2.1","host":"example.com","method":"DELETE","uri":"/api/devices/ec7a2870-9c81-4a9f-a488-71a8fe1eec02","user_agent":"","status":204,"error":"","latency":3361224,"latency_human":"3.361224ms","bytes_in":0,"bytes_out":0} -{"time":"2026-02-10T16:47:57.195331909Z","id":"92e8c350-f0ce-49ac-8f63-664f40f6a560","remote_ip":"192.0.2.1","host":"example.com","method":"DELETE","uri":"/api/devices/ec7a2870-9c81-4a9f-a488-71a8fe1eec02","user_agent":"","status":204,"error":"","latency":3373387,"latency_human":"3.373387ms","bytes_in":0,"bytes_out":0} ---- PASS: TestDeleteDevice (0.08s) -=== RUN TestDeviceAuthentication -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 [REQUEST] {"request_id":"4439a694-80f5-4ecd-9881-1c2ee2cae6b4","timestamp":"2026-02-10T16:47:57.217388521Z","method":"GET","path":"/api/devices","headers":{"Authorization":"Bearer dev_a415d1b1-5364-4076-835f-86c24810b69b"},"remote_addr":"192.0.2.1","duration":9958,"status_code":200,"response_size":0,"error":"code=401, message=invalid or expired jwt, internal=token is malformed: token contains an invalid number of segments"} -{"time":"2026-02-10T16:47:57.217431791Z","id":"4439a694-80f5-4ecd-9881-1c2ee2cae6b4","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/devices","user_agent":"","status":401,"error":"code=401, message=invalid or expired jwt, internal=token is malformed: token contains an invalid number of segments","latency":41968,"latency_human":"41.968ยตs","bytes_in":0,"bytes_out":37} -{"time":"2026-02-10T16:47:57.217439375Z","id":"4439a694-80f5-4ecd-9881-1c2ee2cae6b4","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/devices","user_agent":"","status":401,"error":"","latency":49723,"latency_human":"49.723ยตs","bytes_in":0,"bytes_out":37} ---- PASS: TestDeviceAuthentication (0.02s) -=== RUN TestListPendingRegistrations -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/device_test.go:236 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestListPendingRegistrations - Messages: Failed to create test user ---- FAIL: TestListPendingRegistrations (0.00s) -=== RUN TestApproveDeviceRegistration -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/device_test.go:257 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestApproveDeviceRegistration - Messages: Failed to create test user ---- FAIL: TestApproveDeviceRegistration (0.00s) -=== RUN TestRejectDeviceRegistration -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/device_test.go:296 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestRejectDeviceRegistration - Messages: Failed to create test user ---- FAIL: TestRejectDeviceRegistration (0.00s) -=== RUN TestScannerEndpoints -=== RUN TestScannerEndpoints/POST_/api/scanner/scan_-_Scan_without_admin_role -=== RUN TestScannerEndpoints/POST_/api/scanner/watch/start_-_Start_watch_mode -=== RUN TestScannerEndpoints/POST_/api/scanner/watch/stop_-_Stop_watch_mode -=== RUN TestScannerEndpoints/GET_/api/scanner/watch/status_-_Get_watch_mode_status -=== RUN TestScannerEndpoints/GET_/api/scanner/status/:jobId_-_Get_job_status -=== RUN TestScannerEndpoints/GET_/api/scanner/status/:jobId_-_Job_not_found -=== RUN TestScannerEndpoints/POST_/api/scanner/scan_-_Successful_scan_(background_job) -=== RUN TestScannerEndpoints/POST_/api/scanner/start_-_Start_scanner_without_admin_role -=== RUN TestScannerEndpoints/POST_/api/scanner/start_-_Start_scanner_successfully -=== RUN TestScannerEndpoints/POST_/api/scanner/stop_-_Stop_scanner_without_admin_role -=== RUN TestScannerEndpoints/POST_/api/scanner/stop_-_Stop_scanner_successfully ---- PASS: TestScannerEndpoints (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/scan_-_Scan_without_admin_role (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/watch/start_-_Start_watch_mode (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/watch/stop_-_Stop_watch_mode (0.00s) - --- PASS: TestScannerEndpoints/GET_/api/scanner/watch/status_-_Get_watch_mode_status (0.00s) - --- PASS: TestScannerEndpoints/GET_/api/scanner/status/:jobId_-_Get_job_status (0.00s) - --- PASS: TestScannerEndpoints/GET_/api/scanner/status/:jobId_-_Job_not_found (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/scan_-_Successful_scan_(background_job) (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/start_-_Start_scanner_without_admin_role (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/start_-_Start_scanner_successfully (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/stop_-_Stop_scanner_without_admin_role (0.00s) - --- PASS: TestScannerEndpoints/POST_/api/scanner/stop_-_Stop_scanner_successfully (0.00s) -=== RUN TestEdgeCases -=== RUN TestEdgeCases/Empty_request_body -=== RUN TestEdgeCases/Malformed_JSON -=== RUN TestEdgeCases/Very_large_payload -=== RUN TestEdgeCases/SQL_Injection_attempt -=== RUN TestEdgeCases/XSS_attempt_in_fields -=== RUN TestEdgeCases/Rate_limiting_simulation ---- PASS: TestEdgeCases (0.00s) - --- PASS: TestEdgeCases/Empty_request_body (0.00s) - --- PASS: TestEdgeCases/Malformed_JSON (0.00s) - --- PASS: TestEdgeCases/Very_large_payload (0.00s) - --- PASS: TestEdgeCases/SQL_Injection_attempt (0.00s) - --- PASS: TestEdgeCases/XSS_attempt_in_fields (0.00s) - --- PASS: TestEdgeCases/Rate_limiting_simulation (0.00s) -=== RUN TestHTMXRequests -=== RUN TestHTMXRequests/Registration_with_HTMX_header -=== RUN TestHTMXRequests/Registration_error_with_HTMX_header ---- PASS: TestHTMXRequests (0.00s) - --- PASS: TestHTMXRequests/Registration_with_HTMX_header (0.00s) - --- PASS: TestHTMXRequests/Registration_error_with_HTMX_header (0.00s) -=== RUN TestConcurrentRequests -=== RUN TestConcurrentRequests/Multiple_concurrent_requests ---- PASS: TestConcurrentRequests (0.00s) - --- PASS: TestConcurrentRequests/Multiple_concurrent_requests (0.00s) -=== RUN TestJWTValidation -=== RUN TestJWTValidation/Valid_JWT_format -=== RUN TestJWTValidation/Invalid_JWT_-_no_Bearer_prefix -=== RUN TestJWTValidation/Invalid_JWT_-_malformed ---- PASS: TestJWTValidation (0.00s) - --- PASS: TestJWTValidation/Valid_JWT_format (0.00s) - --- PASS: TestJWTValidation/Invalid_JWT_-_no_Bearer_prefix (0.00s) - --- PASS: TestJWTValidation/Invalid_JWT_-_malformed (0.00s) -=== RUN TestListMediaItemsFiltering -=== RUN TestListMediaItemsFiltering/No_user_context_-_GET_/api/media-items/filtered_without_authentication -=== RUN TestListMediaItemsFiltering/User_context_-_Filter_by_genre -=== RUN TestListMediaItemsFiltering/User_context_-_Filter_by_language -=== RUN TestListMediaItemsFiltering/User_context_-_Filter_by_year_range -=== RUN TestListMediaItemsFiltering/User_context_-_Filter_by_has_cover -=== RUN TestListMediaItemsFiltering/User_context_-_Combine_multiple_filters -=== RUN TestListMediaItemsFiltering/User_context_-_Filter_with_pagination -=== RUN TestListMediaItemsFiltering/User_context_-_Filter_with_sorting ---- PASS: TestListMediaItemsFiltering (0.00s) - --- PASS: TestListMediaItemsFiltering/No_user_context_-_GET_/api/media-items/filtered_without_authentication (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Filter_by_genre (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Filter_by_language (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Filter_by_year_range (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Filter_by_has_cover (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Combine_multiple_filters (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Filter_with_pagination (0.00s) - --- PASS: TestListMediaItemsFiltering/User_context_-_Filter_with_sorting (0.00s) -=== RUN TestGoroutineCleanup -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - goroutine_leak_test.go:18: Initial goroutine count: 2042 -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - goroutine_leak_test.go:27: Goroutines while running: 2067 (delta: +25) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:57 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - goroutine_leak_test.go:35: Goroutines after shutdown: 2066 (delta: 24) - goroutine_leak_test.go:43: WARNING: 24 goroutines still running after shutdown (may be expected for test infrastructure) ---- PASS: TestGoroutineCleanup (0.80s) -=== RUN TestKoboInitialization -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/kobo_test.go:23 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestKoboInitialization - Messages: Failed to create test user ---- FAIL: TestKoboInitialization (0.00s) -=== RUN TestKoboLibrarySync -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/kobo_test.go:48 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestKoboLibrarySync - Messages: Failed to create test user ---- FAIL: TestKoboLibrarySync (0.00s) -=== RUN TestKoboMarkupSync -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/kobo_test.go:72 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestKoboMarkupSync - Messages: Failed to create test user ---- FAIL: TestKoboMarkupSync (0.00s) -=== RUN TestKoboBookmarkSync -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/kobo_test.go:128 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestKoboBookmarkSync - Messages: Failed to create test user ---- FAIL: TestKoboBookmarkSync (0.00s) -=== RUN TestKoboAnalyticsGettests -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/kobo_test.go:169 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestKoboAnalyticsGettests - Messages: Failed to create test user ---- FAIL: TestKoboAnalyticsGettests (0.00s) -=== RUN TestKoboDeviceHeaderParsing -=== RUN TestKoboDeviceHeaderParsing/valid_device_header ---- PASS: TestKoboDeviceHeaderParsing (0.00s) - --- PASS: TestKoboDeviceHeaderParsing/valid_device_header (0.00s) -=== RUN TestKOReaderSyncProgress_RequestBody -=== RUN TestKOReaderSyncProgress_RequestBody/valid_request_body -=== RUN TestKOReaderSyncProgress_RequestBody/request_with_multiple_books -=== RUN TestKOReaderSyncProgress_RequestBody/request_with_highlights_and_bookmarks ---- PASS: TestKOReaderSyncProgress_RequestBody (0.00s) - --- PASS: TestKOReaderSyncProgress_RequestBody/valid_request_body (0.00s) - --- PASS: TestKOReaderSyncProgress_RequestBody/request_with_multiple_books (0.00s) - --- PASS: TestKOReaderSyncProgress_RequestBody/request_with_highlights_and_bookmarks (0.00s) -=== RUN TestKOReaderMetadataParsing -=== RUN TestKOReaderMetadataParsing/parse_progress_data -=== RUN TestKOReaderMetadataParsing/parse_annotation_data ---- PASS: TestKOReaderMetadataParsing (0.00s) - --- PASS: TestKOReaderMetadataParsing/parse_progress_data (0.00s) - --- PASS: TestKOReaderMetadataParsing/parse_annotation_data (0.00s) -=== RUN TestKOReaderResponseFormats -=== RUN TestKOReaderResponseFormats/sync_progress_response -=== RUN TestKOReaderResponseFormats/metadata_response -=== RUN TestKOReaderResponseFormats/library_response ---- PASS: TestKOReaderResponseFormats (0.00s) - --- PASS: TestKOReaderResponseFormats/sync_progress_response (0.00s) - --- PASS: TestKOReaderResponseFormats/metadata_response (0.00s) - --- PASS: TestKOReaderResponseFormats/library_response (0.00s) -=== RUN TestKOReaderErrorHandling -=== RUN TestKOReaderErrorHandling/invalid_UUID_format -=== RUN TestKOReaderErrorHandling/missing_authorization_header -=== RUN TestKOReaderErrorHandling/invalid_percentage_value ---- PASS: TestKOReaderErrorHandling (0.00s) - --- PASS: TestKOReaderErrorHandling/invalid_UUID_format (0.00s) - --- PASS: TestKOReaderErrorHandling/missing_authorization_header (0.00s) - --- PASS: TestKOReaderErrorHandling/invalid_percentage_value (0.00s) -=== RUN TestKOReaderDeviceMatching -=== RUN TestKOReaderDeviceMatching/match_by_UUID -=== RUN TestKOReaderDeviceMatching/match_by_file_path -=== RUN TestKOReaderDeviceMatching/match_by_title_and_author ---- PASS: TestKOReaderDeviceMatching (0.00s) - --- PASS: TestKOReaderDeviceMatching/match_by_UUID (0.00s) - --- PASS: TestKOReaderDeviceMatching/match_by_file_path (0.00s) - --- PASS: TestKOReaderDeviceMatching/match_by_title_and_author (0.00s) -=== RUN TestAuthMiddleware -=== RUN TestAuthMiddleware/Missing_JWT -=== RUN TestAuthMiddleware/Invalid_JWT_format -=== RUN TestAuthMiddleware/Valid_JWT_format ---- PASS: TestAuthMiddleware (0.00s) - --- PASS: TestAuthMiddleware/Missing_JWT (0.00s) - --- PASS: TestAuthMiddleware/Invalid_JWT_format (0.00s) - --- PASS: TestAuthMiddleware/Valid_JWT_format (0.00s) -=== RUN TestLibraryCreationUnauthorized ---- PASS: TestLibraryCreationUnauthorized (0.00s) -=== RUN TestLibraryCreationWithValidAdmin ---- PASS: TestLibraryCreationWithValidAdmin (0.00s) -=== RUN TestLibraryTypesResponse ---- PASS: TestLibraryTypesResponse (0.00s) -=== RUN TestUserVisibleLibraries ---- PASS: TestUserVisibleLibraries (0.00s) -=== RUN TestMediaItemsList ---- PASS: TestMediaItemsList (0.00s) -=== RUN TestJSONValidation -=== RUN TestJSONValidation/Invalid_JSON -=== RUN TestJSONValidation/Invalid_library_type -=== RUN TestJSONValidation/Valid_request ---- PASS: TestJSONValidation (0.00s) - --- PASS: TestJSONValidation/Invalid_JSON (0.00s) - --- PASS: TestJSONValidation/Invalid_library_type (0.00s) - --- PASS: TestJSONValidation/Valid_request (0.00s) -=== RUN TestErrorHandling -=== RUN TestErrorHandling/Missing_library_ID -=== RUN TestErrorHandling/Invalid_UUID -=== RUN TestErrorHandling/Nonexistent_user_library ---- PASS: TestErrorHandling (0.00s) - --- PASS: TestErrorHandling/Missing_library_ID (0.00s) - --- PASS: TestErrorHandling/Invalid_UUID (0.00s) - --- PASS: TestErrorHandling/Nonexistent_user_library (0.00s) -=== RUN TestTest - main_test.go:8: ๐Ÿงช Comprehensive test suite verification - main_test.go:9: โœ… Testing framework is properly configured - main_test.go:10: ๐Ÿ“‹ Test discovery and execution should work correctly - main_test.go:11: ๐ŸŽฏ All edge cases should be covered ---- PASS: TestTest (0.00s) -=== RUN TestMediaBulkOperations -=== RUN TestMediaBulkOperations/BulkDeleteBooks_WithoutAuth -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"3ef776d6-3ee8-4876-916b-cac0ff628ca2","timestamp":"2026-02-10T16:47:58.042305103Z","method":"POST","path":"/api/books/bulk-delete","headers":{"Accept-Encoding":"gzip","Content-Length":"53","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"book_ids":["b416a4fc-fca7-4e3a-901f-6035198febaf"]},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":11521,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.042337864Z","id":"3ef776d6-3ee8-4876-916b-cac0ff628ca2","remote_ip":"127.0.0.1","host":"127.0.0.1:33325","method":"POST","uri":"/api/books/bulk-delete","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":31429,"latency_human":"31.429ยตs","bytes_in":53,"bytes_out":39} -{"time":"2026-02-10T16:47:58.042342883Z","id":"3ef776d6-3ee8-4876-916b-cac0ff628ca2","remote_ip":"127.0.0.1","host":"127.0.0.1:33325","method":"POST","uri":"/api/books/bulk-delete","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":37669,"latency_human":"37.669ยตs","bytes_in":53,"bytes_out":39} -=== RUN TestMediaBulkOperations/BulkDeleteBooks_EmptyBookIDs -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:40 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkDeleteBooks_EmptyBookIDs - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkDeleteBooks_InvalidBookIDs -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:63 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkDeleteBooks_InvalidBookIDs - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkDeleteBooks_WithValidBooks -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:94 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkDeleteBooks_WithValidBooks - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkDeleteBooks_InvalidRequestBody -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:132 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkDeleteBooks_InvalidRequestBody - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkUpdateBooks_WithoutAuth -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"17f21138-0f79-4bd2-874c-f0b1cc5e0e6f","timestamp":"2026-02-10T16:47:58.050928688Z","method":"POST","path":"/api/books/bulk-update","headers":{"Accept-Encoding":"gzip","Content-Length":"81","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"book_ids":["2781a621-a63b-4db0-8db0-1b6e49d8a236"],"updates":{"tags":["test"]}},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":11612,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.050962361Z","id":"17f21138-0f79-4bd2-874c-f0b1cc5e0e6f","remote_ip":"127.0.0.1","host":"127.0.0.1:44915","method":"POST","uri":"/api/books/bulk-update","user_agent":"Go-http-client/1.1","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":38371,"latency_human":"38.371ยตs","bytes_in":81,"bytes_out":39} -{"time":"2026-02-10T16:47:58.050970606Z","id":"17f21138-0f79-4bd2-874c-f0b1cc5e0e6f","remote_ip":"127.0.0.1","host":"127.0.0.1:44915","method":"POST","uri":"/api/books/bulk-update","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":48410,"latency_human":"48.41ยตs","bytes_in":81,"bytes_out":39} -=== RUN TestMediaBulkOperations/BulkUpdateBooks_EmptyBookIDs -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:174 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkUpdateBooks_EmptyBookIDs - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkUpdateBooks_InvalidBookIDs -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:200 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkUpdateBooks_InvalidBookIDs - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkUpdateBooks_UpdateTags -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:234 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkUpdateBooks_UpdateTags - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkUpdateBooks_UpdateReadingStatus -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:274 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkUpdateBooks_UpdateReadingStatus - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkUpdateBooks_UpdateMultipleFields -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:308 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkUpdateBooks_UpdateMultipleFields - Messages: Failed to create test user -=== RUN TestMediaBulkOperations/BulkUpdateBooks_InvalidRequestBody -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_bulk_test.go:344 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaBulkOperations/BulkUpdateBooks_InvalidRequestBody - Messages: Failed to create test user ---- FAIL: TestMediaBulkOperations (0.02s) - --- PASS: TestMediaBulkOperations/BulkDeleteBooks_WithoutAuth (0.00s) - --- FAIL: TestMediaBulkOperations/BulkDeleteBooks_EmptyBookIDs (0.00s) - --- FAIL: TestMediaBulkOperations/BulkDeleteBooks_InvalidBookIDs (0.00s) - --- FAIL: TestMediaBulkOperations/BulkDeleteBooks_WithValidBooks (0.00s) - --- FAIL: TestMediaBulkOperations/BulkDeleteBooks_InvalidRequestBody (0.00s) - --- PASS: TestMediaBulkOperations/BulkUpdateBooks_WithoutAuth (0.00s) - --- FAIL: TestMediaBulkOperations/BulkUpdateBooks_EmptyBookIDs (0.00s) - --- FAIL: TestMediaBulkOperations/BulkUpdateBooks_InvalidBookIDs (0.00s) - --- FAIL: TestMediaBulkOperations/BulkUpdateBooks_UpdateTags (0.00s) - --- FAIL: TestMediaBulkOperations/BulkUpdateBooks_UpdateReadingStatus (0.00s) - --- FAIL: TestMediaBulkOperations/BulkUpdateBooks_UpdateMultipleFields (0.00s) - --- FAIL: TestMediaBulkOperations/BulkUpdateBooks_InvalidRequestBody (0.00s) -=== RUN TestMediaItemISBNNormalization -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_item_isbn_test.go:49 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaItemISBNNormalization - Messages: Failed to create test user ---- FAIL: TestMediaItemISBNNormalization (0.00s) -=== RUN TestMediaItemISBNEdgeCases -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_item_isbn_test.go:137 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaItemISBNEdgeCases - Messages: Failed to create test user ---- FAIL: TestMediaItemISBNEdgeCases (0.00s) -=== RUN TestMediaItemsPagination -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_item_isbn_test.go:222 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaItemsPagination - Messages: Failed to create test user ---- FAIL: TestMediaItemsPagination (0.00s) -=== RUN TestMediaItemLibraryRequirement -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_item_isbn_test.go:329 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestMediaItemLibraryRequirement - Messages: Failed to create test user ---- FAIL: TestMediaItemLibraryRequirement (0.00s) -=== RUN TestUpdateMediaItemISBN -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/media_item_isbn_test.go:392 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestUpdateMediaItemISBN - Messages: Failed to create test user ---- FAIL: TestUpdateMediaItemISBN (0.00s) -=== RUN TestUsernameWhitespaceValidation - new_fixes_test.go:16: Requires integration test with real handler ---- SKIP: TestUsernameWhitespaceValidation (0.00s) -=== RUN TestRoleCaseNormalization - new_fixes_test.go:22: Requires integration test with real handler ---- SKIP: TestRoleCaseNormalization (0.00s) -=== RUN TestPaginationMaxLimit - new_fixes_test.go:28: Already tested in TestPaginationAndFiltering ---- SKIP: TestPaginationMaxLimit (0.00s) -=== RUN TestPaginationNegativeOffset - new_fixes_test.go:34: Already tested in TestPaginationAndFiltering ---- SKIP: TestPaginationNegativeOffset (0.00s) -=== RUN TestRateLimiter ---- PASS: TestRateLimiter (0.00s) -=== RUN TestOPDSEndpoints -=== RUN TestOPDSEndpoints/GetDeviceCatalog_WithoutDeviceAuth -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"df564519-8bbb-4ab5-8dc8-e54d6bc92e34","timestamp":"2026-02-10T16:47:58.071109891Z","method":"GET","path":"/opds/devices/e928cb4b-50c5-4dc7-bea7-4ce3d2f330dc/catalog","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":636842,"status_code":500,"response_size":407} -{"time":"2026-02-10T16:47:58.071762011Z","id":"df564519-8bbb-4ab5-8dc8-e54d6bc92e34","remote_ip":"127.0.0.1","host":"127.0.0.1:41935","method":"GET","uri":"/opds/devices/e928cb4b-50c5-4dc7-bea7-4ce3d2f330dc/catalog","user_agent":"Go-http-client/1.1","status":500,"error":"","latency":649885,"latency_human":"649.885ยตs","bytes_in":0,"bytes_out":407} -{"time":"2026-02-10T16:47:58.071767711Z","id":"df564519-8bbb-4ab5-8dc8-e54d6bc92e34","remote_ip":"127.0.0.1","host":"127.0.0.1:41935","method":"GET","uri":"/opds/devices/e928cb4b-50c5-4dc7-bea7-4ce3d2f330dc/catalog","user_agent":"Go-http-client/1.1","status":500,"error":"","latency":657660,"latency_human":"657.66ยตs","bytes_in":0,"bytes_out":407} - opds_test.go:28: - Error Trace: /app/cmd/server/tests/opds_test.go:28 - Error: Should be true - Test: TestOPDSEndpoints/GetDeviceCatalog_WithoutDeviceAuth -=== RUN TestOPDSEndpoints/GetDeviceCatalog_InvalidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"ecd4f8ea-9366-451e-8bca-eb3c91fe16a3","timestamp":"2026-02-10T16:47:58.072439507Z","method":"GET","path":"/opds/devices/invalid-uuid/catalog","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":569447,"status_code":500,"response_size":407} -{"time":"2026-02-10T16:47:58.073020796Z","id":"ecd4f8ea-9366-451e-8bca-eb3c91fe16a3","remote_ip":"127.0.0.1","host":"127.0.0.1:35025","method":"GET","uri":"/opds/devices/invalid-uuid/catalog","user_agent":"Go-http-client/1.1","status":500,"error":"","latency":580206,"latency_human":"580.206ยตs","bytes_in":0,"bytes_out":407} -{"time":"2026-02-10T16:47:58.073024733Z","id":"ecd4f8ea-9366-451e-8bca-eb3c91fe16a3","remote_ip":"127.0.0.1","host":"127.0.0.1:35025","method":"GET","uri":"/opds/devices/invalid-uuid/catalog","user_agent":"Go-http-client/1.1","status":500,"error":"","latency":585707,"latency_human":"585.707ยตs","bytes_in":0,"bytes_out":407} - opds_test.go:43: - Error Trace: /app/cmd/server/tests/opds_test.go:43 - Error: Not equal: - expected: 400 - actual : 500 - Test: TestOPDSEndpoints/GetDeviceCatalog_InvalidDeviceID -=== RUN TestOPDSEndpoints/GetDeviceCatalog_ValidDevice -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:50 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEndpoints/GetDeviceCatalog_ValidDevice - Messages: Failed to create test user -=== RUN TestOPDSEndpoints/SearchDeviceCatalog_InvalidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"474a9de8-9902-42e9-854a-f4cfdcbe4302","timestamp":"2026-02-10T16:47:58.075491228Z","method":"GET","path":"/opds/devices/invalid-uuid/search","query_params":{"query":"test"},"headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":24846,"status_code":400,"response_size":407} -{"time":"2026-02-10T16:47:58.075534418Z","id":"474a9de8-9902-42e9-854a-f4cfdcbe4302","remote_ip":"127.0.0.1","host":"127.0.0.1:45027","method":"GET","uri":"/opds/devices/invalid-uuid/search?query=test","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":37479,"latency_human":"37.479ยตs","bytes_in":0,"bytes_out":407} -{"time":"2026-02-10T16:47:58.075538997Z","id":"474a9de8-9902-42e9-854a-f4cfdcbe4302","remote_ip":"127.0.0.1","host":"127.0.0.1:45027","method":"GET","uri":"/opds/devices/invalid-uuid/search?query=test","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":47758,"latency_human":"47.758ยตs","bytes_in":0,"bytes_out":407} -=== RUN TestOPDSEndpoints/SearchDeviceCatalog_ValidDevice -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:85 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEndpoints/SearchDeviceCatalog_ValidDevice - Messages: Failed to create test user -=== RUN TestOPDSEndpoints/GetDeviceNavigation_InvalidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"00d9974c-76b6-4576-92c3-09779cc344cb","timestamp":"2026-02-10T16:47:58.077672805Z","method":"GET","path":"/opds/devices/invalid-uuid/nav","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":573533,"status_code":500,"response_size":407} -{"time":"2026-02-10T16:47:58.078261086Z","id":"00d9974c-76b6-4576-92c3-09779cc344cb","remote_ip":"127.0.0.1","host":"127.0.0.1:39785","method":"GET","uri":"/opds/devices/invalid-uuid/nav","user_agent":"Go-http-client/1.1","status":500,"error":"","latency":586648,"latency_human":"586.648ยตs","bytes_in":0,"bytes_out":407} -{"time":"2026-02-10T16:47:58.078267227Z","id":"00d9974c-76b6-4576-92c3-09779cc344cb","remote_ip":"127.0.0.1","host":"127.0.0.1:39785","method":"GET","uri":"/opds/devices/invalid-uuid/nav","user_agent":"Go-http-client/1.1","status":500,"error":"","latency":594312,"latency_human":"594.312ยตs","bytes_in":0,"bytes_out":407} - opds_test.go:111: - Error Trace: /app/cmd/server/tests/opds_test.go:111 - Error: Not equal: - expected: 400 - actual : 500 - Test: TestOPDSEndpoints/GetDeviceNavigation_InvalidDeviceID -=== RUN TestOPDSEndpoints/GetDeviceNavigation_ValidDevice -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:118 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEndpoints/GetDeviceNavigation_ValidDevice - Messages: Failed to create test user -=== RUN TestOPDSEndpoints/DownloadBook_InvalidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"c539ca1c-7ab3-47df-9495-1d8009ab0919","timestamp":"2026-02-10T16:47:58.081285285Z","method":"GET","path":"/opds/devices/invalid-uuid/download/93d22eeb-da4e-408c-adf1-3740df6f3c0c","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":21790,"status_code":400,"response_size":30} -{"time":"2026-02-10T16:47:58.081327553Z","id":"c539ca1c-7ab3-47df-9495-1d8009ab0919","remote_ip":"127.0.0.1","host":"127.0.0.1:38089","method":"GET","uri":"/opds/devices/invalid-uuid/download/93d22eeb-da4e-408c-adf1-3740df6f3c0c","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":39945,"latency_human":"39.945ยตs","bytes_in":0,"bytes_out":30} -{"time":"2026-02-10T16:47:58.081335829Z","id":"c539ca1c-7ab3-47df-9495-1d8009ab0919","remote_ip":"127.0.0.1","host":"127.0.0.1:38089","method":"GET","uri":"/opds/devices/invalid-uuid/download/93d22eeb-da4e-408c-adf1-3740df6f3c0c","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":49212,"latency_human":"49.212ยตs","bytes_in":0,"bytes_out":30} -=== RUN TestOPDSEndpoints/DownloadBook_InvalidBookID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"717a123f-90db-4841-aa3c-2a52c00ba7cf","timestamp":"2026-02-10T16:47:58.082082844Z","method":"GET","path":"/opds/devices/9516c404-a449-4323-a905-ec8d1d7dda6a/download/invalid-uuid","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":37550,"status_code":400,"response_size":28} -{"time":"2026-02-10T16:47:58.082136734Z","id":"717a123f-90db-4841-aa3c-2a52c00ba7cf","remote_ip":"127.0.0.1","host":"127.0.0.1:40389","method":"GET","uri":"/opds/devices/9516c404-a449-4323-a905-ec8d1d7dda6a/download/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":60863,"latency_human":"60.863ยตs","bytes_in":0,"bytes_out":28} -{"time":"2026-02-10T16:47:58.082141974Z","id":"717a123f-90db-4841-aa3c-2a52c00ba7cf","remote_ip":"127.0.0.1","host":"127.0.0.1:40389","method":"GET","uri":"/opds/devices/9516c404-a449-4323-a905-ec8d1d7dda6a/download/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":67115,"latency_human":"67.115ยตs","bytes_in":0,"bytes_out":28} -=== RUN TestOPDSEndpoints/DownloadBook_ValidIDs -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:167 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEndpoints/DownloadBook_ValidIDs - Messages: Failed to create test user -=== RUN TestOPDSEndpoints/GetCoverImage_InvalidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"f720aa50-4633-445d-8a7a-2602ccca52f2","timestamp":"2026-02-10T16:47:58.085224642Z","method":"GET","path":"/opds/devices/invalid-uuid/cover/cf36cf65-4992-43aa-8a0b-a9465121b3ce","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":15739,"status_code":400,"response_size":30} -{"time":"2026-02-10T16:47:58.085260218Z","id":"f720aa50-4633-445d-8a7a-2602ccca52f2","remote_ip":"127.0.0.1","host":"127.0.0.1:43379","method":"GET","uri":"/opds/devices/invalid-uuid/cover/cf36cf65-4992-43aa-8a0b-a9465121b3ce","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":34093,"latency_human":"34.093ยตs","bytes_in":0,"bytes_out":30} -{"time":"2026-02-10T16:47:58.085266149Z","id":"f720aa50-4633-445d-8a7a-2602ccca52f2","remote_ip":"127.0.0.1","host":"127.0.0.1:43379","method":"GET","uri":"/opds/devices/invalid-uuid/cover/cf36cf65-4992-43aa-8a0b-a9465121b3ce","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":41647,"latency_human":"41.647ยตs","bytes_in":0,"bytes_out":30} -=== RUN TestOPDSEndpoints/GetCoverImage_InvalidBookID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"3ac842a2-ed2b-4b50-b90a-33ff8f0b73f1","timestamp":"2026-02-10T16:47:58.08591926Z","method":"GET","path":"/opds/devices/aa8f073e-306e-46fa-8cc6-5ea153a83f04/cover/invalid-uuid","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":9077,"status_code":400,"response_size":28} -{"time":"2026-02-10T16:47:58.085938416Z","id":"3ac842a2-ed2b-4b50-b90a-33ff8f0b73f1","remote_ip":"127.0.0.1","host":"127.0.0.1:41493","method":"GET","uri":"/opds/devices/aa8f073e-306e-46fa-8cc6-5ea153a83f04/cover/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":17913,"latency_human":"17.913ยตs","bytes_in":0,"bytes_out":28} -{"time":"2026-02-10T16:47:58.085943475Z","id":"3ac842a2-ed2b-4b50-b90a-33ff8f0b73f1","remote_ip":"127.0.0.1","host":"127.0.0.1:41493","method":"GET","uri":"/opds/devices/aa8f073e-306e-46fa-8cc6-5ea153a83f04/cover/invalid-uuid","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":24756,"latency_human":"24.756ยตs","bytes_in":0,"bytes_out":28} -=== RUN TestOPDSEndpoints/GetCoverImage_ValidIDs -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Error listing pending items: failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:218 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEndpoints/GetCoverImage_ValidIDs - Messages: Failed to create test user -=== RUN TestOPDSEndpoints/ListFormats_InvalidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"a81453b7-b47e-422a-ac9f-c1e329c29470","timestamp":"2026-02-10T16:47:58.088381598Z","method":"GET","path":"/opds/devices/invalid-uuid/formats/c088efd6-44dc-4ae1-89ab-5e87a369792e","headers":{"Accept-Encoding":"gzip","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":18845,"status_code":400,"response_size":30} -{"time":"2026-02-10T16:47:58.088427192Z","id":"a81453b7-b47e-422a-ac9f-c1e329c29470","remote_ip":"127.0.0.1","host":"127.0.0.1:46327","method":"GET","uri":"/opds/devices/invalid-uuid/formats/c088efd6-44dc-4ae1-89ab-5e87a369792e","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":44362,"latency_human":"44.362ยตs","bytes_in":0,"bytes_out":30} -{"time":"2026-02-10T16:47:58.088433494Z","id":"a81453b7-b47e-422a-ac9f-c1e329c29470","remote_ip":"127.0.0.1","host":"127.0.0.1:46327","method":"GET","uri":"/opds/devices/invalid-uuid/formats/c088efd6-44dc-4ae1-89ab-5e87a369792e","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":51005,"latency_human":"51.005ยตs","bytes_in":0,"bytes_out":30} -=== RUN TestOPDSEndpoints/ListFormats_ValidDeviceID -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:253 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEndpoints/ListFormats_ValidDeviceID - Messages: Failed to create test user ---- FAIL: TestOPDSEndpoints (0.02s) - --- FAIL: TestOPDSEndpoints/GetDeviceCatalog_WithoutDeviceAuth (0.00s) - --- FAIL: TestOPDSEndpoints/GetDeviceCatalog_InvalidDeviceID (0.00s) - --- FAIL: TestOPDSEndpoints/GetDeviceCatalog_ValidDevice (0.00s) - --- PASS: TestOPDSEndpoints/SearchDeviceCatalog_InvalidDeviceID (0.00s) - --- FAIL: TestOPDSEndpoints/SearchDeviceCatalog_ValidDevice (0.00s) - --- FAIL: TestOPDSEndpoints/GetDeviceNavigation_InvalidDeviceID (0.00s) - --- FAIL: TestOPDSEndpoints/GetDeviceNavigation_ValidDevice (0.00s) - --- PASS: TestOPDSEndpoints/DownloadBook_InvalidDeviceID (0.00s) - --- PASS: TestOPDSEndpoints/DownloadBook_InvalidBookID (0.00s) - --- FAIL: TestOPDSEndpoints/DownloadBook_ValidIDs (0.00s) - --- PASS: TestOPDSEndpoints/GetCoverImage_InvalidDeviceID (0.00s) - --- PASS: TestOPDSEndpoints/GetCoverImage_InvalidBookID (0.00s) - --- FAIL: TestOPDSEndpoints/GetCoverImage_ValidIDs (0.00s) - --- PASS: TestOPDSEndpoints/ListFormats_InvalidDeviceID (0.00s) - --- FAIL: TestOPDSEndpoints/ListFormats_ValidDeviceID (0.00s) -=== RUN TestOPDSConversion -=== RUN TestOPDSConversion/DownloadKEPUB_FormatParameter -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:276 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSConversion/DownloadKEPUB_FormatParameter - Messages: Failed to create test user -=== RUN TestOPDSConversion/DownloadEPUB_DefaultFormat -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:298 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSConversion/DownloadEPUB_DefaultFormat - Messages: Failed to create test user -=== RUN TestOPDSConversion/Download_UnsupportedFormat -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:319 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSConversion/Download_UnsupportedFormat - Messages: Failed to create test user ---- FAIL: TestOPDSConversion (0.01s) - --- FAIL: TestOPDSConversion/DownloadKEPUB_FormatParameter (0.00s) - --- FAIL: TestOPDSConversion/DownloadEPUB_DefaultFormat (0.00s) - --- FAIL: TestOPDSConversion/Download_UnsupportedFormat (0.00s) -=== RUN TestOPDSEdgeCases -=== RUN TestOPDSEdgeCases/Catalog_EmptyLibrary -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:343 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEdgeCases/Catalog_EmptyLibrary - Messages: Failed to create test user -=== RUN TestOPDSEdgeCases/Search_SpecialCharacters -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:362 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEdgeCases/Search_SpecialCharacters - Messages: Failed to create test user -=== RUN TestOPDSEdgeCases/Search_EmptyQuery -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/opds_test.go:382 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestOPDSEdgeCases/Search_EmptyQuery - Messages: Failed to create test user ---- FAIL: TestOPDSEdgeCases (0.01s) - --- FAIL: TestOPDSEdgeCases/Catalog_EmptyLibrary (0.00s) - --- FAIL: TestOPDSEdgeCases/Search_SpecialCharacters (0.00s) - --- FAIL: TestOPDSEdgeCases/Search_EmptyQuery (0.00s) -=== RUN TestListAllQueueItems_Admin -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"c6f291d1-14df-4557-a74f-bd4797374c7c","timestamp":"2026-02-10T16:47:58.104720053Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":580006,"status_code":401,"response_size":32} -{"time":"2026-02-10T16:47:58.105320968Z","id":"c6f291d1-14df-4557-a74f-bd4797374c7c","remote_ip":"127.0.0.1","host":"127.0.0.1:36661","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":598591,"latency_human":"598.591ยตs","bytes_in":59,"bytes_out":32} -{"time":"2026-02-10T16:47:58.105328071Z","id":"c6f291d1-14df-4557-a74f-bd4797374c7c","remote_ip":"127.0.0.1","host":"127.0.0.1:36661","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":608198,"latency_human":"608.198ยตs","bytes_in":59,"bytes_out":32} - queue_test.go:254: - Error Trace: /app/cmd/server/tests/queue_test.go:254 - /app/cmd/server/tests/queue_test.go:23 - Error: Should be true - Test: TestListAllQueueItems_Admin - Messages: Should have access_token ---- FAIL: TestListAllQueueItems_Admin (0.00s) -=== RUN TestGetDeviceQueueStats -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/queue_test.go:41 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestGetDeviceQueueStats - Messages: Failed to create test user ---- FAIL: TestGetDeviceQueueStats (0.00s) -=== RUN TestListDeviceQueueItems -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/queue_test.go:78 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestListDeviceQueueItems - Messages: Failed to create test user ---- FAIL: TestListDeviceQueueItems (0.00s) -=== RUN TestRetryQueueItem -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/queue_test.go:115 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestRetryQueueItem - Messages: Failed to create test user ---- FAIL: TestRetryQueueItem (0.00s) -=== RUN TestDeleteQueueItem -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/queue_test.go:131 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestDeleteQueueItem - Messages: Failed to create test user ---- FAIL: TestDeleteQueueItem (0.00s) -=== RUN TestClearDeviceQueue -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) - test_helpers.go:406: - Error Trace: /app/cmd/server/tests/test_helpers.go:406 - /app/cmd/server/tests/test_helpers.go:351 - /app/cmd/server/tests/queue_test.go:147 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestClearDeviceQueue - Messages: Failed to create test user ---- FAIL: TestClearDeviceQueue (0.01s) -=== RUN TestQueueEndpoints_Unauthorized -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -=== RUN TestQueueEndpoints_Unauthorized/ListAllQueueItems -2026/02/10 16:47:58 [REQUEST] {"request_id":"c062045d-5208-4898-b7cc-cefb1c505a1f","timestamp":"2026-02-10T16:47:58.125347664Z","method":"GET","path":"/api/queue/items","remote_addr":"192.0.2.1","duration":12283,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.125383881Z","id":"c062045d-5208-4898-b7cc-cefb1c505a1f","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/queue/items","user_agent":"","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":35276,"latency_human":"35.276ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:58.125389652Z","id":"c062045d-5208-4898-b7cc-cefb1c505a1f","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/queue/items","user_agent":"","status":401,"error":"","latency":43230,"latency_human":"43.23ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestQueueEndpoints_Unauthorized/GetDeviceQueueStats -2026/02/10 16:47:58 [REQUEST] {"request_id":"f6e87541-e5e8-441a-b81b-8dddb3104de1","timestamp":"2026-02-10T16:47:58.125413095Z","method":"GET","path":"/api/queue/devices/test-device-id/stats","remote_addr":"192.0.2.1","duration":1072,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.125423344Z","id":"f6e87541-e5e8-441a-b81b-8dddb3104de1","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/queue/devices/test-device-id/stats","user_agent":"","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":10299,"latency_human":"10.299ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:58.125425929Z","id":"f6e87541-e5e8-441a-b81b-8dddb3104de1","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/queue/devices/test-device-id/stats","user_agent":"","status":401,"error":"","latency":13174,"latency_human":"13.174ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestQueueEndpoints_Unauthorized/ListDeviceQueueItems -2026/02/10 16:47:58 [REQUEST] {"request_id":"d0c471eb-fd2e-42ff-a328-911fe22f2622","timestamp":"2026-02-10T16:47:58.125446357Z","method":"GET","path":"/api/queue/devices/test-device-id/items","remote_addr":"192.0.2.1","duration":531,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.125451537Z","id":"d0c471eb-fd2e-42ff-a328-911fe22f2622","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/queue/devices/test-device-id/items","user_agent":"","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":5200,"latency_human":"5.2ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:58.125453761Z","id":"d0c471eb-fd2e-42ff-a328-911fe22f2622","remote_ip":"192.0.2.1","host":"example.com","method":"GET","uri":"/api/queue/devices/test-device-id/items","user_agent":"","status":401,"error":"","latency":7685,"latency_human":"7.685ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestQueueEndpoints_Unauthorized/RetryQueueItem -2026/02/10 16:47:58 [REQUEST] {"request_id":"c87fd593-feff-4751-b9f3-c10176e56022","timestamp":"2026-02-10T16:47:58.125473037Z","method":"POST","path":"/api/queue/items/test-item-id/retry","remote_addr":"192.0.2.1","duration":1302,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.125479949Z","id":"c87fd593-feff-4751-b9f3-c10176e56022","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/queue/items/test-item-id/retry","user_agent":"","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":6933,"latency_human":"6.933ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:58.125482043Z","id":"c87fd593-feff-4751-b9f3-c10176e56022","remote_ip":"192.0.2.1","host":"example.com","method":"POST","uri":"/api/queue/items/test-item-id/retry","user_agent":"","status":401,"error":"","latency":9217,"latency_human":"9.217ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestQueueEndpoints_Unauthorized/DeleteQueueItem -2026/02/10 16:47:58 [REQUEST] {"request_id":"0b4a8ea3-8db7-43ca-91fd-044f28030dc4","timestamp":"2026-02-10T16:47:58.125491781Z","method":"DELETE","path":"/api/queue/items/test-item-id","remote_addr":"192.0.2.1","duration":511,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.125497071Z","id":"0b4a8ea3-8db7-43ca-91fd-044f28030dc4","remote_ip":"192.0.2.1","host":"example.com","method":"DELETE","uri":"/api/queue/items/test-item-id","user_agent":"","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":5340,"latency_human":"5.34ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:58.125499075Z","id":"0b4a8ea3-8db7-43ca-91fd-044f28030dc4","remote_ip":"192.0.2.1","host":"example.com","method":"DELETE","uri":"/api/queue/items/test-item-id","user_agent":"","status":401,"error":"","latency":7494,"latency_human":"7.494ยตs","bytes_in":0,"bytes_out":39} -=== RUN TestQueueEndpoints_Unauthorized/ClearDeviceQueue -2026/02/10 16:47:58 [REQUEST] {"request_id":"126cfb8a-a9d9-4fd3-ae6c-38ec020ad918","timestamp":"2026-02-10T16:47:58.125515656Z","method":"DELETE","path":"/api/queue/devices/test-device-id/clear","remote_addr":"192.0.2.1","duration":481,"status_code":200,"response_size":0,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header"} -{"time":"2026-02-10T16:47:58.125519843Z","id":"126cfb8a-a9d9-4fd3-ae6c-38ec020ad918","remote_ip":"192.0.2.1","host":"example.com","method":"DELETE","uri":"/api/queue/devices/test-device-id/clear","user_agent":"","status":401,"error":"code=401, message=missing or malformed jwt, internal=missing value in request header","latency":4248,"latency_human":"4.248ยตs","bytes_in":0,"bytes_out":39} -{"time":"2026-02-10T16:47:58.125521687Z","id":"126cfb8a-a9d9-4fd3-ae6c-38ec020ad918","remote_ip":"192.0.2.1","host":"example.com","method":"DELETE","uri":"/api/queue/devices/test-device-id/clear","user_agent":"","status":401,"error":"","latency":6242,"latency_human":"6.242ยตs","bytes_in":0,"bytes_out":39} ---- PASS: TestQueueEndpoints_Unauthorized (0.00s) - --- PASS: TestQueueEndpoints_Unauthorized/ListAllQueueItems (0.00s) - --- PASS: TestQueueEndpoints_Unauthorized/GetDeviceQueueStats (0.00s) - --- PASS: TestQueueEndpoints_Unauthorized/ListDeviceQueueItems (0.00s) - --- PASS: TestQueueEndpoints_Unauthorized/RetryQueueItem (0.00s) - --- PASS: TestQueueEndpoints_Unauthorized/DeleteQueueItem (0.00s) - --- PASS: TestQueueEndpoints_Unauthorized/ClearDeviceQueue (0.00s) -=== RUN TestRefreshTokenFlow -=== RUN TestRefreshTokenFlow/RefreshToken_MissingToken -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"e7b3a2bb-75c5-40d9-b842-ca9b4193e70e","timestamp":"2026-02-10T16:47:58.126082637Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"2","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":40666,"status_code":400,"response_size":123} -{"time":"2026-02-10T16:47:58.12614312Z","id":"e7b3a2bb-75c5-40d9-b842-ca9b4193e70e","remote_ip":"127.0.0.1","host":"127.0.0.1:37701","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":50033,"latency_human":"50.033ยตs","bytes_in":2,"bytes_out":123} -{"time":"2026-02-10T16:47:58.126147838Z","id":"e7b3a2bb-75c5-40d9-b842-ca9b4193e70e","remote_ip":"127.0.0.1","host":"127.0.0.1:37701","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":65191,"latency_human":"65.191ยตs","bytes_in":2,"bytes_out":123} -=== RUN TestRefreshTokenFlow/RefreshToken_InvalidTokenFormat -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"bdae5fed-aeb1-4b5d-a581-92fa71f15a0e","timestamp":"2026-02-10T16:47:58.126745988Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"41","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"refresh_token":"not-a-valid-jwt-token"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":19776,"status_code":400,"response_size":41} -{"time":"2026-02-10T16:47:58.12677399Z","id":"bdae5fed-aeb1-4b5d-a581-92fa71f15a0e","remote_ip":"127.0.0.1","host":"127.0.0.1:34129","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":27070,"latency_human":"27.07ยตs","bytes_in":41,"bytes_out":41} -{"time":"2026-02-10T16:47:58.126778147Z","id":"bdae5fed-aeb1-4b5d-a581-92fa71f15a0e","remote_ip":"127.0.0.1","host":"127.0.0.1:34129","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":32380,"latency_human":"32.38ยตs","bytes_in":41,"bytes_out":41} - refresh_token_test.go:50: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:50 - Error: Not equal: - expected: 401 - actual : 400 - Test: TestRefreshTokenFlow/RefreshToken_InvalidTokenFormat -=== RUN TestRefreshTokenFlow/RefreshToken_ExpiredToken -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"f4320bd2-a494-4788-bc25-8fa04e859a7d","timestamp":"2026-02-10T16:47:58.127375836Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"89","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjAwMDAwMDB9.expired"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":22582,"status_code":400,"response_size":41} -{"time":"2026-02-10T16:47:58.127408416Z","id":"f4320bd2-a494-4788-bc25-8fa04e859a7d","remote_ip":"127.0.0.1","host":"127.0.0.1:43647","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":31799,"latency_human":"31.799ยตs","bytes_in":89,"bytes_out":41} -{"time":"2026-02-10T16:47:58.127412744Z","id":"f4320bd2-a494-4788-bc25-8fa04e859a7d","remote_ip":"127.0.0.1","host":"127.0.0.1:43647","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":37890,"latency_human":"37.89ยตs","bytes_in":89,"bytes_out":41} - refresh_token_test.go:71: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:71 - Error: Not equal: - expected: 401 - actual : 400 - Test: TestRefreshTokenFlow/RefreshToken_ExpiredToken -=== RUN TestRefreshTokenFlow/RefreshToken_ValidToken -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"24608392-21b7-4351-975d-6de943df3fc2","timestamp":"2026-02-10T16:47:58.127990937Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":696131,"status_code":401,"response_size":32} -{"time":"2026-02-10T16:47:58.128696966Z","id":"24608392-21b7-4351-975d-6de943df3fc2","remote_ip":"127.0.0.1","host":"127.0.0.1:33651","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":704457,"latency_human":"704.457ยตs","bytes_in":59,"bytes_out":32} -{"time":"2026-02-10T16:47:58.128701385Z","id":"24608392-21b7-4351-975d-6de943df3fc2","remote_ip":"127.0.0.1","host":"127.0.0.1:33651","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":709767,"latency_human":"709.767ยตs","bytes_in":59,"bytes_out":32} - refresh_token_test.go:93: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:93 - Error: Not equal: - expected: 200 - actual : 401 - Test: TestRefreshTokenFlow/RefreshToken_ValidToken -=== RUN TestRefreshTokenFlow/RefreshToken_InvalidRequestBody -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"066e6650-1c29-4f40-bd3b-0442c7a6ce6e","timestamp":"2026-02-10T16:47:58.12986333Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"12","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":21339,"status_code":400,"response_size":28} -{"time":"2026-02-10T16:47:58.129892975Z","id":"066e6650-1c29-4f40-bd3b-0442c7a6ce6e","remote_ip":"127.0.0.1","host":"127.0.0.1:44577","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":29134,"latency_human":"29.134ยตs","bytes_in":12,"bytes_out":28} -{"time":"2026-02-10T16:47:58.129900709Z","id":"066e6650-1c29-4f40-bd3b-0442c7a6ce6e","remote_ip":"127.0.0.1","host":"127.0.0.1:44577","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":33412,"latency_human":"33.412ยตs","bytes_in":12,"bytes_out":28} -=== RUN TestRefreshTokenFlow/RefreshToken_MissingContentType -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"327cd2d2-7c85-4554-8e00-a14919422e9e","timestamp":"2026-02-10T16:47:58.130638668Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"30","User-Agent":"Go-http-client/1.1"},"body":{"refresh_token":"some-token"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":21520,"status_code":400,"response_size":28} -{"time":"2026-02-10T16:47:58.130670517Z","id":"327cd2d2-7c85-4554-8e00-a14919422e9e","remote_ip":"127.0.0.1","host":"127.0.0.1:33307","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":30716,"latency_human":"30.716ยตs","bytes_in":30,"bytes_out":28} -{"time":"2026-02-10T16:47:58.130675085Z","id":"327cd2d2-7c85-4554-8e00-a14919422e9e","remote_ip":"127.0.0.1","host":"127.0.0.1:33307","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":35916,"latency_human":"35.916ยตs","bytes_in":30,"bytes_out":28} ---- FAIL: TestRefreshTokenFlow (0.01s) - --- PASS: TestRefreshTokenFlow/RefreshToken_MissingToken (0.00s) - --- FAIL: TestRefreshTokenFlow/RefreshToken_InvalidTokenFormat (0.00s) - --- FAIL: TestRefreshTokenFlow/RefreshToken_ExpiredToken (0.00s) - --- FAIL: TestRefreshTokenFlow/RefreshToken_ValidToken (0.00s) - --- PASS: TestRefreshTokenFlow/RefreshToken_InvalidRequestBody (0.00s) - --- PASS: TestRefreshTokenFlow/RefreshToken_MissingContentType (0.00s) -=== RUN TestRefreshTokenSecurity -=== RUN TestRefreshTokenSecurity/RefreshToken_ReuseProtection -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"58b77c88-51dd-4cce-b3b8-d02e79c3cbc0","timestamp":"2026-02-10T16:47:58.131580595Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":940725,"status_code":401,"response_size":32} -{"time":"2026-02-10T16:47:58.132536608Z","id":"58b77c88-51dd-4cce-b3b8-d02e79c3cbc0","remote_ip":"127.0.0.1","host":"127.0.0.1:39475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":954460,"latency_human":"954.46ยตs","bytes_in":59,"bytes_out":32} -{"time":"2026-02-10T16:47:58.132543631Z","id":"58b77c88-51dd-4cce-b3b8-d02e79c3cbc0","remote_ip":"127.0.0.1","host":"127.0.0.1:39475","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":961724,"latency_human":"961.724ยตs","bytes_in":59,"bytes_out":32} - refresh_token_test.go:187: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:187 - Error: Not equal: - expected: 200 - actual : 401 - Test: TestRefreshTokenSecurity/RefreshToken_ReuseProtection -=== RUN TestRefreshTokenSecurity/RefreshToken_TokenTampering -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"9c90abbe-99bc-40f3-87ca-2d780b812ffd","timestamp":"2026-02-10T16:47:58.13334063Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":694037,"status_code":401,"response_size":32} -{"time":"2026-02-10T16:47:58.134047872Z","id":"9c90abbe-99bc-40f3-87ca-2d780b812ffd","remote_ip":"127.0.0.1","host":"127.0.0.1:40707","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":705449,"latency_human":"705.449ยตs","bytes_in":59,"bytes_out":32} -{"time":"2026-02-10T16:47:58.1340526Z","id":"9c90abbe-99bc-40f3-87ca-2d780b812ffd","remote_ip":"127.0.0.1","host":"127.0.0.1:40707","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":711961,"latency_human":"711.961ยตs","bytes_in":59,"bytes_out":32} - refresh_token_test.go:241: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:241 - Error: Not equal: - expected: 200 - actual : 401 - Test: TestRefreshTokenSecurity/RefreshToken_TokenTampering ---- FAIL: TestRefreshTokenSecurity (0.00s) - --- FAIL: TestRefreshTokenSecurity/RefreshToken_ReuseProtection (0.00s) - --- FAIL: TestRefreshTokenSecurity/RefreshToken_TokenTampering (0.00s) -=== RUN TestRefreshTokenEdgeCases -=== RUN TestRefreshTokenEdgeCases/RefreshToken_EmptyStringToken -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"7771c152-c3bf-4b0c-9dc0-b7a7c5348633","timestamp":"2026-02-10T16:47:58.134819703Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"20","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"refresh_token":""},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":25026,"status_code":400,"response_size":123} -{"time":"2026-02-10T16:47:58.134854918Z","id":"7771c152-c3bf-4b0c-9dc0-b7a7c5348633","remote_ip":"127.0.0.1","host":"127.0.0.1:35675","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":34053,"latency_human":"34.053ยตs","bytes_in":20,"bytes_out":123} -{"time":"2026-02-10T16:47:58.13486649Z","id":"7771c152-c3bf-4b0c-9dc0-b7a7c5348633","remote_ip":"127.0.0.1","host":"127.0.0.1:35675","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":39253,"latency_human":"39.253ยตs","bytes_in":20,"bytes_out":123} -=== RUN TestRefreshTokenEdgeCases/RefreshToken_NullToken -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"648bba08-f0e3-4e7a-8298-c003bcbd4e5f","timestamp":"2026-02-10T16:47:58.135449331Z","method":"POST","path":"/api/auth/refresh","headers":{"Accept-Encoding":"gzip","Content-Length":"22","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"refresh_token":null},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":18584,"status_code":400,"response_size":123} -{"time":"2026-02-10T16:47:58.135478405Z","id":"648bba08-f0e3-4e7a-8298-c003bcbd4e5f","remote_ip":"127.0.0.1","host":"127.0.0.1:39479","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":28062,"latency_human":"28.062ยตs","bytes_in":22,"bytes_out":123} -{"time":"2026-02-10T16:47:58.135482021Z","id":"648bba08-f0e3-4e7a-8298-c003bcbd4e5f","remote_ip":"127.0.0.1","host":"127.0.0.1:39479","method":"POST","uri":"/api/auth/refresh","user_agent":"Go-http-client/1.1","status":400,"error":"","latency":32861,"latency_human":"32.861ยตs","bytes_in":22,"bytes_out":123} -=== RUN TestRefreshTokenEdgeCases/RefreshToken_ResponseStructure -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"a27bdaca-d56f-4bbd-b8ea-6e8880d6ee39","timestamp":"2026-02-10T16:47:58.136901154Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":937108,"status_code":401,"response_size":32} -{"time":"2026-02-10T16:47:58.137872425Z","id":"a27bdaca-d56f-4bbd-b8ea-6e8880d6ee39","remote_ip":"127.0.0.1","host":"127.0.0.1:38851","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":970210,"latency_human":"970.21ยตs","bytes_in":59,"bytes_out":32} -{"time":"2026-02-10T16:47:58.137878547Z","id":"a27bdaca-d56f-4bbd-b8ea-6e8880d6ee39","remote_ip":"127.0.0.1","host":"127.0.0.1:38851","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":976501,"latency_human":"976.501ยตs","bytes_in":59,"bytes_out":32} - refresh_token_test.go:330: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:330 - Error: Not equal: - expected: 200 - actual : 401 - Test: TestRefreshTokenEdgeCases/RefreshToken_ResponseStructure -=== RUN TestRefreshTokenEdgeCases/RefreshToken_TokenType -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 Starting sync queue processor (interval: 5s, batch: 50) -2026/02/10 16:47:58 [REQUEST] {"request_id":"ac1621f0-1ef7-4f63-b5b5-23ec35b532cf","timestamp":"2026-02-10T16:47:58.138781381Z","method":"POST","path":"/api/auth/login","headers":{"Accept-Encoding":"gzip","Content-Length":"59","Content-Type":"application/json","User-Agent":"Go-http-client/1.1"},"body":{"login":"testuser@example.com","password":"Test@Pass123!"},"remote_addr":"127.0.0.1","user_agent":"Go-http-client/1.1","duration":672738,"status_code":401,"response_size":32} -{"time":"2026-02-10T16:47:58.139494424Z","id":"ac1621f0-1ef7-4f63-b5b5-23ec35b532cf","remote_ip":"127.0.0.1","host":"127.0.0.1:45949","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":702833,"latency_human":"702.833ยตs","bytes_in":59,"bytes_out":32} -{"time":"2026-02-10T16:47:58.139501627Z","id":"ac1621f0-1ef7-4f63-b5b5-23ec35b532cf","remote_ip":"127.0.0.1","host":"127.0.0.1:45949","method":"POST","uri":"/api/auth/login","user_agent":"Go-http-client/1.1","status":401,"error":"","latency":724765,"latency_human":"724.765ยตs","bytes_in":59,"bytes_out":32} - refresh_token_test.go:382: - Error Trace: /app/cmd/server/tests/refresh_token_test.go:382 - Error: Not equal: - expected: 200 - actual : 401 - Test: TestRefreshTokenEdgeCases/RefreshToken_TokenType ---- FAIL: TestRefreshTokenEdgeCases (0.01s) - --- PASS: TestRefreshTokenEdgeCases/RefreshToken_EmptyStringToken (0.00s) - --- PASS: TestRefreshTokenEdgeCases/RefreshToken_NullToken (0.00s) - --- FAIL: TestRefreshTokenEdgeCases/RefreshToken_ResponseStructure (0.00s) - --- FAIL: TestRefreshTokenEdgeCases/RefreshToken_TokenType (0.00s) -=== RUN TestRegisterEndpoint -=== RUN TestRegisterEndpoint/Valid_registration_with_all_fields -=== RUN TestRegisterEndpoint/Valid_registration_with_only_required_fields -=== RUN TestRegisterEndpoint/Registration_with_role_specified -=== RUN TestRegisterEndpoint/Invalid_email_format -=== RUN TestRegisterEndpoint/Email_already_exists -=== RUN TestRegisterEndpoint/Username_already_exists -=== RUN TestRegisterEndpoint/Username_too_short -=== RUN TestRegisterEndpoint/Username_too_long -=== RUN TestRegisterEndpoint/Password_too_short -=== RUN TestRegisterEndpoint/Missing_required_field_-_email -=== RUN TestRegisterEndpoint/Missing_required_field_-_username -=== RUN TestRegisterEndpoint/Missing_required_field_-_password -=== RUN TestRegisterEndpoint/Invalid_JSON_payload -=== RUN TestRegisterEndpoint/Invalid_role_value -=== RUN TestRegisterEndpoint/Empty_email -=== RUN TestRegisterEndpoint/Empty_username -=== RUN TestRegisterEndpoint/Empty_password -=== RUN TestRegisterEndpoint/Whitespace-only_username -=== RUN TestRegisterEndpoint/Empty_JSON_request_body ---- PASS: TestRegisterEndpoint (0.00s) - --- PASS: TestRegisterEndpoint/Valid_registration_with_all_fields (0.00s) - --- PASS: TestRegisterEndpoint/Valid_registration_with_only_required_fields (0.00s) - --- PASS: TestRegisterEndpoint/Registration_with_role_specified (0.00s) - --- PASS: TestRegisterEndpoint/Invalid_email_format (0.00s) - --- PASS: TestRegisterEndpoint/Email_already_exists (0.00s) - --- PASS: TestRegisterEndpoint/Username_already_exists (0.00s) - --- PASS: TestRegisterEndpoint/Username_too_short (0.00s) - --- PASS: TestRegisterEndpoint/Username_too_long (0.00s) - --- PASS: TestRegisterEndpoint/Password_too_short (0.00s) - --- PASS: TestRegisterEndpoint/Missing_required_field_-_email (0.00s) - --- PASS: TestRegisterEndpoint/Missing_required_field_-_username (0.00s) - --- PASS: TestRegisterEndpoint/Missing_required_field_-_password (0.00s) - --- PASS: TestRegisterEndpoint/Invalid_JSON_payload (0.00s) - --- PASS: TestRegisterEndpoint/Invalid_role_value (0.00s) - --- PASS: TestRegisterEndpoint/Empty_email (0.00s) - --- PASS: TestRegisterEndpoint/Empty_username (0.00s) - --- PASS: TestRegisterEndpoint/Empty_password (0.00s) - --- PASS: TestRegisterEndpoint/Whitespace-only_username (0.00s) - --- PASS: TestRegisterEndpoint/Empty_JSON_request_body (0.00s) -=== RUN TestLoginEndpoint -=== RUN TestLoginEndpoint/Valid_login_with_email -=== RUN TestLoginEndpoint/Valid_login_with_username -=== RUN TestLoginEndpoint/Invalid_password -=== RUN TestLoginEndpoint/User_not_found -=== RUN TestLoginEndpoint/Missing_login_field -=== RUN TestLoginEndpoint/Missing_password_field -=== RUN TestLoginEndpoint/Empty_login -=== RUN TestLoginEndpoint/Empty_password -=== RUN TestLoginEndpoint/Invalid_JSON_payload -=== RUN TestLoginEndpoint/Empty_request_body ---- PASS: TestLoginEndpoint (0.00s) - --- PASS: TestLoginEndpoint/Valid_login_with_email (0.00s) - --- PASS: TestLoginEndpoint/Valid_login_with_username (0.00s) - --- PASS: TestLoginEndpoint/Invalid_password (0.00s) - --- PASS: TestLoginEndpoint/User_not_found (0.00s) - --- PASS: TestLoginEndpoint/Missing_login_field (0.00s) - --- PASS: TestLoginEndpoint/Missing_password_field (0.00s) - --- PASS: TestLoginEndpoint/Empty_login (0.00s) - --- PASS: TestLoginEndpoint/Empty_password (0.00s) - --- PASS: TestLoginEndpoint/Invalid_JSON_payload (0.00s) - --- PASS: TestLoginEndpoint/Empty_request_body (0.00s) -=== RUN TestSearchMediaItemsTests -=== RUN TestSearchMediaItemsTests/No_user_context_-_GET_/api/media-items/search_without_authentication -=== RUN TestSearchMediaItemsTests/User_context_-_GET_/api/media-items/search_with_valid_authentication -=== RUN TestSearchMediaItemsTests/Admin_context_-_GET_/api/media-items/search_with_admin_token -=== RUN TestSearchMediaItemsTests/Search_with_missing_query_parameter -=== RUN TestSearchMediaItemsTests/Search_with_no_results_found_(404) -=== RUN TestSearchMediaItemsTests/Search_with_partial_match -=== RUN TestSearchMediaItemsTests/Search_with_fuzzy_match_fallback -=== RUN TestSearchMediaItemsTests/Search_by_author_name -=== RUN TestSearchMediaItemsTests/Search_with_special_characters ---- PASS: TestSearchMediaItemsTests (0.00s) - --- PASS: TestSearchMediaItemsTests/No_user_context_-_GET_/api/media-items/search_without_authentication (0.00s) - --- PASS: TestSearchMediaItemsTests/User_context_-_GET_/api/media-items/search_with_valid_authentication (0.00s) - --- PASS: TestSearchMediaItemsTests/Admin_context_-_GET_/api/media-items/search_with_admin_token (0.00s) - --- PASS: TestSearchMediaItemsTests/Search_with_missing_query_parameter (0.00s) - --- PASS: TestSearchMediaItemsTests/Search_with_no_results_found_(404) (0.00s) - --- PASS: TestSearchMediaItemsTests/Search_with_partial_match (0.00s) - --- PASS: TestSearchMediaItemsTests/Search_with_fuzzy_match_fallback (0.00s) - --- PASS: TestSearchMediaItemsTests/Search_by_author_name (0.00s) - --- PASS: TestSearchMediaItemsTests/Search_with_special_characters (0.00s) -=== RUN TestSearchIntegrationWithRealDatabase -=== RUN TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items -=== RUN TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Register_admin_user -=== RUN TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Register_regular_user -=== RUN TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Create_library_with_admin_token -=== RUN TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/User_searches_for_existing_media_items -=== RUN TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Admin_searches_all_items_including_hidden ---- PASS: TestSearchIntegrationWithRealDatabase (0.00s) - --- PASS: TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items (0.00s) - --- PASS: TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Register_admin_user (0.00s) - --- PASS: TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Register_regular_user (0.00s) - --- PASS: TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Create_library_with_admin_token (0.00s) - --- PASS: TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/User_searches_for_existing_media_items (0.00s) - --- PASS: TestSearchIntegrationWithRealDatabase/Setup_-_Register_admin_and_user,_create_library_with_media_items/Admin_searches_all_items_including_hidden (0.00s) -=== RUN TestPasswordComplexity -=== RUN TestPasswordComplexity/Valid_password_with_all_requirements -=== RUN TestPasswordComplexity/Missing_uppercase -=== RUN TestPasswordComplexity/Missing_lowercase -=== RUN TestPasswordComplexity/Missing_number -=== RUN TestPasswordComplexity/Missing_special_char -=== RUN TestPasswordComplexity/Too_short -=== RUN TestPasswordComplexity/Minimum_valid_password ---- PASS: TestPasswordComplexity (0.00s) - --- PASS: TestPasswordComplexity/Valid_password_with_all_requirements (0.00s) - --- PASS: TestPasswordComplexity/Missing_uppercase (0.00s) - --- PASS: TestPasswordComplexity/Missing_lowercase (0.00s) - --- PASS: TestPasswordComplexity/Missing_number (0.00s) - --- PASS: TestPasswordComplexity/Missing_special_char (0.00s) - --- PASS: TestPasswordComplexity/Too_short (0.00s) - --- PASS: TestPasswordComplexity/Minimum_valid_password (0.00s) -=== RUN TestAccountLockout -=== RUN TestAccountLockout/Failed_login_attempts_tracking -=== RUN TestAccountLockout/Clear_attempts_unlocks_account ---- PASS: TestAccountLockout (0.00s) - --- PASS: TestAccountLockout/Failed_login_attempts_tracking (0.00s) - --- PASS: TestAccountLockout/Clear_attempts_unlocks_account (0.00s) -=== RUN TestRateLimiterSecurity ---- PASS: TestRateLimiterSecurity (0.00s) -=== RUN TestJWTExpiration ---- PASS: TestJWTExpiration (0.00s) -=== RUN TestRefreshTokenExpiration ---- PASS: TestRefreshTokenExpiration (0.00s) -=== RUN TestPasswordRequirementsList ---- PASS: TestPasswordRequirementsList (0.00s) -=== RUN TestDatabaseTransactionManager ---- PASS: TestDatabaseTransactionManager (0.00s) -=== RUN TestErrorHandlingTypes ---- PASS: TestErrorHandlingTypes (0.00s) -=== RUN TestSimpleSetup - setup_test.go:8: ๐Ÿ”ง Test setup verification - setup_test.go:11: โœ… Go compilation successful - setup_test.go:14: ๐Ÿš€ Test runner is working - setup_test.go:17: โœ… Basic test completed successfully ---- PASS: TestSimpleSetup (0.00s) -=== RUN TestListMediaItemsSorting -=== RUN TestListMediaItemsSorting/No_user_context_-_GET_/api/media-items_without_authentication -=== RUN TestListMediaItemsSorting/User_context_-_GET_/api/media-items_with_title_ASC_sort -=== RUN TestListMediaItemsSorting/User_context_-_GET_/api/media-items_with_author_DESC_sort -=== RUN TestListMediaItemsSorting/Admin_context_-_GET_/api/media-items_with_page_count_DESC_sort -=== RUN TestListMediaItemsSorting/User_context_-_Invalid_sort_parameter_defaults_to_created_at_DESC -=== RUN TestListMediaItemsSorting/User_context_-_Sort_by_genre_ASC -=== RUN TestListMediaItemsSorting/User_context_-_Sort_by_copyright_year_DESC -=== RUN TestListMediaItemsSorting/User_context_-_Sort_with_pagination ---- PASS: TestListMediaItemsSorting (0.00s) - --- PASS: TestListMediaItemsSorting/No_user_context_-_GET_/api/media-items_without_authentication (0.00s) - --- PASS: TestListMediaItemsSorting/User_context_-_GET_/api/media-items_with_title_ASC_sort (0.00s) - --- PASS: TestListMediaItemsSorting/User_context_-_GET_/api/media-items_with_author_DESC_sort (0.00s) - --- PASS: TestListMediaItemsSorting/Admin_context_-_GET_/api/media-items_with_page_count_DESC_sort (0.00s) - --- PASS: TestListMediaItemsSorting/User_context_-_Invalid_sort_parameter_defaults_to_created_at_DESC (0.00s) - --- PASS: TestListMediaItemsSorting/User_context_-_Sort_by_genre_ASC (0.00s) - --- PASS: TestListMediaItemsSorting/User_context_-_Sort_by_copyright_year_DESC (0.00s) - --- PASS: TestListMediaItemsSorting/User_context_-_Sort_with_pagination (0.00s) -=== RUN TestSyncIntegration_OfflineDetector_DeviceStatusDetection - sync_integration_test.go:53: - Error Trace: /app/cmd/server/tests/sync_integration_test.go:53 - /app/cmd/server/tests/sync_integration_test.go:79 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestSyncIntegration_OfflineDetector_DeviceStatusDetection ---- FAIL: TestSyncIntegration_OfflineDetector_DeviceStatusDetection (0.00s) -=== RUN TestSyncIntegration_OfflineDetector_OfflineThreshold - sync_integration_test.go:53: - Error Trace: /app/cmd/server/tests/sync_integration_test.go:53 - /app/cmd/server/tests/sync_integration_test.go:95 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestSyncIntegration_OfflineDetector_OfflineThreshold ---- FAIL: TestSyncIntegration_OfflineDetector_OfflineThreshold (0.00s) -=== RUN TestSyncIntegration_OfflineDetector_GetDeviceStatus - sync_integration_test.go:53: - Error Trace: /app/cmd/server/tests/sync_integration_test.go:53 - /app/cmd/server/tests/sync_integration_test.go:113 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestSyncIntegration_OfflineDetector_GetDeviceStatus ---- FAIL: TestSyncIntegration_OfflineDetector_GetDeviceStatus (0.00s) -=== RUN TestSyncIntegration_OfflineDetector_ForceReconnectDevice - sync_integration_test.go:53: - Error Trace: /app/cmd/server/tests/sync_integration_test.go:53 - /app/cmd/server/tests/sync_integration_test.go:129 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestSyncIntegration_OfflineDetector_ForceReconnectDevice ---- FAIL: TestSyncIntegration_OfflineDetector_ForceReconnectDevice (0.00s) -=== RUN TestSyncIntegration_QueueProcessor_EnqueueProgress - sync_integration_test.go:174: - Error Trace: /app/cmd/server/tests/sync_integration_test.go:174 - Error: Received unexpected error: - failed to connect to `host=db user=postgres database=bookhoard`: server error (FATAL: sorry, too many clients already (SQLSTATE 53300)) - Test: TestSyncIntegration_QueueProcessor_EnqueueProgress ---- FAIL: TestSyncIntegration_QueueProcessor_EnqueueProgress (0.00s) -=== RUN TestSystemSettingsHandler -=== RUN TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_without_auth -=== RUN TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_non-admin -=== RUN TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_admin - system_settings_test.go:77: - Error Trace: /app/cmd/server/tests/system_settings_test.go:77 - Error: Not equal: - expected: 200 - actual : 403 - Test: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_admin - system_settings_test.go:81: - Error Trace: /app/cmd/server/tests/system_settings_test.go:81 - Error: Received unexpected error: - EOF - Test: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_admin - system_settings_test.go:82: - Error Trace: /app/cmd/server/tests/system_settings_test.go:82 - Error: Not equal: - expected: float64(60) - actual : () - Test: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_admin - system_settings_test.go:83: - Error Trace: /app/cmd/server/tests/system_settings_test.go:83 - Error: Not equal: - expected: bool(true) - actual : () - Test: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_admin -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_without_auth -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_as_non-admin -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_low_(14_minutes) - system_settings_test.go:175: - Error Trace: /app/cmd/server/tests/system_settings_test.go:175 - Error: Not equal: - expected: 400 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_low_(14_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_high_(1441_minutes) - system_settings_test.go:175: - Error Trace: /app/cmd/server/tests/system_settings_test.go:175 - Error: Not equal: - expected: 400 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_high_(1441_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_low_(0_minutes) - system_settings_test.go:175: - Error Trace: /app/cmd/server/tests/system_settings_test.go:175 - Error: Not equal: - expected: 400 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_low_(0_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_negative_(-10) - system_settings_test.go:175: - Error Trace: /app/cmd/server/tests/system_settings_test.go:175 - Error: Not equal: - expected: 400 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_negative_(-10) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_missing_required_field - system_settings_test.go:203: - Error Trace: /app/cmd/server/tests/system_settings_test.go:203 - Error: Not equal: - expected: 400 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_missing_required_field -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) - system_settings_test.go:251: - Error Trace: /app/cmd/server/tests/system_settings_test.go:251 - Error: Not equal: - expected: 200 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) - system_settings_test.go:255: - Error Trace: /app/cmd/server/tests/system_settings_test.go:255 - Error: Received unexpected error: - EOF - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) - system_settings_test.go:256: - Error Trace: /app/cmd/server/tests/system_settings_test.go:256 - Error: Not equal: - expected: float64(15) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) - system_settings_test.go:257: - Error Trace: /app/cmd/server/tests/system_settings_test.go:257 - Error: Not equal: - expected: bool(true) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) - system_settings_test.go:258: - Error Trace: /app/cmd/server/tests/system_settings_test.go:258 - Error: Not equal: - expected: string("scan settings updated successfully") - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) - system_settings_test.go:251: - Error Trace: /app/cmd/server/tests/system_settings_test.go:251 - Error: Not equal: - expected: 200 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) - system_settings_test.go:255: - Error Trace: /app/cmd/server/tests/system_settings_test.go:255 - Error: Received unexpected error: - EOF - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) - system_settings_test.go:256: - Error Trace: /app/cmd/server/tests/system_settings_test.go:256 - Error: Not equal: - expected: float64(60) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) - system_settings_test.go:257: - Error Trace: /app/cmd/server/tests/system_settings_test.go:257 - Error: Not equal: - expected: bool(true) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) - system_settings_test.go:258: - Error Trace: /app/cmd/server/tests/system_settings_test.go:258 - Error: Not equal: - expected: string("scan settings updated successfully") - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) - system_settings_test.go:251: - Error Trace: /app/cmd/server/tests/system_settings_test.go:251 - Error: Not equal: - expected: 200 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) - system_settings_test.go:255: - Error Trace: /app/cmd/server/tests/system_settings_test.go:255 - Error: Received unexpected error: - EOF - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) - system_settings_test.go:256: - Error Trace: /app/cmd/server/tests/system_settings_test.go:256 - Error: Not equal: - expected: float64(1440) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) - system_settings_test.go:257: - Error Trace: /app/cmd/server/tests/system_settings_test.go:257 - Error: Not equal: - expected: bool(true) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) - system_settings_test.go:258: - Error Trace: /app/cmd/server/tests/system_settings_test.go:258 - Error: Not equal: - expected: string("scan settings updated successfully") - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) - system_settings_test.go:251: - Error Trace: /app/cmd/server/tests/system_settings_test.go:251 - Error: Not equal: - expected: 200 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) - system_settings_test.go:255: - Error Trace: /app/cmd/server/tests/system_settings_test.go:255 - Error: Received unexpected error: - EOF - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) - system_settings_test.go:256: - Error Trace: /app/cmd/server/tests/system_settings_test.go:256 - Error: Not equal: - expected: float64(120) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) - system_settings_test.go:257: - Error Trace: /app/cmd/server/tests/system_settings_test.go:257 - Error: Not equal: - expected: bool(false) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) - system_settings_test.go:258: - Error Trace: /app/cmd/server/tests/system_settings_test.go:258 - Error: Not equal: - expected: string("scan settings updated successfully") - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) - system_settings_test.go:251: - Error Trace: /app/cmd/server/tests/system_settings_test.go:251 - Error: Not equal: - expected: 200 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) - system_settings_test.go:255: - Error Trace: /app/cmd/server/tests/system_settings_test.go:255 - Error: Received unexpected error: - EOF - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) - system_settings_test.go:256: - Error Trace: /app/cmd/server/tests/system_settings_test.go:256 - Error: Not equal: - expected: float64(30) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) - system_settings_test.go:257: - Error Trace: /app/cmd/server/tests/system_settings_test.go:257 - Error: Not equal: - expected: bool(true) - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) - system_settings_test.go:258: - Error Trace: /app/cmd/server/tests/system_settings_test.go:258 - Error: Not equal: - expected: string("scan settings updated successfully") - actual : () - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) -=== RUN TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_JSON - system_settings_test.go:283: - Error Trace: /app/cmd/server/tests/system_settings_test.go:283 - Error: Not equal: - expected: 400 - actual : 403 - Test: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_JSON ---- FAIL: TestSystemSettingsHandler (0.00s) - --- PASS: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_without_auth (0.00s) - --- PASS: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_non-admin (0.00s) - --- FAIL: TestSystemSettingsHandler/GET_/api/libraries/scan-settings_-_Get_settings_as_admin (0.00s) - --- PASS: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_without_auth (0.00s) - --- PASS: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_as_non-admin (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_low_(14_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_high_(1441_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_too_low_(0_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_frequency/Frequency_negative_(-10) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_missing_required_field (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(15_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(60_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(1440_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(120_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_valid_data/Valid_frequency_(30_minutes) (0.00s) - --- FAIL: TestSystemSettingsHandler/PUT_/api/libraries/scan-settings_-_Update_with_invalid_JSON (0.00s) -=== RUN TestSystemSettingsIntegration -=== RUN TestSystemSettingsIntegration/System_settings_affect_all_libraries_equally -=== RUN TestSystemSettingsIntegration/Disabling_auto_scan_stops_all_library_scans - system_settings_test.go:306: - Error Trace: /app/cmd/server/tests/system_settings_test.go:306 - Error: Not equal: - expected: "Scans should not run when auto_scan_enabled is false" - actual : "Scans should not run" - - Diff: - --- Expected - +++ Actual - @@ -1 +1 @@ - -Scans should not run when auto_scan_enabled is false - +Scans should not run - Test: TestSystemSettingsIntegration/Disabling_auto_scan_stops_all_library_scans -=== RUN TestSystemSettingsIntegration/Valid_frequency_range_enforcement ---- FAIL: TestSystemSettingsIntegration (0.00s) - --- PASS: TestSystemSettingsIntegration/System_settings_affect_all_libraries_equally (0.00s) - --- FAIL: TestSystemSettingsIntegration/Disabling_auto_scan_stops_all_library_scans (0.00s) - --- PASS: TestSystemSettingsIntegration/Valid_frequency_range_enforcement (0.00s) -=== RUN TestTestRunner - testrunner_test.go:8: ๐Ÿงช Go test runner verification - testrunner_test.go:9: โœ… Testing framework is properly configured - testrunner_test.go:10: ๐Ÿ“‹ Package structure is correct - testrunner_test.go:13: ๐Ÿ“ All tests should be discoverable and runnable - testrunner_test.go:20: โœ… Test runner verification completed ---- PASS: TestTestRunner (0.00s) -=== RUN TestPhase1Integration -=== RUN TestPhase1Integration/Cleanup_ExistingTestUser - universal_progress_integration_test.go:34: Cleanup: No existing test user to delete (server not available) -=== RUN TestPhase1Integration/Step1_CreateFirstUser - universal_progress_integration_test.go:108: - Error Trace: /app/cmd/server/tests/universal_progress_integration_test.go:108 - Error: Received unexpected error: - Post "http://localhost:8765/api/auth/register": dial tcp [::1]:8765: connect: connection refused - Test: TestPhase1Integration/Step1_CreateFirstUser ---- FAIL: TestPhase1Integration (0.00s) - --- PASS: TestPhase1Integration/Cleanup_ExistingTestUser (0.00s) - --- FAIL: TestPhase1Integration/Step1_CreateFirstUser (0.00s) -panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked] -[signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x10304cd] - -goroutine 7630 [running]: -testing.tRunner.func1.2({0x11c2420, 0x1e28230}) - /usr/local/go/src/testing/testing.go:1872 +0x237 -testing.tRunner.func1() - /usr/local/go/src/testing/testing.go:1875 +0x35b -panic({0x11c2420?, 0x1e28230?}) - /usr/local/go/src/runtime/panic.go:783 +0x132 -bookhoard/cmd/server/tests.TestPhase1Integration.func2(0xc00257a700) - /app/cmd/server/tests/universal_progress_integration_test.go:109 +0x26d -testing.tRunner(0xc00257a700, 0x140bc48) - /usr/local/go/src/testing/testing.go:1934 +0xea -created by testing.(*T).Run in goroutine 7622 - /usr/local/go/src/testing/testing.go:1997 +0x465 -FAIL bookhoard/cmd/server/tests 7.634s -FAIL -Error: executing /usr/bin/podman-compose --profile tests run --rm tests: exit status 1 -make: *** [Makefile:39: test-integration] Error 1 diff --git a/cmd/server/tests/TEST_CLEANUP_PATTERN.md b/cmd/server/tests/TEST_CLEANUP_PATTERN.md deleted file mode 100644 index 2af6491..0000000 --- a/cmd/server/tests/TEST_CLEANUP_PATTERN.md +++ /dev/null @@ -1,283 +0,0 @@ -# Test Resource Cleanup Pattern - -## Overview - -This document describes the **TestServerSetup** pattern used for automatic resource cleanup in integration tests, which prevents database connection leaks and goroutine leaks. - -## Problem - -Prior to this pattern, integration tests had resource leaks: - -```go -// OLD PATTERN (BROKEN) -func TestExample(t *testing.T) { - ts, db, _ := setupTestServer(t) - defer ts.Close() // โŒ Only closes HTTP server - - // ... test code ... - // โŒ dbPool never closed - // โŒ connManager.StartCleanupTask() goroutine never stopped - // โŒ queueProcessor.Start() goroutine never stopped -} -``` - -**Impact:** -- Each test leaked ~4 database connections (pgxpool default max_conns) -- Leaked 2+ goroutines per test (cleanup task, queue processor) -- ~160 tests = potential 640+ leaked connections -- PostgreSQL max_connections = 100 โ†’ exhaustion after ~25 tests - -## Solution - -### TestServerSetup Struct - -Location: `/cmd/server/tests/test_helpers.go` - -```go -// TestServerSetup manages the lifecycle of a test server with proper resource cleanup -type TestServerSetup struct { - Server *httptest.Server - DB *database.Queries - DBPool *pgxpool.Pool - Config *config.Config - ConnManager *wsync.ConnectionManager - QueueProcessor *wsync.SyncQueueProcessor - CleanupCancel context.CancelFunc // For connManager cleanup task - QueueCtx context.Context // For queueProcessor - QueueCancel context.CancelFunc // For queueProcessor - mu sync.Mutex - closed bool -} - -// Close cleans up all resources in the correct order -func (s *TestServerSetup) Close() error { - s.mu.Lock() - defer s.mu.Unlock() - - if s.closed { - return nil - } - - // 1. Stop queue processor goroutine - if s.QueueCancel != nil { - s.QueueCancel() - s.QueueCancel = nil - } - - // 2. Stop connection manager cleanup task - if s.CleanupCancel != nil { - s.CleanupCancel() - s.CleanupCancel = nil - } - - // 3. Close HTTP server - if s.Server != nil { - s.Server.Close() - s.Server = nil - } - - // 4. Close database pool (waits for all connections to release) - if s.DBPool != nil { - s.DBPool.Close() - s.DBPool = nil - } - - s.closed = true - return nil -} -``` - -### setupTestServer Function - -```go -func setupTestServer(t *testing.T) *TestServerSetup { - cfg := config.LoadConfig() - // ... config setup ... - - // Create database pool - dbPool, err := pgxpool.New(context.Background(), cfg.DatabaseURL()) - require.NoError(t, err) - - // Create connManager and capture cleanup cancel function - connManager := wsync.NewConnectionManager() - cleanupCancel := connManager.StartCleanupTask() // โ† Returns CancelFunc! - - // Create queue processor with cancellable context - queueProcessor := wsync.NewSyncQueueProcessor(queries) - queueCtx, queueCancel := context.WithCancel(context.Background()) - go queueProcessor.Start(queueCtx) // โ† Now cancellable! - - // ... create handlers, router, etc ... - - ts := httptest.NewServer(e) - - setup := &TestServerSetup{ - Server: ts, - DB: queries, - DBPool: dbPool, - Config: cfg, - ConnManager: connManager, - QueueProcessor: queueProcessor, - CleanupCancel: cleanupCancel, // โ† Saved for cleanup - QueueCtx: queueCtx, - QueueCancel: queueCancel, // โ† Saved for cleanup - } - - // AUTOMATIC CLEANUP via t.Cleanup() - t.Cleanup(func() { - if err := setup.Close(); err != nil { - t.Errorf("Failed to cleanup test server: %v", err) - } - }) - - return setup -} -``` - -## Usage - -### NEW PATTERN (Correct) - -```go -func TestExample(t *testing.T) { - setup := setupTestServer(t) - // No defer needed! t.Cleanup handles it automatically - - // Access resources through setup - token := loginTestUser(t, setup.Server, setup.DB) - mediaID := createTestMediaItemID(t, setup.Server, token) - - req, _ := http.NewRequest("GET", setup.Server.URL+"/api/test", nil) - // ... test code ... - - // When test completes (pass or fail), setup.Close() is called automatically -} -``` - -### Nested Tests - -```go -func TestWithSubtests(t *testing.T) { - setup := setupTestServer(t) - // setup is available in outer scope - - t.Run("subtest 1", func(t *testing.T) { - // setup is available here too - req, _ := http.NewRequest("POST", setup.Server.URL+"/api/test", nil) - // ... - }) - - t.Run("subtest 2", func(t *testing.T) { - // Each subtest shares the same setup - // Cleanup happens when outer test completes - }) -} -``` - -### Helper Functions - -**IMPORTANT:** Helper functions that take `ts *httptest.Server` as parameter: - -```go -// CORRECT: Helper uses ts parameter -func createTestLibrary(t *testing.T, ts *httptest.Server, token string) string { - req, _ := http.NewRequest("POST", ts.URL+"/api/libraries", ...) - // ... -} - -// CORRECT: Call helper with setup.Server -func TestSomething(t *testing.T) { - setup := setupTestServer(t) - libID := createTestLibrary(t, setup.Server, token, "test-lib") -} -``` - -## Resource Cleanup Order - -When `setup.Close()` is called (automatically via `t.Cleanup()`): - -1. **Stop Queue Processor** (`QueueCancel()`) - - Stops goroutine processing sync queue - - Releases queue resources - -2. **Stop Connection Manager** (`CleanupCancel()`) - - Stops goroutine cleaning stale WebSocket connections - - Releases WebSocket resources - -3. **Close HTTP Server** (`Server.Close()`) - - Stops accepting new connections - - Shuts down HTTP server gracefully - -4. **Close Database Pool** (`DBPool.Close()`) - - Waits for all connections to be released - - Returns connections to pool - - Closes all database connections - -## Benefits - -โœ… **No manual cleanup needed** - `t.Cleanup()` handles it automatically -โœ… **Works even if test panics** - Go runtime calls cleanup -โœ… **Thread-safe** - Mutex prevents double-close issues -โœ… **Idempotent** - Can call `Close()` multiple times safely -โœ… **Catches test failures** - Cleanup happens even on test failure - -## Migration Guide - -To migrate an existing test: - -**Before:** -```go -func TestOld(t *testing.T) { - ts, db, _ := setupTestServer(t) - defer ts.Close() - - token := loginTestUser(t, ts, db) - req, _ := http.NewRequest("GET", ts.URL+"/api/test", nil) -} -``` - -**After:** -```go -func TestNew(t *testing.T) { - setup := setupTestServer(t) - // No defer needed - - token := loginTestUser(t, setup.Server, setup.DB) - req, _ := http.NewRequest("GET", setup.Server.URL+"/api/test", nil) -} -``` - -## Verification - -Check that cleanup is working: - -```bash -# Before tests -podman exec bookhoard_db psql -U postgres -d bookhoard -c \ - "SELECT count(*) FROM pg_stat_activity WHERE datname = 'bookhoard';" -# Should be: 3 (app + 2 idle) - -# Run tests -go test -v ./cmd/server/tests/ - -# After tests -podman exec bookhoard_db psql -U postgres -d bookhoard -c \ - "SELECT count(*) FROM pg_stat_activity WHERE datname = 'bookhoard';" -# Should still be: 3 (not 3 + number of tests ร— 4) -``` - -## Implementation History - -- **Created**: 2026-02-10 -- **Commits**: - - `f3141f1` - Create TestServerSetup struct - - `6c61046` - Update all test files - - `5b32b59` - Fix edge cases - - `f15bf21` - Fix t.Run block issues - - `bb2ba14` - Final compilation fixes - -## Related Files - -- `/cmd/server/tests/test_helpers.go` - TestServerSetup implementation -- `/cmd/server/tests/*.go` - All test files using the pattern -- `PROJECT_GUIDELINES.md` - Project coding standards diff --git a/cmd/server/tests/TEST_COVERAGE.md b/cmd/server/tests/TEST_COVERAGE.md deleted file mode 100644 index 42eefbc..0000000 --- a/cmd/server/tests/TEST_COVERAGE.md +++ /dev/null @@ -1,393 +0,0 @@ -# Test Coverage Report - -This document provides a comprehensive overview of all test scenarios covering possible failure points in the Bookhoard application. - -## Test Files - -### 1. registration_test.go -**Tests for User Registration Endpoint (`POST /api/auth/register`)** - -#### Success Cases: -- Valid registration with all fields -- Valid registration with only required fields -- Registration with role specified - -#### Validation Errors: -- Invalid email format -- Email already exists -- Username already exists -- Username too short (< 3 characters) -- Username too long (> 50 characters) -- Password too short (< 6 characters) -- Missing required fields (email, username, password) -- Invalid JSON payload -- Invalid role value -- Empty email, username, or password -- Whitespace-only username -- Empty JSON request body - ---- - -### 2. login_test.go (Included in registration_test.go) -**Tests for User Login Endpoint (`POST /api/auth/login`)** - -#### Success Cases: -- Valid login with email -- Valid login with username - -#### Authentication Errors: -- Invalid password -- User not found (invalid credentials) - -#### Validation Errors: -- Missing login field -- Missing password field -- Empty login or password -- Invalid JSON payload -- Empty request body - ---- - -### 3. ebook_test.go -**Tests for Ebook and Media Item Endpoints** - -#### Ebook Endpoints (`/api/ebooks`): -- `GET /api/ebooks` - List ebooks (with/without auth, pagination) -- `GET /api/ebooks/:id` - Get specific ebook (invalid UUID, non-existent) -- `POST /api/ebooks` - Create ebook (admin only, validation) -- `PUT /api/ebooks/:id` - Update ebook (admin only) -- `DELETE /api/ebooks/:id` - Delete ebook (admin only) - -#### Media Item Endpoints (`/api/media-items`): -- `GET /api/media-items` - List items (with/without library filter, invalid library_id) -- `GET /api/media-items/:id` - Get specific item (non-existent) - -#### Reading Progress (`/api/ebooks/:id/progress`): -- `GET` - Get progress (without auth) -- `PUT` - Update progress (invalid page numbers, invalid total pages) -- `DELETE` - Delete progress - -#### Ratings (`/api/ebooks/:id/rating`): -- Create rating with invalid scores (0, 11, valid range 1-10) -- Valid ratings (1, 5, 10) - ---- - -### 4. user_test.go -**Tests for User Profile and Account Management** - -#### Profile Management: -- `GET /api/auth/profile` - Get profile (without auth, with auth) -- `PUT /api/auth/profile` - Update profile (without auth, valid data) - -#### Field Updates: -- `PUT /api/auth/email`: - - Update to existing email (conflict) - - Invalid email format - - Empty email value -- `PUT /api/auth/username`: - - Update to existing username (conflict) - - Invalid length (too short, too long) -- `PUT /api/auth/password`: - - Wrong current password - - Mismatched passwords - - New password too short -- `PUT /api/auth/theme`: - - Update theme (valid) - - Empty theme value - -#### Account Deletion (`DELETE /api/auth/account`): -- Delete without auth -- Delete as last admin (forbidden) -- Delete successfully -- Admin delete another user -- Non-admin tries to delete another user (forbidden) - -#### Admin-Only Endpoints: -- `GET /api/auth/users` - List users (without admin role, with admin role) - -#### Scan Settings (`/api/library/scan-settings`): -- `GET` - Get settings (without auth) -- `PUT` - Update settings: - - Invalid frequency (too low, too high) - - Valid frequency update - ---- - -### 5. library_test_comprehensive.go -**Tests for Library Management** - -#### Library Operations (`/api/libraries`): -- `POST` - Create library: - - Without admin role (forbidden) - - Invalid library type - - Missing required fields -- `GET /:id`: - - Invalid UUID - - Non-existent library -- `PUT /:id`: - - Without admin role (forbidden) -- `DELETE /:id`: - - Without admin role (forbidden) - - Invalid UUID - -#### Library Folders (`/api/libraries/:id/folders`): -- `POST` - Add folder: - - Without admin role - - Invalid library ID - - Missing folder path -- `GET` - Get folders: - - Without admin role -- `DELETE` - Delete folder: - - Without admin role - -#### Library Visibility (`/api/libraries/visibility`): -- `POST` - Set visibility: - - Without auth - - Invalid library ID - - Successful update -- `GET /visible` - Get visible libraries: - - Without auth - - With auth - -#### Library Statistics (`/api/libraries/:id/stats`): -- `GET`: - - Without admin role - - Invalid library ID - - Successful retrieval - -#### Library Types (`/api/libraries/types`): -- `GET` - Get all library types - ---- - -### 6. edge_cases_test.go -**Tests for Edge Cases and Special Scenarios** - -#### Scanner Endpoints (`/api/scanner`): -- `POST /scan`: - - Without admin role - - Without folder paths - - Invalid folder paths - - Successful scan -- `POST /start`: - - Without admin role - - Successful start -- `POST /stop`: - - Without admin role - - Successful stop - -#### Edge Cases: -- Empty request body -- Malformed JSON -- Very large payload -- SQL injection attempt -- XSS attempt in fields -- Rate limiting simulation - -#### HTMX-Specific Responses: -- Registration with HTMX header (HTML response with script) -- Registration error with HTMX header (HTML error message) - -#### Concurrent Requests: -- Multiple concurrent requests (basic load testing) - -#### JWT Validation: -- Valid JWT format -- No Bearer prefix -- Malformed JWT - -#### Pagination and Filtering: -- Negative limit -- Negative offset -- Very large limit -- Valid pagination parameters - ---- - -### 7. auth_test.go (Existing) -**Tests for Authentication Middleware** - -#### JWT Middleware: -- Missing JWT header -- Invalid JWT format -- Valid JWT format - -#### Library Access Control: -- Library creation without admin (unauthorized) -- Library creation with valid admin -- Library types response -- User visible libraries -- Media items list with filtering -- JSON validation -- Error handling - ---- - -### 8. notes_highlights_test.go (Existing) -**Tests for Media Notes and Highlights** - -#### Notes (`/api/media-items/:id/notes`): -- GET without auth -- POST validation (empty content) -- Valid note creation payload - -#### Highlights (`/api/media-items/:id/highlights`): -- GET without auth -- POST validation (empty selection) -- Valid highlight creation -- Color validation - -#### Backward Compatibility (`/api/ebooks/:id/notes` and `/highlights`): -- GET without auth for both - ---- - -### 9. library_test.go (Existing) -**Tests for Library Features** - -#### Comprehensive Library Tests: -- Auth middleware variations -- Library creation authorization -- Library types response -- User library visibility -- Media items list -- JSON validation scenarios -- Error handling scenarios - ---- - -### 10. setup_test.go, main_test.go, testrunner_test.go (Existing) -**Test Infrastructure** - -- Basic test setup verification -- Test runner verification -- Simple setup tests - ---- - -## Summary of Test Coverage by Component - -### Authentication & Authorization -โœ… Registration (all validation cases) -โœ… Login (authentication failures) -โœ… JWT validation (format, expiration, etc.) -โœ… Role-based access control (admin vs user) -โœ… Profile management -โœ… Password updates -โœ… Account deletion (including last admin protection) - -### User Management -โœ… Email updates (validation, conflicts) -โœ… Username updates (validation, conflicts) -โœ… Theme updates -โœ… Admin-only endpoints -โœ… User list (admin only) -โœ… Scan settings management - -### Library Management -โœ… Create/Read/Update/Delete libraries (admin only) -โœ… Library types -โœ… Library folder management -โœ… Library visibility controls -โœ… Library statistics -โœ… Invalid UUID handling - -### Media/Ebook Management -โœ… List media items (with filtering) -โœ… Create/Update/Delete ebooks (admin only) -โœ… Reading progress (CRUD operations) -โœ… Ratings (validation, CRUD operations) -โœ… Invalid UUID handling -โœ… Non-existent resource handling - -### Notes & Highlights -โœ… Notes CRUD operations -โœ… Highlights CRUD operations -โœ… Content validation -โœ… Color validation -โœ… Backward compatibility with ebook endpoints - -### Scanner Operations -โœ… Scan operations (admin only) -โœ… Start/stop scanner (admin only) -โœ… Invalid folder path handling -โœ… Missing folder path validation - -### Security & Edge Cases -โœ… SQL injection attempts -โœ… XSS attempts -โœ… Rate limiting -โœ… Large payload handling -โœ… Malformed JSON -โœ… Empty request bodies -โœ… Concurrent requests - -### API Behavior -โœ… HTMX-specific responses -โœ… JSON validation -โœ… Pagination (negative, too large, valid) -โœ… Query parameter validation -โœ… Error response formats - ---- - -## Areas for Further Testing - -### Integration Tests (Not Yet Implemented) -- Full user flow: Register โ†’ Login โ†’ Create library โ†’ Scan โ†’ Read -- End-to-end database operations -- File system operations (scanner) - -### Performance Tests (Not Yet Implemented) -- Large dataset handling -- Concurrent user load -- Memory usage under load - -### Database Tests (Not Yet Implemented) -- Database connection failures -- Query timeouts -- Constraint violations -- Transaction rollback scenarios - -### File System Tests (Not Yet Implemented) -- Scanner with real ebook files -- Cover image handling -- File permission errors -- Disk space errors - ---- - -## Running Tests - -### Run all tests: -```bash -go test ./cmd/server/tests/... -``` - -### Run specific test file: -```bash -go test -v ./cmd/server/tests/registration_test.go -``` - -### Run with coverage: -```bash -go test -cover ./cmd/server/tests/... -``` - -### Run specific test case: -```bash -go test -v -run TestRegistration/Invalid_email_format ./cmd/server/tests/... -``` - ---- - -## Notes - -- All tests follow the AAA (Arrange, Act, Assert) pattern -- Tests use httptest for HTTP handler testing -- Mock handlers simulate actual application behavior -- Both positive and negative test cases are covered -- Security scenarios (SQL injection, XSS) are tested -- Role-based access is thoroughly tested -- Input validation is comprehensively covered diff --git a/cmd/server/tests/universal_progress_integration_test.go b/cmd/server/tests/universal_progress_integration_test.go deleted file mode 100644 index ae9b6c9..0000000 --- a/cmd/server/tests/universal_progress_integration_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const baseTestURL = "http://localhost:8765/api" - -// Integration test sequence for Phase 1 Universal Progress -func TestPhase1Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - // Cleanup: Try to delete test user if it exists from previous test runs - t.Run("Cleanup_ExistingTestUser", func(t *testing.T) { - // Try to login as the test user first - loginReq := map[string]interface{}{ - "login": "admin@bookhoard.test", - "password": "TestPassword123!@#", - } - - body, _ := json.Marshal(loginReq) - resp, err := http.Post(baseTestURL+"/auth/login", "application/json", bytes.NewBuffer(body)) - if err != nil { - t.Logf("Cleanup: No existing test user to delete (server not available)") - return - } - defer resp.Body.Close() - - // If login succeeds, try to delete the user - if resp.StatusCode == http.StatusOK { - var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - - if token, ok := result["access_token"].(string); ok && token != "" { - // Delete the user using the token - req, _ := http.NewRequest("DELETE", baseTestURL+"/auth/account", bytes.NewBuffer([]byte{})) - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - delResp, err := client.Do(req) - if err == nil { - defer delResp.Body.Close() - if delResp.StatusCode == http.StatusNoContent { - t.Logf("Cleanup: Deleted existing test user") - } else { - t.Logf("Cleanup: Could not delete existing test user (HTTP %d)", delResp.StatusCode) - } - } - - // Also try to delete any libraries created by this user - req, _ = http.NewRequest("GET", baseTestURL+"/libraries", bytes.NewBuffer([]byte{})) - req.Header.Set("Authorization", "Bearer "+token) - - listResp, err := client.Do(req) - if err == nil { - defer listResp.Body.Close() - if listResp.StatusCode == http.StatusOK { - var libsResult map[string]interface{} - json.NewDecoder(listResp.Body).Decode(&libsResult) - - if data, ok := libsResult["data"].([]interface{}); ok { - for _, lib := range data { - if libMap, ok := lib.(map[string]interface{}); ok { - if libID, ok := libMap["id"].(string); ok { - // Delete the library - req, _ = http.NewRequest("DELETE", baseTestURL+"/libraries/"+libID, bytes.NewBuffer([]byte{})) - req.Header.Set("Authorization", "Bearer "+token) - delLibResp, _ := client.Do(req) - if delLibResp != nil { - delLibResp.Body.Close() - } - } - } - } - } - } - } - } - } - - // Wait a bit for cleanup to complete - time.Sleep(500 * time.Millisecond) - }) - - // Step 1: Create first user (should be admin) - t.Run("Step1_CreateFirstUser", func(t *testing.T) { - userReq := map[string]interface{}{ - "email": "admin@bookhoard.test", - "username": "admin", - "password": "TestPassword123!@#", - "first_name": "Admin", - "last_name": "User", - } - - body, _ := json.Marshal(userReq) - resp, err := http.Post(baseTestURL+"/auth/register", "application/json", bytes.NewBuffer(body)) - assert.NoError(t, err) - defer resp.Body.Close() - - // Accept 201 (Created) or 409 (Conflict if already exists from previous incomplete test run) - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusConflict { - t.Fatalf("Expected 201 or 409, got %d", resp.StatusCode) - } - - var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - - // If we got 409, the user already exists, so we need to login to get the token - if resp.StatusCode == http.StatusConflict { - t.Logf("User already exists, logging in instead...") - loginReq := map[string]interface{}{ - "login": "admin@bookhoard.test", - "password": "TestPassword123!@#", - } - body, _ := json.Marshal(loginReq) - resp2, err := http.Post(baseTestURL+"/auth/login", "application/json", bytes.NewBuffer(body)) - assert.NoError(t, err) - defer resp2.Body.Close() - - assert.Equal(t, http.StatusOK, resp2.StatusCode) - json.NewDecoder(resp2.Body).Decode(&result) - } - - if result["user"] != nil { - user, ok := result["user"].(map[string]interface{}) - assert.True(t, ok, "User field should exist") - assert.Equal(t, "admin", user["username"]) - assert.Equal(t, "admin", user["role"], "First user should be admin") - } - - t.Logf("โœ… Step 1 PASSED: First user created with admin role") - }) - - // Login as admin - var adminToken string - t.Run("LoginAsAdmin", func(t *testing.T) { - loginReq := map[string]interface{}{ - "login": "admin@bookhoard.test", - "password": "TestPassword123!@#", - } - - body, _ := json.Marshal(loginReq) - resp, err := http.Post(baseTestURL+"/auth/login", "application/json", bytes.NewBuffer(body)) - assert.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) - - token, ok := result["access_token"].(string) - assert.True(t, ok, "Should have access_token") - adminToken = token - assert.NotEmpty(t, adminToken) - }) - - // Step 2: Create first library with ebook type - var libraryID string - t.Run("Step2_CreateFirstLibrary", func(t *testing.T) { - libraryReq := map[string]interface{}{ - "name": "Test Library", - "description": "Integration test library", - "type": "ebooks", - } - - body, _ := json.Marshal(libraryReq) - req, _ := http.NewRequest("POST", baseTestURL+"/libraries", bytes.NewBuffer(body)) - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - assert.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusCreated, resp.StatusCode) - - var result map[string]interface{} - err = json.NewDecoder(resp.Body).Decode(&result) - assert.NoError(t, err) - - // Safe extraction of library ID with nil check - if result["id"] == nil { - t.Fatalf("Expected library ID in response, got nil") - } - - var ok bool - libraryID, ok = result["id"].(string) - if !ok { - t.Fatalf("Expected library ID to be string, got %T", result["id"]) - } - - assert.NotEmpty(t, libraryID) - assert.Equal(t, "Test Library", result["name"]) - - t.Logf("โœ… Step 2 PASSED: First library created with ID: %s", libraryID) - }) - - // Step 3: Add /app/uploads folder to the library - t.Run("Step3_AddUploadsFolder", func(t *testing.T) { - folderReq := map[string]interface{}{ - "folder_path": getUploadPath(), - } - - body, _ := json.Marshal(folderReq) - url := fmt.Sprintf("%s/libraries/%s/folders", baseTestURL, libraryID) - req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body)) - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - assert.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, http.StatusCreated, resp.StatusCode) - - var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - - assert.Equal(t, getUploadPath(), result["folder_path"]) - - t.Logf("โœ… Step 3 PASSED: %s folder added to library", getUploadPath()) - }) - - // Step 4: Scan the library - t.Run("Step4_ScanLibrary", func(t *testing.T) { - scanReq := map[string]interface{}{ - "library_id": libraryID, - } - - body, _ := json.Marshal(scanReq) - req, _ := http.NewRequest("POST", baseTestURL+"/scanner/scan", bytes.NewBuffer(body)) - req.Header.Set("Authorization", "Bearer "+adminToken) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - assert.NoError(t, err) - defer resp.Body.Close() - - // Accept 200 or 202 - assert.Contains(t, []int{http.StatusOK, http.StatusAccepted}, resp.StatusCode) - - var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - - assert.Equal(t, "success", result["status"]) - - t.Logf("โœ… Step 4 PASSED: Library scan initiated") - }) - - // Wait for scan to complete - time.Sleep(2 * time.Second) - - // Step 5: List media-items - t.Run("Step5_ListMediaItems", func(t *testing.T) { - url := fmt.Sprintf("%s/libraries/%s/media-items", baseTestURL, libraryID) - req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", "Bearer "+adminToken) - - client := &http.Client{} - resp, err := client.Do(req) - assert.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) - - data, ok := result["data"].([]interface{}) - assert.True(t, ok, "Data field should exist") - assert.True(t, len(data) >= 0, "Should return data array") - - t.Logf("โœ… Step 5 PASSED: Media items listed (count: %d)", len(data)) - }) -} - -// Helper function to read response body -func readBody(resp *http.Response) string { - body, _ := io.ReadAll(resp.Body) - return string(body) -}