- Add /bookshelf route as default page for logged-in users - Update login and register handlers to redirect to /bookshelf - Update homepage to auto-redirect to /bookshelf when logged in - Preserve /dashboard route for backward compatibility - Update test redirects to use /bookshelf Changes: - main.go: Add /bookshelf protected route - auth.go: Change login/register redirects from /api/dashboard to /bookshelf (2 locations) - edge_cases_test.go: Update test redirect to /bookshelf - Maintains backward compatibility with existing /dashboard route This makes the beautiful bookshelf the default landing page for all authenticated users while keeping the old dashboard accessible.
664 lines
20 KiB
Go
664 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestScannerEndpoints tests ebook scanner operations
|
|
func TestScannerEndpoints(t *testing.T) {
|
|
t.Run("POST /api/scanner/scan - Scan without admin role", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"folder_paths": []string{"/path/to/ebooks"},
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/scanner/scan", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer user-token")
|
|
req.Header.Set("X-User-Role", "user")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
userRole := r.Header.Get("X-User-Role")
|
|
if userRole != "admin" {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":"admin access required"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
})
|
|
|
|
t.Run("POST /api/scanner/watch/start - Start watch mode", func(t *testing.T) {
|
|
libraryID := uuid.New().String()
|
|
|
|
payload := map[string]interface{}{
|
|
"library_id": libraryID,
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/scanner/watch/start", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
response := map[string]interface{}{
|
|
"message": "watch mode started for library",
|
|
"library_id": libraryID,
|
|
}
|
|
json.NewEncoder(w).Encode(response)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rr.Body.Bytes(), &response)
|
|
assert.Equal(t, "watch mode started for library", response["message"])
|
|
assert.Equal(t, libraryID, response["library_id"])
|
|
})
|
|
|
|
t.Run("POST /api/scanner/watch/stop - Stop watch mode", func(t *testing.T) {
|
|
libraryID := uuid.New().String()
|
|
|
|
payload := map[string]interface{}{
|
|
"library_id": libraryID,
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/scanner/watch/stop", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
response := map[string]interface{}{
|
|
"message": "watch mode stopped for library",
|
|
"library_id": libraryID,
|
|
}
|
|
json.NewEncoder(w).Encode(response)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rr.Body.Bytes(), &response)
|
|
assert.Equal(t, "watch mode stopped for library", response["message"])
|
|
assert.Equal(t, libraryID, response["library_id"])
|
|
})
|
|
|
|
t.Run("GET /api/scanner/watch/status - Get watch mode status", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/scanner/watch/status", nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
response := map[string]interface{}{
|
|
"watching_libraries": []string{
|
|
uuid.New().String(),
|
|
uuid.New().String(),
|
|
},
|
|
"total_watching": 2,
|
|
}
|
|
json.NewEncoder(w).Encode(response)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rr.Body.Bytes(), &response)
|
|
assert.Equal(t, float64(2), response["total_watching"])
|
|
assert.NotEmpty(t, response["watching_libraries"])
|
|
})
|
|
|
|
t.Run("GET /api/scanner/status/:jobId - Get job status", func(t *testing.T) {
|
|
jobID := uuid.New().String()
|
|
|
|
req := httptest.NewRequest("GET", "/api/scanner/status/"+jobID, nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
response := map[string]interface{}{
|
|
"job_id": jobID,
|
|
"status": "completed",
|
|
"error": "",
|
|
"result": map[string]string{"message": "scan completed"},
|
|
"progress": 1.0,
|
|
}
|
|
json.NewEncoder(w).Encode(response)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rr.Body.Bytes(), &response)
|
|
assert.Equal(t, jobID, response["job_id"])
|
|
assert.Equal(t, "completed", response["status"])
|
|
})
|
|
|
|
t.Run("GET /api/scanner/status/:jobId - Job not found", func(t *testing.T) {
|
|
jobID := uuid.New().String()
|
|
|
|
req := httptest.NewRequest("GET", "/api/scanner/status/"+jobID, nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte(`{"error":"job not found"}`))
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusNotFound, rr.Code)
|
|
})
|
|
|
|
t.Run("POST /api/scanner/scan - Successful scan (background job)", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"folder_paths": []string{"/valid/path"},
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/scanner/scan", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusAccepted)
|
|
response := map[string]interface{}{
|
|
"message": "scan job enqueued",
|
|
"job_id": uuid.New().String(),
|
|
"status": "pending",
|
|
}
|
|
json.NewEncoder(w).Encode(response)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusAccepted, rr.Code)
|
|
|
|
var response map[string]interface{}
|
|
json.Unmarshal(rr.Body.Bytes(), &response)
|
|
assert.Equal(t, "scan job enqueued", response["message"])
|
|
assert.NotEmpty(t, response["job_id"])
|
|
assert.Equal(t, "pending", response["status"])
|
|
})
|
|
|
|
t.Run("POST /api/scanner/start - Start scanner without admin role", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/scanner/start", nil)
|
|
req.Header.Set("Authorization", "Bearer user-token")
|
|
req.Header.Set("X-User-Role", "user")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
userRole := r.Header.Get("X-User-Role")
|
|
if userRole != "admin" {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":"admin access required"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
})
|
|
|
|
t.Run("POST /api/scanner/start - Start scanner successfully", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/scanner/start", nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"message":"scanner started"}`))
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
})
|
|
|
|
t.Run("POST /api/scanner/stop - Stop scanner without admin role", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/scanner/stop", nil)
|
|
req.Header.Set("Authorization", "Bearer user-token")
|
|
req.Header.Set("X-User-Role", "user")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
userRole := r.Header.Get("X-User-Role")
|
|
if userRole != "admin" {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"error":"admin access required"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusForbidden, rr.Code)
|
|
})
|
|
|
|
t.Run("POST /api/scanner/stop - Stop scanner successfully", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/scanner/stop", nil)
|
|
req.Header.Set("Authorization", "Bearer admin-token")
|
|
req.Header.Set("X-User-Role", "admin")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"message":"scanner stopped"}`))
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
})
|
|
}
|
|
|
|
// TestEdgeCases tests various edge cases and boundary conditions
|
|
func TestEdgeCases(t *testing.T) {
|
|
t.Run("Empty request body", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer([]byte("")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid request"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("Malformed JSON", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer([]byte("{invalid json}")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid request"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("Very large payload", func(t *testing.T) {
|
|
largeString := string(make([]byte, 100000))
|
|
payload := map[string]interface{}{
|
|
"email": largeString + "@example.com",
|
|
"username": "user",
|
|
"password": "password123",
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"email too long"}`))
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("SQL Injection attempt", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"email": "test@example.com'; DROP TABLE users; --",
|
|
"username": "sqlinjection",
|
|
"password": "password123",
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid request"}`))
|
|
return
|
|
}
|
|
|
|
email := req["email"].(string)
|
|
if len(email) > 255 {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"email too long"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
// Should either succeed (if sanitized) or fail with validation error
|
|
assert.True(t, rr.Code == http.StatusCreated || rr.Code == http.StatusBadRequest)
|
|
})
|
|
|
|
t.Run("XSS attempt in fields", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"email": "test@example.com",
|
|
"username": "<script>alert('xss')</script>",
|
|
"password": "password123",
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req map[string]interface{}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid request"}`))
|
|
return
|
|
}
|
|
|
|
username := req["username"].(string)
|
|
if len(username) > 50 {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"username too long"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusCreated, rr.Code)
|
|
})
|
|
|
|
t.Run("Rate limiting simulation", func(t *testing.T) {
|
|
requestCount := 0
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requestCount++
|
|
if requestCount > 10 {
|
|
w.WriteHeader(http.StatusTooManyRequests)
|
|
w.Write([]byte(`{"error":"too many requests"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
for i := 0; i < 15; i++ {
|
|
req := httptest.NewRequest("GET", "/api/libraries/types", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if i >= 10 {
|
|
assert.Equal(t, http.StatusTooManyRequests, rr.Code)
|
|
} else {
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestHTMXRequests tests HTMX-specific responses
|
|
func TestHTMXRequests(t *testing.T) {
|
|
t.Run("Registration with HTMX header", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"email": "htmx@example.com",
|
|
"username": "htmxuser",
|
|
"password": "password123",
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("HX-Request", "true")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
isHTMX := r.Header.Get("HX-Request") == "true"
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
if isHTMX {
|
|
w.Write([]byte(`<div class="text-green-500">Registration successful! Redirecting...</div>
|
|
<script>
|
|
localStorage.setItem('token', 'fake-token');
|
|
window.location.href = '/bookshelf';
|
|
</script>`))
|
|
} else {
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"token": "fake-token",
|
|
"user": map[string]interface{}{
|
|
"id": uuid.New().String(),
|
|
"email": "htmx@example.com",
|
|
"username": "htmxuser",
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusCreated, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "<script>")
|
|
})
|
|
|
|
t.Run("Registration error with HTMX header", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"email": "existing@example.com",
|
|
"username": "newuser",
|
|
"password": "password123",
|
|
}
|
|
jsonData, _ := json.Marshal(payload)
|
|
|
|
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewBuffer(jsonData))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("HX-Request", "true")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusConflict)
|
|
w.Write([]byte(`<div class="text-red-500">Email already exists</div>`))
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusConflict, rr.Code)
|
|
assert.Contains(t, rr.Body.String(), "text-red-500")
|
|
})
|
|
}
|
|
|
|
// TestConcurrentRequests tests concurrent request handling
|
|
func TestConcurrentRequests(t *testing.T) {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"message":"ok"}`))
|
|
})
|
|
|
|
t.Run("Multiple concurrent requests", func(t *testing.T) {
|
|
for i := 0; i < 10; i++ {
|
|
req := httptest.NewRequest("GET", "/api/libraries/types", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestJWTValidation tests various JWT scenarios
|
|
func TestJWTValidation(t *testing.T) {
|
|
t.Run("Valid JWT format", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/libraries/types", nil)
|
|
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if !containsPrefix(authHeader, "Bearer ") {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
})
|
|
|
|
t.Run("Invalid JWT - no Bearer prefix", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/libraries/types", nil)
|
|
req.Header.Set("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" || !containsPrefix(authHeader, "Bearer ") {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
|
})
|
|
|
|
t.Run("Invalid JWT - malformed", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/libraries/types", nil)
|
|
req.Header.Set("Authorization", "Bearer invalid.token.here")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
|
})
|
|
}
|
|
|
|
// TestPaginationAndFiltering tests query parameter handling
|
|
func TestPaginationAndFiltering(t *testing.T) {
|
|
t.Run("Negative limit", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/ebooks?limit=-10&offset=0", nil)
|
|
req.Header.Set("Authorization", "Bearer valid-token")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
limit := r.URL.Query().Get("limit")
|
|
if limit == "-10" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"limit must be positive"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("Negative offset", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/ebooks?limit=10&offset=-5", nil)
|
|
req.Header.Set("Authorization", "Bearer valid-token")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
offset := r.URL.Query().Get("offset")
|
|
if offset == "-5" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"offset must be non-negative"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("Very large limit", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/ebooks?limit=10000&offset=0", nil)
|
|
req.Header.Set("Authorization", "Bearer valid-token")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
limit := r.URL.Query().Get("limit")
|
|
if limit == "10000" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"limit too large"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
|
})
|
|
|
|
t.Run("Valid pagination", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/ebooks?limit=20&offset=0", nil)
|
|
req.Header.Set("Authorization", "Bearer valid-token")
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ebooks := []map[string]interface{}{
|
|
{"id": uuid.New().String(), "title": "Book 1"},
|
|
{"id": uuid.New().String(), "title": "Book 2"},
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
json.NewEncoder(w).Encode(ebooks)
|
|
})
|
|
|
|
handler.ServeHTTP(rr, req)
|
|
assert.Equal(t, http.StatusOK, rr.Code)
|
|
})
|
|
}
|