- Caddy: custom image with caddy-ratelimit plugin, security headers (X-Frame-Options, HSTS, CSP-adjacent, etc.), per-IP rate limiting on auth/scrape/global zones, static error pages (502/503/504), fix routing to remove /api/scrape/* and /api/chapter-text-preview/* direct-to-backend (were bypassing SvelteKit auth middleware) - docker-compose: Caddy build context + error volume, Watchtower service (label-enable mode, 5 min poll), watchtower labels on backend/runner/ui - Scraper: ScrapeChapterList uses retryGet (9 attempts, Retry-After backoff) to fix 429-induced chapter list failures; upTo param stops pagination early for range scrapes - UI: Browse→Catalogue rename (routes, API, links), admin scrape page Continue/Retry buttons, +error.svelte branded error page, type cleanup (removed dead exports, added BookPreviewMeta/BookPreviewResponse to scraper.ts) - Meilisearch: meta_updated field, sort=update fix, facet distribution - Docs: reorganise into docs/d2/ and docs/mermaid/ subdirectories, update all diagrams to reflect Caddy/Watchtower/routing changes, add api-routing.d2 ownership map with auth-level colour coding, regenerate SVGs
142 lines
5.2 KiB
Go
142 lines
5.2 KiB
Go
package bookstore_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/libnovel/backend/internal/bookstore"
|
|
"github.com/libnovel/backend/internal/domain"
|
|
)
|
|
|
|
// ── Mock that satisfies all bookstore interfaces ──────────────────────────────
|
|
|
|
type mockStore struct{}
|
|
|
|
// BookWriter
|
|
func (m *mockStore) WriteMetadata(_ context.Context, _ domain.BookMeta) error { return nil }
|
|
func (m *mockStore) WriteChapter(_ context.Context, _ string, _ domain.Chapter) error { return nil }
|
|
func (m *mockStore) WriteChapterRefs(_ context.Context, _ string, _ []domain.ChapterRef) error {
|
|
return nil
|
|
}
|
|
func (m *mockStore) ChapterExists(_ context.Context, _ string, _ domain.ChapterRef) bool {
|
|
return false
|
|
}
|
|
|
|
// BookReader
|
|
func (m *mockStore) ReadMetadata(_ context.Context, _ string) (domain.BookMeta, bool, error) {
|
|
return domain.BookMeta{}, false, nil
|
|
}
|
|
func (m *mockStore) ListBooks(_ context.Context) ([]domain.BookMeta, error) { return nil, nil }
|
|
func (m *mockStore) LocalSlugs(_ context.Context) (map[string]bool, error) {
|
|
return map[string]bool{}, nil
|
|
}
|
|
func (m *mockStore) MetadataMtime(_ context.Context, _ string) int64 { return 0 }
|
|
func (m *mockStore) ReadChapter(_ context.Context, _ string, _ int) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (m *mockStore) ListChapters(_ context.Context, _ string) ([]domain.ChapterInfo, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockStore) CountChapters(_ context.Context, _ string) int { return 0 }
|
|
func (m *mockStore) ReindexChapters(_ context.Context, _ string) (int, error) { return 0, nil }
|
|
|
|
// RankingStore
|
|
func (m *mockStore) WriteRankingItem(_ context.Context, _ domain.RankingItem) error { return nil }
|
|
func (m *mockStore) ReadRankingItems(_ context.Context) ([]domain.RankingItem, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockStore) RankingFreshEnough(_ context.Context, _ time.Duration) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
// AudioStore
|
|
func (m *mockStore) AudioObjectKey(_ string, _ int, _ string) string { return "" }
|
|
func (m *mockStore) AudioExists(_ context.Context, _ string) bool { return false }
|
|
func (m *mockStore) PutAudio(_ context.Context, _ string, _ []byte) error { return nil }
|
|
|
|
// PresignStore
|
|
func (m *mockStore) PresignChapter(_ context.Context, _ string, _ int, _ time.Duration) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (m *mockStore) PresignAudio(_ context.Context, _ string, _ time.Duration) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (m *mockStore) PresignAvatarUpload(_ context.Context, _, _ string) (string, string, error) {
|
|
return "", "", nil
|
|
}
|
|
func (m *mockStore) PresignAvatarURL(_ context.Context, _ string) (string, bool, error) {
|
|
return "", false, nil
|
|
}
|
|
func (m *mockStore) PutAvatar(_ context.Context, _, _, _ string, _ []byte) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (m *mockStore) DeleteAvatar(_ context.Context, _ string) error { return nil }
|
|
|
|
// ProgressStore
|
|
func (m *mockStore) GetProgress(_ context.Context, _, _ string) (domain.ReadingProgress, bool) {
|
|
return domain.ReadingProgress{}, false
|
|
}
|
|
func (m *mockStore) SetProgress(_ context.Context, _ string, _ domain.ReadingProgress) error {
|
|
return nil
|
|
}
|
|
func (m *mockStore) AllProgress(_ context.Context, _ string) ([]domain.ReadingProgress, error) {
|
|
return nil, nil
|
|
}
|
|
func (m *mockStore) DeleteProgress(_ context.Context, _, _ string) error { return nil }
|
|
|
|
// ── Compile-time interface satisfaction ───────────────────────────────────────
|
|
|
|
var _ bookstore.BookWriter = (*mockStore)(nil)
|
|
var _ bookstore.BookReader = (*mockStore)(nil)
|
|
var _ bookstore.RankingStore = (*mockStore)(nil)
|
|
var _ bookstore.AudioStore = (*mockStore)(nil)
|
|
var _ bookstore.PresignStore = (*mockStore)(nil)
|
|
var _ bookstore.ProgressStore = (*mockStore)(nil)
|
|
|
|
// ── Behavioural tests ─────────────────────────────────────────────────────────
|
|
|
|
func TestBookWriter_WriteMetadata_ReturnsNilError(t *testing.T) {
|
|
var w bookstore.BookWriter = &mockStore{}
|
|
if err := w.WriteMetadata(context.Background(), domain.BookMeta{Slug: "test"}); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBookReader_ReadMetadata_NotFound(t *testing.T) {
|
|
var r bookstore.BookReader = &mockStore{}
|
|
_, found, err := r.ReadMetadata(context.Background(), "unknown")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if found {
|
|
t.Error("expected not found")
|
|
}
|
|
}
|
|
|
|
func TestRankingStore_RankingFreshEnough_ReturnsFalse(t *testing.T) {
|
|
var s bookstore.RankingStore = &mockStore{}
|
|
fresh, err := s.RankingFreshEnough(context.Background(), time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if fresh {
|
|
t.Error("expected false")
|
|
}
|
|
}
|
|
|
|
func TestAudioStore_AudioExists_ReturnsFalse(t *testing.T) {
|
|
var s bookstore.AudioStore = &mockStore{}
|
|
if s.AudioExists(context.Background(), "audio/slug/1/af_bella.mp3") {
|
|
t.Error("expected false")
|
|
}
|
|
}
|
|
|
|
func TestProgressStore_GetProgress_NotFound(t *testing.T) {
|
|
var s bookstore.ProgressStore = &mockStore{}
|
|
_, found := s.GetProgress(context.Background(), "session-1", "slug")
|
|
if found {
|
|
t.Error("expected not found")
|
|
}
|
|
}
|