The view reads directly and makes no consumer: a consumer's deliver subject publishes anywhere
Review of the view (research 036): granted CONSUMER.CREATE on the bucket's stream, the view made a push consumer delivering to mesh.mod.mesh-issues.event.opened, and a module subscribed there received the bucket's entry as the tracker's event — measured on 2.11.17. The server does not hold a deliver subject to its creator's permissions, so a consumer grant is a publish to any subject. The view now binds and reads directly, nothing else: STREAM.INFO, DIRECT.GET by key, and the batch DIRECT.GET (multi_last) that lists every key's newest value into its inbox. No watch: the page re-reads the key a tracker event names (every event carries the number). The live test lists with the batch, follows a moved event to the re-read, and is refused the consumer and the watch with nothing reaching the event subject; the mutation (CONSUMER.CREATE back) fails three tests. The credential says how to read, and that the step making it work is the next `bus upgrade` while a new bus build waits, not a push.
This commit is contained in:
@@ -16,10 +16,11 @@ import (
|
||||
// The view against a real server, over WebSocket (novox/hq research 036): the composed user list is
|
||||
// what the server reads, the listener is the shape the bus module declares (no TLS, compression on,
|
||||
// reached across the overlay only), and the view with its credential binds the issue tracker's bucket,
|
||||
// reads a key, watches a change land — and is refused every write: a put, a delete, an event.
|
||||
// lists it with one batch read, follows a tracker event to re-read the key it names — and is refused
|
||||
// every write: a put, a delete, an event, and a consumer delivering onto the tracker's event subject.
|
||||
//
|
||||
// go test ./internal/broker/ -run TestTheView
|
||||
func TestTheViewWatchesTheIssuesOverWebSocketAndWritesNothing(t *testing.T) {
|
||||
func TestTheViewReadsTheIssuesOverWebSocketAndWritesNothing(t *testing.T) {
|
||||
hash := func(password string) string {
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
@@ -29,7 +30,7 @@ func TestTheViewWatchesTheIssuesOverWebSocketAndWritesNothing(t *testing.T) {
|
||||
}
|
||||
const node = "anchor"
|
||||
tracker := Principal{Kind: KindModule, Node: node, Module: "mesh-issues", State: []string{"issues"},
|
||||
Emits: []string{"opened"}, PasswordHash: hash("tracker")}
|
||||
Emits: []string{"opened", "moved"}, PasswordHash: hash("tracker")}
|
||||
accounts, err := ComposeAccounts([]Principal{
|
||||
{Kind: KindController, PasswordHash: hash("controller")},
|
||||
tracker,
|
||||
@@ -111,30 +112,89 @@ func TestTheViewWatchesTheIssuesOverWebSocketAndWritesNothing(t *testing.T) {
|
||||
} else if !strings.Contains(string(got.Value()), `"number":365`) {
|
||||
t.Fatalf("the view read %q", got.Value())
|
||||
}
|
||||
watch, err := vkv.WatchAll()
|
||||
if err != nil {
|
||||
t.Fatalf("the view could not watch the bucket: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = watch.Stop() })
|
||||
seen := func(key string) {
|
||||
t.Helper()
|
||||
deadline := time.After(10 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case e := <-watch.Updates():
|
||||
if e != nil && e.Key() == key {
|
||||
return
|
||||
}
|
||||
case <-deadline:
|
||||
t.Fatalf("the view's watch never saw %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
seen("365")
|
||||
if _, err := tkv.Put("366", []byte(`{"number":366,"status":"open"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen("366")
|
||||
|
||||
// The list: one batch read, the newest value of every key, into the view's own inbox, ended by the
|
||||
// server's end-of-batch status (204).
|
||||
listed := map[string]string{}
|
||||
inbox := view.NewRespInbox()
|
||||
batch, err := view.SubscribeSync(inbox)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := view.PublishRequest("$JS.API.DIRECT.GET.KV_"+ViewBucket, inbox,
|
||||
[]byte(`{"multi_last":["$KV.`+ViewBucket+`.>"]}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
m, err := batch.NextMsg(5 * time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("the view's batch read ended without its end-of-batch (%d keys so far): %v", len(listed), err)
|
||||
}
|
||||
if m.Header.Get("Status") == "204" {
|
||||
break
|
||||
}
|
||||
if status := m.Header.Get("Status"); status != "" {
|
||||
t.Fatalf("the batch read answered %s %s", status, m.Header.Get("Description"))
|
||||
}
|
||||
listed[m.Header.Get("Nats-Subject")] = string(m.Data)
|
||||
}
|
||||
_ = batch.Unsubscribe()
|
||||
for _, key := range []string{"365", "366"} {
|
||||
if !strings.Contains(listed["$KV."+ViewBucket+"."+key], `"number":`+key) {
|
||||
t.Errorf("the batch read did not list %s: %v", key, listed)
|
||||
}
|
||||
}
|
||||
|
||||
// A change followed: the tracker moves 365 and says so; the view hears the event and reads the key
|
||||
// it names.
|
||||
moved, err := view.SubscribeSync("mesh.mod.mesh-issues.event.moved")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = view.Flush()
|
||||
if _, err := tkv.Put("365", []byte(`{"number":365,"status":"located"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := trackerConn.Publish("mesh.mod.mesh-issues.event.moved", []byte(`{"number":365,"to":"located"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := moved.NextMsg(5 * time.Second); err != nil {
|
||||
t.Fatalf("the view did not hear the tracker's event: %v", err)
|
||||
}
|
||||
if got, err := vkv.Get("365"); err != nil || !strings.Contains(string(got.Value()), "located") {
|
||||
t.Fatalf("after the event the view read %v (%v)", got, err)
|
||||
}
|
||||
|
||||
// **The hole a watch would open, shut** (ViewReads): a consumer delivering onto the tracker's event
|
||||
// subject would have the server republish the bucket there, as the tracker. Refused, and nothing
|
||||
// reaches a module listening on that subject.
|
||||
listener, err := trackerConn.SubscribeSync("mesh.mod.mesh-issues.event.opened")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = trackerConn.Flush()
|
||||
if _, err := vjs.AddConsumer("KV_"+ViewBucket, &nats.ConsumerConfig{Name: "w", DeliverSubject: "mesh.mod.mesh-issues.event.opened",
|
||||
AckPolicy: nats.AckNonePolicy, FilterSubject: "$KV." + ViewBucket + ".>"}); err == nil {
|
||||
t.Error("the view made a consumer")
|
||||
}
|
||||
if _, err := vkv.WatchAll(); err == nil {
|
||||
t.Error("the view made a watch, which is a consumer")
|
||||
}
|
||||
if m, err := listener.NextMsg(2 * time.Second); err == nil {
|
||||
t.Errorf("a message reached the tracker's event subject from the view: %q", m.Data)
|
||||
}
|
||||
// The refusals of the consumer create land as permission violations too; drained before the writes.
|
||||
drain := time.After(500 * time.Millisecond)
|
||||
for draining := true; draining; {
|
||||
select {
|
||||
case <-refused:
|
||||
case <-drain:
|
||||
draining = false
|
||||
}
|
||||
}
|
||||
|
||||
// And every write is refused: the server says so, and the bucket is unchanged.
|
||||
if _, err := vkv.Put("367", []byte(`{"number":367}`)); err == nil {
|
||||
|
||||
Reference in New Issue
Block a user