109 lines
4.7 KiB
Go
109 lines
4.7 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The fault 04-ISSUES/036 records: a media stack is several modules — a library server, the
|
|
// acquisition managers, a download client — that must share directories on one machine. Written
|
|
// as owned `directory` resources, two of them on one node are refused as rivalrous owners, which
|
|
// is right in general and wrong here: sharing those directories is the whole point of the stack.
|
|
//
|
|
// Declared as accesses to an operator-owned path (novox/hq ADR 0051), they co-resolve. An access
|
|
// is not a resource and never enters the duplicate-owner map, so this is the exact case the
|
|
// resolver refuses above when the same path is owned — and does not, when it is merely accessed.
|
|
func TestTwoModulesAccessingOnePathCoResolve(t *testing.T) {
|
|
a := mod("a", nil, nil, nil)
|
|
a.Accesses = []Access{{Path: "/services/media/downloads", Mode: AccessReadWrite}}
|
|
b := mod("b", nil, nil, nil)
|
|
b.Accesses = []Access{{Path: "/services/media/downloads", Mode: AccessRead}}
|
|
|
|
got, err := Resolve(shelf(a, b), []string{"a", "b"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatalf("two modules accessing one operator-owned path were refused: %v", err)
|
|
}
|
|
|
|
// And each accessing module's grant reaches the machine as its own `access` resource, so the
|
|
// host can find the path present or refuse — both naming the one operator-owned path.
|
|
var accesses int
|
|
for _, r := range mustDeclare(t, got) {
|
|
if r["type"] == "access" && r["path"] == "/services/media/downloads" {
|
|
accesses++
|
|
}
|
|
}
|
|
if accesses != 2 {
|
|
t.Errorf("expected both modules' accesses in the declaration, got %d", accesses)
|
|
}
|
|
}
|
|
|
|
// The mirror of the case above, so the sharing vocabulary does not weaken the rule it sits beside:
|
|
// two modules OWNING one path is still refused, exactly as before accesses existed.
|
|
func TestTwoModulesOwningOnePathAreStillRefused(t *testing.T) {
|
|
a := mod("a", nil, nil, nil)
|
|
a.Resources = []map[string]any{
|
|
{"id": "lib", "type": "directory", "path": "/services/media/movies", "mode": "0755"}}
|
|
b := mod("b", nil, nil, nil)
|
|
b.Resources = []map[string]any{
|
|
{"id": "lib", "type": "directory", "path": "/services/media/movies", "mode": "0755"}}
|
|
|
|
_, err := Resolve(shelf(a, b), []string{"a", "b"}, workstation(), World{})
|
|
if err == nil {
|
|
t.Fatal("two modules owning the same directory were both assigned")
|
|
}
|
|
if !strings.Contains(err.Error(), "/services/media/movies") {
|
|
t.Errorf("the refusal does not name the path: %v", err)
|
|
}
|
|
}
|
|
|
|
// Shared data is the operator's, owned by no module (novox/hq ADR 0051). A module owning the path
|
|
// another module was told to expect the operator to provide would create and chown it — and the
|
|
// two intentions cannot both hold, so it is refused by name. This is what keeps the distinction
|
|
// machine-checkable rather than a convention nobody enforces.
|
|
func TestAModuleMayNotOwnWhatAnotherAccesses(t *testing.T) {
|
|
owns := mod("owns", nil, nil, nil)
|
|
owns.Resources = []map[string]any{
|
|
{"id": "lib", "type": "directory", "path": "/services/media/movies", "mode": "0755"}}
|
|
uses := mod("uses", nil, nil, nil)
|
|
uses.Accesses = []Access{{Path: "/services/media/movies", Mode: AccessRead}}
|
|
|
|
_, err := Resolve(shelf(owns, uses), []string{"owns", "uses"}, workstation(), World{})
|
|
if err == nil {
|
|
t.Fatal("a module owning a path another accesses was allowed")
|
|
}
|
|
if !strings.Contains(err.Error(), "/services/media/movies") ||
|
|
!strings.Contains(err.Error(), "the operator's") {
|
|
t.Errorf("the refusal does not explain the ownership conflict: %v", err)
|
|
}
|
|
}
|
|
|
|
// A manifest states an access's extent, the way a listening port states its source. An absolute
|
|
// path and a mode of read or read-write; anything else is refused rather than acted on wrongly.
|
|
func TestAnAccessIsValidated(t *testing.T) {
|
|
good := []byte(`{"module":"m","accesses":[{"path":"/services/media","mode":"read-write"}]}`)
|
|
if _, err := ParseManifest(good); err != nil {
|
|
t.Fatalf("a valid access was refused: %v", err)
|
|
}
|
|
|
|
// Mode is optional and narrows to read — the safe default, because the danger with an access
|
|
// is being given more than was meant, not less.
|
|
bare := []byte(`{"module":"m","accesses":[{"path":"/services/media"}]}`)
|
|
m, err := ParseManifest(bare)
|
|
if err != nil {
|
|
t.Fatalf("an access with no mode was refused: %v", err)
|
|
}
|
|
if m.Accesses[0].At() != AccessRead {
|
|
t.Errorf("an access with no mode is %q, want %q", m.Accesses[0].At(), AccessRead)
|
|
}
|
|
|
|
relative := []byte(`{"module":"m","accesses":[{"path":"services/media","mode":"read"}]}`)
|
|
if _, err := ParseManifest(relative); err == nil {
|
|
t.Error("a relative access path was accepted")
|
|
}
|
|
|
|
wrong := []byte(`{"module":"m","accesses":[{"path":"/services/media","mode":"append"}]}`)
|
|
if _, err := ParseManifest(wrong); err == nil {
|
|
t.Error("an access with an unknown mode was accepted")
|
|
}
|
|
}
|