package catalogue import ( "encoding/json" "strings" "testing" ) // Each test names the decision it defends: novox/hq ADR 0225, which refines ADR 0049 after issue 263. // The provision a module requires sets the bound on its identity, not the tightest backend anywhere. func TestABoundIsTheProvisionsOwn(t *testing.T) { store := Manifest{Module: "objects", Provides: []Offer{{Name: "s3-bucket", Scope: ScopeMesh, Identity: &OfferIdentity{Max: 20, In: "an S3 access key"}}}, Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}} database := Manifest{Module: "db", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh, Identity: &OfferIdentity{Max: 63, In: "a PostgreSQL role"}}}, Receives: map[string]string{"postgres-database": "/var/lib/mesh/db/mesh.json"}} if b := store.IdentityBoundOf("s3-bucket"); b.Max != 20 || b.In != "an S3 access key" { t.Errorf("an object store's stated bound was not taken: %+v", b) } if b := database.IdentityBoundOf("postgres-database"); b.Max != 63 { t.Errorf("a database's stated bound was not taken: %+v", b) } // mesh_workstation_keycloak is 25: refused by the object store, accepted by the database. if CheckIdentityWithin("workstation", "keycloak", store.IdentityBoundOf("s3-bucket")) == nil { t.Error("a 25-character identity fit a 20-character access key") } if err := CheckIdentityWithin("workstation", "keycloak", database.IdentityBoundOf("postgres-database")); err != nil { t.Errorf("a database consumer paid the object store's limit: %v", err) } } // What an offer leaves unsaid follows from whether its provider can keep a name at all. func TestAnUnstatedBoundFollowsWhatTheProviderIsTold(t *testing.T) { // Told nothing about its consumers — no receives, nothing served from their identity: no bound. // The resolver provision is exactly this, and its consumers paid an object store's limit (263). resolver := Manifest{Module: "resolver", Provides: []Offer{{Name: "wildcard-resolution", Scope: ScopeMesh}}} if b := resolver.IdentityBoundOf("wildcard-resolution"); b.Bounded() { t.Errorf("a provision that is told nothing of its consumers bounds them: %+v", b) } // Told each consumer and silent about its backend: the old global bound, not none. told := Manifest{Module: "told", Provides: []Offer{{Name: "thing", Scope: ScopeMesh}}, Receives: map[string]string{"thing": "/var/lib/mesh/told/mesh.json"}} if b := told.IdentityBoundOf("thing"); b.Max != DefaultIdentityLimit { t.Errorf("a provider told its consumers and silent about its backend is not held to %d: %+v", DefaultIdentityLimit, b) } // Serving a value built from the identity is being told it, too. serving := Manifest{Module: "serving", Provides: []Offer{{Name: "bucket", Scope: ScopeMesh}}, Serves: map[string]map[string]any{"bucket": {"name": "b-${consumer:as:dns}"}}} if b := serving.IdentityBoundOf("bucket"); b.Max != DefaultIdentityLimit { t.Errorf("a provider deriving a name from its consumers is not bounded: %+v", b) } // And `false` says it outright, even for a provider that receives. routes := Manifest{Module: "routes", Provides: []Offer{{Name: "route", Scope: ScopeMesh, Identity: &OfferIdentity{None: true}}}, Receives: map[string]string{"route": "/var/lib/mesh/routes/mesh.json"}} if b := routes.IdentityBoundOf("route"); b.Bounded() { t.Errorf("`identity: false` still bounds: %+v", b) } } // The field reads as written and writes back the same, and refuses what says nothing. func TestAnOffersIdentityIsParsedStrictly(t *testing.T) { for _, raw := range []string{ `{"name":"s3-bucket","scope":"mesh","identity":{"max":20,"in":"an S3 access key"}}`, `{"name":"wildcard-resolution","scope":"mesh","identity":false}`, `{"name":"redis-cache","scope":"mesh","identity":{"in":"a Redis ACL user"}}`, } { var o Offer if err := json.Unmarshal([]byte(raw), &o); err != nil { t.Fatalf("%s: %v", raw, err) } back, err := json.Marshal(o) if err != nil || string(back) != raw { t.Errorf("did not round-trip:\n%s\n%s (%v)", raw, back, err) } } for _, raw := range []string{ `{"name":"x","identity":true}`, `{"name":"x","identity":{"max":20,"in":"y","most":3}}`, } { var o Offer if err := json.Unmarshal([]byte(raw), &o); err == nil { t.Errorf("accepted %s", raw) } } bad := Manifest{Module: "bad", Version: "1", Provides: []Offer{ {Name: "unsaid", Scope: ScopeMesh, Identity: &OfferIdentity{Max: 20}}, {Name: "tiny", Scope: ScopeMesh, Identity: &OfferIdentity{Max: 4, In: "nothing usable"}}, }} raw, err := json.Marshal(bad) if err != nil { t.Fatal(err) } _, err = ParseManifest(raw) if err == nil || !strings.Contains(err.Error(), "without saying what keeps them") || !strings.Contains(err.Error(), "the shortest the mesh makes") { t.Fatalf("a bound with no `in`, or too short for any identity, was accepted: %v", err) } } // Refused before merge: the catalogue check judges each module's identity, on the longest machine // name, against the bound of every provision it wants — and names the module and the slug to set. func TestTheCatalogueCheckRefusesAnIdentityThatOverflowsWhatItRequires(t *testing.T) { shelf := Shelf{ "objects": {Module: "objects", Provides: []Offer{{Name: "s3-bucket", Scope: ScopeMesh, Identity: &OfferIdentity{Max: 20, In: "an S3 access key"}}}, Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}}, "photoalbum": {Module: "photoalbum", Requires: []string{"s3-bucket"}}, "files": {Module: "files", Requires: []string{"s3-bucket"}}, } problems := IdentityProblems(shelf, 6) if len(problems) != 1 || !strings.Contains(problems[0], "photoalbum wants s3-bucket") || !strings.Contains(problems[0], `"mesh_nnnnnn_photoalbum", 22`) || !strings.Contains(problems[0], "`slug` of at most 8 characters") { t.Fatalf("one overflow, named with its remedy, was expected: %q", problems) } // A longer machine name refuses more: the check is about the mesh's machines, not one. if got := IdentityProblems(shelf, 10); len(got) != 2 { t.Fatalf("on a 10-character name both overflow (mesh_nnnnnnnnnn_files is 21): %q", got) } // And a slug is the remedy it names. album := shelf["photoalbum"] album.Slug = "album" shelf["photoalbum"] = album if got := IdentityProblems(shelf, 6); len(got) != 0 { t.Fatalf("a slug that fits is still refused: %q", got) } } // Tonight's case (issue 263): networkmanager, no slug, requiring the mesh's resolver provision on a // machine with a six-character name. Under ADR 0049's one bound it was refused, and its provider's // whole machine with it; the resolver keeps no name, so it is not refused at all. func TestARequirementOnAKeylessProvisionComposesWithALongName(t *testing.T) { shelf := Shelf{ "resolver": {Module: "resolver", Provides: []Offer{{Name: "wildcard-resolution", Scope: ScopeMesh}}}, "networkmanager": {Module: "networkmanager", Requires: []string{"wildcard-resolution"}}, } if CheckIdentity("laptop", "networkmanager") == nil { t.Fatal("the regression is not reproduced: mesh_laptop_networkmanager fits the old global bound") } if got := IdentityProblems(shelf, 6); len(got) != 0 { t.Fatalf("a requirement on a keyless provision was refused for its length: %q", got) } r := Resolution{Node: "laptop", Modules: []Manifest{shelf["networkmanager"]}, Needs: []Needed{{Name: "wildcard-resolution", From: "anchor", For: "networkmanager", Identity: shelf["resolver"].IdentityBoundOf("wildcard-resolution")}}} if got := r.Overflowing(); len(got) != 0 { t.Fatalf("a keyless requirement is reported as overflowing: %+v", got) } } // The consumer's side of the same judgement the provider's composition makes: what `status` says. func TestAnOverflowingRequirementIsNamedFromTheConsumersSide(t *testing.T) { bound := IdentityBound{Max: 20, In: "an S3 access key"} r := Resolution{Node: "laptop", Modules: []Manifest{{Module: "photoalbum"}, {Module: "files"}, {Module: "gallery", Slug: "gal"}}, Needs: []Needed{ {Name: "s3-bucket", From: "anchor", For: "photoalbum", Identity: bound}, {Name: "s3-bucket", From: "anchor", For: "photoalbum", Local: "second", Identity: bound}, {Name: "s3-bucket", From: "anchor", For: "files", Identity: bound}, {Name: "s3-bucket", From: "anchor", For: "gallery", Identity: bound}, {Name: "licence", From: "records", For: "photoalbum", ByRecord: true, Identity: bound}, }} got := r.Overflowing() if len(got) != 1 || got[0].Module != "photoalbum" || got[0].Identity != "mesh_laptop_photoalbum" || got[0].Provider != "anchor" { t.Fatalf("one overflow, once, was expected: %+v", got) } if said := got[0].String(); !strings.Contains(said, "an S3 access key keeps 20") || !strings.Contains(said, "slug") { t.Fatalf("the overflow does not say what keeps it or the remedy: %s", said) } } // An identity served as a DNS label is never truncated, so the offer's bound must keep it in one. func TestAnIdentityServedAsADNSLabelIsBoundedToOne(t *testing.T) { serves := map[string]map[string]any{"bucket": {"name": "${consumer:as:dns}"}} wide := Manifest{Module: "wide", Serves: serves, Provides: []Offer{{Name: "bucket", Scope: ScopeMesh, Identity: &OfferIdentity{Max: 255, In: "a client id"}}}} if got := CheckServes(wide); len(got) != 1 || !strings.Contains(got[0], "a label keeps 63") { t.Fatalf("a 255-character bound on a DNS label passed: %q", got) } unsaid := Manifest{Module: "unsaid", Serves: serves, Provides: []Offer{{Name: "bucket", Scope: ScopeMesh}}} if got := CheckServes(unsaid); len(got) != 0 { t.Fatalf("the default bound (20) on a DNS label was refused: %q", got) } }