//go:build integration package process import ( "errors" "strings" "testing" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" stdio "git.novox.be/novox/mesh-sdk/go" ) // 1188/AC1: given a process connection to a test bus granted the bucket mesh-mail_sends, when it creates a // key, reads it, and updates it with the revision it read, then each succeeds and returns the new // revision; an update with a stale revision fails as stdio.ErrChanged; a create of an existing key fails // as stdio.ErrChanged. func TestStateCreateGetUpdateOnAGrantedBucket(t *testing.T) { url, account := grantedSendProcess(t) _, adminJetStream := adminJS(t, url) if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_sends"}); err != nil { t.Fatalf("the admin could not make the bucket: %v", err) } conn, err := Connect(account) if err != nil { t.Fatal(err) } defer conn.Close() s := conn.State("sends") rev, err := s.Create("key", map[string]any{"v": 1}) if err != nil { t.Fatalf("Create on a granted bucket: %v", err) } if rev == 0 { t.Fatal("Create returned no revision") } e, err := s.Get("key") if err != nil || e == nil { t.Fatalf("Get after Create: %+v, %v", e, err) } if e.Revision != rev || string(e.Value) != `{"v":1}` { t.Fatalf("Get read back %+v, want revision %d and {\"v\":1}", e, rev) } rev2, err := s.Update("key", map[string]any{"v": 2}, rev) if err != nil { t.Fatalf("Update at the revision read: %v", err) } if rev2 == rev { t.Fatal("Update did not return a new revision") } if _, err := s.Update("key", map[string]any{"v": 3}, rev); !errors.Is(err, stdio.ErrChanged) { t.Fatalf("Update at a stale revision: %v, want stdio.ErrChanged", err) } if _, err := s.Create("key", map[string]any{"v": 4}); !errors.Is(err, stdio.ErrChanged) { t.Fatalf("Create of an existing key: %v, want stdio.ErrChanged", err) } } // 1188/AC4: given an account not granted a bucket, when it writes there, then the bus's refusal comes // back as an error naming the bucket. The bucket exists (made by the admin) so the only reason Create // can fail is the missing grant, never "bucket not found"; and Conn's own ErrorHandler (conn.go // withRefusal/publishRefused) catches the denied publish at once, so this returns promptly rather than // after a timeout. func TestStateRefusesAnUngrantedBucketNamingIt(t *testing.T) { url, account := grantedSendProcess(t) _, adminJetStream := adminJS(t, url) if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_unrelated"}); err != nil { t.Fatalf("the admin could not make the bucket: %v", err) } conn, err := Connect(account) if err != nil { t.Fatal(err) } defer conn.Close() s := conn.State("unrelated") _, err = s.Create("key", map[string]any{"v": 1}) if err == nil { t.Fatal("writing to an ungranted bucket was not refused") } if !errors.Is(err, nats.ErrPermissionViolation) { t.Errorf("the refusal %v does not wrap nats.ErrPermissionViolation", err) } if !strings.Contains(err.Error(), "mesh-mail_unrelated") { t.Errorf("the refusal %q does not name the bucket mesh-mail_unrelated", err.Error()) } } // 1840/S1: given a process connection granted its bucket, when it puts keys whole and lists them, then Put // returns a new revision each time and Keys lists them sorted (none for a bucket with none). func TestStatePutAndKeysOnAGrantedBucket(t *testing.T) { url, account := grantedSendProcess(t) _, adminJetStream := adminJS(t, url) if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_sends"}); err != nil { t.Fatalf("the admin could not make the bucket: %v", err) } conn, err := Connect(account) if err != nil { t.Fatal(err) } defer conn.Close() s := conn.State("sends") if keys, err := s.Keys(); err != nil || len(keys) != 0 { t.Fatalf("Keys of an empty bucket: %v, %v", keys, err) } rev, err := s.Put("b", map[string]any{"v": 1}) if err != nil || rev == 0 { t.Fatalf("Put on a granted bucket: %d, %v", rev, err) } rev2, err := s.Put("b", map[string]any{"v": 2}) if err != nil || rev2 <= rev { t.Fatalf("Put over a key: %d, %v", rev2, err) } if _, err := s.Put("a", 1); err != nil { t.Fatal(err) } keys, err := s.Keys() if err != nil || strings.Join(keys, ",") != "a,b" { t.Fatalf("Keys: %v, %v, want a,b", keys, err) } } // 1840/S2: given a process connection granted its bucket, which already holds two keys and a deleted one, // when it watches, then it gets the two current values marked Current, and then a later put and a delete in // order; a narrowed watch gets only the keys it names. func TestStateWatchOnAGrantedBucket(t *testing.T) { url, account := grantedSendProcess(t) _, adminJetStream := adminJS(t, url) kv, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_sends"}) if err != nil { t.Fatalf("the admin could not make the bucket: %v", err) } ctx := ctxFor(t) for k, v := range map[string]string{"a.x": `1`, "a.y": `2`, "b.z": `3`, "gone": `4`} { if _, err := kv.Put(ctx, k, []byte(v)); err != nil { t.Fatal(err) } } if err := kv.Delete(ctx, "gone"); err != nil { t.Fatal(err) } conn, err := Connect(account) if err != nil { t.Fatal(err) } defer conn.Close() s := conn.State("sends") all := make(chan stdio.StateChange, 16) if err := s.Watch("", func(c stdio.StateChange) error { all <- c; return nil }); err != nil { t.Fatalf("Watch on a granted bucket: %v", err) } current := map[string]bool{} for len(all) > 0 { c := <-all if !c.Current || c.Op != "put" { t.Fatalf("a current value arrived as %+v", c) } current[c.Key] = true } if len(current) != 3 || current["gone"] { t.Fatalf("current values %v, want a.x, a.y, b.z and not gone", current) } narrow := make(chan stdio.StateChange, 16) if err := s.Watch("a.*", func(c stdio.StateChange) error { narrow <- c; return nil }); err != nil { t.Fatal(err) } if len(narrow) != 2 { t.Fatalf("a narrowed watch handed %d current values, want 2", len(narrow)) } // the current values come in the order of their last revision, not of their keys: taken as a set. narrowCurrent := map[string]bool{} for len(narrow) > 0 { c := <-narrow if !c.Current || c.Op != "put" { t.Fatalf("a narrowed watch's current value arrived as %+v", c) } narrowCurrent[c.Key] = true } if !narrowCurrent["a.x"] || !narrowCurrent["a.y"] { t.Fatalf("a narrowed watch's current values %v, want a.x and a.y", narrowCurrent) } if _, err := s.Put("a.x", 5); err != nil { t.Fatal(err) } if err := kv.Delete(ctx, "a.y"); err != nil { t.Fatal(err) } for _, want := range []struct{ key, op string }{{"a.x", "put"}, {"a.y", "delete"}} { select { case c := <-all: if c.Key != want.key || c.Op != want.op || c.Current { t.Fatalf("got %+v, want %s %s, not current", c, want.op, want.key) } case <-time.After(10 * time.Second): t.Fatalf("no %s of %s", want.op, want.key) } } if _, err := s.Put("b.q", 6); err != nil { t.Fatal(err) } for _, want := range []string{"a.x", "a.y"} { select { case c := <-narrow: if c.Key != want || c.Current { t.Fatalf("narrowed watch got %+v, want the change of %s", c, want) } case <-time.After(10 * time.Second): t.Fatalf("narrowed watch missed %s", want) } } select { case c := <-narrow: t.Fatalf("narrowed watch got %+v for a key it did not name", c) case <-time.After(500 * time.Millisecond): } } // 1840/AC8: given an account not granted a bucket, when it watches it, then the refusal names the bucket. func TestStateWatchRefusesAnUngrantedBucketNamingIt(t *testing.T) { url, account := grantedSendProcess(t) _, adminJetStream := adminJS(t, url) if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_unrelated"}); err != nil { t.Fatalf("the admin could not make the bucket: %v", err) } conn, err := Connect(account) if err != nil { t.Fatal(err) } defer conn.Close() err = conn.State("unrelated").Watch("", func(stdio.StateChange) error { return nil }) if err == nil || !errors.Is(err, nats.ErrPermissionViolation) || !strings.Contains(err.Error(), "mesh-mail_unrelated") { t.Fatalf("Watch on an ungranted bucket: %v, want a refusal naming mesh-mail_unrelated", err) } } // 1840/S1: given a process connection granted its bucket, when it deletes a key, then the key is gone and // deleting it again is no error. func TestStateDeleteOnAGrantedBucket(t *testing.T) { url, account := grantedSendProcess(t) _, adminJetStream := adminJS(t, url) if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-mail_sends"}); err != nil { t.Fatalf("the admin could not make the bucket: %v", err) } conn, err := Connect(account) if err != nil { t.Fatal(err) } defer conn.Close() s := conn.State("sends") if _, err := s.Put("k", 1); err != nil { t.Fatal(err) } if err := s.Delete("k"); err != nil { t.Fatalf("Delete on a granted bucket: %v", err) } if e, err := s.Get("k"); err != nil || e != nil { t.Fatalf("Get after Delete: %+v, %v", e, err) } if err := s.Delete("k"); err != nil { t.Fatalf("Delete of an absent key: %v", err) } }