chore: remove obsolete planning documents and session logs

Remove temporary planning documents that are no longer needed:
- IMPLEMENTATION_EXACT.md
- IMPLEMENTATION_PLAN.md
- TEST_RELIABILITY_PLAN.md
- baseline-results.txt
- cmd/server/tests/TEST_CLEANUP_PATTERN.md
- cmd/server/tests/TEST_COVERAGE.md
- cmd/server/tests/universal_progress_integration_test.go

These were internal planning documents and temporary test files that have
served their purpose and are now being cleaned up from the repository.
This commit is contained in:
2026-02-13 21:49:37 -05:00
parent 0ac1c58bc7
commit ae68cbf5dc
7 changed files with 0 additions and 11131 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-3016
View File
File diff suppressed because it is too large Load Diff
-283
View File
@@ -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
-393
View File
@@ -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
@@ -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)
}