nats_bucket_expiry reads a bucket's stream, sets MaxAge (a duration, or none), reads it back, records a hand act through mesh-hand-acts and says bucket-expiry-set, so the lease write can take the 15 seconds off the lease bucket and the hand-back can restore them. Refuses a missing why and a name that is no bucket. Two new unit tests (stand-in embedded server) in a module at 26 of 500; no test removed, none needed to stay in budget. (issue 774, 774/S6) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
124 lines
5.4 KiB
Go
124 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/handacts"
|
|
"github.com/nats-io/nats-server/v2/server"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// A stand-in bus: the upstream server the mesh runs, in this process, with a bucket made as the controller makes its
|
|
// lease bucket (15 seconds) and a plain stream that is no bucket.
|
|
func expiryBus(t *testing.T) (Bus, jetstream.JetStream) {
|
|
t.Helper()
|
|
s, err := server.NewServer(&server.Options{Port: -1, JetStream: true, StoreDir: t.TempDir(), NoLog: true, NoSigs: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go s.Start()
|
|
if !s.ReadyForConnections(5 * time.Second) {
|
|
t.Fatal("the server did not start")
|
|
}
|
|
t.Cleanup(s.Shutdown)
|
|
nc, err := nats.Connect(s.ClientURL())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(nc.Close)
|
|
js, _ := jetstream.New(nc)
|
|
ctx := context.Background()
|
|
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: "mesh-controller_lease", TTL: 15 * time.Second}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ASSIGNMENTS", Subjects: []string{"a.>"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return func(context.Context) (jetstream.JetStream, func(), error) { return js, func() {}, nil }, js
|
|
}
|
|
|
|
func maxAge(t *testing.T, js jetstream.JetStream, stream string) time.Duration {
|
|
t.Helper()
|
|
s, err := js.Stream(context.Background(), stream)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s.CachedInfo().Config.MaxAge
|
|
}
|
|
|
|
// 774/S6/AC1: a bucket with a 15 s expiry set to none reads back with none, one hand act and bucket-expiry-set;
|
|
// restored to 15 s it reads back with 15 s again; asking for what it already has records nothing.
|
|
func TestABucketsExpiryIsSetAndRestoredAndRecorded(t *testing.T) {
|
|
bus, js := expiryBus(t)
|
|
var acts []handacts.Request
|
|
var events []string
|
|
record := func(q handacts.Request) error { acts = append(acts, q); return nil }
|
|
said := func(e string, _ any) error { events = append(events, e); return nil }
|
|
call := func(max string, extra map[string]any) (BucketExpirySet, error) {
|
|
a := map[string]any{"bucket": "mesh-controller_lease", "max_age": max, "why": "the lease write", "warrant": "ask-1"}
|
|
for k, v := range extra {
|
|
a[k] = v
|
|
}
|
|
out, err := SetBucketExpiry(context.Background(), bus, said, record, a)
|
|
if err != nil {
|
|
return BucketExpirySet{}, err
|
|
}
|
|
return out.(BucketExpirySet), nil
|
|
}
|
|
if out, err := call("none", map[string]any{"dry_run": true}); err != nil || out.Changed || maxAge(t, js, "KV_mesh-controller_lease") != 15*time.Second {
|
|
t.Fatalf("a dry run changed the bucket or failed: %+v %v", out, err)
|
|
}
|
|
out, err := call("none", nil)
|
|
if err != nil || !out.Changed || out.Was != "15s" || out.Now != "none" || maxAge(t, js, "KV_mesh-controller_lease") != 0 {
|
|
t.Fatalf("none: %+v %v", out, err)
|
|
}
|
|
if len(acts) != 1 || !strings.Contains(acts[0].What, "mesh-controller_lease from 15s to none") || !strings.Contains(acts[0].What, "ask-1") || acts[0].Why != "the lease write" {
|
|
t.Errorf("the hand act is %+v", acts)
|
|
}
|
|
if len(events) != 1 || events[0] != "bucket-expiry-set" {
|
|
t.Errorf("events are %v", events)
|
|
}
|
|
if out, err = call("none", nil); err != nil || out.Changed || len(acts) != 1 || len(events) != 1 {
|
|
t.Errorf("asking again for none changed or recorded something: %+v %v acts %d events %d", out, err, len(acts), len(events))
|
|
}
|
|
if out, err = call("15s", nil); err != nil || !out.Changed || maxAge(t, js, "KV_mesh-controller_lease") != 15*time.Second || len(acts) != 2 {
|
|
t.Errorf("restore: %+v %v", out, err)
|
|
}
|
|
// A hand act the seat does not take is said loudly, not as success.
|
|
_, err = SetBucketExpiry(context.Background(), bus, said, func(handacts.Request) error { return errors.New("no responders") }, map[string]any{"bucket": "mesh-controller_lease", "max_age": "none", "why": "x"})
|
|
if err == nil || !strings.Contains(err.Error(), "WAS changed") {
|
|
t.Errorf("an unrecorded act was answered %v", err)
|
|
}
|
|
}
|
|
|
|
// 774/S6/AC2: no why, a stream that is no bucket, and a bad expiry change nothing and say why.
|
|
func TestABucketExpiryRefusesWithoutWhyOrABucket(t *testing.T) {
|
|
bus, js := expiryBus(t)
|
|
record := func(handacts.Request) error { t.Fatal("an act was recorded for a refused call"); return nil }
|
|
said := func(string, any) error { t.Fatal("an event was said for a refused call"); return nil }
|
|
for _, c := range []struct {
|
|
args map[string]any
|
|
want string
|
|
}{
|
|
{map[string]any{"bucket": "mesh-controller_lease", "max_age": "none"}, "why is required"},
|
|
{map[string]any{"bucket": "mesh-controller_lease", "max_age": "none", "why": " "}, "why is required"},
|
|
{map[string]any{"bucket": "ASSIGNMENTS", "max_age": "none", "why": "x"}, "no key-value bucket"},
|
|
{map[string]any{"bucket": "nothing", "max_age": "none", "why": "x"}, "no key-value bucket"},
|
|
{map[string]any{"bucket": "a.b", "max_age": "none", "why": "x"}, "bucket is required"},
|
|
{map[string]any{"bucket": "mesh-controller_lease", "max_age": "", "why": "x"}, "max_age is required"},
|
|
{map[string]any{"bucket": "mesh-controller_lease", "max_age": "-5s", "why": "x"}, "max_age is required"},
|
|
} {
|
|
if _, err := SetBucketExpiry(context.Background(), bus, said, record, c.args); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%v: %v, want a refusal saying %q", c.args, err, c.want)
|
|
}
|
|
}
|
|
if got := maxAge(t, js, "KV_mesh-controller_lease"); got != 15*time.Second {
|
|
t.Errorf("a refused call left the expiry at %s", got)
|
|
}
|
|
}
|