186 lines
7.4 KiB
Go
186 lines
7.4 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Issue 1591: `shares` on a requirement of s3-bucket (novox/hq ADR 0468 §8).
|
|
|
|
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() Manifest {
|
|
return Manifest{Module: "minio", Version: "1", Provides: Offers("s3-bucket"),
|
|
Receives: map[string]string{"s3-bucket": "/grants/mesh.json"}}
|
|
}
|
|
|
|
func givenToMinio(t *testing.T, out []map[string]any) []Contribution {
|
|
t.Helper()
|
|
for _, r := range out {
|
|
if r["path"] != "/grants/mesh.json" {
|
|
continue
|
|
}
|
|
var parsed struct {
|
|
Given []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/AC1: parsed, the shares are on the manifest; stored, `requires` is plain names and the side key holds them.
|
|
func TestSharesParseAndAreStoredBesideRequires(t *testing.T) {
|
|
m := mustParse(t, filesWithShares)
|
|
want := []BucketShare{{To: "seat:node-files", Methods: []string{"put"}}, {To: "print-and-scan", Methods: []string{"get", "put"}}}
|
|
if !reflect.DeepEqual(m.RequireShares["s3-bucket"], want) {
|
|
t.Fatalf("the shares read as %+v", m.RequireShares)
|
|
}
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var keys map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &keys); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(keys["requires"]) != `["shell","s3-bucket"]` {
|
|
t.Errorf("the stored requires is not plain names: %s", keys["requires"])
|
|
}
|
|
if !strings.Contains(string(keys[RequireSharesKey]), `"seat:node-files"`) {
|
|
t.Errorf("the side key does not hold the shares: %s", raw)
|
|
}
|
|
var again Manifest
|
|
if err := json.Unmarshal(raw, &again); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(again, m) {
|
|
t.Fatalf("not lossless:\n%+v\n%+v", again, m)
|
|
}
|
|
}
|
|
|
|
// 1591/AC2: a malformed share is refused, naming the entry and ADR 0468 §8.
|
|
func TestAMalformedShareIsRefused(t *testing.T) {
|
|
cases := map[string]string{
|
|
"unknown method": `{"name":"s3-bucket","shares":[{"to":"a","methods":["delete"]}]}`,
|
|
"empty to": `{"name":"s3-bucket","shares":[{"to":"","methods":["get"]}]}`,
|
|
"malformed to": `{"name":"s3-bucket","shares":[{"to":"Seat:X y","methods":["get"]}]}`,
|
|
"empty methods": `{"name":"s3-bucket","shares":[{"to":"a","methods":[]}]}`,
|
|
"duplicate to": `{"name":"s3-bucket","shares":[{"to":"a","methods":["get"]},{"to":"a","methods":["put"]}]}`,
|
|
"duplicate verb": `{"name":"s3-bucket","shares":[{"to":"a","methods":["get","get"]}]}`,
|
|
"other require": `{"name":"route","shares":[{"to":"a","methods":["get"]}]}`,
|
|
}
|
|
for name, entry := range cases {
|
|
_, err := ParseManifest([]byte(`{"module":"files","version":"1","requires":[` + entry + `]}`))
|
|
if err == nil {
|
|
t.Errorf("%s: accepted", name)
|
|
continue
|
|
}
|
|
if !strings.Contains(err.Error(), "ADR 0468 §8") || !strings.Contains(err.Error(), "requires") {
|
|
t.Errorf("%s: the error does not name the entry and the ADR: %v", name, err)
|
|
}
|
|
}
|
|
// The stored form is held to the same rules.
|
|
_, err := ParseManifest([]byte(`{"module":"files","version":"1","requires":["s3-bucket"],
|
|
"requires-shares":{"s3-bucket":[{"to":"a","methods":[]}]}}`))
|
|
if err == nil || !strings.Contains(err.Error(), "ADR 0468 §8") {
|
|
t.Errorf("the stored form was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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 := Resolution{Node: "laptop", Modules: []Manifest{m}}
|
|
values, asks, err := remote.ContributionsFrom("s3-bucket", "files", 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(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := provider.Declaration(Rendering{Grants: []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(), World{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err = both.Declaration(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/AC4: only the shares change, so the module is not recreated and held for no one; the provider's file changes.
|
|
func TestChangingOnlyTheSharesDoesNotRecreateTheConsumer(t *testing.T) {
|
|
was := mustParse(t, filesWithShares)
|
|
now := mustParse(t, `{"module":"files","version":"1","requires":["shell",
|
|
{"name":"s3-bucket","shares":[{"to":"print-and-scan","methods":["get"]}]}]}`)
|
|
if said := Recreates(was, now).Say("files"); said != "" {
|
|
t.Errorf("a change of shares recreates the module: %s", said)
|
|
}
|
|
if said := Holds(was, now); said != "" {
|
|
t.Errorf("a change of shares holds the module: %s", said)
|
|
}
|
|
before := Resolution{Node: "laptop", Modules: []Manifest{was}}
|
|
after := Resolution{Node: "laptop", Modules: []Manifest{now}}
|
|
a, _, _ := before.ContributionsFrom("s3-bucket", "files", SettingsBy{})
|
|
b, _, _ := after.ContributionsFrom("s3-bucket", "files", SettingsBy{})
|
|
if sharesOf(t, a) == sharesOf(t, b) {
|
|
t.Fatalf("the provider's values did not change: %s", sharesOf(t, a))
|
|
}
|
|
}
|
|
|
|
// 1591/AC5: a consumer whose contributes to s3-bucket names `shares` itself is refused.
|
|
func TestAConsumerCannotContributeSharesItself(t *testing.T) {
|
|
_, err := ParseManifest([]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 := Manifest{Module: "files", Requires: []string{"s3-bucket"},
|
|
Contributes: map[string]map[string]any{"s3-bucket": {"shares": "x"}}}
|
|
r := Resolution{Node: "laptop", Modules: []Manifest{m}}
|
|
if _, _, err := r.ContributionsFrom("s3-bucket", "files", SettingsBy{}); err == nil {
|
|
t.Error("a contributed `shares` reached the grant")
|
|
}
|
|
}
|