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)) } }