Files
silo-server/internal/scanner/scanner_test.go
T

132 lines
4.0 KiB
Go

package scanner
import (
"context"
"os"
"path/filepath"
"sort"
"testing"
"time"
"github.com/Silo-Server/silo-server/internal/models"
)
func TestCollectLogicalFilePaths_PreservesLogicalSymlinkRootPaths(t *testing.T) {
t.Parallel()
base := t.TempDir()
physicalRoot := filepath.Join(base, "real")
logicalRoot := filepath.Join(base, "library")
seasonDir := filepath.Join(physicalRoot, "Season 1")
if err := os.MkdirAll(seasonDir, 0o755); err != nil {
t.Fatalf("mkdir season dir: %v", err)
}
filePath := filepath.Join(seasonDir, "Episode 01.mkv")
if err := os.WriteFile(filePath, []byte("test"), 0o644); err != nil {
t.Fatalf("write media file: %v", err)
}
if err := os.Symlink(physicalRoot, logicalRoot); err != nil {
t.Skipf("symlinks not supported on this platform: %v", err)
}
files, err := collectLogicalFilePaths(context.Background(), []string{logicalRoot}, "series")
if err != nil {
t.Fatalf("collect logical paths: %v", err)
}
want := filepath.Join(logicalRoot, "Season 1", "Episode 01.mkv")
if len(files) != 1 {
t.Fatalf("files len = %d, want 1 (%v)", len(files), files)
}
if files[0] != want {
t.Fatalf("files[0] = %q, want %q", files[0], want)
}
}
func TestCollectLogicalFilePaths_DedupesSharedPhysicalDirsAndCycles(t *testing.T) {
t.Parallel()
base := t.TempDir()
physicalRoot := filepath.Join(base, "real")
aliasRoot := filepath.Join(base, "alias")
loopPath := filepath.Join(physicalRoot, "loop")
if err := os.MkdirAll(physicalRoot, 0o755); err != nil {
t.Fatalf("mkdir root: %v", err)
}
filePath := filepath.Join(physicalRoot, "Movie.mkv")
if err := os.WriteFile(filePath, []byte("test"), 0o644); err != nil {
t.Fatalf("write media file: %v", err)
}
if err := os.Symlink(physicalRoot, aliasRoot); err != nil {
t.Skipf("symlinks not supported on this platform: %v", err)
}
if err := os.Symlink(physicalRoot, loopPath); err != nil {
t.Skipf("symlinks not supported on this platform: %v", err)
}
files, err := collectLogicalFilePaths(context.Background(), []string{physicalRoot, aliasRoot}, "movie")
if err != nil {
t.Fatalf("collect logical paths: %v", err)
}
sort.Strings(files)
want := []string{filepath.Join(physicalRoot, "Movie.mkv")}
if len(files) != len(want) {
t.Fatalf("files len = %d, want %d (%v)", len(files), len(want), files)
}
for i := range want {
if files[i] != want[i] {
t.Fatalf("files[%d] = %q, want %q", i, files[i], want[i])
}
}
}
func TestShouldSkipStableConfirmedFile_DoesNotSkipAssignmentChanges(t *testing.T) {
t.Parallel()
modifiedAt := time.Now().UTC().Truncate(time.Microsecond)
existing := &models.MediaFile{
ContentID: "matched-content",
FileSize: 1_000,
FileModifiedAt: &modifiedAt,
}
if shouldSkipStableConfirmedFile(existing, "matched", 1_000, modifiedAt, []string{"root_assignment_changed"}, false) {
t.Fatal("expected assignment changes to bypass stable-file skip")
}
if !shouldSkipStableConfirmedFile(existing, "matched", 1_000, modifiedAt, nil, false) {
t.Fatal("expected unchanged matched file to use stable-file skip")
}
}
func TestScanStateUpdateReasons_DetectsMissingExternalSubtitle(t *testing.T) {
t.Parallel()
modifiedAt := time.Now().UTC().Truncate(time.Microsecond)
existing := &scanStateFile{
ContentID: "matched-content",
FileSize: 1_000,
FileModifiedAt: &modifiedAt,
ExternalSubtitlePaths: []string{filepath.Join(t.TempDir(), "Movie.en.srt")},
}
reasons := scanStateUpdateReasons(existing, 1_000, modifiedAt, fileRootAssignment{}, fileGroupAssignment{}, "movies", false)
if !testStringSliceContains(reasons, "external_subtitle_missing") {
t.Fatalf("expected external_subtitle_missing reason, got %#v", reasons)
}
if shouldSkipStableConfirmedScanState(existing, "matched", 1_000, modifiedAt, reasons, false) {
t.Fatal("expected missing external subtitle to bypass stable scan-state skip")
}
}
func testStringSliceContains(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}