Files
silo-server/internal/scantrigger/scantrigger_test.go
T
Silo Server Migration b2fae0aa70 refactor(scantrigger): drop redundant Target.LibraryID field
- Read library ID from Target.Folder.ID everywhere
- Guard scan queue enqueue against nil Folder
- Simplify admin API key auth error plumbing
2026-05-25 12:11:56 -04:00

211 lines
5.7 KiB
Go

package scantrigger
import (
"context"
"errors"
"net/http"
"os"
"path/filepath"
"testing"
"github.com/Silo-Server/silo-server/internal/catalog"
"github.com/Silo-Server/silo-server/internal/models"
)
type fakeFolderRepo struct {
folders []*models.MediaFolder
listCalls int
}
func (r *fakeFolderRepo) GetByID(_ context.Context, id int) (*models.MediaFolder, error) {
for _, folder := range r.folders {
if folder.ID == id {
return folder, nil
}
}
return nil, catalog.ErrFolderNotFound
}
func (r *fakeFolderRepo) List(context.Context) ([]*models.MediaFolder, error) {
r.listCalls++
return r.folders, nil
}
func TestResolverClassifiesLibraryRoot(t *testing.T) {
root := t.TempDir()
repo := &fakeFolderRepo{folders: []*models.MediaFolder{{
ID: 7,
Name: "Movies",
Enabled: true,
Paths: []string{root},
}}}
target, err := NewResolver(repo).Resolve(context.Background(), Request{Path: root})
if err != nil {
t.Fatalf("Resolve returned error: %v", err)
}
if target.Folder == nil || target.Folder.ID != 7 || target.Mode != ModeLibrary || target.Path != "" {
t.Fatalf("unexpected target: %#v", target)
}
}
func TestResolverClassifiesSubtree(t *testing.T) {
root := t.TempDir()
subtree := filepath.Join(root, "Show")
if err := os.Mkdir(subtree, 0o755); err != nil {
t.Fatal(err)
}
repo := &fakeFolderRepo{folders: []*models.MediaFolder{{
ID: 8,
Name: "TV",
Enabled: true,
Paths: []string{root},
}}}
target, err := NewResolver(repo).Resolve(context.Background(), Request{Path: subtree})
if err != nil {
t.Fatalf("Resolve returned error: %v", err)
}
if target.Folder == nil || target.Folder.ID != 8 || target.Mode != ModeSubtree || target.Path != filepath.Clean(subtree) {
t.Fatalf("unexpected target: %#v", target)
}
}
func TestResolverClassifiesVideoFile(t *testing.T) {
root := t.TempDir()
filePath := filepath.Join(root, "Movie (2024).mkv")
if err := os.WriteFile(filePath, []byte("test"), 0o644); err != nil {
t.Fatal(err)
}
repo := &fakeFolderRepo{folders: []*models.MediaFolder{{
ID: 9,
Name: "Movies",
Enabled: true,
Paths: []string{root},
}}}
target, err := NewResolver(repo).Resolve(context.Background(), Request{Path: filePath})
if err != nil {
t.Fatalf("Resolve returned error: %v", err)
}
if target.Folder == nil || target.Folder.ID != 9 || target.Mode != ModeFile || target.Path != filepath.Clean(filePath) {
t.Fatalf("unexpected target: %#v", target)
}
}
func TestResolverRejectsDisabledLibrary(t *testing.T) {
root := t.TempDir()
repo := &fakeFolderRepo{folders: []*models.MediaFolder{{
ID: 10,
Name: "Disabled",
Enabled: false,
Paths: []string{root},
}}}
_, err := NewResolver(repo).Resolve(context.Background(), Request{Path: root})
var reqErr *RequestError
if !errors.As(err, &reqErr) {
t.Fatalf("expected RequestError, got %T: %v", err, err)
}
if reqErr.Status != http.StatusConflict || reqErr.Code != "conflict" {
t.Fatalf("unexpected error: %#v", reqErr)
}
}
func TestResolveAllIsAllOrFail(t *testing.T) {
root := t.TempDir()
valid := filepath.Join(root, "Movie.mkv")
if err := os.WriteFile(valid, []byte("test"), 0o644); err != nil {
t.Fatal(err)
}
repo := &fakeFolderRepo{folders: []*models.MediaFolder{{
ID: 11,
Name: "Movies",
Enabled: true,
Paths: []string{root},
}}}
_, err := NewResolver(repo).ResolveAll(context.Background(), []Request{
{Path: valid},
{Path: filepath.Join(root, "missing.mkv")},
})
var reqErr *RequestError
if !errors.As(err, &reqErr) {
t.Fatalf("expected RequestError, got %T: %v", err, err)
}
if reqErr.Message != "Path does not exist" {
t.Fatalf("unexpected error message: %q", reqErr.Message)
}
}
func TestResolveAllReusesPathOnlyLibraryList(t *testing.T) {
root := t.TempDir()
first := filepath.Join(root, "First.mkv")
second := filepath.Join(root, "Second.mkv")
for _, path := range []string{first, second} {
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
t.Fatal(err)
}
}
repo := &fakeFolderRepo{folders: []*models.MediaFolder{{
ID: 12,
Name: "Movies",
Enabled: true,
Paths: []string{root},
}}}
targets, err := NewResolver(repo).ResolveAll(context.Background(), []Request{
{Path: first},
{Path: second},
})
if err != nil {
t.Fatalf("ResolveAll returned error: %v", err)
}
if len(targets) != 2 {
t.Fatalf("expected two targets, got %d", len(targets))
}
if repo.listCalls != 1 {
t.Fatalf("expected one folder list lookup, got %d", repo.listCalls)
}
}
type fakeQueue struct {
calls []Target
batches [][]Target
batchErr error
}
func (q *fakeQueue) EnqueueScan(_ context.Context, folderID int, mode, path, trigger string) (bool, error) {
q.calls = append(q.calls, Target{Folder: &models.MediaFolder{ID: folderID}, Mode: mode, Path: path, Trigger: trigger})
return true, nil
}
func (q *fakeQueue) EnqueueScans(_ context.Context, targets []Target) error {
copied := append([]Target(nil), targets...)
q.batches = append(q.batches, copied)
if q.batchErr != nil {
return q.batchErr
}
q.calls = append(q.calls, targets...)
return nil
}
func TestEnqueueAllUsesBatchQueue(t *testing.T) {
queue := &fakeQueue{}
folder := &models.MediaFolder{ID: 1}
targets := []Target{
{Folder: folder, Mode: ModeFile, Path: "/media/one.mkv", Trigger: "autoscan"},
{Folder: folder, Mode: ModeFile, Path: "/media/two.mkv", Trigger: "autoscan"},
}
if err := EnqueueAll(context.Background(), queue, targets); err != nil {
t.Fatalf("EnqueueAll returned error: %v", err)
}
if len(queue.batches) != 1 {
t.Fatalf("expected one batch enqueue, got %d", len(queue.batches))
}
if len(queue.calls) != 2 {
t.Fatalf("expected two queued calls from batch, got %d", len(queue.calls))
}
}