Files
bookhoard/cmd/server/tests/universal_progress_integration_test.go
T
john-okeefe 4bdd08602c test: rename test files to better reflect their purpose
- Rename phase1_integration_test.go to universal_progress_integration_test.go
  (tests universal reading progress feature)
- Rename ebook_scanner_phase2_test.go to ebook_scanner_hash_test.go
  (tests hash calculation and file identification utilities)

These renames make the test suite more maintainable and self-documenting.
2026-02-07 17:06:45 -05:00

297 lines
8.9 KiB
Go

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": "/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) {
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)
}