package catalogue import ( "strings" "testing" ) // The manifest in a repository and the manifest the mesh holds are two documents. // // A digest is not knowable until something is built, so a repository carrying one would be a // repository whose file is wrong the moment anybody edits anything — and the mesh would be pinning // a value nobody could have checked. func buildable() Manifest { return Manifest{ Module: "meshboard", Version: "1", Build: &Build{Artifacts: []Artifact{ {Name: "server", Kind: ArtifactImage, From: "Dockerfile"}, {Name: "theme", Kind: ArtifactArchive, From: "files"}, }}, Resources: []map[string]any{ {"id": "svc", "type": "container", "name": "meshboard", "artifact": "server"}, {"id": "look", "type": "archive", "path": "/opt/meshboard", "artifact": "theme"}, {"id": "dir", "type": "directory", "path": "/opt/meshboard"}, }, } } func wasBuilt() []Built { return []Built{ {Name: "server", Kind: ArtifactImage, Reference: "registry.invalid/meshboard@sha256:" + strings.Repeat("a", 64)}, {Name: "theme", Kind: ArtifactArchive, Reference: "https://store.invalid/theme.tar.gz", Digest: "sha256:" + strings.Repeat("b", 64)}, } } func TestAResourceNamingAnArtifactBecomesOneNamingTheThing(t *testing.T) { got, err := buildable().Resolve(wasBuilt()) if err != nil { t.Fatal(err) } for _, r := range got.Resources { if r["artifact"] != nil { // A build-time word the host has never heard of. Leaving it would be a field the // strict decoder refuses on the machine, at the worst moment. t.Fatalf("%v still names an artifact: %v", r["id"], r) } } if got.Resources[0]["image"] != "registry.invalid/meshboard@sha256:"+strings.Repeat("a", 64) { t.Fatalf("the image was not filled in: %v", got.Resources[0]) } if got.Resources[1]["source"] != "https://store.invalid/theme.tar.gz" || got.Resources[1]["digest"] != "sha256:"+strings.Repeat("b", 64) { t.Fatalf("the archive was not filled in: %v", got.Resources[1]) } // And a resource that names nothing is untouched. if got.Resources[2]["path"] != "/opt/meshboard" || len(got.Resources[2]) != 3 { t.Fatalf("an ordinary resource was changed: %v", got.Resources[2]) } // The built manifest carries no build section: it is the derived document, and something // holding both would invite somebody to build from it again. if got.Build != nil { t.Fatal("the built manifest still says how to build itself") } } func TestAnArtifactDeclaredAndNotBuiltIsRefused(t *testing.T) { // A build that did not do what the manifest asked. Said here rather than by a machine later // reporting an empty image. _, err := buildable().Resolve(wasBuilt()[:1]) if err == nil { t.Fatal("a manifest resolved with an artifact missing") } if !strings.Contains(err.Error(), "theme") { t.Fatalf("the refusal does not name what is missing: %v", err) } // And it must blame the build rather than the resource. A resource asking for something that // does not exist is a manifest fault; a build not producing what the manifest declared is a // build fault, and telling somebody to fix the wrong one costs an afternoon. if !strings.Contains(err.Error(), "the build did not produce") { t.Fatalf("the refusal blames the wrong thing: %v", err) } } func TestAResourceNamingSomethingTheModuleDoesNotBuildIsRefused(t *testing.T) { m := buildable() m.Resources = append(m.Resources, map[string]any{"id": "other", "type": "archive", "path": "/x", "artifact": "nothing"}) if _, err := m.Resolve(wasBuilt()); err == nil { t.Fatal("a resource naming an artifact nobody builds was accepted") } } func TestAModuleThatBuildsNothingIsOrdinary(t *testing.T) { // Most of what a person installs is configuration. Requiring an empty build section would be // a field that exists to be left blank. m := Manifest{Module: "shell", Version: "1", Resources: []map[string]any{ {"id": "rc", "type": "file", "path": "/etc/zsh/zshrc", "content": "setopt"}, }} got, err := m.Resolve(nil) if err != nil { t.Fatal(err) } if len(got.Resources) != 1 { t.Fatalf("got %v", got.Resources) } } func TestTwoArtifactsWithOneNameAreRefused(t *testing.T) { _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ {"name":"x","kind":"image","from":"Dockerfile"}, {"name":"x","kind":"archive","from":"files"}]}}`)) if err == nil { t.Fatal("two artifacts with one name were accepted") } if !strings.Contains(err.Error(), "could mean either") { t.Fatalf("unhelpful refusal: %v", err) } } func TestAnArtifactBuiltFromOutsideItsRepositoryIsRefused(t *testing.T) { // A build reads its own repository and nothing else. A path leaving it would make what gets // built depend on whatever happens to be on the machine doing the building. for _, from := range []string{"/etc/passwd", "../elsewhere"} { _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ {"name":"x","kind":"archive","from":"` + from + `"}]}}`)) if err == nil { t.Fatalf("%q was accepted as a build input", from) } if !strings.Contains(err.Error(), "outside its own repository") { t.Fatalf("refused for the wrong reason: %v", err) } } } func TestAnArtifactOfAnUnknownKindIsRefused(t *testing.T) { _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[ {"name":"x","kind":"binary","from":"main.go"}]}}`)) if err == nil { t.Fatal("an artifact of an unknown kind was accepted") } }