KOReader push (processBookAnnotations) accepts deleted_highlights and deleted_bookmarks arrays of dedup keys and tombstones the matching rows, after the upserts so a key present in both lists resolves to 'deleted' (the newer intent). Deletions remain soft: rows stay restorable from the history and echo to other devices as tombstones on their next pull. A stale device replay of the annotation cannot resurrect the tombstone — device pushes carry no modification timestamp, so the save loses to the delete. Absence from these arrays is never a delete, keeping category toggles safe. New annotation-history endpoints (annotation_history.go, media.go): GET /api/media-items/:id/annotations/deleted POST /api/media-items/:id/annotations/:annotationId/restore DELETE /api/media-items/:id/annotations/:annotationId All scoped to the authenticated user and the route's book; the DELETE is the permanent purge (annotation_type required in query or body). MediaDetail gains DeletedAnnotations, populated by the book page route via the shared DeletedAnnotationsForBook builder, so the server-rendered history ships with the page instead of requiring a client round-trip. Binding tests cover the plugin's exact wire shape and the legacy plugin case (arrays omitted -> empty).
53 lines
1.6 KiB
Go
53 lines
1.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// The device pushes deletions as dedup-key arrays on the progress request.
|
|
// Verify the wire shape the plugin sends (lua json.encode of
|
|
// { deleted_highlights = { { dedup_key = "..." } } }) binds correctly.
|
|
func TestKOReaderProgressRequest_DeletedAnnotationsBinding(t *testing.T) {
|
|
payload := `{
|
|
"books": [{
|
|
"sha256": "d1b1c6123d6206017b40798744ed994f00803b97d22ce51bea32e95e1ce7a164",
|
|
"title": "1984",
|
|
"percentage": 0.42,
|
|
"deleted_highlights": [
|
|
{ "dedup_key": "abc123" },
|
|
{ "dedup_key": "def456" }
|
|
],
|
|
"deleted_bookmarks": [
|
|
{ "dedup_key": "789xyz" }
|
|
]
|
|
}]
|
|
}`
|
|
|
|
var req KOReaderProgressRequest
|
|
err := json.Unmarshal([]byte(payload), &req)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, req.Books, 1)
|
|
|
|
book := req.Books[0]
|
|
assert.Len(t, book.DeletedHighlights, 2)
|
|
assert.Equal(t, "abc123", book.DeletedHighlights[0].DedupKey)
|
|
assert.Equal(t, "def456", book.DeletedHighlights[1].DedupKey)
|
|
assert.Len(t, book.DeletedBookmarks, 1)
|
|
assert.Equal(t, "789xyz", book.DeletedBookmarks[0].DedupKey)
|
|
}
|
|
|
|
// A request without the arrays (older plugins) must bind with them empty —
|
|
// deletion propagation is strictly opt-in per push.
|
|
func TestKOReaderProgressRequest_DeletedAnnotationsOmitted(t *testing.T) {
|
|
payload := `{"books": [{"sha256": "x", "title": "t", "percentage": 0.1}]}`
|
|
|
|
var req KOReaderProgressRequest
|
|
err := json.Unmarshal([]byte(payload), &req)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, req.Books[0].DeletedHighlights)
|
|
assert.Empty(t, req.Books[0].DeletedBookmarks)
|
|
}
|