package main import ( "context" "errors" "strings" "testing" "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "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 []meshhandactsclient.Request var events []string record := func(q meshhandactsclient.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(meshhandactsclient.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(meshhandactsclient.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) } }