Files
john-okeefe f8a6c3d227 Phase 3 Week 7: Add KOReader routes, tests, and documentation
- Add KOReader sync endpoints to main application router
- Create Bruno API collection for testing KOReader endpoints
- Add integration tests for KOReader functionality
- Include comprehensive README with setup instructions
- Test coverage for progress, metadata, library, and bookmarks sync
- Part of Phase 3 KOReader Integration implementation
2026-01-30 20:55:20 -05:00

298 lines
7.4 KiB
Go

package main
import (
"bytes"
"encoding/json"
"net/http/httptest"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestKOReaderSyncProgress_RequestBody(t *testing.T) {
t.Run("valid request body", func(t *testing.T) {
reqBody := map[string]interface{}{
"books": []map[string]interface{}{
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"percentage": 0.45,
"progress": 0.45,
"chapter": 5,
},
},
"sync_mode": "immediate",
}
body, err := json.Marshal(reqBody)
assert.NoError(t, err)
assert.Contains(t, string(body), "percentage")
req := httptest.NewRequest("POST", "/api/sync/koreader/progress", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "/api/sync/koreader/progress", req.URL.Path)
})
t.Run("request with multiple books", func(t *testing.T) {
reqBody := map[string]interface{}{
"books": []map[string]interface{}{
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"percentage": 0.25,
},
{
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"percentage": 0.50,
},
},
"sync_mode": "checkpoint",
}
body, err := json.Marshal(reqBody)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
books := parsed["books"].([]interface{})
assert.Len(t, books, 2)
})
t.Run("request with highlights and bookmarks", func(t *testing.T) {
reqBody := map[string]interface{}{
"book_uuid": "550e8400-e29b-41d4-a716-446655440000",
"bookmarks": []map[string]interface{}{
{
"chapter": 3,
"page": 45,
"text": "Bookmarked text",
},
},
"highlights": []map[string]interface{}{
{
"chapter": 3,
"page": 45,
"text": "highlighted text",
"color": "#ffff00",
},
},
}
body, err := json.Marshal(reqBody)
assert.NoError(t, err)
assert.Contains(t, string(body), "bookmarks")
assert.Contains(t, string(body), "highlights")
})
}
func TestKOReaderMetadataParsing(t *testing.T) {
t.Run("parse progress data", func(t *testing.T) {
progressData := map[string]interface{}{
"percentage": 0.45,
"chapter": 5,
"epubcfi": "epubcfi(/6/4/2:15)",
"character": int64(15432),
"page": 89,
"total_pages": 200,
"chapter_progress": 0.234,
}
body, err := json.Marshal(progressData)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
assert.Equal(t, 0.45, parsed["percentage"])
assert.Equal(t, "epubcfi(/6/4/2:15)", parsed["epubcfi"])
})
t.Run("parse annotation data", func(t *testing.T) {
annotations := map[string]interface{}{
"highlights": []map[string]interface{}{
{
"text": "highlighted text",
"pos0": "epubcfi(/6/4/2:15)",
"pos1": "epubcfi(/6/4/2:20)",
"color": "#ffff00",
},
},
"notes": []map[string]interface{}{
{
"text": "My note",
"pos0": "epubcfi(/6/4/2:15)",
},
},
}
body, err := json.Marshal(annotations)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
highlights := parsed["highlights"].([]interface{})
assert.Len(t, highlights, 1)
notes := parsed["notes"].([]interface{})
assert.Len(t, notes, 1)
})
}
func TestKOReaderResponseFormats(t *testing.T) {
t.Run("sync progress response", func(t *testing.T) {
response := map[string]interface{}{
"sync_status": "accepted",
"books_synced": 1,
"conflicts": []interface{}{},
"timestamp": "2026-01-30T20:00:00Z",
"device_updated": true,
}
body, err := json.Marshal(response)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
assert.Equal(t, "accepted", parsed["sync_status"])
assert.Equal(t, float64(1), parsed["books_synced"])
})
t.Run("metadata response", func(t *testing.T) {
response := map[string]interface{}{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Test Book",
"authors": []string{"Test Author"},
"progress": map[string]interface{}{
"percentage": 0.45,
"chapter": 5,
},
"annotations": map[string]interface{}{
"highlights": []interface{}{},
"notes": []interface{}{},
},
}
body, err := json.Marshal(response)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
assert.Equal(t, "Test Book", parsed["title"])
assert.NotNil(t, parsed["progress"])
})
t.Run("library response", func(t *testing.T) {
response := map[string]interface{}{
"library_sync": []map[string]interface{}{
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Book 1",
"author": "Author 1",
"percent_read": 45.0,
"pages_remaining": 115,
"bookmark_count": 3,
},
},
"total_books": 1,
"last_sync": "2026-01-30T20:00:00Z",
}
body, err := json.Marshal(response)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
assert.Equal(t, float64(1), parsed["total_books"])
})
}
func TestKOReaderErrorHandling(t *testing.T) {
t.Run("invalid UUID format", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/sync/koreader/metadata/invalid-uuid", nil)
// This should fail UUID parsing
_, err := uuid.Parse(req.URL.Path[len("/api/sync/koreader/metadata/"):])
assert.Error(t, err)
})
t.Run("missing authorization header", func(t *testing.T) {
req := httptest.NewRequest("POST", "/api/sync/koreader/progress", nil)
req.Header.Set("Content-Type", "application/json")
authHeader := req.Header.Get("Authorization")
assert.Empty(t, authHeader)
})
t.Run("invalid percentage value", func(t *testing.T) {
progressData := map[string]interface{}{
"percentage": 1.5, // Invalid: > 1.0
}
body, err := json.Marshal(progressData)
assert.NoError(t, err)
var parsed map[string]interface{}
err = json.Unmarshal(body, &parsed)
assert.NoError(t, err)
percentage := parsed["percentage"].(float64)
assert.Greater(t, percentage, 1.0)
})
}
func TestKOReaderDeviceMatching(t *testing.T) {
t.Run("match by UUID", func(t *testing.T) {
bookUUID := "550e8400-e29b-41d4-a716-446655440000"
bookData := map[string]interface{}{
"uuid": bookUUID,
"percentage": 0.45,
}
body, _ := json.Marshal(bookData)
var parsed map[string]interface{}
json.Unmarshal(body, &parsed)
assert.Equal(t, bookUUID, parsed["uuid"])
})
t.Run("match by file path", func(t *testing.T) {
bookData := map[string]interface{}{
"file_path": "/path/to/book.epub",
"percentage": 0.45,
}
body, _ := json.Marshal(bookData)
var parsed map[string]interface{}
json.Unmarshal(body, &parsed)
assert.Equal(t, "/path/to/book.epub", parsed["file_path"])
})
t.Run("match by title and author", func(t *testing.T) {
bookData := map[string]interface{}{
"title": "Test Book",
"authors": []string{"Test Author"},
"percentage": 0.45,
}
body, _ := json.Marshal(bookData)
var parsed map[string]interface{}
json.Unmarshal(body, &parsed)
assert.Equal(t, "Test Book", parsed["title"])
assert.NotNil(t, parsed["authors"])
})
}