package overlay import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "sub2api-cn-relay-manager/internal/pack" ) func TestApplyCopiesSourceAndAppliesPatch(t *testing.T) { sourceDir := t.TempDir() if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } packDir := t.TempDir() patchBody := strings.Join([]string{ "diff --git a/backend/hello.txt b/backend/hello.txt", "--- a/backend/hello.txt", "+++ b/backend/hello.txt", "@@ -1 +1 @@", "-hello", "+patched", "", }, "\n") patchPath := filepath.Join(packDir, "overlays", "sample.patch") if err := os.MkdirAll(filepath.Dir(patchPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(patchPath, []byte(patchBody), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } result, err := Apply(context.Background(), ApplyRequest{ PackDir: packDir, SourceDir: sourceDir, Overlays: []pack.HostOverlay{{ OverlayID: "sample", PatchPath: "overlays/sample.patch", }}, }) if err != nil { t.Fatalf("Apply() error = %v", err) } body, err := os.ReadFile(filepath.Join(result.OutputDir, "backend", "hello.txt")) if err != nil { t.Fatalf("ReadFile() error = %v", err) } if string(body) != "patched\n" { t.Fatalf("patched file = %q, want %q", string(body), "patched\n") } if _, err := os.Stat(result.MetadataFilePath); err != nil { t.Fatalf("Stat(metadata) error = %v", err) } } func TestApplySupportsRelativePackDir(t *testing.T) { workspaceDir := t.TempDir() originalWD, err := os.Getwd() if err != nil { t.Fatalf("Getwd() error = %v", err) } if err := os.Chdir(workspaceDir); err != nil { t.Fatalf("Chdir() error = %v", err) } t.Cleanup(func() { _ = os.Chdir(originalWD) }) sourceDir := filepath.Join(workspaceDir, "source") if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } packDir := filepath.Join(workspaceDir, "pack") patchPath := filepath.Join(packDir, "overlays", "sample.patch") if err := os.MkdirAll(filepath.Dir(patchPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } patchBody := strings.Join([]string{ "diff --git a/backend/hello.txt b/backend/hello.txt", "--- a/backend/hello.txt", "+++ b/backend/hello.txt", "@@ -1 +1 @@", "-hello", "+patched", "", }, "\n") if err := os.WriteFile(patchPath, []byte(patchBody), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } result, err := Apply(context.Background(), ApplyRequest{ PackDir: "pack", SourceDir: sourceDir, Overlays: []pack.HostOverlay{{ OverlayID: "sample", PatchPath: "overlays/sample.patch", }}, }) if err != nil { t.Fatalf("Apply() with relative pack dir error = %v", err) } body, err := os.ReadFile(filepath.Join(result.OutputDir, "backend", "hello.txt")) if err != nil { t.Fatalf("ReadFile() error = %v", err) } if string(body) != "patched\n" { t.Fatalf("patched file = %q, want %q", string(body), "patched\n") } } func TestFilterOverlaysRejectsMissingOverlayID(t *testing.T) { _, err := FilterOverlays([]pack.HostOverlay{{OverlayID: "sample"}}, "missing") if err == nil || !strings.Contains(err.Error(), `overlay "missing" did not match`) { t.Fatalf("FilterOverlays() error = %v, want missing overlay detail", err) } } func TestApplyRejectsNestedOutputDir(t *testing.T) { sourceDir := t.TempDir() if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } _, err := Apply(context.Background(), ApplyRequest{ PackDir: t.TempDir(), SourceDir: sourceDir, OutputDir: filepath.Join(sourceDir, "nested-output"), Overlays: []pack.HostOverlay{{OverlayID: "sample", PatchPath: "overlays/sample.patch"}}, }) if err == nil || !strings.Contains(err.Error(), "must not be nested inside source dir") { t.Fatalf("Apply() error = %v, want nested output rejection", err) } } func TestApplyPatchFileRejectsMissingPatch(t *testing.T) { err := applyPatchFile(context.Background(), t.TempDir(), filepath.Join(t.TempDir(), "missing.patch")) if err == nil || !strings.Contains(err.Error(), "stat patch file") { t.Fatalf("applyPatchFile() error = %v, want missing patch stat error", err) } } func TestApplyPatchFileRejectsInvalidPatch(t *testing.T) { outputDir := t.TempDir() patchPath := filepath.Join(t.TempDir(), "invalid.patch") if err := os.WriteFile(patchPath, []byte("not a patch\n"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } err := applyPatchFile(context.Background(), outputDir, patchPath) if err == nil { t.Fatal("applyPatchFile() error = nil, want invalid patch failure") } } func TestWriteMetadataIncludesSourceDirAndOverlays(t *testing.T) { metadataPath := filepath.Join(t.TempDir(), metadataFileName) overlays := []pack.HostOverlay{{OverlayID: "sample", PatchPath: "overlays/sample.patch"}} if err := writeMetadata(metadataPath, "/tmp/source", overlays); err != nil { t.Fatalf("writeMetadata() error = %v", err) } body, err := os.ReadFile(metadataPath) if err != nil { t.Fatalf("ReadFile() error = %v", err) } var decoded map[string]any if err := json.Unmarshal(body, &decoded); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if got, _ := decoded["source_dir"].(string); got != "/tmp/source" { t.Fatalf("source_dir = %q, want %q", got, "/tmp/source") } applied, ok := decoded["applied_overlays"].([]any) if !ok || len(applied) != 1 { t.Fatalf("applied_overlays = %#v, want one overlay", decoded["applied_overlays"]) } } func TestCopyTreeSkipsGitAndPreservesSymlink(t *testing.T) { sourceDir := t.TempDir() outputDir := filepath.Join(t.TempDir(), "output") if err := os.MkdirAll(filepath.Join(sourceDir, ".git", "objects"), 0o755); err != nil { t.Fatalf("MkdirAll(.git) error = %v", err) } if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { t.Fatalf("MkdirAll(backend) error = %v", err) } if err := os.WriteFile(filepath.Join(sourceDir, ".git", "config"), []byte("ignored"), 0o644); err != nil { t.Fatalf("WriteFile(.git/config) error = %v", err) } if err := os.WriteFile(filepath.Join(sourceDir, "backend", "hello.txt"), []byte("hello\n"), 0o644); err != nil { t.Fatalf("WriteFile(hello.txt) error = %v", err) } if err := os.Symlink(filepath.Join("backend", "hello.txt"), filepath.Join(sourceDir, "hello-link")); err != nil { t.Fatalf("Symlink() error = %v", err) } if err := copyTree(sourceDir, outputDir); err != nil { t.Fatalf("copyTree() error = %v", err) } if _, err := os.Stat(filepath.Join(outputDir, ".git", "config")); !os.IsNotExist(err) { t.Fatalf("output .git/config error = %v, want not exist", err) } target, err := os.Readlink(filepath.Join(outputDir, "hello-link")) if err != nil { t.Fatalf("Readlink() error = %v", err) } if target != filepath.Join("backend", "hello.txt") { t.Fatalf("symlink target = %q, want backend/hello.txt", target) } } func TestApplyRejectsExistingOutputDir(t *testing.T) { sourceDir := t.TempDir() if err := os.MkdirAll(filepath.Join(sourceDir, "backend"), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } outputDir := filepath.Join(t.TempDir(), "existing-output") if err := os.MkdirAll(outputDir, 0o755); err != nil { t.Fatalf("MkdirAll(outputDir) error = %v", err) } _, err := Apply(context.Background(), ApplyRequest{ PackDir: t.TempDir(), SourceDir: sourceDir, OutputDir: outputDir, Overlays: []pack.HostOverlay{{OverlayID: "sample", PatchPath: "overlays/sample.patch"}}, }) if err == nil || !strings.Contains(err.Error(), "already exists") { t.Fatalf("Apply() error = %v, want existing output rejection", err) } } func TestCopyFileRejectsMissingSource(t *testing.T) { err := copyFile(filepath.Join(t.TempDir(), "missing.txt"), filepath.Join(t.TempDir(), "target.txt"), 0o644) if err == nil { t.Fatal("copyFile() error = nil, want missing source failure") } }