Machine gains Grants (provision, consumer node, consumer module, local name; no sealed copy) and Bindings (consumer module, provision, provider node, provider module), both omitted when empty. Format moves 1 to 2, because a reader of 1 must refuse a snapshot that says what it cannot read (GrantsFrom names the number); MediaType stays, a reader of 2 reads a snapshot of 1 as having none. Scrubber.Grant and Scrubber.Binding put the machine names under pseudonyms as Pins' are. go/compose's Input.Issued and Node.Bound docs name them as where an assigner fills those from; no logic changes there. Tests: all new ones failed first (the types did not exist); the copied ones are exempt. Unit level 1.19 s (bound 2 s).
131 lines
5.4 KiB
Go
131 lines
5.4 KiB
Go
package facts
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A snapshot the controller wrote before grants and bindings existed is read as one that has none. The body
|
|
// is the format 1 document facts_test.go's TestANewerSnapshotIsRefusedNotHalfRead reads (mesh-controller
|
|
// 6c2a8d62, internal/facts/facts_test.go), with the fields of a machine that format 1 wrote.
|
|
func TestASnapshotWithoutGrantsOrBindingsReadsAsHavingNone(t *testing.T) {
|
|
f, err := Decode([]byte(`{"facts": 1, "machines": [{"name": "abc", "length": 3}, {"name": "defgh", "length": 5,
|
|
"pins": [{"provision": "store", "machine": "xyz", "module": "postgres"}]}]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range f.Machines {
|
|
if len(m.Grants) != 0 || len(m.Bindings) != 0 {
|
|
t.Errorf("%s reads grants %v and bindings %v from a snapshot that carries none", m.Name, m.Grants, m.Bindings)
|
|
}
|
|
}
|
|
body, err := f.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(body), "grants") || strings.Contains(string(body), "bindings") {
|
|
t.Errorf("a machine with none wrote them anyway:\n%s", body)
|
|
}
|
|
}
|
|
|
|
// A snapshot that carries them reads back what it carried, and writes the same bytes again.
|
|
func TestGrantsAndBindingsRoundTripByteForByte(t *testing.T) {
|
|
f := Facts{Format: Format, Taken: time.Date(2026, 10, 20, 8, 0, 0, 0, time.UTC), Machines: []Machine{{
|
|
Name: "abcdef", Length: 6,
|
|
Grants: []Grant{
|
|
{Provision: "store", ConsumerNode: "ghijkl", ConsumerModule: "mesh-issues", Local: "database"},
|
|
{Provision: "store", ConsumerNode: "abcdef", ConsumerModule: "records"},
|
|
},
|
|
Bindings: []Binding{
|
|
{ConsumerModule: "records", Provision: "store", ProviderNode: "abcdef", ProviderModule: "postgres"},
|
|
},
|
|
}}}
|
|
first, err := f.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{`"grants"`, `"bindings"`, `"consumer-node": "ghijkl"`, `"consumer-module": "mesh-issues"`,
|
|
`"provider-node": "abcdef"`, `"provider-module": "postgres"`, `"local": "database"`} {
|
|
if !strings.Contains(string(first), want) {
|
|
t.Errorf("the snapshot does not say %s:\n%s", want, first)
|
|
}
|
|
}
|
|
back, err := Decode(first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := back.Encode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(first) != string(second) {
|
|
t.Errorf("the snapshot did not round-trip:\nfirst %s\nsecond %s", first, second)
|
|
}
|
|
m, _ := back.Machine("abcdef")
|
|
// Sorted, so two snapshots of one mesh are the same bytes: the grant for the lower node name comes first.
|
|
if len(m.Grants) != 2 || m.Grants[0].ConsumerNode != "abcdef" || len(m.Bindings) != 1 {
|
|
t.Errorf("read back grants %+v and bindings %+v", m.Grants, m.Bindings)
|
|
}
|
|
}
|
|
|
|
// A machine name in a grant or a binding is scrubbed as a pin's is: to the machine's pseudonym, never kept.
|
|
func TestTheScrubberScrubsTheMachineNamesOfGrantsAndBindings(t *testing.T) {
|
|
s := NewScrubber()
|
|
pin := Pin{Provision: "store", Machine: s.Machine("homeserver"), Module: "postgres"}
|
|
grant := s.Grant("store", "laptop2", "mesh-issues", "database")
|
|
binding := s.Binding("records", "store", "homeserver", "postgres")
|
|
if grant.ConsumerNode != s.Machine("laptop2") || grant.ConsumerNode == "laptop2" {
|
|
t.Errorf("the grant's consumer node is %q", grant.ConsumerNode)
|
|
}
|
|
if binding.ProviderNode != pin.Machine || binding.ProviderNode == "homeserver" {
|
|
t.Errorf("the binding's provider node is %q, not the pin's pseudonym %q", binding.ProviderNode, pin.Machine)
|
|
}
|
|
if grant.Provision != "store" || grant.ConsumerModule != "mesh-issues" || grant.Local != "database" ||
|
|
binding.ConsumerModule != "records" || binding.Provision != "store" || binding.ProviderModule != "postgres" {
|
|
t.Errorf("the scrubber changed what is not a machine's name: %+v %+v", grant, binding)
|
|
}
|
|
if len(grant.ConsumerNode) != len("laptop2") {
|
|
t.Errorf("the pseudonym %q does not keep the length of the name", grant.ConsumerNode)
|
|
}
|
|
}
|
|
|
|
// A grant says that a credential was made and for whom, never the credential: no field of either type is
|
|
// a sealed value, a secret or a hash.
|
|
func TestNoFieldOfAGrantOrABindingHoldsASecret(t *testing.T) {
|
|
for _, typ := range []reflect.Type{reflect.TypeOf(Grant{}), reflect.TypeOf(Binding{})} {
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
name := typ.Field(i).Name
|
|
for _, banned := range []string{"Sealed", "Secret", "Hash", "Password"} {
|
|
if strings.Contains(strings.ToLower(name), strings.ToLower(banned)) {
|
|
t.Errorf("%s.%s may hold a secret", typ.Name(), name)
|
|
}
|
|
}
|
|
if typ.Field(i).Type.Kind() != reflect.String {
|
|
t.Errorf("%s.%s is a %s, not a name", typ.Name(), name, typ.Field(i).Type)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The format number says from which number a snapshot carries them: a reader of 1 refuses 2 rather than
|
|
// judge without them, and a reader of 2 reads 1.
|
|
func TestTheFormatSaysFromWhichNumberGrantsAndBindingsAreWritten(t *testing.T) {
|
|
if GrantsFrom != 2 {
|
|
t.Errorf("grants and bindings are written from format %d, not 2", GrantsFrom)
|
|
}
|
|
if Format < GrantsFrom {
|
|
t.Errorf("the format is %d, below %d, where grants and bindings begin", Format, GrantsFrom)
|
|
}
|
|
if _, err := Decode([]byte(`{"facts": 2, "machines": []}`)); err != nil {
|
|
t.Errorf("format 2 is not read: %v", err)
|
|
}
|
|
if _, err := Decode([]byte(`{"facts": 1, "machines": []}`)); err != nil {
|
|
t.Errorf("format 1 is not read: %v", err)
|
|
}
|
|
if _, err := Decode([]byte(`{"facts": 3, "machines": []}`)); err == nil {
|
|
t.Error("format 3 is read by a reader of format 2")
|
|
}
|
|
}
|