105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package handlers
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"net/url"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestHandleListUnmatchedItemsExcludesMangaChapters verifies that manga
|
||
|
|
// chapter rows (type='ebook' items linked into a series via manga_chapters)
|
||
|
|
// do not appear in the admin Unmatched queue, while genuinely pending items
|
||
|
|
// still do. Regression test for issue #204.
|
||
|
|
func TestHandleListUnmatchedItemsExcludesMangaChapters(t *testing.T) {
|
||
|
|
dsn := os.Getenv("SILO_TEST_DATABASE_URL")
|
||
|
|
if dsn == "" {
|
||
|
|
t.Skip("set SILO_TEST_DATABASE_URL to run DB-backed unmatched items handler test")
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx := context.Background()
|
||
|
|
pool, err := pgxpool.New(ctx, dsn)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("connect db: %v", err)
|
||
|
|
}
|
||
|
|
t.Cleanup(pool.Close)
|
||
|
|
|
||
|
|
suffix := time.Now().UnixNano()
|
||
|
|
// Embed a unique per-run tag in every seeded title and scope the search
|
||
|
|
// query to it, so concurrent or leftover rows in a shared test database
|
||
|
|
// cannot skew the exact-total assertion below.
|
||
|
|
tag := fmt.Sprintf("issue204-%d", suffix)
|
||
|
|
seriesID := fmt.Sprintf("manga-series-%d", suffix)
|
||
|
|
chapterID := fmt.Sprintf("manga-chapter-%d", suffix)
|
||
|
|
pendingID := fmt.Sprintf("pending-ebook-%d", suffix)
|
||
|
|
t.Cleanup(func() {
|
||
|
|
_, _ = pool.Exec(ctx, `DELETE FROM media_items WHERE content_id = ANY($1)`,
|
||
|
|
[]string{chapterID, seriesID, pendingID})
|
||
|
|
})
|
||
|
|
|
||
|
|
for _, row := range []struct {
|
||
|
|
id, typ, title, status string
|
||
|
|
}{
|
||
|
|
{seriesID, "manga", tag + " Series", "matched"},
|
||
|
|
{chapterID, "ebook", tag + " Series c001", "pending"},
|
||
|
|
{pendingID, "ebook", tag + " Plain Ebook", "pending"},
|
||
|
|
} {
|
||
|
|
if _, err := pool.Exec(ctx, `
|
||
|
|
INSERT INTO media_items (content_id, type, title, status, genres)
|
||
|
|
VALUES ($1, $2, $3, $4, '{}'::text[])
|
||
|
|
`, row.id, row.typ, row.title, row.status); err != nil {
|
||
|
|
t.Fatalf("seed media item %s: %v", row.id, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := pool.Exec(ctx, `
|
||
|
|
INSERT INTO manga_chapters (chapter_content_id, series_content_id, chapter_index)
|
||
|
|
VALUES ($1, $2, 1)
|
||
|
|
`, chapterID, seriesID); err != nil {
|
||
|
|
t.Fatalf("seed manga chapter link: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
h := NewLibraryHandler(nil, nil, nil, pool, nil)
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/libraries/unmatched-items?q="+url.QueryEscape(tag), nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
h.HandleListUnmatchedItems(rec, req)
|
||
|
|
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status = %d, want 200 (body: %s)", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
var resp struct {
|
||
|
|
Items []struct {
|
||
|
|
ContentID string `json:"content_id"`
|
||
|
|
} `json:"items"`
|
||
|
|
Total int `json:"total"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||
|
|
t.Fatalf("decode response: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var sawChapter, sawPending bool
|
||
|
|
for _, item := range resp.Items {
|
||
|
|
switch item.ContentID {
|
||
|
|
case chapterID:
|
||
|
|
sawChapter = true
|
||
|
|
case pendingID:
|
||
|
|
sawPending = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !sawPending {
|
||
|
|
t.Errorf("pending ebook %s missing from unmatched list", pendingID)
|
||
|
|
}
|
||
|
|
if sawChapter {
|
||
|
|
t.Errorf("manga chapter %s should be excluded from unmatched list", chapterID)
|
||
|
|
}
|
||
|
|
if resp.Total != 1 {
|
||
|
|
t.Errorf("total = %d, want 1 (chapter must be excluded from the count too)", resp.Total)
|
||
|
|
}
|
||
|
|
}
|