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:
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user