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
This commit is contained in:
+8
-1
@@ -50,9 +50,9 @@ func main() {
|
||||
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
|
||||
libraryHandler := handlers.NewLibraryHandler(queries)
|
||||
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
|
||||
koreaderHandler := handlers.NewKOReaderHandler(queries)
|
||||
|
||||
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
|
||||
_ = deviceAuthMiddleware
|
||||
|
||||
e := echo.New()
|
||||
|
||||
@@ -181,6 +181,13 @@ func main() {
|
||||
e.GET("/devices/approve/:registration_id", deviceHandler.ApproveDevice)
|
||||
e.POST("/devices/reject/:registration_id", deviceHandler.RejectDevice)
|
||||
|
||||
// KOReader sync routes (device authentication required)
|
||||
koreaderSync := e.Group("/api/sync/koreader")
|
||||
koreaderSync.POST("/progress", deviceAuthMiddleware.Authenticate(koreaderHandler.SyncProgress))
|
||||
koreaderSync.GET("/metadata/:uuid", deviceAuthMiddleware.Authenticate(koreaderHandler.GetMetadata))
|
||||
koreaderSync.GET("/library", deviceAuthMiddleware.Authenticate(koreaderHandler.GetLibrary))
|
||||
koreaderSync.POST("/bookmarks", deviceAuthMiddleware.Authenticate(koreaderHandler.SyncBookmarks))
|
||||
|
||||
// Device management routes (protected - require user auth)
|
||||
devices := protected.Group("/devices")
|
||||
devices.GET("", deviceHandler.ListDevices)
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
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"])
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user