Release / build-and-push (push) Successful in 2m14s
Progress, highlights, notes, and bookmarks entered position conversion through three different doors: progress converted inline with an uncached converter, annotations through the facade, bookmarks not at all (the raw xpointer was stored verbatim, cfi_position stayed empty, and the web drawer's goToBookmark silently no-ops on cfi-less entries). Unify on the facade (ConvertToCanonical/ConvertFromCanonical): - annotationEpub context resolved once per push: media item + EPUB path shared by every annotation instead of re-fetched per entry - progress forward: the inline block becomes one facade call; non-reflowable formats pass through unchanged, and the cached converter stops re-parsing the book on every sync - progress reverse: convertCFIToXPointer delegates to reverseConvertCFI, keeping the stored percentage in play for the fallback ladder - bookmarks (bulk progress and /sync-bookmarks): pos0 resolves structural-only — bookmark text is a display label, never book text, so no context is supplied; webUsableCFI stores the result only for structural/exact epubcfi landings, discarding href/percentage results rather than storing dead drawer links. Also records percentage_location and origin_source on the legacy endpoint. - percentages thread through: highlights/notes/bookmarks pass the device percentage or the derived section percentage instead of a hardcoded 0, so the last-resort fallback lands near the true position instead of the document start - extendCFIByLength end-derivation now also fires on structural starts (it had silently stopped matching when the structural rung began landing starts with precision 'structural' rather than 'exact') Tests: the drop-cap xpointer through the facade with empty context (the bookmark scenario) must land structurally, not doc-start; webUsableCFI table covers the store/discard gate.
105 lines
3.3 KiB
Go
105 lines
3.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
wsync "bookhoard/internal/sync"
|
|
|
|
"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)
|
|
}
|
|
|
|
// The web drawer navigates bookmarks by CFI, so only high-confidence
|
|
// conversions may be stored: href/percentage/fallback results would
|
|
// become dead links. This is the sole gate for KOReader→web bookmark
|
|
// positions (bookmark text is a label, never book text, so the
|
|
// conversion runs structural-only with empty context).
|
|
func TestWebUsableCFI(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
loc wsync.CanonicalLocator
|
|
expected string
|
|
}{
|
|
{
|
|
name: "structural landing stored",
|
|
loc: wsync.CanonicalLocator{CFI: "epubcfi(/6/52!/4/28/2/1:0)", Precision: "structural", Percentage: 0.52},
|
|
expected: "epubcfi(/6/52!/4/28/2/1:0)",
|
|
},
|
|
{
|
|
name: "exact text-search landing stored",
|
|
loc: wsync.CanonicalLocator{CFI: "epubcfi(/6/52!/4/28/2/1:0)", Precision: "exact", Percentage: 0.52},
|
|
expected: "epubcfi(/6/52!/4/28/2/1:0)",
|
|
},
|
|
{
|
|
name: "percentage guess discarded",
|
|
loc: wsync.CanonicalLocator{CFI: "epubcfi(/6/52!/4/2/1:0)", Precision: "percentage", Percentage: 0.52},
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "fallback passthrough (raw xpointer) discarded",
|
|
loc: wsync.CanonicalLocator{CFI: "/body/DocFragment[2]/body/p[3]", Precision: "fallback", Percentage: 0.52},
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "section href discarded (not a CFI)",
|
|
loc: wsync.CanonicalLocator{CFI: "ch10.xhtml", Precision: "section", Percentage: 0.52},
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "structural but non-CFI value discarded",
|
|
loc: wsync.CanonicalLocator{CFI: "ch10.xhtml#h1", Precision: "structural", Percentage: 0.52},
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.expected, webUsableCFI(tc.loc))
|
|
})
|
|
}
|
|
}
|