//go:build ignore package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "testing" "time" "github.com/stretchr/testify/assert" ) const baseTestURL = "http://localhost:8765/api" // End-to-end integration test for complete application setup flow func TestFullApplicationSetup(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) }