package resolution import ( "encoding/json" "strings" "testing" mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) const filesWithShares = `{"module":"files","version":"1","requires":["shell", {"name":"s3-bucket","shares":[{"to":"seat:node-files","methods":["put"]},{"to":"print-and-scan","methods":["get","put"]}]}]}` func minioLike() mcat.Manifest { return mcat.Manifest{Module: "minio", Version: "1", Provides: mcat.Offers("s3-bucket"), Receives: map[string]string{"s3-bucket": "/grants/mesh.json"}} } func givenToMinio(t *testing.T, out []map[string]any) []mcat.Contribution { t.Helper() for _, r := range out { if r["path"] != "/grants/mesh.json" { continue } var parsed struct { Given []mcat.Contribution `json:"given"` } if err := json.Unmarshal([]byte(r["content"].(string)), &parsed); err != nil { t.Fatal(err) } return parsed.Given } t.Fatalf("minio was given no file: %v", out) return nil } func sharesOf(t *testing.T, values map[string]any) string { t.Helper() raw, err := json.Marshal(values["shares"]) if err != nil { t.Fatal(err) } return string(raw) } const sortedSharesJSON = `[{"methods":["get","put"],"to":"print-and-scan"},{"methods":["put"],"to":"seat:node-files"}]` // 1591/AC3: the values a consumer gives carry the shares sorted by `to`, remote (grant) and co-located alike. func TestTheGrantCarriesTheSharesSorted(t *testing.T) { m := mustParse(t, filesWithShares) remote := mcat.Resolution{Node: "laptop", Modules: []mcat.Manifest{m}} values, asks, err := ContributionsFrom(remote, "s3-bucket", "files", mcat.SettingsBy{}) if err != nil || !asks { t.Fatalf("asks %v, err %v", asks, err) } if got := sharesOf(t, values); got != sortedSharesJSON { t.Fatalf("the grant values carry %s", got) } // As the provider's file shows it, through a grant. provider, err := Resolve(shelf(minioLike()), []string{"minio"}, workstation(), mcat.World{}) if err != nil { t.Fatal(err) } out, err := Declaration(provider, mcat.Rendering{Grants: []mcat.Grant{{Provision: "s3-bucket", Consumer: "laptop", From: "files", Values: values}}}) if err != nil { t.Fatal(err) } given := givenToMinio(t, out) if len(given) != 1 || sharesOf(t, given[0].Values) != sortedSharesJSON { t.Fatalf("the remote consumer's shares did not reach the file: %+v", given) } // Co-located: the consumer contributes to s3-bucket too, and no grant is passed. co := mustParse(t, `{"module":"files","version":"1","contributes":{"s3-bucket":{"name":"files"}}, "requires":[{"name":"s3-bucket","shares":[{"to":"seat:node-files","methods":["put"]},{"to":"print-and-scan","methods":["put","get"]}]}]}`) both, err := Resolve(shelf(minioLike(), co), []string{"files"}, workstation(), mcat.World{}) if err != nil { t.Fatal(err) } out, err = Declaration(both, mcat.Rendering{}) if err != nil { t.Fatal(err) } given = givenToMinio(t, out) if len(given) != 1 || sharesOf(t, given[0].Values) != sortedSharesJSON || given[0].Values["name"] != "files" { t.Fatalf("the co-located consumer's shares did not reach the file: %+v", given) } } // 1591/AC5: a consumer whose contributes to s3-bucket names `shares` itself is refused. func TestAConsumerCannotContributeSharesItself(t *testing.T) { _, err := decodeForTest([]byte(`{"module":"files","version":"1", "contributes":{"s3-bucket":{"shares":"x"}}, "requires":[{"name":"s3-bucket","shares":[{"to":"a","methods":["get"]}]}]}`)) if err == nil || !strings.Contains(err.Error(), "ADR 0468 ยง8") { t.Errorf("both written, accepted: %v", err) } // Without the field the grant is still refused, so the provider is never told something the manifest did not say. m := mcat.Manifest{Module: "files", Requires: []string{"s3-bucket"}, Contributes: map[string]map[string]any{"s3-bucket": {"shares": "x"}}} r := mcat.Resolution{Node: "laptop", Modules: []mcat.Manifest{m}} if _, _, err := ContributionsFrom(r, "s3-bucket", "files", mcat.SettingsBy{}); err == nil { t.Error("a contributed `shares` reached the grant") } }