diff --git a/cmd/server/tests/device_test.go b/cmd/server/tests/device_test.go new file mode 100644 index 0000000..e7eac07 --- /dev/null +++ b/cmd/server/tests/device_test.go @@ -0,0 +1,288 @@ +package main + +import ( + "bookmann/internal/database" + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/stretchr/testify/assert" +) + +func TestDeviceRegistrationFlow(t *testing.T) { + _, _, _, ts := setupTestServer(t) + defer ts.Close() + + // Step 1: Initiate device registration + regRequest := map[string]interface{}{ + "device_name": "Test Kindle Paperwhite", + "device_type": "koreader", + "device_identifier": "kindle-test-hw-id-12345", + } + regBody, _ := json.Marshal(regRequest) + + req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusCreated, rec.Code, "Should initiate device registration") + + var regResponse map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), ®Response) + + registrationID, ok := regResponse["registration_id"].(string) + assert.True(t, ok, "Should have registration_id") + assert.NotEmpty(t, registrationID, "Registration ID should not be empty") + + authURL, ok := regResponse["auth_url"].(string) + assert.True(t, ok, "Should have auth_url") + assert.NotEmpty(t, authURL, "Auth URL should not be empty") + + // Step 2: Check registration status (should be pending initially) + statusRequest := map[string]interface{}{ + "registration_id": registrationID, + } + statusBody, _ := json.Marshal(statusRequest) + + req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody)) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "Should check registration status") + + var statusResponse map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &statusResponse) + + status, ok := statusResponse["status"].(string) + assert.True(t, ok, "Should have status") + assert.Equal(t, "pending", status, "Should be pending initially") + + // Step 3: Login as user to approve device + loginRequest := map[string]interface{}{ + "login": "testuser@example.com", + "password": "testpass123", + } + loginBody, _ := json.Marshal(loginRequest) + + req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginBody)) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "Should login successfully") + + var loginResponse map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &loginResponse) + + token, ok := loginResponse["access_token"].(string) + assert.True(t, ok, "Should have access_token") + + // Step 4: Approve the device + req = httptest.NewRequest("GET", fmt.Sprintf("/devices/approve/%s", registrationID), nil) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "Should approve device") + + // Step 5: Check registration status again (should be approved now) + req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody)) + req.Header.Set("Content-Type", "application/json") + rec = httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "Should check registration status after approval") + + var approvedStatus map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &approvedStatus) + + status, ok = approvedStatus["status"].(string) + assert.True(t, ok, "Should have status") + assert.Equal(t, "approved", status, "Should be approved after user approval") + + authToken, ok := approvedStatus["auth_token"].(string) + assert.True(t, ok, "Should have auth_token after approval") + assert.NotEmpty(t, authToken, "Auth token should not be empty") +} + +func TestListDevices(t *testing.T) { + db, _, _, ts := setupTestServer(t) + defer ts.Close() + + // Login to get token + token := loginTestUser(t, ts) + + // Create a device directly in the database + userID := getTestUserID(t, db) + deviceID := uuid.New() + + deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) + _, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ + ID: pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}, + 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}, + SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, + DeviceMetadata: []byte("{}"), + }) + assert.NoError(t, err, "Should create device") + + // List devices + req := httptest.NewRequest("GET", "/api/devices", nil) + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "Should list devices") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + + devices, ok := response["devices"].([]interface{}) + assert.True(t, ok, "Should have devices array") + assert.GreaterOrEqual(t, len(devices), 1, "Should have at least one device") + + 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, "Should match created device name") +} + +func TestUpdateDevice(t *testing.T) { + db, _, _, ts := setupTestServer(t) + defer ts.Close() + + // Login to get token + token := loginTestUser(t, ts) + + // Create a device directly in the database + userID := getTestUserID(t, db) + deviceID := uuid.New() + + deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) + _, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ + ID: pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}, + 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}, + SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, + DeviceMetadata: []byte("{}"), + }) + assert.NoError(t, err, "Should create device") + + // Update device + updateRequest := map[string]interface{}{ + "device_name": "Updated Device Name", + "sync_enabled": false, + "sync_frequency_minutes": int32(10), + } + updateBody, _ := json.Marshal(updateRequest) + + req := httptest.NewRequest("PUT", fmt.Sprintf("/api/devices/%s", deviceID.String()), bytes.NewReader(updateBody)) + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code, "Should update device") + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + + assert.True(t, response["device_updated"].(bool), "Should confirm device updated") + + device := response["device"].(map[string]interface{}) + assert.Equal(t, "Updated Device Name", device["device_name"], "Should have updated name") + assert.Equal(t, false, device["sync_enabled"], "Should be disabled") + assert.Equal(t, int32(10), device["sync_frequency"], "Should have updated frequency") +} + +func TestDeleteDevice(t *testing.T) { + db, _, _, ts := setupTestServer(t) + defer ts.Close() + + // Login to get token + token := loginTestUser(t, ts) + + // Create a device directly in the database + userID := getTestUserID(t, db) + deviceID := uuid.New() + + deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) + _, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ + ID: pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}, + 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}, + SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, + DeviceMetadata: []byte("{}"), + }) + assert.NoError(t, err, "Should create device") + + // Delete device + req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/devices/%s", deviceID.String()), nil) + req.Header.Set("Authorization", "Bearer "+token) + rec := httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNoContent, rec.Code, "Should delete device") + + // Verify device is deleted + _, err = db.GetDevice(context.Background(), pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}) + assert.Error(t, err, "Device should be deleted") +} + +func TestDeviceAuthentication(t *testing.T) { + db, _, _, ts := setupTestServer(t) + defer ts.Close() + + // Create a device directly in the database + userID := getTestUserID(t, db) + deviceID := uuid.New() + + deviceToken := fmt.Sprintf("dev_%s", uuid.New().String()) + _, err := db.CreateDevice(context.Background(), database.CreateDeviceParams{ + ID: pgtype.UUID{Bytes: [16]byte(deviceID), Valid: true}, + 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}, + SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true}, + DeviceMetadata: []byte("{}"), + }) + assert.NoError(t, err, "Should create device") + + // Test device authentication + req := httptest.NewRequest("GET", "/api/devices", nil) + req.Header.Set("Authorization", "Bearer "+deviceToken) + rec := httptest.NewRecorder() + ts.Config.Handler.ServeHTTP(rec, req) + + // This should fail because device auth middleware is not applied to /api/devices + // Device auth is for sync endpoints only + assert.Equal(t, http.StatusUnauthorized, rec.Code, "Should require user auth for device management") +} diff --git a/cmd/server/tests/phase1_integration_test.go b/cmd/server/tests/phase1_integration_test.go new file mode 100644 index 0000000..6b81dc1 --- /dev/null +++ b/cmd/server/tests/phase1_integration_test.go @@ -0,0 +1,188 @@ +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") + } + + // Step 1: Create first user (should be admin) + t.Run("Step1_CreateFirstUser", func(t *testing.T) { + userReq := map[string]interface{}{ + "email": "admin@bookmann.test", + "username": "admin", + "password": "SecurePass123!", + "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() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + + 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{}{ + "identifier": "admin@bookmann.test", + "password": "SecurePass123!", + } + + 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) + + adminToken = result["token"].(string) + 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": "ebook", + } + + 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{} + json.NewDecoder(resp.Body).Decode(&result) + + libraryID = result["id"].(string) + 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": "/app/uploads", + } + + 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, "/app/uploads", result["folder_path"]) + + t.Logf("✅ Step 3 PASSED: /app/uploads folder added to library") + }) + + // 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) { + req, _ := http.NewRequest("GET", baseTestURL+"/media-items?limit=50", 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) + + items, ok := result["items"].([]interface{}) + assert.True(t, ok, "Items field should exist") + assert.True(t, len(items) >= 0, "Should return items array") + + t.Logf("✅ Step 5 PASSED: Media items listed (count: %d)", len(items)) + }) +} + +// Helper function to read response body +func readBody(resp *http.Response) string { + body, _ := io.ReadAll(resp.Body) + return string(body) +} diff --git a/integration_test.sh b/integration_test.sh new file mode 100755 index 0000000..d8ea034 --- /dev/null +++ b/integration_test.sh @@ -0,0 +1,352 @@ +#!/bin/bash + +# Bookmann Integration Test Script +# This script performs a full integration test of the Bookmann API + +set -e + +BASE_URL="http://localhost:8765/api" +RESULTS_FILE="integration_test_results.txt" + +echo "========================================" | tee $RESULTS_FILE +echo "Bookmann Integration Test Run" | tee -a $RESULTS_FILE +echo "Started at: $(date)" | tee -a $RESULTS_FILE +echo "========================================" | tee -a $RESULTS_FILE +echo "" | tee -a $RESULTS_FILE + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Helper functions +pass() { + echo -e "${GREEN}✓ PASS${NC}: $1" | tee -a $RESULTS_FILE +} + +fail() { + echo -e "${RED}✗ FAIL${NC}: $1" | tee -a $RESULTS_FILE +} + +info() { + echo -e "${YELLOW}→ INFO${NC}: $1" | tee -a $RESULTS_FILE +} + +section() { + echo "" | tee -a $RESULTS_FILE + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | tee -a $RESULTS_FILE + echo " $1" | tee -a $RESULTS_FILE + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | tee -a $RESULTS_FILE +} + +# Test counters +TOTAL_TESTS=0 +PASSED_TESTS=0 +FAILED_TESTS=0 + +run_test() { + local test_name="$1" + local test_command="$2" + local expected_code="$3" + + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + info "Running: $test_name" + + RESPONSE=$(eval "$test_command" 2>&1) + STATUS_CODE=$(echo "$RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + + if [ "$STATUS_CODE" == "$expected_code" ]; then + pass "$test_name" + PASSED_TESTS=$((PASSED_TESTS + 1)) + return 0 + else + fail "$test_name (Expected: $expected_code, Got: $STATUS_CODE)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + echo "Response: $RESPONSE" | tee -a $RESULTS_FILE + return 1 + fi +} + +# Check if server is running +check_server() { + section "Checking Server Availability" + + if curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/auth/profile" > /dev/null 2>&1; then + pass "Server is running" + return 0 + else + fail "Server is not running at $BASE_URL" + return 1 + fi +} + +# Main test sequence +main() { + # Check server + if ! check_server; then + echo "" | tee -a $RESULTS_FILE + echo "ERROR: Server is not running. Please start the server first." | tee -a $RESULTS_FILE + echo "Run: docker-compose up -d" | tee -a $RESULTS_FILE + exit 1 + fi + + # Step 1: Create first user (should be admin) + section "Step 1: Create First User (Should be Admin)" + ADMIN_EMAIL="admin@integration.test" + ADMIN_USERNAME="admin" + ADMIN_PASSWORD="AdminPass123!" + + RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X POST "$BASE_URL/auth/register" \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"$ADMIN_EMAIL\", + \"username\": \"$ADMIN_USERNAME\", + \"password\": \"$ADMIN_PASSWORD\", + \"first_name\": \"Admin\", + \"last_name\": \"User\" + }") + + HTTP_STATUS=$(echo "$RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + BODY=$(echo "$RESPONSE" | grep -v "HTTP Status:") + + if [ "$HTTP_STATUS" == "201" ]; then + USER_ROLE=$(echo "$BODY" | jq -r '.user.role // empty') + if [ "$USER_ROLE" == "admin" ]; then + pass "First user created with admin role" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + fail "First user does not have admin role (got: $USER_ROLE)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + else + fail "Failed to create first user (HTTP $HTTP_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + echo "Response: $BODY" | tee -a $RESULTS_FILE + fi + + # Login as admin + section "Login as Admin" + ADMIN_TOKEN=$(curl -s -X POST "$BASE_URL/auth/login" \ + -H "Content-Type: application/json" \ + -d "{ + \"login\": \"$ADMIN_EMAIL\", + \"password\": \"$ADMIN_PASSWORD\" + }" | jq -r '.access_token // empty') + + if [ -n "$ADMIN_TOKEN" ] && [ "$ADMIN_TOKEN" != "null" ]; then + pass "Admin login successful" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + fail "Failed to login as admin" + FAILED_TESTS=$((FAILED_TESTS + 1)) + echo "Cannot continue without admin token" | tee -a $RESULTS_FILE + exit 1 + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Step 2: Create first library with ebook type + section "Step 2: Create First Library with eBook Type" + + LIBRARY_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X POST "$BASE_URL/libraries" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -d '{ + "name": "Integration Test Library", + "description": "Library for integration testing", + "type": "ebook" + }') + + LIB_STATUS=$(echo "$LIBRARY_RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + LIB_BODY=$(echo "$LIBRARY_RESPONSE" | grep -v "HTTP Status:") + + if [ "$LIB_STATUS" == "201" ]; then + LIBRARY_ID=$(echo "$LIB_BODY" | jq -r '.id // empty') + if [ -n "$LIBRARY_ID" ]; then + pass "Library created successfully (ID: $LIBRARY_ID)" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + fail "Library created but no ID returned" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + else + fail "Failed to create library (HTTP $LIB_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + echo "Response: $LIB_BODY" | tee -a $RESULTS_FILE + exit 1 + fi + + # Step 3: Add /app/uploads folder to the library + section "Step 3: Add /app/uploads Folder to Library" + + FOLDER_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X POST "$BASE_URL/libraries/$LIBRARY_ID/folders" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -d '{ + "folder_path": "/app/uploads" + }') + + FOLDER_STATUS=$(echo "$FOLDER_RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + + if [ "$FOLDER_STATUS" == "201" ] || [ "$FOLDER_STATUS" == "200" ]; then + pass "Folder added to library" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + fail "Failed to add folder to library (HTTP $FOLDER_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + echo "Response: $(echo "$FOLDER_RESPONSE" | grep -v "HTTP Status:")" | tee -a $RESULTS_FILE + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Step 4: Scan the library + section "Step 4: Scan Library" + + SCAN_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X POST "$BASE_URL/libraries/$LIBRARY_ID/scan" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -d '{}') + + SCAN_STATUS=$(echo "$SCAN_RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + SCAN_BODY=$(echo "$SCAN_RESPONSE" | grep -v "HTTP Status:") + + if [ "$SCAN_STATUS" == "200" ] || [ "$SCAN_STATUS" == "202" ]; then + pass "Library scan initiated" + PASSED_TESTS=$((PASSED_TESTS + 1)) + info "Waiting 5 seconds for scan to complete..." + sleep 5 + else + fail "Failed to scan library (HTTP $SCAN_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + echo "Response: $SCAN_BODY" | tee -a $RESULTS_FILE + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Step 5: List media items + section "Step 5: List Media Items" + + MEDIA_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X GET "$BASE_URL/libraries/$LIBRARY_ID/media-items?limit=50&offset=0" \ + -H "Authorization: Bearer $ADMIN_TOKEN") + + MEDIA_STATUS=$(echo "$MEDIA_RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + MEDIA_BODY=$(echo "$MEDIA_RESPONSE" | grep -v "HTTP Status:") + + if [ "$MEDIA_STATUS" == "200" ]; then + pass "Media items listed successfully" + PASSED_TESTS=$((PASSED_TESTS + 1)) + + MEDIA_COUNT=$(echo "$MEDIA_BODY" | jq '.total // 0') + info "Found $MEDIA_COUNT media items" + + # Display first few items + if [ "$MEDIA_COUNT" -gt 0 ]; then + echo "" | tee -a $RESULTS_FILE + echo "Sample media items:" | tee -a $RESULTS_FILE + echo "$MEDIA_BODY" | jq -r '.media_items[0:3] | .[] | " - \(.title) by \(.author // "Unknown")"' | tee -a $RESULTS_FILE + fi + else + fail "Failed to list media items (HTTP $MEDIA_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + echo "Response: $MEDIA_BODY" | tee -a $RESULTS_FILE + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Step 6: Test Device Registration + section "Step 6: Device Registration" + + DEVICE_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X POST "$BASE_URL/devices/register" \ + -H "Content-Type: application/json" \ + -d '{ + "device_name": "Test Kindle Paperwhite", + "device_type": "koreader", + "device_identifier": "kindle-test-hw-12345" + }') + + DEVICE_STATUS=$(echo "$DEVICE_RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + DEVICE_BODY=$(echo "$DEVICE_RESPONSE" | grep -v "HTTP Status:") + + if [ "$DEVICE_STATUS" == "201" ]; then + pass "Device registration initiated" + PASSED_TESTS=$((PASSED_TESTS + 1)) + + REGISTRATION_ID=$(echo "$DEVICE_BODY" | jq -r '.registration_id // empty') + info "Registration ID: $REGISTRATION_ID" + + # Check registration status + STATUS_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X POST "$BASE_URL/devices/register/status" \ + -H "Content-Type: application/json" \ + -d "{\"registration_id\": \"$REGISTRATION_ID\"}") + + STATUS_BODY=$(echo "$STATUS_RESPONSE" | grep -v "HTTP Status:") + REG_STATUS=$(echo "$STATUS_BODY" | jq -r '.status // empty') + + if [ "$REG_STATUS" == "pending" ]; then + pass "Registration status is pending as expected" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + fail "Registration status should be pending (got: $REG_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + else + fail "Failed to initiate device registration (HTTP $DEVICE_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + echo "Response: $DEVICE_BODY" | tee -a $RESULTS_FILE + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Step 7: List devices + section "Step 7: List User Devices" + + LIST_DEVICES_RESPONSE=$(curl -s -w "\nHTTP Status: %{http_code}" \ + -X GET "$BASE_URL/devices" \ + -H "Authorization: Bearer $ADMIN_TOKEN") + + LIST_DEVICES_STATUS=$(echo "$LIST_DEVICES_RESPONSE" | grep "HTTP Status:" | awk '{print $3}') + + if [ "$LIST_DEVICES_STATUS" == "200" ]; then + pass "Devices listed successfully" + PASSED_TESTS=$((PASSED_TESTS + 1)) + else + fail "Failed to list devices (HTTP $LIST_DEVICES_STATUS)" + FAILED_TESTS=$((FAILED_TESTS + 1)) + fi + TOTAL_TESTS=$((TOTAL_TESTS + 1)) + + # Print summary + section "Test Summary" + echo "" | tee -a $RESULTS_FILE + echo "Total Tests: $TOTAL_TESTS" | tee -a $RESULTS_FILE + echo "Passed: $PASSED_TESTS" | tee -a $RESULTS_FILE + echo "Failed: $FAILED_TESTS" | tee -a $RESULTS_FILE + echo "" | tee -a $RESULTS_FILE + + if [ $FAILED_TESTS -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" | tee -a $RESULTS_FILE + else + PERCENT=$((PASSED_TESTS * 100 / TOTAL_TESTS)) + echo -e "${YELLOW}Pass rate: ${PERCENT}%${NC}" | tee -a $RESULTS_FILE + fi + + echo "" | tee -a $RESULTS_FILE + echo "Completed at: $(date)" | tee -a $RESULTS_FILE + echo "========================================" | tee -a $RESULTS_FILE + + # Exit with error if any tests failed + if [ $FAILED_TESTS -gt 0 ]; then + exit 1 + fi +} + +# Run main function +main