package pipeline import ( "encoding/json" "path/filepath" "testing" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/buildinfo" "git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/erf" ) const validAsset = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // directManifestFixture returns a minimal valid direct source manifest using the // running builder identity, so validation does not reject it on builder mismatch. func directManifestFixture() *SourceBuildManifest { return &SourceBuildManifest{ Schema: 1, BuilderID: buildinfo.String(), ModuleHAKs: []string{"core_01"}, HAKs: []SourceManifestHAK{ { Name: "core_01", Group: "core", Priority: 1, MaxBytes: 1024, Assets: []string{"core/a.tga"}, }, }, AssetSources: map[string]SourceAsset{ "core/a.tga": {SHA256: validAsset, SizeBytes: 4}, }, } } func writeManifestFixture(t *testing.T, manifest *SourceBuildManifest) string { t.Helper() raw, err := json.Marshal(manifest) if err != nil { t.Fatalf("marshal fixture: %v", err) } path := filepath.Join(t.TempDir(), "build-source-manifest.json") mustWriteFile(t, path, string(raw)) return path } func TestLoadSourceBuildManifestDirect(t *testing.T) { path := writeManifestFixture(t, directManifestFixture()) manifest, err := loadSourceBuildManifest(path) if err != nil { t.Fatalf("load: %v", err) } if manifest.Schema != 1 { t.Fatalf("schema = %d, want 1", manifest.Schema) } if manifest.BuilderID != buildinfo.String() { t.Fatalf("builder_id = %q, want %q", manifest.BuilderID, buildinfo.String()) } if len(manifest.HAKs) != 1 || manifest.HAKs[0].Name != "core_01" { t.Fatalf("unexpected haks: %#v", manifest.HAKs) } source, ok := manifest.AssetSources["core/a.tga"] if !ok || source.SHA256 != validAsset || source.SizeBytes != 4 { t.Fatalf("unexpected asset source: %#v", manifest.AssetSources) } if err := validateDirectSourceManifest(manifest); err != nil { t.Fatalf("validate valid manifest: %v", err) } } func TestValidateDirectSourceManifestRejectsUnsupportedSchema(t *testing.T) { manifest := directManifestFixture() manifest.Schema = 2 if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected schema rejection") } } func TestValidateDirectSourceManifestRejectsBadSHA(t *testing.T) { manifest := directManifestFixture() manifest.AssetSources["core/a.tga"] = SourceAsset{SHA256: "NOTHEX", SizeBytes: 4} if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected bad sha rejection") } } func TestValidateDirectSourceManifestRejectsNegativeSize(t *testing.T) { manifest := directManifestFixture() manifest.AssetSources["core/a.tga"] = SourceAsset{SHA256: validAsset, SizeBytes: -1} if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected negative size rejection") } } func TestValidateDirectSourceManifestRejectsMissingSource(t *testing.T) { manifest := directManifestFixture() delete(manifest.AssetSources, "core/a.tga") if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected missing source rejection") } } func TestValidateDirectSourceManifestRejectsUnusedSource(t *testing.T) { manifest := directManifestFixture() manifest.AssetSources["core/orphan.tga"] = SourceAsset{SHA256: validAsset, SizeBytes: 4} if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected unused source rejection") } } func TestValidateDirectSourceManifestRejectsDuplicateAssetAcrossHAKs(t *testing.T) { manifest := directManifestFixture() manifest.HAKs = append(manifest.HAKs, SourceManifestHAK{ Name: "core_02", Group: "core", Priority: 1, MaxBytes: 1024, Assets: []string{"core/a.tga"}, }) if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected duplicate-asset rejection") } } func TestValidateDirectSourceManifestRejectsTraversalPath(t *testing.T) { manifest := directManifestFixture() manifest.HAKs[0].Assets = []string{"../escape.tga"} manifest.AssetSources = map[string]SourceAsset{ "../escape.tga": {SHA256: validAsset, SizeBytes: 4}, } if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected traversal rejection") } } func TestValidateDirectSourceManifestRejectsBuilderMismatch(t *testing.T) { manifest := directManifestFixture() manifest.BuilderID = "crucible deadbeef" if err := validateDirectSourceManifest(manifest); err == nil { t.Fatal("expected builder mismatch rejection") } } // Two distinct source paths can collapse to the same resref+type; the chunker // must reject that rather than let one silently shadow the other in the ERF. func TestChunksFromManifestRejectsResrefCollision(t *testing.T) { tga, ok := erf.HAKResourceTypeForExtension("tga") if !ok { t.Fatal("tga is not a hak resource type") } mk := func(rel, resref string) assetResource { r := erf.Resource{Name: resref, Type: tga} return assetResource{Rel: rel, Resource: r, Size: erf.ArchiveSize([]erf.Resource{r})} } entry := BuildManifestHAK{Name: "core_01", Group: "core"} collide := []assetResource{mk("creature/foo.tga", "foo"), mk("placeable/foo.tga", "foo")} entry.Assets = []string{"creature/foo.tga", "placeable/foo.tga"} if _, err := chunksFromManifest(collide, []BuildManifestHAK{entry}); err == nil { t.Fatal("expected resref+type collision rejection") } distinct := []assetResource{mk("creature/foo.tga", "foo"), mk("placeable/bar.tga", "bar")} entry.Assets = []string{"creature/foo.tga", "placeable/bar.tga"} if _, err := chunksFromManifest(distinct, []BuildManifestHAK{entry}); err != nil { t.Fatalf("distinct resrefs should pack cleanly: %v", err) } } func TestContentAddressedBlobPath(t *testing.T) { root := "/var/cache/blobs" got, err := contentAddressedBlobPath(root, validAsset) if err != nil { t.Fatalf("blob path: %v", err) } want := filepath.Join(root, "sha256", "aa", "aa", validAsset) if got != want { t.Fatalf("blob path = %q, want %q", got, want) } if _, err := contentAddressedBlobPath(root, "NOTHEX"); err == nil { t.Fatal("expected invalid sha rejection") } if _, err := contentAddressedBlobPath("", validAsset); err == nil { t.Fatal("expected empty root rejection") } }