package debugbundle import ( "bytes" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const testBundleRunID = "run-42-00000000000000000000000000000001" func TestAllocateCreatesRestrictiveSummaryAndTrace(t *testing.T) { parent := t.TempDir() fixed := time.Unix(0, 42).UTC() bundle, err := Allocate(parent, testBundleRunID, fixed) if err != nil { t.Fatal(err) } if bundle.RunID() != testBundleRunID || bundle.CreatedAt() != fixed || bundle.SummaryRoot() != filepath.Join(bundle.Path(), "summary") || bundle.TraceRoot() != filepath.Join(bundle.Path(), "trace") { t.Fatalf("bundle=%#v", bundle) } for _, path := range []string{bundle.Path(), bundle.SummaryRoot(), bundle.TraceRoot()} { info, err := os.Stat(path) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0o700 { t.Fatalf("%s mode=%#o", path, info.Mode().Perm()) } } if err := bundle.Summary().WriteError("failed"); err != nil { t.Fatal(err) } info, err := os.Stat(filepath.Join(bundle.SummaryRoot(), ArtifactErrorLog)) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0o600 { t.Fatalf("file mode=%#o", info.Mode().Perm()) } } func TestAllocateRejectsExistingBundleWithoutChangingIt(t *testing.T) { parent := t.TempDir() bundlePath := filepath.Join(parent, testBundleRunID) if err := os.Mkdir(bundlePath, 0o700); err != nil { t.Fatal(err) } sentinelPath := filepath.Join(bundlePath, "sentinel") sentinel := []byte("existing bundle") if err := os.WriteFile(sentinelPath, sentinel, 0o600); err != nil { t.Fatal(err) } if _, err := Allocate(parent, testBundleRunID, time.Unix(0, 42)); err == nil || !strings.Contains(err.Error(), "already exists") { t.Fatalf("Allocate() error = %v, want collision", err) } if got, err := os.ReadFile(sentinelPath); err != nil || !bytes.Equal(got, sentinel) { t.Fatalf("sentinel = %q, %v", got, err) } } func TestAllocateRejectsUnsafeRunIDsBeforeCreatingParent(t *testing.T) { for _, runID := range []string{"", ".", "..", "../escape", `..\\escape`, "/absolute", " trailing "} { t.Run(runID, func(t *testing.T) { parent := filepath.Join(t.TempDir(), "debug") if _, err := Allocate(parent, runID, time.Time{}); err == nil { t.Fatalf("Allocate(%q) succeeded", runID) } if _, err := os.Stat(parent); !os.IsNotExist(err) { t.Fatalf("debug parent exists or stat failed: %v", err) } }) } } func TestSummaryWriterWritesEverySummaryArtifact(t *testing.T) { bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42)) if err != nil { t.Fatal(err) } summary := bundle.Summary() if err := summary.WriteInvocation(Invocation{Operation: "run"}); err != nil { t.Fatal(err) } if err := summary.WriteRedactedEffectiveConfig(testRedactedSummaryPayload{}); err != nil { t.Fatal(err) } if err := summary.WriteResolvedPipeline(testRedactedResolvedPipelinePayload{}); err != nil { t.Fatal(err) } if err := summary.WriteResolvedReferences(nil); err != nil { t.Fatal(err) } if err := summary.WriteCheckpointEvents(nil); err != nil { t.Fatal(err) } if err := summary.WriteRunManifest(artifacts.RunManifest{RunID: bundle.RunID()}); err != nil { t.Fatal(err) } if err := summary.WriteChunkPlan(artifacts.ChunkPlanSummary{}); err != nil { t.Fatal(err) } if err := summary.WriteRunReport(RunReport{RunID: bundle.RunID(), PipelineID: "test"}); err != nil { t.Fatal(err) } if err := summary.WriteWarnings([]contracts.Warning{{ReasonCode: "test"}}); err != nil { t.Fatal(err) } if err := summary.WriteError("failed"); err != nil { t.Fatal(err) } for _, name := range []string{ ArtifactInvocationMetadata, ArtifactEffectiveConfig, ArtifactResolvedPipeline, ArtifactResolvedReferences, ArtifactCheckpointEvents, ArtifactRunManifest, ArtifactChunkPlan, ArtifactRunReport, ArtifactWarnings, ArtifactErrorLog, } { info, err := os.Stat(filepath.Join(bundle.SummaryRoot(), name)) if err != nil { t.Fatalf("summary artifact %q: %v", name, err) } if info.Mode().Perm() != 0o600 { t.Fatalf("summary artifact %q mode=%#o", name, info.Mode().Perm()) } } } func TestSummaryWriterConfinesArtifacts(t *testing.T) { bundle, err := Allocate(t.TempDir(), testBundleRunID, time.Unix(0, 42)) if err != nil { t.Fatal(err) } if err := bundle.Summary().WriteJSON("../outside.json", map[string]any{}); err == nil { t.Fatal("accepted traversal") } if err := bundle.Summary().WriteBytes(`trace\\x`, []byte("x")); err == nil { t.Fatal("accepted backslash") } } type testRedactedSummaryPayload struct{} func (testRedactedSummaryPayload) RedactedSummaryPayload() any { return map[string]any{"redacted": true} } type testRedactedResolvedPipelinePayload struct{} func (testRedactedResolvedPipelinePayload) RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline { return pipeline.ResolvedPipeline{ID: "redacted"} }