With no send left nothing else writes a missed grant, so the take-in of a done key returns it and tries again every half minute under an urgent condition until it is written. plan-moved and sent go with the plans. The controller's first-start deletions are said on a granted subject, a node hears only its backup holder, and the operator's account writes the hand path, an object replaced included (ADR 0539 clauses 2, 7, 10 and 13).
146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
//go:build integration
|
|
|
|
package broker
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats-server/v2/server"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// 1945 (the review of A1, H1): on a bus running the user list this controller composes, the operator's account (a
|
|
// person who may invoke every tool) walks release 6's hand path end to end: it writes a node's want, that node's
|
|
// composition and its digest, and stores a file over 64 KiB in the node's object bucket and replaces it; a person with
|
|
// fewer tools may do none of it.
|
|
func TestNatsTheOperatorWritesTheHandPathOnTheComposedUserList(t *testing.T) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte("pw"), bcrypt.MinCost)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
users, err := Users(Records{Nodes: []string{"ace"},
|
|
People: map[string][]string{"jochen": {"*"}, "guest": {"mesh-controller.status"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := range users {
|
|
users[i].PasswordHash = string(hash)
|
|
}
|
|
accounts, err := ComposeAccounts(users)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conf := filepath.Join(t.TempDir(), "bus.conf")
|
|
if err := os.WriteFile(conf, []byte("listen: 127.0.0.1:-1\njetstream { store_dir: "+`"`+t.TempDir()+`"`+" }\n"+accounts), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts, err := server.ProcessConfigFile(conf)
|
|
if err != nil {
|
|
t.Fatalf("the composed accounts do not parse: %v", err)
|
|
}
|
|
opts.NoLog, opts.NoSigs = true, true
|
|
s, err := server.NewServer(opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go s.Start()
|
|
if !s.ReadyForConnections(10 * time.Second) {
|
|
t.Fatal("the bus did not come up")
|
|
}
|
|
t.Cleanup(s.Shutdown)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
connect := func(user string) jetstream.JetStream {
|
|
t.Helper()
|
|
nc, err := nats.Connect(s.ClientURL(), nats.UserInfo(user, "pw"), nats.CustomInboxPrefix("_INBOX."+user))
|
|
if err != nil {
|
|
t.Fatalf("%s cannot connect: %v", user, err)
|
|
}
|
|
t.Cleanup(nc.Close)
|
|
js, err := jetstream.New(nc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return js
|
|
}
|
|
|
|
// The buckets as the controller and the assigner make them.
|
|
cjs := connect(ControllerName)
|
|
if err := EnsureDeclarationFiles(ctx, cjs, "ace"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := EnsureComposed(ctx, cjs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := cjs.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "composed-ace", History: 5}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The assigner's bucket, made here by the controller's account, which may create any stream.
|
|
if _, err := cjs.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "mesh-delivery-assigner_wants", History: 20}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
short := func(c context.Context) (context.Context, context.CancelFunc) {
|
|
return context.WithTimeout(c, 5*time.Second)
|
|
}
|
|
for _, who := range []struct {
|
|
user string
|
|
may bool
|
|
}{{"person.jochen", true}, {"person.guest", false}} {
|
|
js := connect(who.user)
|
|
try := func(what string, act func(context.Context) error) {
|
|
t.Helper()
|
|
c, stop := short(ctx)
|
|
defer stop()
|
|
err := act(c)
|
|
if (err == nil) != who.may {
|
|
t.Errorf("%s: %s: %v (may: %v)", who.user, what, err, who.may)
|
|
}
|
|
}
|
|
try("write ace's want", func(c context.Context) error {
|
|
kv, err := js.KeyValue(c, "mesh-delivery-assigner_wants")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = kv.Put(c, "want.ace.mesh-controller", []byte(`{"v":1}`))
|
|
return err
|
|
})
|
|
try("write ace's composition", func(c context.Context) error {
|
|
kv, err := js.KeyValue(c, "composed-ace")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = kv.Put(c, "ace.mesh-controller.aaaa", []byte(`{"v":1}`))
|
|
return err
|
|
})
|
|
try("write its digest", func(c context.Context) error {
|
|
kv, err := js.KeyValue(c, delivery.ControllerComposedBucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = kv.Put(c, "ace.mesh-controller.aaaa", []byte(`{"v":1}`))
|
|
return err
|
|
})
|
|
try("store and replace a file over 64 KiB", func(c context.Context) error {
|
|
store, err := js.ObjectStore(c, "declaration-files-ace")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := bytes.Repeat([]byte("x"), 200<<10)
|
|
if _, err := store.PutBytes(c, "f00d", body); err != nil {
|
|
return err
|
|
}
|
|
_, err = store.PutBytes(c, "f00d", append(body, 'y'))
|
|
return err
|
|
})
|
|
}
|
|
}
|