package testutil import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/state" "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 } type DestinationStateOptions struct { PipelineID string DestinationID string DistributorVersion string PublishedAt time.Time } 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 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 WriteDestinationState(t testing.TB, root, relative string, manifest bundle.Manifest, opts DestinationStateOptions) state.DistributorState { t.Helper() bundleRoot := filepath.Join(root, filepath.FromSlash(relative)) if err := os.MkdirAll(bundleRoot, 0o755); err != nil { t.Fatalf("mkdir destination bundle: %v", err) } destinationState := DestinationState(manifest, opts) writeJSONFile(t, filepath.Join(bundleRoot, storage.StateFileName), destinationState) return destinationState } func DestinationState(manifest bundle.Manifest, opts DestinationStateOptions) state.DistributorState { return state.DistributorState{ SchemaVersion: state.SchemaVersion, DistributorVersion: opts.DistributorVersion, PipelineID: defaultString(opts.PipelineID, "reports"), DestinationID: defaultString(opts.DestinationID, "archive"), PublishedAt: defaultPublishedAt(opts.PublishedAt), Source: state.SourceState{Manifest: manifest}, Outputs: sourceOutputs(manifest), } } func ReadDestinationState(t testing.TB, path string) state.DistributorState { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("read destination state: %v", err) } destinationState, err := state.Parse(data) if err != nil { t.Fatalf("parse destination state: %v", err) } return destinationState } 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 sourceOutputs(manifest bundle.Manifest) []state.OutputFile { outputs := make([]state.OutputFile, 0, len(manifest.Files)) for _, file := range manifest.Files { outputs = append(outputs, state.OutputFile{ Path: file.Path, Kind: state.OutputKindSource, SourcePath: file.Path, SHA256: file.SHA256, Size: file.Size, }) } return outputs } 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 } func defaultPublishedAt(value time.Time) time.Time { if value.IsZero() { return time.Date(2026, 5, 30, 11, 12, 0, 0, time.UTC) } return value }