package membership_test import ( "path/filepath" "testing" "time" "github.com/enmanuel/unibus/pkg/blobstore" "github.com/enmanuel/unibus/pkg/client" "github.com/enmanuel/unibus/pkg/frame" "github.com/enmanuel/unibus/pkg/membership" "github.com/enmanuel/unibus/pkg/room" ) // TestClientCreateRoomRefreshPublishFlow is the issue 0006e DoD: under enforce+ACL // a peer creates a room AFTER connecting, and pub/sub works without manual // intervention because the client follows the membership-change contract // (CreateRoom -> RefreshSession -> Subscribe/Publish), exactly as cmd/chat and // cmd/worker now do. This is the end-to-end flow through the client API, proving // the ACL is usable under enforce rather than something an operator must disable. func TestClientCreateRoomRefreshPublishFlow(t *testing.T) { dir := t.TempDir() store, err := membership.Open(filepath.Join(dir, "unibus.db")) if err != nil { t.Fatalf("store: %v", err) } t.Cleanup(func() { store.Close() }) alice, bob := mustID(t), mustID(t) mustAddUser(t, store, alice, "alice") mustAddUser(t, store, bob, "bob") srv := startACLNats(t, store) // data plane: enforce + per-subject ACL blobs, _ := blobstore.New(filepath.Join(dir, "blobs")) ctrl := newCtrl(t, store, blobs) aliceC, err := client.NewWithOptions(srv.ClientURL(), ctrl, alice, client.Options{UseNkey: true}) if err != nil { t.Fatalf("connect alice: %v", err) } defer aliceC.Close() bobC, err := client.NewWithOptions(srv.ClientURL(), ctrl, bob, client.Options{UseNkey: true}) if err != nil { t.Fatalf("connect bob: %v", err) } defer bobC.Close() // alice creates a room AFTER connecting: the subject was not in her ACL at // connect time, so she must refresh to publish on it (the worker contract). roomID, err := aliceC.CreateRoom("room.flow.x", room.ModeNATS) if err != nil { t.Fatalf("alice create room: %v", err) } if err := aliceC.RefreshSession(); err != nil { t.Fatalf("alice refresh: %v", err) } // alice invites bob; bob joins then refreshes to gain the subject (the chat // subscriber contract), and only then subscribes. if err := aliceC.Invite(roomID, bobC.Endpoint()); err != nil { t.Fatalf("alice invite bob: %v", err) } if err := bobC.Join(roomID); err != nil { t.Fatalf("bob join: %v", err) } if err := bobC.RefreshSession(); err != nil { t.Fatalf("bob refresh: %v", err) } got := make(chan string, 4) sub, err := bobC.Subscribe(roomID, func(_ frame.Frame, plaintext []byte) { got <- string(plaintext) }) if err != nil { t.Fatalf("bob subscribe after refresh: %v", err) } defer sub.Unsubscribe() time.Sleep(200 * time.Millisecond) // let the subscription settle if err := aliceC.Publish(roomID, []byte("hello-under-acl")); err != nil { t.Fatalf("alice publish after refresh: %v", err) } select { case msg := <-got: if msg != "hello-under-acl" { t.Fatalf("bob got %q", msg) } case <-time.After(3 * time.Second): t.Fatalf("bob did not receive the message: the create->refresh->subscribe flow is broken under enforce+ACL") } }