The device catalog feed was unusable on paged OPDS clients such as
KOReader: it sliced results into pages but never advertised how to reach
the next page, so clients could only ever fetch the first page (~50 books)
and could not search the catalog.
GetDeviceCatalog:
- Emit the full set of OPDS pagination link relations (self, start, first,
previous, next, last) pointing at catalog?page=N&per_page=M, with the
device auth token appended for path-based auth.
- Emit OpenSearch totalResults/itemsPerPage/startIndex metadata.
- Point rel=search at the OpenSearch description (correct MIME type).
SearchDeviceCatalog now branches on the q parameter:
- No q: return an OpenSearch description document whose Url template
contains the {searchTerms} placeholder, so clients can formulate a query.
- With q: return the existing acquisition results feed, now including
totalResults.
A pure addCatalogPaginationLinks helper holds the page/URL logic so it can
be unit tested without a database. New handler tests cover middle/first/
last/single/empty pages (correct presence of next/previous) and token
appending.
Ordering is intentionally left unchanged (created_at DESC, grouped by
library).
120 lines
3.7 KiB
Go
120 lines
3.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/opds"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// rels collects the rel attributes of all links currently on the feed.
|
|
func rels(feed *opds.Feed) []string {
|
|
out := make([]string, 0, len(feed.Links))
|
|
for _, l := range feed.Links {
|
|
out = append(out, l.Rel)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func containsRel(feed *opds.Feed, rel string) bool {
|
|
for _, l := range feed.Links {
|
|
if l.Rel == rel {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestAddCatalogPaginationLinks_MiddlePage(t *testing.T) {
|
|
feed := opds.NewFeed("urn:uuid:dev", "Library")
|
|
// 1814 items, 50 per page => 37 pages; on page 2
|
|
addCatalogPaginationLinks(feed, "http://h/opds/devices/dev/catalog", 2, 50, 1814, "tok")
|
|
|
|
assert.True(t, containsRel(feed, "self"))
|
|
assert.True(t, containsRel(feed, "start"))
|
|
assert.True(t, containsRel(feed, "first"))
|
|
assert.True(t, containsRel(feed, "last"))
|
|
assert.True(t, containsRel(feed, "previous"), "middle page must have previous")
|
|
assert.True(t, containsRel(feed, "next"), "middle page must have next")
|
|
|
|
// self must point to the current page
|
|
var selfHref string
|
|
for _, l := range feed.Links {
|
|
if l.Rel == "self" {
|
|
selfHref = l.Href
|
|
}
|
|
}
|
|
assert.Contains(t, selfHref, "page=2&per_page=50")
|
|
assert.Contains(t, selfHref, "token=tok")
|
|
|
|
// next must advance the page
|
|
var nextHref string
|
|
for _, l := range feed.Links {
|
|
if l.Rel == "next" {
|
|
nextHref = l.Href
|
|
}
|
|
}
|
|
assert.Contains(t, nextHref, "page=3")
|
|
|
|
// OpenSearch metadata
|
|
require.NotNil(t, feed.TotalResults)
|
|
assert.Equal(t, 1814, *feed.TotalResults)
|
|
require.NotNil(t, feed.ItemsPerPage)
|
|
assert.Equal(t, 50, *feed.ItemsPerPage)
|
|
require.NotNil(t, feed.StartIndex)
|
|
assert.Equal(t, 51, *feed.StartIndex, "startIndex should be 1-based offset of first item on page 2")
|
|
}
|
|
|
|
func TestAddCatalogPaginationLinks_FirstPage_NoPrevious(t *testing.T) {
|
|
feed := opds.NewFeed("urn:uuid:dev", "Library")
|
|
addCatalogPaginationLinks(feed, "http://h/opds/devices/dev/catalog", 1, 50, 1814, "")
|
|
|
|
rels := rels(feed)
|
|
assert.NotContains(t, rels, "previous", "first page must not have previous")
|
|
assert.Contains(t, rels, "next")
|
|
}
|
|
|
|
func TestAddCatalogPaginationLinks_LastPage_NoNext(t *testing.T) {
|
|
feed := opds.NewFeed("urn:uuid:dev", "Library")
|
|
addCatalogPaginationLinks(feed, "http://h/opds/devices/dev/catalog", 37, 50, 1814, "")
|
|
|
|
rels := rels(feed)
|
|
assert.NotContains(t, rels, "next", "last page must not have next")
|
|
assert.Contains(t, rels, "previous")
|
|
}
|
|
|
|
func TestAddCatalogPaginationLinks_SinglePage(t *testing.T) {
|
|
feed := opds.NewFeed("urn:uuid:dev", "Library")
|
|
addCatalogPaginationLinks(feed, "http://h/opds/devices/dev/catalog", 1, 50, 10, "")
|
|
|
|
rels := rels(feed)
|
|
assert.NotContains(t, rels, "previous")
|
|
assert.NotContains(t, rels, "next")
|
|
// still emits self/start/first/last
|
|
assert.Contains(t, rels, "self")
|
|
assert.Contains(t, rels, "last")
|
|
}
|
|
|
|
func TestAddCatalogPaginationLinks_EmptyCatalog(t *testing.T) {
|
|
feed := opds.NewFeed("urn:uuid:dev", "Library")
|
|
addCatalogPaginationLinks(feed, "http://h/opds/devices/dev/catalog", 1, 50, 0, "")
|
|
|
|
rels := rels(feed)
|
|
assert.NotContains(t, rels, "next")
|
|
assert.NotContains(t, rels, "previous")
|
|
assert.NotContains(t, rels, "last", "empty catalog should not advertise a last page")
|
|
require.NotNil(t, feed.TotalResults)
|
|
assert.Equal(t, 0, *feed.TotalResults)
|
|
}
|
|
|
|
func TestAddCatalogPaginationLinks_TokenAppended(t *testing.T) {
|
|
feed := opds.NewFeed("urn:uuid:dev", "Library")
|
|
addCatalogPaginationLinks(feed, "http://h/opds/devices/dev/catalog", 1, 50, 100, "abc")
|
|
|
|
xml, err := feed.GenerateXMLString()
|
|
require.NoError(t, err)
|
|
assert.True(t, strings.Count(xml, "token=abc") >= 3, "token should be appended to generated links")
|
|
}
|