package testutil import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" ) const DefaultBundleID = "weather.daily.brentwood.2026-05-30" var DefaultCreated = time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC) type SourceFile struct { Path string Data string } type BundleOptions struct { ID string Created time.Time Files []SourceFile ExtraFiles []SourceFile } func DefaultSourceFiles() []SourceFile { return []SourceFile{ {Path: "report.md", Data: "# Report\nSunny.\n"}, {Path: "summary.txt", Data: "Summary\n"}, } } func ValidManifest(opts BundleOptions) bundle.Manifest { files := sourceFiles(opts) manifestFiles := make([]bundle.ManifestFile, 0, len(files)) for _, file := range files { data := []byte(file.Data) manifestFiles = append(manifestFiles, bundle.ManifestFile{ Path: file.Path, SHA256: bundle.FileDigest(data), Size: int64(len(data)), }) } manifest := bundle.Manifest{ SchemaVersion: 1, ID: defaultBundleID(opts.ID), Created: defaultCreated(opts.Created), Files: manifestFiles, } manifest.Digest = bundle.BundleDigest(manifest.Files) return manifest } func WriteSourceBundle(t testing.TB, root, relative string, opts BundleOptions) bundle.Manifest { t.Helper() bundleRoot := filepath.Join(root, filepath.FromSlash(relative)) if err := os.MkdirAll(bundleRoot, 0o755); err != nil { t.Fatalf("mkdir source bundle: %v", err) } manifest := ValidManifest(opts) for _, file := range sourceFiles(opts) { path := filepath.Join(bundleRoot, filepath.FromSlash(file.Path)) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("mkdir source file parent: %v", err) } if err := os.WriteFile(path, []byte(file.Data), 0o600); err != nil { t.Fatalf("write source file: %v", err) } } writeJSONFile(t, filepath.Join(bundleRoot, bundle.ManifestName), manifest) return manifest } func WriteFakeSourceBundle(t testing.TB, backend *fake.Backend, relative string, opts BundleOptions) bundle.Bundle { t.Helper() manifest := ValidManifest(opts) for _, file := range sourceFiles(opts) { path := joinStoragePath(t, relative, file.Path) if _, err := backend.WriteFile(context.Background(), path, []byte(file.Data), storage.WriteOptions{}); err != nil { t.Fatalf("write fake source file: %v", err) } } data, err := json.MarshalIndent(manifest, "", " ") if err != nil { t.Fatalf("marshal manifest: %v", err) } manifestPath := joinStoragePath(t, relative, bundle.ManifestName) if _, err := backend.WriteFile(context.Background(), manifestPath, append(data, '\n'), storage.WriteOptions{}); err != nil { t.Fatalf("write fake manifest: %v", err) } return bundle.Bundle{RootRelativePath: relative, Manifest: manifest} } func WriteFakeFile(t testing.TB, backend *fake.Backend, path, data string) { t.Helper() if _, err := backend.WriteFile(context.Background(), path, []byte(data), storage.WriteOptions{}); err != nil { t.Fatalf("write fake file %s: %v", path, err) } } func AssertFakeFile(t testing.TB, backend *fake.Backend, path, want string) { t.Helper() data, err := backend.ReadFile(context.Background(), path) if err != nil { t.Fatalf("read fake file %s: %v", path, err) } if got := string(data); got != want { t.Fatalf("fake file %s = %q, want %q", path, got, want) } } func AssertFakeMissing(t testing.TB, backend *fake.Backend, path string) { t.Helper() if _, err := backend.Stat(context.Background(), path); !storage.IsNotFound(err) { t.Fatalf("fake file %s stat error = %v, want not found", path, err) } } func WriteMinimalLocalConfig(t testing.TB, sourceRoot, destinationRoot string) string { t.Helper() return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` `) } func WriteFanoutLocalConfig(t testing.TB, sourceRoot, firstDestinationRoot, secondDestinationRoot string) string { t.Helper() return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive-one backend: local path: `+firstDestinationRoot+` - id: archive-two backend: local path: `+secondDestinationRoot+` `) } func WriteLocalConfigWithPublishPolicy(t testing.TB, sourceRoot, destinationRoot string, publishSource, publishHTML bool) string { t.Helper() transformConfig := "" if publishHTML { transformConfig = ` transform: markdown_to_html: enabled: true mode: sidecar` } return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` publish: source: `+fmt.Sprintf("%t", publishSource)+` html: `+fmt.Sprintf("%t", publishHTML)+transformConfig+` `) } func WriteLocalConfigWithPathMapping(t testing.TB, sourceRoot, destinationRoot, mode string) string { t.Helper() return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` path_mapping: mode: `+mode+` `) } func WriteLocalConfigWithLinks(t testing.TB, sourceRoot, destinationRoot, pathMapping, baseURL, primary string, publishSource, publishHTML bool, transformMode string) string { t.Helper() transformConfig := "" if publishHTML { transformConfig = ` transform: markdown_to_html: enabled: true mode: ` + transformMode } return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` path_mapping: mode: `+pathMapping+` links: base_url: `+baseURL+` primary: `+primary+` publish: source: `+fmt.Sprintf("%t", publishSource)+` html: `+fmt.Sprintf("%t", publishHTML)+transformConfig+` `) } func WriteLocalConfigWithMarkdownTransform(t testing.TB, sourceRoot, destinationRoot string, publishSource, publishHTML bool, mode, input string) string { t.Helper() enabled := publishHTML inputConfig := "" if input != "" { inputConfig = ` input: ` + input } return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+destinationRoot+` publish: source: `+fmt.Sprintf("%t", publishSource)+` html: `+fmt.Sprintf("%t", publishHTML)+` transform: markdown_to_html: enabled: `+fmt.Sprintf("%t", enabled)+` mode: `+mode+inputConfig+` `) } func WriteMixedPolicyFanoutLocalConfig(t testing.TB, sourceRoot, archiveDestination, htmlDestination string) string { t.Helper() return writeConfigFile(t, ` pipelines: - id: reports source: backend: local path: `+sourceRoot+` destinations: - id: archive backend: local path: `+archiveDestination+` publish: source: true html: false - id: html backend: local path: `+htmlDestination+` publish: source: false html: true transform: markdown_to_html: enabled: true mode: sidecar `) } func AssertFile(t testing.TB, path, want string) { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read file %s: %v", path, err) } if got := string(data); got != want { t.Fatalf("%s = %q, want %q", path, got, want) } } func AssertFileContains(t testing.TB, path, want string) { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read file %s: %v", path, err) } if !strings.Contains(string(data), want) { t.Fatalf("%s = %q, want substring %q", path, data, want) } } func sourceFiles(opts BundleOptions) []SourceFile { files := opts.Files if files == nil { files = DefaultSourceFiles() } else { files = append([]SourceFile(nil), files...) } files = append(files, opts.ExtraFiles...) return files } func writeConfigFile(t testing.TB, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.yml") if err := os.WriteFile(path, []byte(strings.TrimSpace(body)+"\n"), 0o600); err != nil { t.Fatalf("write config: %v", err) } return path } func writeJSONFile(t testing.TB, path string, value any) { t.Helper() data, err := json.MarshalIndent(value, "", " ") if err != nil { t.Fatalf("marshal json: %v", err) } data = append(data, '\n') if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatalf("write json file: %v", err) } } func joinStoragePath(t testing.TB, root, path string) string { t.Helper() joined, err := storage.Join(root, path) if err != nil { t.Fatalf("join storage path: %v", err) } return joined } func defaultBundleID(value string) string { return defaultString(value, DefaultBundleID) } func defaultString(value, fallback string) string { if value != "" { return value } return fallback } func defaultCreated(value time.Time) time.Time { if value.IsZero() { return DefaultCreated } return value }