package process import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "sync" "testing" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" ) // 1171/AC2: given an account file broker- in the controller's form {url, fingerprint, node, // module, user, password, claims}, when the process package reads it, then it yields those fields and // tolerates claims. func TestReadAccountYieldsEveryFieldAndToleratesClaims(t *testing.T) { path := writeAccount(t, map[string]any{ "url": "nats://novox:4222", "fingerprint": "sha256:ab", "node": "novox", "module": "mesh-mail", "user": "novox.mesh-mail.send", "password": "secret-pw", "claims": []map[string]any{{"seat": "operator-channel", "verbs": []string{"tell"}}}, }) a, err := ReadAccount(path) if err != nil { t.Fatalf("a well-formed account file was refused: %v", err) } if a.URL != "nats://novox:4222" || a.Fingerprint != "sha256:ab" || a.Node != "novox" || a.Module != "mesh-mail" || a.User != "novox.mesh-mail.send" || a.Password != "secret-pw" { t.Fatalf("the account read back: %+v", a) } } // 1171/AC2: a process account carries no claims at all (heldProcessAccount); reading it is not refused for // the field being absent. func TestReadAccountToleratesNoClaimsAtAll(t *testing.T) { path := writeAccount(t, map[string]any{ "url": "nats://novox:4222", "node": "novox", "module": "mesh-mail", "user": "novox.mesh-mail.send", "password": "secret-pw", }) a, err := ReadAccount(path) if err != nil { t.Fatalf("a process account with no claims was refused: %v", err) } if a.User != "novox.mesh-mail.send" { t.Fatalf("the account read back: %+v", a) } } // 1171/AC2: a file missing url, user or password is refused, naming the field; the password never appears // in the error even when it is the field present and another is missing. func TestReadAccountRefusesAFileMissingAField(t *testing.T) { t.Run("1945: a refused bucket stops a start only when the part cannot work without it", func(t *testing.T) { refusal := errors.New("nats: permissions violation for publish to $JS.API.STREAM.INFO.KV_mesh-delivery-gate_source") for _, c := range []struct { name string err error }{{"refused", refusal}, {"not answered", context.DeadlineExceeded}} { var said []string logf := func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) } if kv := OptionalBucket(context.Background(), refusingBus{c.err}, "b", "b is not heard", logf); kv != nil { t.Errorf("%s: an optional bucket answered %v", c.name, kv) } NotWatched(logf, "b", c.err, "b is not heard") if len(said) != 2 || !strings.Contains(said[0], c.err.Error()) || !strings.Contains(said[1], "cannot be watched") { t.Errorf("%s: said %q", c.name, said) } if _, err := EssentialBucket(context.Background(), refusingBus{c.err}, "builder", "b", "build"); err == nil || !strings.Contains(err.Error(), "grant of b") || !strings.Contains(err.Error(), "the builder cannot build") { t.Errorf("%s: an essential bucket: %v", c.name, err) } } }) t.Run("1790: a bucket missing at start is bound when the bus says it was created, and what was written meanwhile is kept", func(t *testing.T) { bus := &laterBus{} hear := &heldSubscriber{} var said []string logf := func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) } b := AwaitBucket(context.Background(), bus, hear, "m_hearing", "the health tool cannot say it", logf) if hear.subject != "$JS.EVENT.ADVISORY.STREAM.CREATED.KV_m_hearing" { t.Fatalf("heard %q", hear.subject) } ready := make(chan jetstream.KeyValue, 1) b.Ready(func(kv jetstream.KeyValue) { ready <- kv }) for _, v := range []string{"one", "two"} { if _, err := b.Put(context.Background(), "dead-letters", []byte(v)); !errors.Is(err, ErrNotYet) { t.Fatalf("a write while missing: %v", err) } } if b.KV() != nil || len(ready) > 0 { t.Fatal("bound before the bucket exists") } bus.create() hear.handle(&nats.Msg{Subject: hear.subject}) select { case <-ready: case <-time.After(5 * time.Second): t.Fatal("not bound within 5s of the bus saying the bucket was created") } if got := bus.kv.puts; strings.Join(got, ",") != "dead-letters=two" { t.Errorf("written when it appeared: %v, want the latest value only", got) } if _, err := b.Put(context.Background(), "dead-letters", []byte("three")); err != nil { t.Errorf("a write once bound: %v", err) } if len(said) != 2 || !strings.Contains(said[0], "cannot be read yet") || !strings.Contains(said[1], "appeared") { t.Errorf("said %q", said) } // Without the advisory heard, the next use binds it. quiet := &laterBus{} c := AwaitBucket(context.Background(), quiet, nil, "m_hearing", "x", nil) quiet.create() if c.KV() == nil { t.Error("a use after the bucket appeared did not bind it") } }) for _, tc := range []struct { field string values map[string]any }{ {"url", map[string]any{"node": "novox", "module": "mesh-mail", "user": "novox.mesh-mail.send", "password": "secret-pw"}}, {"user", map[string]any{"url": "nats://novox:4222", "node": "novox", "module": "mesh-mail", "password": "secret-pw"}}, {"password", map[string]any{"url": "nats://novox:4222", "node": "novox", "module": "mesh-mail", "user": "novox.mesh-mail.send"}}, } { t.Run(tc.field, func(t *testing.T) { path := writeAccount(t, tc.values) _, err := ReadAccount(path) if err == nil { t.Fatalf("an account file missing %s was accepted", tc.field) } if !strings.Contains(err.Error(), tc.field) { t.Errorf("the error %q does not name the missing field %s", err.Error(), tc.field) } if strings.Contains(err.Error(), "secret-pw") { t.Errorf("the password appears in the error: %q", err.Error()) } }) } } func writeAccount(t *testing.T, fields map[string]any) string { t.Helper() b, err := json.Marshal(fields) if err != nil { t.Fatal(err) } path := filepath.Join(t.TempDir(), "broker-send") if err := os.WriteFile(path, b, 0o600); err != nil { t.Fatal(err) } return path } // refusingBus refuses every bucket with err. type refusingBus struct{ err error } func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) { return nil, r.err } // laterBus has no bucket until create, and then one. type laterBus struct { mu sync.Mutex kv *keptKV } func (l *laterBus) create() { l.mu.Lock(); l.kv = &keptKV{}; l.mu.Unlock() } func (l *laterBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) { l.mu.Lock() defer l.mu.Unlock() if l.kv == nil { return nil, jetstream.ErrBucketNotFound } return l.kv, nil } // keptKV records each put as key=value. type keptKV struct { jetstream.KeyValue puts []string } func (k *keptKV) Put(_ context.Context, key string, value []byte) (uint64, error) { k.puts = append(k.puts, key+"="+string(value)) return uint64(len(k.puts)), nil } // heldSubscriber keeps the one subscription made, for the test to deliver to. type heldSubscriber struct { subject string handle nats.MsgHandler } func (h *heldSubscriber) Subscribe(subject string, handle nats.MsgHandler) (*nats.Subscription, error) { h.subject, h.handle = subject, handle return nil, nil }