package main import ( "archive/tar" "compress/gzip" "os" "path/filepath" "reflect" "sort" "testing" "time" ) func TestExtractedDirName(t *testing.T) { t.Parallel() tests := []struct { name string in string want string }{ {name: "plain", in: "demo.tar.gz", want: "demo"}, {name: "mixed case suffix", in: "Demo.TAR.GZ", want: "Demo"}, {name: "other name", in: "demo.zip", want: "demo.zip"}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := extractedDirName(tt.in) if got != tt.want { t.Fatalf("extractedDirName(%q) = %q, want %q", tt.in, got, tt.want) } }) } } func TestIsStableEnough(t *testing.T) { t.Parallel() now := time.Date(2026, 4, 17, 12, 0, 0, 0, time.UTC) cfg := config{ stableFor: 30 * time.Second, stableScans: 3, } tests := []struct { name string stableCount int modTime time.Time want bool }{ {name: "not enough scans", stableCount: 2, modTime: now.Add(-1 * time.Minute), want: false}, {name: "not enough age", stableCount: 3, modTime: now.Add(-20 * time.Second), want: false}, {name: "enough scans and age", stableCount: 3, modTime: now.Add(-45 * time.Second), want: true}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := isStableEnough(cfg, tt.stableCount, fileSnapshot{modTime: tt.modTime}, now) if got != tt.want { t.Fatalf("isStableEnough(...) = %t, want %t", got, tt.want) } }) } } func TestParseExtensions(t *testing.T) { t.Parallel() got, err := parseExtensions(".csv, txt;PDF \n tar.gz") if err != nil { t.Fatalf("parseExtensions() error = %v", err) } want := []string{".csv", ".txt", ".pdf", ".tar.gz"} if !reflect.DeepEqual(got, want) { t.Fatalf("parseExtensions() = %#v, want %#v", got, want) } } func TestEvaluateGenericDuplicateNameSize(t *testing.T) { t.Parallel() dir := t.TempDir() sourcePath := filepath.Join(dir, "source.csv") targetPath := filepath.Join(dir, "target.csv") if err := os.WriteFile(sourcePath, []byte("abcd"), 0o644); err != nil { t.Fatalf("write source error = %v", err) } if err := os.WriteFile(targetPath, []byte("wxyz"), 0o644); err != nil { t.Fatalf("write target error = %v", err) } decision, duplicate, err := evaluateGenericDuplicate(sourcePath, targetPath, genericDedupeNameSize) if err != nil { t.Fatalf("evaluateGenericDuplicate() error = %v", err) } if !duplicate { t.Fatalf("expected same-size file to be treated as duplicate") } if decision == "" { t.Fatalf("expected non-empty decision") } } func TestEvaluateGenericDuplicateNameHash(t *testing.T) { t.Parallel() dir := t.TempDir() sourcePath := filepath.Join(dir, "source.csv") targetPath := filepath.Join(dir, "target.csv") if err := os.WriteFile(sourcePath, []byte("same-content"), 0o644); err != nil { t.Fatalf("write source error = %v", err) } if err := os.WriteFile(targetPath, []byte("same-content"), 0o644); err != nil { t.Fatalf("write target error = %v", err) } _, duplicate, err := evaluateGenericDuplicate(sourcePath, targetPath, genericDedupeNameHash) if err != nil { t.Fatalf("evaluateGenericDuplicate() error = %v", err) } if !duplicate { t.Fatalf("expected same-hash file to be treated as duplicate") } if err := os.WriteFile(targetPath, []byte("DIFF-content"), 0o644); err != nil { t.Fatalf("rewrite target error = %v", err) } _, duplicate, err = evaluateGenericDuplicate(sourcePath, targetPath, genericDedupeNameHash) if err != nil { t.Fatalf("evaluateGenericDuplicate() error = %v", err) } if duplicate { t.Fatalf("expected different-hash file not to be treated as duplicate") } } func TestTransferFileMoveRemovesSource(t *testing.T) { t.Parallel() dir := t.TempDir() sourcePath := filepath.Join(dir, "source.csv") targetPath := filepath.Join(dir, "target.csv") want := []byte("move-me") if err := os.WriteFile(sourcePath, want, 0o644); err != nil { t.Fatalf("write source error = %v", err) } if err := transferFile(sourcePath, targetPath, "move"); err != nil { t.Fatalf("transferFile() error = %v", err) } if _, err := os.Stat(sourcePath); !os.IsNotExist(err) { t.Fatalf("expected source to be removed after move, stat err = %v", err) } got, err := os.ReadFile(targetPath) if err != nil { t.Fatalf("read target error = %v", err) } if !reflect.DeepEqual(got, want) { t.Fatalf("target content = %q, want %q", got, want) } } func TestProcessGenericCandidateMoveDuplicateDeletesSourceAfterHashVerification(t *testing.T) { t.Parallel() rootDir := t.TempDir() sourceDir := filepath.Join(rootDir, "source") targetDir := filepath.Join(rootDir, "target") if err := os.MkdirAll(sourceDir, 0o755); err != nil { t.Fatalf("mkdir source error = %v", err) } if err := os.MkdirAll(targetDir, 0o755); err != nil { t.Fatalf("mkdir target error = %v", err) } sourcePath := filepath.Join(sourceDir, "dup.csv") targetPath := filepath.Join(targetDir, "dup.csv") data := []byte("same-content") if err := os.WriteFile(sourcePath, data, 0o644); err != nil { t.Fatalf("write source error = %v", err) } if err := os.WriteFile(targetPath, data, 0o644); err != nil { t.Fatalf("write target error = %v", err) } cfg := config{ mode: "move", generic: genericRule{ targetDir: targetDir, dedupeMode: genericDedupeNameSize, }, } decision, handled, err := processGenericCandidate(cfg, sourcePath) if err != nil { t.Fatalf("processGenericCandidate() error = %v", err) } if !handled { t.Fatalf("expected duplicate move to be treated as handled") } if decision == "" { t.Fatalf("expected non-empty decision") } if _, err := os.Stat(sourcePath); !os.IsNotExist(err) { t.Fatalf("expected source to be removed, stat err = %v", err) } } func TestProcessGenericCandidateMoveDuplicateKeepsSourceWhenHashDiffers(t *testing.T) { t.Parallel() rootDir := t.TempDir() sourceDir := filepath.Join(rootDir, "source") targetDir := filepath.Join(rootDir, "target") if err := os.MkdirAll(sourceDir, 0o755); err != nil { t.Fatalf("mkdir source error = %v", err) } if err := os.MkdirAll(targetDir, 0o755); err != nil { t.Fatalf("mkdir target error = %v", err) } sourcePath := filepath.Join(sourceDir, "dup.csv") targetPath := filepath.Join(targetDir, "dup.csv") if err := os.WriteFile(sourcePath, []byte("abcd"), 0o644); err != nil { t.Fatalf("write source error = %v", err) } if err := os.WriteFile(targetPath, []byte("wxyz"), 0o644); err != nil { t.Fatalf("write target error = %v", err) } cfg := config{ mode: "move", generic: genericRule{ targetDir: targetDir, dedupeMode: genericDedupeNameSize, }, } _, handled, err := processGenericCandidate(cfg, sourcePath) if err != nil { t.Fatalf("processGenericCandidate() error = %v", err) } if handled { t.Fatalf("expected hash mismatch not to be treated as handled duplicate") } if _, err := os.Stat(sourcePath); err != nil { t.Fatalf("expected source to remain, stat err = %v", err) } } func TestProcessArchiveCandidateMoveDuplicateZipDeletesSource(t *testing.T) { t.Parallel() rootDir := t.TempDir() sourceDir := filepath.Join(rootDir, "source") zipDir := filepath.Join(rootDir, "zip") unzipDir := filepath.Join(rootDir, "unzip") for _, dir := range []string{sourceDir, zipDir, unzipDir} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("mkdir %q error = %v", dir, err) } } sourcePath := filepath.Join(sourceDir, "demo.tar.gz") targetPath := filepath.Join(zipDir, "demo.tar.gz") data := []byte("archive-binary") if err := os.WriteFile(sourcePath, data, 0o644); err != nil { t.Fatalf("write source archive error = %v", err) } if err := os.WriteFile(targetPath, data, 0o644); err != nil { t.Fatalf("write target archive error = %v", err) } cfg := config{ mode: "move", archive: archiveRule{ zipDir: zipDir, unzipDir: unzipDir, }, } decision, handled, err := processArchiveCandidate(cfg, sourcePath) if err != nil { t.Fatalf("processArchiveCandidate() error = %v", err) } if !handled { t.Fatalf("expected duplicate archive move to be treated as handled") } if decision == "" { t.Fatalf("expected non-empty decision") } if _, err := os.Stat(sourcePath); !os.IsNotExist(err) { t.Fatalf("expected source archive to be removed, stat err = %v", err) } } func TestProcessArchiveCandidateMoveDuplicateUnzipDeletesSource(t *testing.T) { t.Parallel() rootDir := t.TempDir() sourceDir := filepath.Join(rootDir, "source") zipDir := filepath.Join(rootDir, "zip") unzipDir := filepath.Join(rootDir, "unzip") for _, dir := range []string{sourceDir, zipDir, unzipDir} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("mkdir %q error = %v", dir, err) } } sourcePath := filepath.Join(sourceDir, "demo.tar.gz") unzipTarget := filepath.Join(unzipDir, "demo") files := map[string]string{ "a.txt": "hello", "nested/b.json": `{"ok":true}`, "nested/c/data": "payload", } if err := writeTarGz(sourcePath, "demo", files); err != nil { t.Fatalf("write tar.gz error = %v", err) } if err := writeExtractedDir(unzipTarget, files); err != nil { t.Fatalf("write extracted dir error = %v", err) } cfg := config{ mode: "move", archive: archiveRule{ zipDir: zipDir, unzipDir: unzipDir, }, } decision, handled, err := processArchiveCandidate(cfg, sourcePath) if err != nil { t.Fatalf("processArchiveCandidate() error = %v", err) } if !handled { t.Fatalf("expected matching unzip directory to be treated as handled duplicate") } if decision == "" { t.Fatalf("expected non-empty decision") } if _, err := os.Stat(sourcePath); !os.IsNotExist(err) { t.Fatalf("expected source archive to be removed, stat err = %v", err) } } func TestProcessArchiveCandidateMoveDuplicateUnzipKeepsSourceWhenContentDiffers(t *testing.T) { t.Parallel() rootDir := t.TempDir() sourceDir := filepath.Join(rootDir, "source") zipDir := filepath.Join(rootDir, "zip") unzipDir := filepath.Join(rootDir, "unzip") for _, dir := range []string{sourceDir, zipDir, unzipDir} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("mkdir %q error = %v", dir, err) } } sourcePath := filepath.Join(sourceDir, "demo.tar.gz") unzipTarget := filepath.Join(unzipDir, "demo") if err := writeTarGz(sourcePath, "demo", map[string]string{"a.txt": "hello"}); err != nil { t.Fatalf("write tar.gz error = %v", err) } if err := writeExtractedDir(unzipTarget, map[string]string{"a.txt": "DIFF"}); err != nil { t.Fatalf("write extracted dir error = %v", err) } cfg := config{ mode: "move", archive: archiveRule{ zipDir: zipDir, unzipDir: unzipDir, }, } _, handled, err := processArchiveCandidate(cfg, sourcePath) if err != nil { t.Fatalf("processArchiveCandidate() error = %v", err) } if handled { t.Fatalf("expected different unzip content not to be treated as handled duplicate") } if _, err := os.Stat(sourcePath); err != nil { t.Fatalf("expected source archive to remain, stat err = %v", err) } } func TestSaveAndLoadSettings(t *testing.T) { t.Parallel() settingsPath := filepath.Join(t.TempDir(), "settings.json") want := formValues{ ArchiveEnabled: true, ArchiveSourceDir: `\\server\share\incoming`, ArchiveZipDir: `D:\zip`, ArchiveUnzipDir: `D:\unzip`, GenericEnabled: true, GenericSourceDir: `\\server\share\others`, GenericTargetDir: `D:\files`, GenericExts: ".csv,.txt", GenericDedupe: genericDedupeNameHash, Interval: "15s", StableFor: "1m", StableScans: "3", Mode: "move", Recursive: true, } if err := saveSettings(settingsPath, want); err != nil { t.Fatalf("saveSettings() error = %v", err) } got, err := loadSettings(settingsPath) if err != nil { t.Fatalf("loadSettings() error = %v", err) } if !reflect.DeepEqual(got, want) { t.Fatalf("loadSettings() = %#v, want %#v", got, want) } } func TestLoadLegacySettings(t *testing.T) { t.Parallel() settingsPath := filepath.Join(t.TempDir(), "legacy.json") legacyJSON := `{ "source_dir": "\\\\server\\share\\incoming", "zip_dir": "D:\\zip", "unzip_dir": "D:\\unzip", "interval": "15s", "stable_for": "1m", "stable_scans": "3", "mode": "copy", "recursive": true }` if err := os.WriteFile(settingsPath, []byte(legacyJSON), 0o644); err != nil { t.Fatalf("write legacy settings error = %v", err) } got, err := loadSettings(settingsPath) if err != nil { t.Fatalf("loadSettings() error = %v", err) } if !got.ArchiveEnabled { t.Fatalf("expected archive rule to be enabled for legacy settings") } if got.ArchiveSourceDir != `\\server\share\incoming` { t.Fatalf("unexpected archive source = %q", got.ArchiveSourceDir) } if got.GenericEnabled { t.Fatalf("did not expect generic rule to be enabled for legacy settings") } if got.GenericDedupe != genericDedupeNameSize { t.Fatalf("expected legacy settings to default to name_size dedupe") } } func writeTarGz(targetPath, rootName string, files map[string]string) error { file, err := os.Create(targetPath) if err != nil { return err } defer file.Close() gzipWriter := gzip.NewWriter(file) defer gzipWriter.Close() tarWriter := tar.NewWriter(gzipWriter) defer tarWriter.Close() paths := make([]string, 0, len(files)) for name := range files { paths = append(paths, name) } sort.Strings(paths) for _, name := range paths { body := []byte(files[name]) header := &tar.Header{ Name: filepath.ToSlash(filepath.Join(rootName, name)), Mode: 0o644, Size: int64(len(body)), } if err := tarWriter.WriteHeader(header); err != nil { return err } if _, err := tarWriter.Write(body); err != nil { return err } } return nil } func writeExtractedDir(root string, files map[string]string) error { for name, body := range files { targetPath := filepath.Join(root, filepath.FromSlash(name)) if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { return err } if err := os.WriteFile(targetPath, []byte(body), 0o644); err != nil { return err } } return nil }