Files
mesh-catalog/modules/minio/cmd/object-store/server_test.go
T
jschoubben 35ff67bda0
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
One commit with exactly the tree of the reviewed head 35b4509c0a, on main 12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
2026-10-30 02:21:51 +01:00

218 lines
8.2 KiB
Go

package main
import (
"errors"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// The grant entry's shape is the mesh's Contribution (mesh-sdk go/catalogue/declaration.go: from, node, as, values,
// derived), written in a `given` list by receivedFile in the same file; `shares` is written into values by
// go/catalogue/bucket_shares.go withShares: [{"to": ..., "methods": [...]}].
const grantFile = `{
"contributions": 1,
"requirement": "s3-bucket",
"given": [
{"from": "brother", "node": "shanks", "as": "brother-key",
"values": {"shares": [{"to": "seat:node-files", "methods": ["put"]}, {"to": "print-and-scan", "methods": ["get"]}]},
"derived": {"bucket": "mesh-shanks-brother"}},
{"from": "nextcloud", "node": "novox", "as": "nextcloud-key", "derived": {"bucket": "mesh-novox-nextcloud"}}
]
}`
// The placement seat's state of who holds which seat (seat-holders): a key per node seat and node, a key per mesh seat.
var seatHolderKeys = map[string][]byte{
"node-files.g14": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"g14","holders":[{"module":"mesh-files","node":"g14"}]}`),
"node-files.shanks": []byte(`{"v":1,"seat":"node-files","scope":"node","node":"shanks","holders":[{"module":"mesh-files","node":"shanks"}]}`),
"node-backup.g14": []byte(`{"v":1,"seat":"node-backup","scope":"node","node":"g14","holders":[{"module":"restic","node":"g14"}]}`),
"printer": []byte(`{"v":1,"seat":"printer","scope":"mesh","holders":[{"module":"brother","node":"shanks"}]}`),
}
type rig struct {
s *server
dir string
now time.Time
seatsCalls int
seatsErr error
}
func newRig(t *testing.T) *rig {
t.Helper()
dir := t.TempDir()
r := &rig{dir: dir, now: time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC)}
write := func(name, content string) string {
p := filepath.Join(dir, name)
if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return p
}
// What the mesh renders from files/object-store.json for a store on novox.internal, port 20001.
cfg := write("object-store.json", `{"scheme":"http","at":"novox.internal","port":"20001","region":"eu-west"}`)
write("links/mesh-shanks-brother.secret", "brother-link-secret\n")
write("links/mesh-novox-nextcloud.secret", "nextcloud-link-secret\n")
grants := write("grants/mesh.json", grantFile)
s, err := newServer(cfg, filepath.Join(dir, "links"), grants)
if err != nil {
t.Fatal(err)
}
s.now = func() time.Time { return r.now }
s.seats = func() (map[string][]byte, error) {
r.seatsCalls++
if r.seatsErr != nil {
return nil, r.seatsErr
}
return seatHolderKeys, nil
}
r.s = s
return r
}
func ask(bucket, method string, seconds any) map[string]any {
a := map[string]any{"bucket": bucket, "key": "doc.pdf", "method": method}
if seconds != nil {
a["seconds"] = seconds
}
return a
}
func linkOf(t *testing.T, got any) *url.URL {
t.Helper()
m := got.(map[string]any)
u, err := url.Parse(m["link"].(string))
if err != nil {
t.Fatal(err)
}
return u
}
// 1516/AC1: brother's own ask gets a link signed with the bucket's link key, for the store's host and port, 300 s.
func TestLinkIsSignedWithTheBucketsLinkKey(t *testing.T) {
r := newRig(t)
got, err := r.s.link("shanks.brother", ask("mesh-shanks-brother", "get", float64(300)))
if err != nil {
t.Fatal(err)
}
u := linkOf(t, got)
if u.Host != "novox.internal:20001" || u.Scheme != "http" || u.Path != "/mesh-shanks-brother/doc.pdf" {
t.Fatalf("link goes to %s", u)
}
q := u.Query()
if cred := q.Get("X-Amz-Credential"); !strings.HasPrefix(cred, linkKeyID("mesh-shanks-brother")+"/20261021/eu-west/s3/") {
t.Fatalf("credential %q is not the bucket's link key", cred)
}
if strings.Contains(u.String(), "meshroot") {
t.Fatal("the root account signed a link")
}
if q.Get("X-Amz-Expires") != "300" || q.Get("X-Amz-Date") != "20261021T120000Z" {
t.Fatalf("expiry %q at %q", q.Get("X-Amz-Expires"), q.Get("X-Amz-Date"))
}
}
// 1516/AC1: the link key id is the one the provisioner makes (modules/minio/links.ts linkKeyId, tested there with the
// same literal).
func TestLinkKeyIDMatchesTheProvisioners(t *testing.T) {
if got := linkKeyID("brother"); got != "link0b4bd77cec705cf5" {
t.Fatalf("linkKeyID = %s", got)
}
}
// 1516/AC2: a share to seat:node-files for put, mesh-files holding node-files on g14: put given, get refused naming the method.
func TestSeatShareGivesTheHoldersTheSharedMethodOnly(t *testing.T) {
r := newRig(t)
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
t.Fatalf("put: %v", err)
}
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "get", nil))
if err == nil || !strings.Contains(err.Error(), "put, not get") {
t.Fatalf("get was not refused naming the method: %v", err)
}
// A module share is judged by name alone.
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err != nil {
t.Fatalf("print-and-scan get: %v", err)
}
}
// 1516/AC3: each of these is refused with its reason.
func TestRefusals(t *testing.T) {
r := newRig(t)
cases := []struct {
name, caller string
args map[string]any
want string
}{
{"a stranger", "g14.restic", ask("mesh-shanks-brother", "get", nil), "not the bucket's consumer"},
{"a seat it does not hold", "g14.restic", ask("mesh-shanks-brother", "put", nil), "not the bucket's consumer"},
{"unproven", "unproven", ask("mesh-shanks-brother", "get", nil), "did not prove"},
{"no grant", "shanks.brother", ask("no-such-bucket", "get", nil), "no grant names bucket"},
{"seconds over 600", "shanks.brother", ask("mesh-shanks-brother", "get", float64(601)), "at most ten minutes"},
{"seconds zero", "shanks.brother", ask("mesh-shanks-brother", "get", float64(0)), "outside 1 to 600"},
{"method delete", "shanks.brother", ask("mesh-shanks-brother", "delete", nil), "not get or put"},
{"another consumer's bucket", "shanks.brother", ask("mesh-novox-nextcloud", "get", nil), "not the bucket's consumer"},
}
for _, c := range cases {
_, err := r.s.link(c.caller, c.args)
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: want a refusal containing %q, got %v", c.name, c.want, err)
}
}
}
// 1516/AC4: with the controller unreadable a seat share is refused naming why; a good read serves a second ask within
// 60 s from the cache and is read again after.
func TestSeatsUnreadableAndCached(t *testing.T) {
r := newRig(t)
r.seatsErr = errors.New("the controller did not answer")
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil))
if err == nil || !strings.Contains(err.Error(), "the controller did not answer") || !strings.Contains(err.Error(), "node-files") {
t.Fatalf("not refused naming why: %v", err)
}
r.seatsErr = nil
put := func() {
t.Helper()
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
t.Fatal(err)
}
}
put() // a failed read is not cached: this reads again
put() // served from the cache
if r.seatsCalls != 2 {
t.Fatalf("the controller was asked %d times, want 2 (one failure, one good read)", r.seatsCalls)
}
r.now = r.now.Add(59 * time.Second)
put()
if r.seatsCalls != 2 {
t.Fatalf("an ask within 60 s read the seats again (%d calls)", r.seatsCalls)
}
r.now = r.now.Add(2 * time.Second)
put()
if r.seatsCalls != 3 {
t.Fatalf("the seats were not read again after 60 s (%d calls)", r.seatsCalls)
}
}
// 1516/AC5: the grant file is read at each call.
func TestGrantFileIsReadAtEachCall(t *testing.T) {
r := newRig(t)
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err != nil {
t.Fatal(err)
}
changed := strings.Replace(grantFile, `{"to": "print-and-scan", "methods": ["get"]}`, `{"to": "someone-else", "methods": ["get"]}`, 1)
if err := os.WriteFile(r.s.receives, []byte(changed), 0o600); err != nil {
t.Fatal(err)
}
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err == nil {
t.Fatal("the second call judged by the old shares")
}
if _, err := r.s.link("novox.someone-else", ask("mesh-shanks-brother", "get", nil)); err != nil {
t.Fatal(err)
}
}