package comparison import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" ) func TestPlanDestination(t *testing.T) { workingDirectory := t.TempDir() absent := filepath.Join(workingDirectory, "absent") plan, err := PlanDestination(workingDirectory, absent, false) if err != nil { t.Fatalf("PlanDestination() error = %v", err) } if plan.Target != absent || plan.Exists { t.Fatalf("PlanDestination() = %#v, want absent target", plan) } nested := filepath.Join(workingDirectory, "missing-parent", "comparison") if _, err := PlanDestination(workingDirectory, nested, false); err != nil { t.Fatalf("PlanDestination(nested) error = %v", err) } if _, err := os.Stat(filepath.Dir(nested)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("read-only plan created parent: stat error = %v", err) } empty := filepath.Join(workingDirectory, "empty") if err := os.Mkdir(empty, 0o755); err != nil { t.Fatal(err) } plan, err = PlanDestination(workingDirectory, empty, false) if err != nil || !plan.Exists { t.Fatalf("PlanDestination(empty) = %#v, %v", plan, err) } file := filepath.Join(workingDirectory, "file") if err := os.WriteFile(file, []byte("not a directory"), 0o600); err != nil { t.Fatal(err) } assertDestinationErrorKind(t, workingDirectory, file, false, DestinationNotDirectory) assertDestinationErrorKind(t, workingDirectory, workingDirectory, false, DestinationWorkingDirectory) assertDestinationErrorKind(t, workingDirectory, string(os.PathSeparator), false, DestinationFilesystemRoot) assertDestinationErrorKind(t, workingDirectory, "relative", false, DestinationInvalidPath) link := filepath.Join(workingDirectory, "link") if err := os.Symlink(empty, link); err != nil { t.Fatal(err) } assertDestinationErrorKind(t, workingDirectory, link, false, DestinationSymlink) dangling := filepath.Join(workingDirectory, "dangling") if err := os.Symlink(filepath.Join(workingDirectory, "missing"), dangling); err != nil { t.Fatal(err) } assertDestinationErrorKind(t, workingDirectory, dangling, false, DestinationSymlink) assertDestinationErrorKind(t, workingDirectory, filepath.Join(dangling, "child"), false, DestinationInspection) nonempty := filepath.Join(workingDirectory, "nonempty") if err := os.Mkdir(nonempty, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(nonempty, "extra"), []byte("x"), 0o600); err != nil { t.Fatal(err) } assertDestinationErrorKind(t, workingDirectory, nonempty, false, DestinationNotEmpty) assertDestinationErrorKind(t, workingDirectory, nonempty, true, DestinationUnrecognized) } func TestPlanDestinationRejectsUnreadableDirectory(t *testing.T) { if os.Geteuid() == 0 { t.Skip("root can inspect directories regardless of mode") } workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "unreadable") if err := os.Mkdir(target, 0o700); err != nil { t.Fatal(err) } if err := os.Chmod(target, 0); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(target, 0o700) }) assertDestinationErrorKind(t, workingDirectory, target, false, DestinationInspection) } func TestRecognizeBundleRejectsUnsafeAndMalformedContents(t *testing.T) { tests := []struct { name string mutate func(t *testing.T, directory string) }{ {name: "extra file", mutate: func(t *testing.T, directory string) { t.Helper() if err := os.WriteFile(filepath.Join(directory, "extra.txt"), []byte("extra"), 0o600); err != nil { t.Fatal(err) } }}, {name: "subdirectory", mutate: func(t *testing.T, directory string) { t.Helper() if err := os.Mkdir(filepath.Join(directory, "nested"), 0o700); err != nil { t.Fatal(err) } }}, {name: "missing report", mutate: func(t *testing.T, directory string) { t.Helper() if err := os.Remove(filepath.Join(directory, "01-weather-light.md")); err != nil { t.Fatal(err) } }}, {name: "symlink report", mutate: func(t *testing.T, directory string) { t.Helper() path := filepath.Join(directory, "01-weather-light.md") if err := os.Remove(path); err != nil { t.Fatal(err) } if err := os.Symlink(filepath.Join(directory, DataPackageFilename), path); err != nil { t.Fatal(err) } }}, {name: "digest mismatch", mutate: func(t *testing.T, directory string) { t.Helper() if err := os.WriteFile(filepath.Join(directory, DataPackageFilename), []byte("altered"), 0o600); err != nil { t.Fatal(err) } }}, {name: "unknown manifest field", mutate: func(t *testing.T, directory string) { t.Helper() path := filepath.Join(directory, ManifestFilename) data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } data = append(data[:len(data)-2], []byte(",\n \"unknown\": true\n}\n")...) if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatal(err) } }}, {name: "traversal report path", mutate: func(t *testing.T, directory string) { t.Helper() path := filepath.Join(directory, ManifestFilename) data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var manifest Manifest if err := json.Unmarshal(data, &manifest); err != nil { t.Fatal(err) } manifest.Results[0].ReportPath = "../outside.md" data, err = json.Marshal(manifest) if err != nil { t.Fatal(err) } if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatal(err) } }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { directory := publishTestBundle(t, testBundle()) test.mutate(t, directory) if _, err := RecognizeBundle(directory); !errors.Is(err, ErrUnrecognizedBundle) { t.Fatalf("RecognizeBundle() error = %v, want ErrUnrecognizedBundle", err) } }) } } func TestPlanDestinationAcceptsRecognizedReplacement(t *testing.T) { directory := publishTestBundle(t, testBundle()) plan, err := PlanDestination(filepath.Dir(directory), directory, true) if err != nil { t.Fatalf("PlanDestination() error = %v", err) } if !plan.Exists { t.Fatal("PlanDestination() did not record recognized existing destination") } } func TestPublishWritesAndReplacesBundle(t *testing.T) { workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "comparison-daily") plan, err := PlanDestination(workingDirectory, target, false) if err != nil { t.Fatal(err) } if err := Publish(context.Background(), plan, testBundle()); err != nil { t.Fatalf("Publish() error = %v", err) } if _, err := RecognizeBundle(target); err != nil { t.Fatalf("RecognizeBundle() error = %v", err) } assertMode(t, target, 0o700) for _, name := range []string{ManifestFilename, DataPackageFilename, "01-weather-light.md"} { assertMode(t, filepath.Join(target, name), 0o600) } next := testBundle() next.Manifest.Results[0].ProfileID = "weather-balanced" next.Manifest.Results[0].ReportPath = "01-weather-balanced.md" next.Reports[0].Path = "01-weather-balanced.md" next.Reports[0].Markdown = []byte("# Replacement\n") plan, err = PlanDestination(workingDirectory, target, true) if err != nil { t.Fatal(err) } if err := Publish(context.Background(), plan, next); err != nil { t.Fatalf("Publish(replace) error = %v", err) } if _, err := os.Stat(filepath.Join(target, "01-weather-light.md")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("stale report stat error = %v, want not exist", err) } if data, err := os.ReadFile(filepath.Join(target, "01-weather-balanced.md")); err != nil || string(data) != "# Replacement\n" { t.Fatalf("replacement report = %q, %v", data, err) } } func TestPublishReplacesEmptyDirectory(t *testing.T) { workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "comparison-daily") if err := os.Mkdir(target, 0o755); err != nil { t.Fatal(err) } plan, err := PlanDestination(workingDirectory, target, false) if err != nil { t.Fatal(err) } if err := Publish(context.Background(), plan, testBundle()); err != nil { t.Fatalf("Publish() error = %v", err) } if _, err := RecognizeBundle(target); err != nil { t.Fatalf("RecognizeBundle() error = %v", err) } } func TestPublishRestoresExistingBundleAfterReplacementFailure(t *testing.T) { workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "comparison-daily") initial := testBundle() initialPlan, err := PlanDestination(workingDirectory, target, false) if err != nil { t.Fatal(err) } if err := Publish(context.Background(), initialPlan, initial); err != nil { t.Fatal(err) } before := directorySnapshot(t, target) next := testBundle() next.Reports[0].Markdown = []byte("# New\n") plan, err := PlanDestination(workingDirectory, target, true) if err != nil { t.Fatal(err) } calls := 0 err = publish(context.Background(), plan, next, publishOperations{rename: func(oldPath, newPath string) error { calls++ if calls == 2 { return errors.New("replace failed") } return os.Rename(oldPath, newPath) }}) if err == nil { t.Fatal("publish() succeeded despite replacement failure") } after := directorySnapshot(t, target) if !equalSnapshots(before, after) { t.Fatal("failed publication changed the prior bundle") } entries, err := os.ReadDir(workingDirectory) if err != nil { t.Fatal(err) } if len(entries) != 1 || entries[0].Name() != filepath.Base(target) { t.Fatalf("failed publication left sibling artifacts: %#v", entries) } } func TestPublishRetainsBackupWhenRestorationFails(t *testing.T) { workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "comparison-daily") initialPlan, err := PlanDestination(workingDirectory, target, false) if err != nil { t.Fatal(err) } if err := Publish(context.Background(), initialPlan, testBundle()); err != nil { t.Fatal(err) } plan, err := PlanDestination(workingDirectory, target, true) if err != nil { t.Fatal(err) } var backupPath string calls := 0 err = publish(context.Background(), plan, testBundle(), publishOperations{rename: func(oldPath, newPath string) error { calls++ if calls == 1 { backupPath = newPath } if calls == 2 || calls == 3 { return errors.New("rename failed") } return os.Rename(oldPath, newPath) }}) if err == nil || !strings.Contains(err.Error(), backupPath) { t.Fatalf("publish() error = %v, want retained backup path %q", err, backupPath) } if info, statErr := os.Stat(backupPath); statErr != nil || !info.IsDir() { t.Fatalf("backup stat = %v, %v, want retained directory", info, statErr) } } func TestPublishCancellationBeforeCommitLeavesNoDestination(t *testing.T) { workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "comparison-daily") plan, err := PlanDestination(workingDirectory, target, false) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) if err := publish(ctx, plan, testBundle(), publishOperations{rename: os.Rename, beforeCommit: cancel}); !errors.Is(err, context.Canceled) { t.Fatalf("Publish() error = %v, want context cancellation", err) } if _, err := os.Lstat(target); !errors.Is(err, os.ErrNotExist) { t.Fatalf("destination stat error = %v, want not exist", err) } } func assertDestinationErrorKind(t *testing.T, workingDirectory, target string, replace bool, want DestinationErrorKind) { t.Helper() _, err := PlanDestination(workingDirectory, target, replace) var destinationError *DestinationError if !errors.As(err, &destinationError) || destinationError.Kind != want { t.Fatalf("PlanDestination(%q) error = %v, want destination error %q", target, err, want) } } func publishTestBundle(t *testing.T, bundle LogicalBundle) string { t.Helper() workingDirectory := t.TempDir() target := filepath.Join(workingDirectory, "comparison-daily") plan, err := PlanDestination(workingDirectory, target, false) if err != nil { t.Fatal(err) } if err := Publish(context.Background(), plan, bundle); err != nil { t.Fatal(err) } return target } func testBundle() LogicalBundle { dataPackage := []byte("report: daily\n") manifest := validManifest() manifest.DataPackage.SHA256 = SHA256(dataPackage) return LogicalBundle{ Manifest: manifest, DataPackage: dataPackage, Reports: []BundleReport{{ Position: 1, Path: "01-weather-light.md", Markdown: []byte("# Daily\n"), }}, } } func assertMode(t *testing.T, path string, want os.FileMode) { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatal(err) } if got := info.Mode().Perm(); got != want { t.Fatalf("mode for %q = %#o, want %#o", path, got, want) } } func directorySnapshot(t *testing.T, directory string) map[string]string { t.Helper() entries, err := os.ReadDir(directory) if err != nil { t.Fatal(err) } snapshot := make(map[string]string, len(entries)) for _, entry := range entries { data, err := os.ReadFile(filepath.Join(directory, entry.Name())) if err != nil { t.Fatal(err) } snapshot[entry.Name()] = string(data) } return snapshot } func equalSnapshots(left, right map[string]string) bool { if len(left) != len(right) { return false } for name, value := range left { if right[name] != value { return false } } return true } func TestRecognizeBundleRejectsTrailingJSONValue(t *testing.T) { directory := publishTestBundle(t, testBundle()) path := filepath.Join(directory, ManifestFilename) data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if err := os.WriteFile(path, append(data, []byte("{}")...), 0o600); err != nil { t.Fatal(err) } if _, err := RecognizeBundle(directory); !errors.Is(err, ErrUnrecognizedBundle) { t.Fatalf("RecognizeBundle() error = %v, want ErrUnrecognizedBundle", err) } } func TestPlanDestinationRejectsWhitespaceTarget(t *testing.T) { workingDirectory := t.TempDir() assertDestinationErrorKind(t, workingDirectory, " \t", false, DestinationInvalidPath) } func TestRecognizeBundleDoesNotAcceptNonRegularManifest(t *testing.T) { directory := t.TempDir() if err := os.Mkdir(filepath.Join(directory, ManifestFilename), 0o700); err != nil { t.Fatal(err) } if _, err := RecognizeBundle(directory); !strings.Contains(err.Error(), "regular file") { t.Fatalf("RecognizeBundle() error = %v, want regular file rejection", err) } }