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)) }) } }