fix(server): kobo markup panic — wire LibraryService into the test harness, guard typed-nil resolver

TestProgressWeb_KoboIntegration 500s'd on every markup with a
last-read-place bookmark: the panic-recovered stack was a nil pointer
dereference in LibraryService.ResolveMediaPath (s.db deref), reached
from KoboHandler.convertKoboCFIToStandard during kepub CFI conversion.

Two-layer bug:

1. Test harness (root cause): the router.Config built in
   test_helpers_test.go omitted LibraryService, so router/sync.go's
   SetLibraryService(cfg.LibraryService) stored a nil
   *services.LibraryService in the interface field. Production wiring
   (main.go:246) was always correct.
2. Latent trap (hardening): `h.libraryService != nil` at the
   convertKoboCFIToStandard call site can never catch a typed nil —
   an interface holding a nil *LibraryService is non-nil. ResolveMediaPath
   now degrades with a "library service not configured" error instead
   of panicking on a nil receiver; callers already handle the error
   by skipping CFI conversion.

Harness-only fix, zero production behavior change: the endpoint now
returns a clean 200 with the position saved wherever kepub conversion
is unavailable instead of a panic-recovered 500.

Verified: TestProgressWeb_KoboIntegration passes all three subtests
(401 unauthenticated, ReadingSync→last-read-place percentage
preservation, last-read-place without prior ReadingSync) and on
immediate rerun — previously it failed every run.
This commit is contained in:
John O'Keefe committed 2026-10-09 14:53:48 -04:00
1 parent 18969a0f01
commit 208fc2c4d6
2 files changed
+11

No files matched your search

+4
View File
@@ -572,6 +572,10 @@ func setupTestServer(t *testing.T) *TestServerSetup {
// the main binary wires database.NewSettingsRegistry(queries) — // the main binary wires database.NewSettingsRegistry(queries) —
// the test harness must too or route registration nil-panics. // the test harness must too or route registration nil-panics.
Settings: database.NewSettingsRegistry(queries), Settings: database.NewSettingsRegistry(queries),
// main.go:246 passes the shared LibraryService here; omitting it
// hands the kobo/kepub CFI converter a typed-nil (kobo markup
// panic-recovered as 500s in TestProgressWeb_KoboIntegration).
LibraryService: libraryService,
} }
router.RegisterRoutes(routerConfig) router.RegisterRoutes(routerConfig)
+7
View File
@@ -299,6 +299,13 @@ func (s *LibraryService) SyncAllowedExtensions(ctx context.Context) {
} }
func (s *LibraryService) ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error) { func (s *LibraryService) ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error) {
// Typed-nil interface guard: a nil *LibraryService held in an
// interface passes `!= nil` checks at call sites; degrade instead
// of panicking (kobo markup 500s were exactly this path).
if s == nil || s.db == nil {
return "", fmt.Errorf("library service not configured")
}
// Get library folders for this library // Get library folders for this library
folders, err := s.db.GetLibraryFolders(ctx, libraryID) folders, err := s.db.GetLibraryFolders(ctx, libraryID)
if err != nil || len(folders) == 0 { if err != nil || len(folders) == 0 {