333 lines
12 KiB
Go
333 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// createTestQueueItem creates a complete test fixture: device, library, media item, and sync queue item
|
|
func createTestQueueItem(t *testing.T, db *database.Queries, userID uuid.UUID) (database.SyncQueue, database.Devices) {
|
|
ctx := context.Background()
|
|
|
|
// 1. Create device
|
|
deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
|
|
device, err := db.CreateDevice(ctx, database.CreateDeviceParams{
|
|
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
|
DeviceName: "Test Device",
|
|
DeviceType: "koreader",
|
|
DeviceIdentifier: "test-device-" + uuid.New().String(),
|
|
AuthToken: deviceToken,
|
|
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
|
AutoSync: pgtype.Bool{Bool: true, Valid: true},
|
|
DeviceMetadata: []byte("{}"),
|
|
})
|
|
require.NoError(t, err, "Should create device")
|
|
|
|
// 2. Get library type
|
|
libraryType, err := db.GetLibraryTypeByName(ctx, "ebooks")
|
|
require.NoError(t, err, "Should find library type")
|
|
|
|
// 3. Create library
|
|
library, err := db.CreateLibrary(ctx, database.CreateLibraryParams{
|
|
Name: "Test Queue Library",
|
|
Description: pgtype.Text{String: "Test library", Valid: true},
|
|
LibraryTypeID: libraryType.ID,
|
|
CreatedByAdminID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
|
})
|
|
require.NoError(t, err, "Should create library")
|
|
|
|
// 4. Create media item
|
|
mediaItem, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{
|
|
LibraryID: library.ID,
|
|
Title: "Test Book for Queue",
|
|
Author: pgtype.Text{String: "Test Author", Valid: true},
|
|
FilePath: "/tmp/test.epub",
|
|
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
|
|
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
|
|
})
|
|
require.NoError(t, err, "Should create media item")
|
|
|
|
// 5. Create sync queue item
|
|
queueItem, err := db.CreateSyncQueueItem(ctx, database.CreateSyncQueueItemParams{
|
|
DeviceID: device.ID,
|
|
MediaItemID: mediaItem.ID,
|
|
SyncType: "progress",
|
|
SyncData: []byte(`{"percentage": 50}`),
|
|
Priority: pgtype.Int4{Int32: 5, Valid: true},
|
|
})
|
|
require.NoError(t, err, "Should create sync queue item")
|
|
|
|
return queueItem, device
|
|
}
|
|
|
|
func TestListAllQueueItems_Admin(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
|
|
token := loginAdminUser(t, setup.Server, setup.DB)
|
|
|
|
req := httptest.NewRequest("GET", "/api/queue/items", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code, "Should list all queue items")
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rec.Body.Bytes(), &response)
|
|
assert.NotNil(t, response["items"], "Should have queue items")
|
|
}
|
|
|
|
func TestGetDeviceQueueStats(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
|
|
token := loginTestUser(t, setup.Server, setup.DB)
|
|
|
|
userID := getTestUserID(t, setup.DB)
|
|
|
|
deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
|
|
device, err := setup.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
|
|
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
|
DeviceName: "Test Device",
|
|
DeviceType: "koreader",
|
|
DeviceIdentifier: "test-device-123",
|
|
AuthToken: deviceToken,
|
|
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
|
AutoSync: pgtype.Bool{Bool: true, Valid: true},
|
|
DeviceMetadata: []byte("{}"),
|
|
})
|
|
require.NoError(t, err, "Should create device")
|
|
|
|
deviceIDBytes := device.ID.Bytes[0:16]
|
|
deviceID, err := uuid.FromBytes(deviceIDBytes)
|
|
require.NoError(t, err, "Should parse device ID")
|
|
|
|
req := httptest.NewRequest("GET", fmt.Sprintf("/api/queue/devices/%s/stats", deviceID.String()), nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code, "Should get device queue stats")
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rec.Body.Bytes(), &response)
|
|
assert.Contains(t, response, "total_count", "Should have total_count")
|
|
assert.Contains(t, response, "pending_count", "Should have pending_count")
|
|
assert.Contains(t, response, "processing_count", "Should have processing_count")
|
|
assert.Contains(t, response, "completed_count", "Should have completed_count")
|
|
assert.Contains(t, response, "failed_count", "Should have failed_count")
|
|
}
|
|
|
|
func TestListDeviceQueueItems(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
|
|
token := loginTestUser(t, setup.Server, setup.DB)
|
|
|
|
userID := getTestUserID(t, setup.DB)
|
|
|
|
deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
|
|
device, err := setup.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
|
|
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
|
DeviceName: "Test Device",
|
|
DeviceType: "koreader",
|
|
DeviceIdentifier: "test-device-123",
|
|
AuthToken: deviceToken,
|
|
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
|
AutoSync: pgtype.Bool{Bool: true, Valid: true},
|
|
DeviceMetadata: []byte("{}"),
|
|
})
|
|
require.NoError(t, err, "Should create device")
|
|
|
|
deviceIDBytes := device.ID.Bytes[0:16]
|
|
deviceID, err := uuid.FromBytes(deviceIDBytes)
|
|
require.NoError(t, err, "Should parse device ID")
|
|
|
|
req := httptest.NewRequest("GET", fmt.Sprintf("/api/queue/devices/%s/items", deviceID.String()), nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code, "Should list device queue items")
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rec.Body.Bytes(), &response)
|
|
items, ok := response["items"].([]interface{})
|
|
assert.True(t, ok, "Should have items array")
|
|
assert.NotNil(t, items, "Should have queue items")
|
|
}
|
|
|
|
func TestRetryQueueItem(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
token := loginTestUser(t, setup.Server, setup.DB)
|
|
|
|
// Get the current test user's ID from database
|
|
user, err := setup.DB.GetUserByEmail(context.Background(), "testuser@example.com")
|
|
require.NoError(t, err, "Should get test user")
|
|
userID, err := uuid.FromBytes(user.ID.Bytes[:])
|
|
require.NoError(t, err, "Should parse user UUID")
|
|
|
|
queueItem, _ := createTestQueueItem(t, setup.DB, userID)
|
|
itemID := uuid.UUID(queueItem.ID.Bytes).String()
|
|
|
|
req := httptest.NewRequest("POST", fmt.Sprintf("/api/queue/items/%s/retry", itemID), nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code, "Should retry queue item")
|
|
}
|
|
|
|
func TestDeleteQueueItem(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
token := loginTestUser(t, setup.Server, setup.DB)
|
|
|
|
// Get the current test user's ID from database
|
|
user, err := setup.DB.GetUserByEmail(context.Background(), "testuser@example.com")
|
|
require.NoError(t, err, "Should get test user")
|
|
userID, err := uuid.FromBytes(user.ID.Bytes[:])
|
|
require.NoError(t, err, "Should parse user UUID")
|
|
|
|
queueItem, _ := createTestQueueItem(t, setup.DB, userID)
|
|
itemID := uuid.UUID(queueItem.ID.Bytes).String()
|
|
|
|
req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/queue/items/%s", itemID), nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete queue item")
|
|
}
|
|
|
|
func TestClearDeviceQueue(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
|
|
token := loginTestUser(t, setup.Server, setup.DB)
|
|
|
|
userID := getTestUserID(t, setup.DB)
|
|
|
|
deviceToken := fmt.Sprintf("dev_%s", uuid.New().String())
|
|
device, err := setup.DB.CreateDevice(context.Background(), database.CreateDeviceParams{
|
|
UserID: pgtype.UUID{Bytes: [16]byte(userID), Valid: true},
|
|
DeviceName: "Test Device",
|
|
DeviceType: "koreader",
|
|
DeviceIdentifier: "test-device-123",
|
|
AuthToken: deviceToken,
|
|
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
|
AutoSync: pgtype.Bool{Bool: true, Valid: true},
|
|
DeviceMetadata: []byte("{}"),
|
|
})
|
|
require.NoError(t, err, "Should create device")
|
|
|
|
deviceIDBytes := device.ID.Bytes[0:16]
|
|
deviceID, err := uuid.FromBytes(deviceIDBytes)
|
|
require.NoError(t, err, "Should parse device ID")
|
|
|
|
req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/queue/devices/%s/clear", deviceID.String()), nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, http.StatusOK, rec.Code, "Should clear device queue")
|
|
}
|
|
|
|
func TestQueueEndpoints_Unauthorized(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
endpoint string
|
|
body []byte
|
|
expectedStatus int
|
|
}{
|
|
{"ListAllQueueItems", "GET", "/api/queue/items", nil, http.StatusUnauthorized},
|
|
{"GetDeviceQueueStats", "GET", "/api/queue/devices/test-device-id/stats", nil, http.StatusUnauthorized},
|
|
{"ListDeviceQueueItems", "GET", "/api/queue/devices/test-device-id/items", nil, http.StatusUnauthorized},
|
|
{"RetryQueueItem", "POST", "/api/queue/items/test-item-id/retry", nil, http.StatusUnauthorized},
|
|
{"DeleteQueueItem", "DELETE", "/api/queue/items/test-item-id", nil, http.StatusUnauthorized},
|
|
{"ClearDeviceQueue", "DELETE", "/api/queue/devices/test-device-id/clear", nil, http.StatusUnauthorized},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tt.method, tt.endpoint, bytes.NewReader(tt.body))
|
|
rec := httptest.NewRecorder()
|
|
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, rec.Code, "Should require authentication")
|
|
})
|
|
}
|
|
}
|
|
|
|
func loginAdminUser(t *testing.T, ts *httptest.Server, db *database.Queries) string {
|
|
ctx := context.Background()
|
|
|
|
// Check if admin user exists and delete them first to ensure fresh state
|
|
// (using database delete directly to bypass "last admin" check)
|
|
user, err := db.GetUserByEmail(ctx, "admin@example.com")
|
|
if err == nil {
|
|
err = db.DeleteUser(ctx, user.ID)
|
|
if err != nil {
|
|
t.Logf("Warning: Could not delete existing admin user: %v", err)
|
|
}
|
|
}
|
|
|
|
// Create a fresh admin user with known password
|
|
// Password: "Test@Pass123!" meets complexity requirements
|
|
// This is the bcrypt hash for "Test@Pass123!"
|
|
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
|
|
|
adminUser, err := db.CreateUser(ctx, database.CreateUserParams{
|
|
Email: "admin@example.com",
|
|
Username: "admin",
|
|
PasswordHash: passwordHash,
|
|
FirstName: pgtype.Text{String: "Admin", Valid: true},
|
|
LastName: pgtype.Text{String: "User", Valid: true},
|
|
Role: "admin",
|
|
Theme: pgtype.Text{String: "tokyo-night", Valid: true},
|
|
})
|
|
require.NoError(t, err, "Failed to create admin user")
|
|
|
|
userUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:])
|
|
require.NoError(t, err, "Should parse admin user UUID")
|
|
|
|
return loginUserWithID(t, ts, db, userUUID, "admin@example.com", "Test@Pass123!")
|
|
}
|
|
|
|
func loginUserWithID(t *testing.T, ts *httptest.Server, db *database.Queries, userID uuid.UUID, email, password string) string {
|
|
loginRequest := map[string]interface{}{
|
|
"login": email,
|
|
"password": password,
|
|
}
|
|
body, _ := json.Marshal(loginRequest)
|
|
|
|
req, _ := http.NewRequest("POST", ts.URL+"/api/auth/login", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err, "Failed to login")
|
|
defer resp.Body.Close()
|
|
|
|
require.Equal(t, http.StatusOK, resp.StatusCode, "Login should succeed")
|
|
|
|
var result map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&result)
|
|
|
|
token, ok := result["access_token"].(string)
|
|
require.True(t, ok, "Should have access_token")
|
|
require.NotEmpty(t, token, "Access token should not be empty")
|
|
|
|
return token
|
|
}
|