Files
bookhoard/cmd/server/tests/library_test.go
T
john-okeefe ff0517d038 fix(library): sync allowed extensions across service, schema, and tests
Add .epub and .pdf to comics type, add .pdf to manga type, and ensure
avif/tiff/tif extensions are consistently included in manga across all
layers. Update test fixtures to match the canonical extension lists.
2026-05-01 14:31:17 -04:00

378 lines
11 KiB
Go

package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestAuthMiddleware verifies JWT middleware works correctly
func TestAuthMiddleware(t *testing.T) {
// Test missing JWT header
t.Run("Missing JWT", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/visible", nil)
rr := httptest.NewRecorder()
// Simulate auth middleware
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"valid token"}`))
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Equal(t, `{"message":"missing or malformed jwt"}`, rr.Body.String())
})
// Test invalid JWT token format
t.Run("Invalid JWT format", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/visible", nil)
req.Header.Set("Authorization", "invalid-token")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"valid token"}`))
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Equal(t, `{"message":"missing or malformed jwt"}`, rr.Body.String())
})
// Test valid JWT token format
t.Run("Valid JWT format", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/libraries/visible", nil)
req.Header.Set("Authorization", "Bearer valid-token")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message":"valid token"}`))
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, `{"message":"valid token"}`, rr.Body.String())
})
}
// TestLibraryCreationUnauthorized verifies non-admins can't create libraries
func TestLibraryCreationUnauthorized(t *testing.T) {
payload := map[string]string{
"name": "Test Library",
"description": "This should fail",
"type": "ebooks",
}
jsonData, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal payload: %v", err)
}
req, err := http.NewRequest("POST", "/api/libraries", bytes.NewBuffer(jsonData))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
// Simulate missing user context (like non-authenticated)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate missing user context (like non-authenticated)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"missing or malformed jwt"}`))
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Equal(t, `{"message":"missing or malformed jwt"}`, rr.Body.String())
}
// TestLibraryCreationWithValidAdmin verifies admins can create libraries
func TestLibraryCreationWithValidAdmin(t *testing.T) {
payload := map[string]string{
"name": "Admin Library",
"description": "This should work",
"type": "comics",
}
jsonData, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal payload: %v", err)
}
req, err := http.NewRequest("POST", "/api/libraries", bytes.NewBuffer(jsonData))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate admin user context
if r.Header.Get("X-User-Role") != "admin" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error":"admin access required"}`))
return
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"id":"test-library-id","name":"Admin Library","type":"comics"}`))
})
req.Header.Set("X-User-Role", "admin")
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
assert.Contains(t, rr.Body.String(), "Admin Library")
}
// TestLibraryTypesResponse verifies library types endpoint returns correct types
func TestLibraryTypesResponse(t *testing.T) {
req, err := http.NewRequest("GET", "/api/libraries/types", nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := []map[string]interface{}{
{
"id": "test-id-1",
"name": "ebooks",
"description": "Ebook files including EPUB, PDF, MOBI, etc.",
"allowed_extensions": []string{".epub", ".pdf", ".mobi", ".azw", ".azw3", ".txt", ".rtf", ".doc", ".docx", ".lit", ".fb2", ".pdb"},
},
{
"id": "test-id-2",
"name": "comics",
"description": "Comic book archives and image formats",
"allowed_extensions": []string{".cbz", ".cbr", ".cb7", ".cbt", ".epub", ".pdf"},
},
{
"id": "test-id-3",
"name": "manga",
"description": "Manga files including archives and image folders",
"allowed_extensions": []string{".cbz", ".cbr", ".epub", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".avif", ".tiff", ".tif"},
},
}
jsonData, _ := json.Marshal(response)
w.Write(jsonData)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "ebooks")
assert.Contains(t, rr.Body.String(), "comics")
assert.Contains(t, rr.Body.String(), "manga")
}
// TestUserVisibleLibraries verifies user library visibility filtering
func TestUserVisibleLibraries(t *testing.T) {
req, err := http.NewRequest("GET", "/api/libraries/visible", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer fake-token")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := []map[string]interface{}{
{
"id": "lib-1",
"name": "User Library 1",
"type_name": "ebooks",
"is_visible": true,
},
}
jsonData, _ := json.Marshal(response)
w.Write(jsonData)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "User Library 1")
assert.Contains(t, rr.Body.String(), "ebooks")
assert.False(t, strings.Contains(rr.Body.String(), "Hidden Admin Library")) // Should not contain hidden library
}
// TestMediaItemsList verifies media items endpoint with library filtering
func TestMediaItemsList(t *testing.T) {
req, err := http.NewRequest("GET", "/api/media-items?library_id=test-lib-1&limit=10&offset=0", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer fake-token")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
response := []map[string]interface{}{
{
"id": "media-1",
"title": "Test Book 1",
"library_name": "User Library 1",
"library_type_name": "ebooks",
},
}
jsonData, _ := json.Marshal(response)
w.Write(jsonData)
})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Test Book 1")
}
// TestJSONValidation verifies proper JSON validation
func TestJSONValidation(t *testing.T) {
testCases := []struct {
name string
payload interface{}
expectedStatus int
expectedError string
}{
{
name: "Invalid JSON",
payload: `{"name": ""}`, // Invalid empty name
expectedStatus: http.StatusBadRequest,
expectedError: "invalid request",
},
{
name: "Invalid library type",
payload: `{"name": "Test", "type": "invalid_type"}`,
expectedStatus: http.StatusBadRequest,
expectedError: "invalid request",
},
{
name: "Valid request",
payload: map[string]string{"name": "Valid Library", "type": "ebooks"},
expectedStatus: http.StatusCreated,
expectedError: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
jsonData, err := json.Marshal(tc.payload)
if err != nil {
t.Fatalf("Failed to marshal payload: %v", err)
}
req, err := http.NewRequest("POST", "/api/libraries", bytes.NewBuffer(jsonData))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate successful creation for valid cases
if tc.expectedStatus == http.StatusCreated {
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"id":"test-id"}`))
} else {
w.WriteHeader(tc.expectedStatus)
w.Write([]byte(`{"error":"` + tc.expectedError + `"}`))
}
})
handler.ServeHTTP(rr, req)
assert.Equal(t, tc.expectedStatus, rr.Code)
if tc.expectedError != "" {
assert.Contains(t, rr.Body.String(), tc.expectedError)
}
})
}
}
// TestErrorHandling verifies proper error responses
func TestErrorHandling(t *testing.T) {
testCases := []struct {
name string
endpoint string
expectedStatus int
}{
{
name: "Missing library ID",
endpoint: "/api/libraries/nonexistent-id",
expectedStatus: http.StatusBadRequest,
},
{
name: "Invalid UUID",
endpoint: "/api/libraries/invalid-uuid",
expectedStatus: http.StatusBadRequest,
},
{
name: "Nonexistent user library",
endpoint: "/api/libraries/visible?user_id=nonexistent-user",
expectedStatus: http.StatusUnauthorized,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest("GET", tc.endpoint, nil)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(tc.expectedStatus)
w.Write([]byte(`{"error":"test error"}`))
})
handler.ServeHTTP(rr, req)
assert.Equal(t, tc.expectedStatus, rr.Code)
})
}
}