ADR 0595 §1: the typed long form {name, storage, template, options,
configuration} decodes into Manifest.BusState, its name also on Requires;
a key it does not know is set aside and named, never refused (ADR 0578 §3).
The requirements tests are one table.
111 lines
4.2 KiB
Go
111 lines
4.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A requirement a module can run without (novox/hq ADR 0330, issue 166). The shape of the issue:
|
|
// grafana runs without a data source and requires `influxdb-api` softly; a mesh without influxdb
|
|
// could not assign grafana at all, and one with influxdb in its catalogue would have had it installed
|
|
// for grafana. And `requires` in its typed long form, a module's bus state (novox/hq ADR 0595 §1).
|
|
func TestARequirementsLongForm(t *testing.T) {
|
|
m := mustParse(t, `{"module":"grafana","version":"1",
|
|
"requires":["route",{"name":"influxdb-api","optional":true},{"name":"oidc-client"}]}`)
|
|
if strings.Join(m.Requires, " ") != "route influxdb-api oidc-client" {
|
|
t.Fatalf("the requirements read as %v", m.Requires)
|
|
}
|
|
if m.IsOptional("route") || m.IsOptional("oidc-client") {
|
|
t.Error("a requirement without `optional` read as optional; hard is the default (ADR 0330 §1)")
|
|
}
|
|
if !m.IsOptional("influxdb-api") {
|
|
t.Error("a requirement marked optional read as hard")
|
|
}
|
|
// And written back as it was read, so a manifest through the store keeps its form.
|
|
again := roundTrip(t, m)
|
|
if !again.IsOptional("influxdb-api") || again.IsOptional("route") || len(again.Requires) != 3 {
|
|
t.Fatalf("written and read back, it says %v optional %v", again.Requires, again.OptionalRequires)
|
|
}
|
|
|
|
// The long form {name, optional} refuses what it does not know.
|
|
for _, raw := range []string{
|
|
`{"module":"x","version":"1","requires":[{"name":"influxdb-api","optinal":true}]}`,
|
|
`{"module":"x","version":"1","requires":[{"optional":true}]}`,
|
|
} {
|
|
if _, err := decodeForTest([]byte(raw)); err == nil {
|
|
t.Errorf("accepted %s", raw)
|
|
}
|
|
}
|
|
|
|
records := BusStateEntry{Name: "records", Storage: "ledger", Template: "ledger",
|
|
Options: map[string]json.RawMessage{"max-mib": json.RawMessage(`1024`)}}
|
|
for _, c := range []struct {
|
|
name string
|
|
requires string
|
|
names string
|
|
busState []BusStateEntry
|
|
aside []string
|
|
}{
|
|
// 1966/AC1: a plain name and a bus-state entry: Requires names both, BusState holds the one entry whole.
|
|
{"a plain name and a bus-state entry", `["postgres-database",
|
|
{"name":"records","storage":"ledger","template":"ledger","options":{"max-mib":1024}}]`,
|
|
"postgres-database records", []BusStateEntry{records}, nil},
|
|
// 1966/AC2: an unknown key is set aside and named, never refused; the entry decodes.
|
|
{"a bus-state entry with a key it does not know", `[
|
|
{"name":"records","storage":"ledger","template":"ledger","colour":"red","options":{"max-mib":1024}}]`,
|
|
"records", []BusStateEntry{records}, []string{"requires[0].colour"}},
|
|
} {
|
|
got, err := decodeForTest([]byte(`{"module":"x","version":"1","requires":` + c.requires + `}`))
|
|
if err != nil {
|
|
t.Errorf("%s: refused: %v", c.name, err)
|
|
continue
|
|
}
|
|
if strings.Join(got.Requires, " ") != c.names {
|
|
t.Errorf("%s: requires read as %v, want %s", c.name, got.Requires, c.names)
|
|
}
|
|
if !reflect.DeepEqual(got.BusState, c.busState) {
|
|
t.Errorf("%s: bus state read as %+v, want %+v", c.name, got.BusState, c.busState)
|
|
}
|
|
for _, path := range c.aside {
|
|
if !strings.Contains(got.SetAside(), path) {
|
|
t.Errorf("%s: %s is not named among what was set aside: %q", c.name, path, got.SetAside())
|
|
}
|
|
}
|
|
if len(c.aside) == 0 && got.SetAside() != "" {
|
|
t.Errorf("%s: set aside %q", c.name, got.SetAside())
|
|
}
|
|
// Stored and read back, the entries stay whole and `requires` stays plain names.
|
|
back := roundTrip(t, got)
|
|
if !reflect.DeepEqual(back.BusState, c.busState) || strings.Join(back.Requires, " ") != c.names {
|
|
t.Errorf("%s: read back as %v %+v", c.name, back.Requires, back.BusState)
|
|
}
|
|
}
|
|
}
|
|
|
|
// roundTrip is m written as the store keeps it and read again; the stored form keeps `requires` plain names.
|
|
func roundTrip(t *testing.T, m Manifest) Manifest {
|
|
t.Helper()
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var stored struct {
|
|
Requires []any `json:"requires"`
|
|
}
|
|
if err := json.Unmarshal(raw, &stored); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range stored.Requires {
|
|
if _, plain := r.(string); !plain {
|
|
t.Errorf("a requirement was stored in the long form: %s", raw)
|
|
}
|
|
}
|
|
var again Manifest
|
|
if err := json.Unmarshal(raw, &again); err != nil {
|
|
t.Fatalf("%v: %s", err, raw)
|
|
}
|
|
return again
|
|
}
|