meetroom_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. package meetroom
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "testing"
  9. "time"
  10. "imuslab.com/arozos/mod/sharedspace"
  11. )
  12. func newTestManager(t *testing.T) *Manager {
  13. t.Helper()
  14. return NewManager(filepath.Join(t.TempDir(), "attachments"))
  15. }
  16. // newSpaceBoundManager returns a room manager wired to a shared-space manager,
  17. // the way MeetRoomInit binds them in production.
  18. func newSpaceBoundManager(t *testing.T) (*Manager, *sharedspace.Manager) {
  19. t.Helper()
  20. m := NewManager(filepath.Join(t.TempDir(), "attachments"))
  21. sm := sharedspace.NewManager(filepath.Join(t.TempDir(), "spaces"), 0)
  22. m.BindSpaceManager(sm)
  23. return m, sm
  24. }
  25. func TestCreateRoom(t *testing.T) {
  26. m := newTestManager(t)
  27. tests := []struct {
  28. name string
  29. host string
  30. title string
  31. password string
  32. wantTitle string
  33. wantProtected bool
  34. }{
  35. {"open room with title", "alice", "Standup", "", "Standup", false},
  36. {"password room", "bob", "Secret sync", "hunter2", "Secret sync", true},
  37. {"default title", "carol", "", "", "carol's Meeting", false},
  38. {"overlong title clipped", "dave", strings.Repeat("x", 200), "", strings.Repeat("x", maxTitleLength), false},
  39. }
  40. for _, tt := range tests {
  41. t.Run(tt.name, func(t *testing.T) {
  42. room := m.CreateRoom(tt.host, tt.title, tt.password)
  43. if len(room.ID) != roomIDLength {
  44. t.Errorf("room ID %q length = %d, want %d", room.ID, len(room.ID), roomIDLength)
  45. }
  46. for _, c := range room.ID {
  47. if c < '0' || c > '9' {
  48. t.Errorf("room ID %q contains non-digit %q", room.ID, c)
  49. }
  50. }
  51. if room.Title != tt.wantTitle {
  52. t.Errorf("title = %q, want %q", room.Title, tt.wantTitle)
  53. }
  54. if room.Host != tt.host {
  55. t.Errorf("host = %q, want %q", room.Host, tt.host)
  56. }
  57. if room.HasPassword() != tt.wantProtected {
  58. t.Errorf("HasPassword() = %v, want %v", room.HasPassword(), tt.wantProtected)
  59. }
  60. if got, ok := m.GetRoom(room.ID); !ok || got != room {
  61. t.Errorf("GetRoom(%q) did not return the created room", room.ID)
  62. }
  63. })
  64. }
  65. }
  66. func TestCreateRoomUniqueIDs(t *testing.T) {
  67. m := newTestManager(t)
  68. seen := map[string]bool{}
  69. for i := 0; i < 100; i++ {
  70. room := m.CreateRoom("host", "", "")
  71. if seen[room.ID] {
  72. t.Fatalf("duplicate room ID generated: %s", room.ID)
  73. }
  74. seen[room.ID] = true
  75. }
  76. if m.RoomCount() != 100 {
  77. t.Errorf("RoomCount() = %d, want 100", m.RoomCount())
  78. }
  79. }
  80. func TestValidateJoin(t *testing.T) {
  81. m := newTestManager(t)
  82. open := m.CreateRoom("alice", "Open", "")
  83. locked := m.CreateRoom("bob", "Locked", "hunter2")
  84. tests := []struct {
  85. name string
  86. roomID string
  87. password string
  88. wantErr error
  89. }{
  90. {"open room no password", open.ID, "", nil},
  91. {"open room ignores password", open.ID, "whatever", nil},
  92. {"locked room correct password", locked.ID, "hunter2", nil},
  93. {"locked room wrong password", locked.ID, "letmein", ErrInvalidPassword},
  94. {"locked room empty password", locked.ID, "", ErrInvalidPassword},
  95. {"unknown room", "000000000", "", ErrRoomNotFound},
  96. }
  97. for _, tt := range tests {
  98. t.Run(tt.name, func(t *testing.T) {
  99. _, err := m.ValidateJoin(tt.roomID, tt.password)
  100. if err != tt.wantErr {
  101. t.Errorf("ValidateJoin(%q, %q) error = %v, want %v", tt.roomID, tt.password, err, tt.wantErr)
  102. }
  103. })
  104. }
  105. }
  106. func TestParticipantLifecycle(t *testing.T) {
  107. m := newTestManager(t)
  108. room := m.CreateRoom("alice", "", "")
  109. host, err := room.AddParticipant("alice")
  110. if err != nil {
  111. t.Fatalf("AddParticipant(alice) error = %v", err)
  112. }
  113. guest, err := room.AddParticipant("bob")
  114. if err != nil {
  115. t.Fatalf("AddParticipant(bob) error = %v", err)
  116. }
  117. if !host.IsHost {
  118. t.Errorf("host participant IsHost = false, want true")
  119. }
  120. if guest.IsHost {
  121. t.Errorf("guest participant IsHost = true, want false")
  122. }
  123. if host.PeerID == guest.PeerID {
  124. t.Errorf("peer IDs collide: %d", host.PeerID)
  125. }
  126. if room.ParticipantCount() != 2 {
  127. t.Errorf("ParticipantCount() = %d, want 2", room.ParticipantCount())
  128. }
  129. if p, ok := room.GetParticipant(guest.PeerID); !ok || p != guest {
  130. t.Errorf("GetParticipant(%d) did not return the guest", guest.PeerID)
  131. }
  132. room.RemoveParticipant(guest.PeerID)
  133. if room.ParticipantCount() != 1 {
  134. t.Errorf("ParticipantCount() after remove = %d, want 1", room.ParticipantCount())
  135. }
  136. if _, open := <-guest.Send; open {
  137. t.Errorf("removed participant's send channel still open")
  138. }
  139. //Removing twice must not panic
  140. room.RemoveParticipant(guest.PeerID)
  141. }
  142. func TestKickParticipant(t *testing.T) {
  143. m := newTestManager(t)
  144. room := m.CreateRoom("alice", "", "")
  145. host, _ := room.AddParticipant("alice")
  146. guest, _ := room.AddParticipant("bob")
  147. //The host cannot be kicked, even by peer ID
  148. if _, ok := room.KickParticipant(host.PeerID); ok {
  149. t.Errorf("KickParticipant(host) = true, want false")
  150. }
  151. if room.ParticipantCount() != 2 {
  152. t.Errorf("ParticipantCount() after failed host kick = %d, want 2", room.ParticipantCount())
  153. }
  154. //A regular guest is removed and returned
  155. kicked, ok := room.KickParticipant(guest.PeerID)
  156. if !ok || kicked != guest {
  157. t.Fatalf("KickParticipant(guest) = (%v, %v), want the guest / true", kicked, ok)
  158. }
  159. if room.ParticipantCount() != 1 {
  160. t.Errorf("ParticipantCount() after kick = %d, want 1", room.ParticipantCount())
  161. }
  162. if _, open := <-guest.Send; open {
  163. t.Errorf("kicked participant's send channel still open")
  164. }
  165. //The kick is recorded in the attendance log as a leave
  166. for _, record := range room.Attendance() {
  167. if record.PeerID == guest.PeerID && record.Present() {
  168. t.Errorf("kicked participant still marked present in attendance")
  169. }
  170. }
  171. //Kicking an unknown peer, or the same peer twice, is a safe no-op
  172. if _, ok := room.KickParticipant(guest.PeerID); ok {
  173. t.Errorf("KickParticipant(already kicked) = true, want false")
  174. }
  175. if _, ok := room.KickParticipant(9999); ok {
  176. t.Errorf("KickParticipant(unknown) = true, want false")
  177. }
  178. }
  179. func TestBroadcastAndSendTo(t *testing.T) {
  180. m := newTestManager(t)
  181. room := m.CreateRoom("alice", "", "")
  182. a, _ := room.AddParticipant("alice")
  183. b, _ := room.AddParticipant("bob")
  184. c, _ := room.AddParticipant("carol")
  185. room.Broadcast([]byte("hello"), a.PeerID)
  186. select {
  187. case msg := <-b.Send:
  188. if string(msg) != "hello" {
  189. t.Errorf("b received %q, want %q", msg, "hello")
  190. }
  191. default:
  192. t.Errorf("b received nothing from broadcast")
  193. }
  194. select {
  195. case msg := <-c.Send:
  196. if string(msg) != "hello" {
  197. t.Errorf("c received %q, want %q", msg, "hello")
  198. }
  199. default:
  200. t.Errorf("c received nothing from broadcast")
  201. }
  202. select {
  203. case msg := <-a.Send:
  204. t.Errorf("excluded sender received %q", msg)
  205. default:
  206. }
  207. if !room.SendTo(b.PeerID, []byte("direct")) {
  208. t.Errorf("SendTo(%d) = false, want true", b.PeerID)
  209. }
  210. if msg := <-b.Send; string(msg) != "direct" {
  211. t.Errorf("b received %q, want %q", msg, "direct")
  212. }
  213. if room.SendTo(9999, []byte("direct")) {
  214. t.Errorf("SendTo(9999) = true for unknown peer, want false")
  215. }
  216. }
  217. func TestAttachmentLifecycle(t *testing.T) {
  218. m := newTestManager(t)
  219. room := m.CreateRoom("alice", "", "")
  220. tests := []struct {
  221. name string
  222. roomID string
  223. fileName string
  224. content string
  225. maxSize int64
  226. wantErr error
  227. }{
  228. {"normal upload", room.ID, "notes.txt", "meeting notes", 1024, nil},
  229. {"unknown room", "000000000", "notes.txt", "data", 1024, ErrRoomNotFound},
  230. {"oversized upload", room.ID, "big.bin", strings.Repeat("A", 100), 10, ErrAttachmentTooLarge},
  231. }
  232. for _, tt := range tests {
  233. t.Run(tt.name, func(t *testing.T) {
  234. att, err := m.SaveAttachment(tt.roomID, tt.fileName, "alice", strings.NewReader(tt.content), tt.maxSize)
  235. if err != tt.wantErr {
  236. t.Fatalf("SaveAttachment() error = %v, want %v", err, tt.wantErr)
  237. }
  238. if err != nil {
  239. return
  240. }
  241. if att.Size != int64(len(tt.content)) {
  242. t.Errorf("attachment size = %d, want %d", att.Size, len(tt.content))
  243. }
  244. stored, ok := m.GetAttachment(tt.roomID, att.ID)
  245. if !ok {
  246. t.Fatalf("GetAttachment(%q) not found", att.ID)
  247. }
  248. data, err := os.ReadFile(stored.DiskPath)
  249. if err != nil {
  250. t.Fatalf("reading stored attachment: %v", err)
  251. }
  252. if !bytes.Equal(data, []byte(tt.content)) {
  253. t.Errorf("stored content = %q, want %q", data, tt.content)
  254. }
  255. })
  256. }
  257. if _, ok := m.GetAttachment(room.ID, "nonexistent"); ok {
  258. t.Errorf("GetAttachment returned ok for unknown file ID")
  259. }
  260. }
  261. func TestCloseRoomCleansUp(t *testing.T) {
  262. m := newTestManager(t)
  263. room := m.CreateRoom("alice", "", "")
  264. p, _ := room.AddParticipant("alice")
  265. att, err := m.SaveAttachment(room.ID, "doc.pdf", "alice", strings.NewReader("content"), 1024)
  266. if err != nil {
  267. t.Fatalf("SaveAttachment() error = %v", err)
  268. }
  269. members := m.CloseRoom(room.ID)
  270. if len(members) != 1 || members[0] != p {
  271. t.Errorf("CloseRoom returned %d members, want the 1 participant", len(members))
  272. }
  273. if _, ok := m.GetRoom(room.ID); ok {
  274. t.Errorf("room still registered after CloseRoom")
  275. }
  276. if _, open := <-p.Send; open {
  277. t.Errorf("participant send channel still open after CloseRoom")
  278. }
  279. if _, err := os.Stat(att.DiskPath); !os.IsNotExist(err) {
  280. t.Errorf("attachment file still on disk after CloseRoom: %v", err)
  281. }
  282. if _, err := room.AddParticipant("bob"); err != ErrRoomClosed {
  283. t.Errorf("AddParticipant on closed room error = %v, want ErrRoomClosed", err)
  284. }
  285. //Closing an unknown room must be a no-op
  286. if members := m.CloseRoom("000000000"); members != nil {
  287. t.Errorf("CloseRoom on unknown ID returned %v, want nil", members)
  288. }
  289. }
  290. func TestSweepIdleRooms(t *testing.T) {
  291. m := newTestManager(t)
  292. idle := m.CreateRoom("alice", "Idle", "")
  293. occupied := m.CreateRoom("bob", "Busy", "")
  294. occupied.AddParticipant("bob")
  295. fresh := m.CreateRoom("carol", "Fresh", "")
  296. //Backdate the idle room's activity clock
  297. idle.mu.Lock()
  298. idle.lastActivity = time.Now().Add(-time.Hour)
  299. idle.mu.Unlock()
  300. occupied.mu.Lock()
  301. occupied.lastActivity = time.Now().Add(-time.Hour)
  302. occupied.mu.Unlock()
  303. closed := m.SweepIdleRooms(30 * time.Minute)
  304. if len(closed) != 1 || closed[0] != idle.ID {
  305. t.Errorf("SweepIdleRooms closed %v, want [%s]", closed, idle.ID)
  306. }
  307. if _, ok := m.GetRoom(occupied.ID); !ok {
  308. t.Errorf("occupied room was swept")
  309. }
  310. if _, ok := m.GetRoom(fresh.ID); !ok {
  311. t.Errorf("fresh room was swept")
  312. }
  313. }
  314. func TestRoomIDFormatting(t *testing.T) {
  315. tests := []struct {
  316. name string
  317. input string
  318. wantFormat string
  319. wantNormalize string
  320. }{
  321. {"standard ID", "123456789", "123-456-789", "123456789"},
  322. {"dashed input", "123-456-789", "123-456-789", "123456789"},
  323. {"spaced input", "123 456 789", "123 456 789", "123456789"},
  324. {"short ID passthrough", "1234", "1234", "1234"},
  325. {"junk stripped", "12a34!56789", "12a34!56789", "123456789"},
  326. }
  327. for _, tt := range tests {
  328. t.Run(tt.name, func(t *testing.T) {
  329. if got := NormalizeRoomID(tt.input); got != tt.wantNormalize {
  330. t.Errorf("NormalizeRoomID(%q) = %q, want %q", tt.input, got, tt.wantNormalize)
  331. }
  332. })
  333. }
  334. //FormatRoomID only reformats full-length normalized IDs
  335. if got := FormatRoomID("123456789"); got != "123-456-789" {
  336. t.Errorf("FormatRoomID = %q, want 123-456-789", got)
  337. }
  338. if got := FormatRoomID("1234"); got != "1234" {
  339. t.Errorf("FormatRoomID(short) = %q, want passthrough", got)
  340. }
  341. }
  342. func TestAttendanceLog(t *testing.T) {
  343. m := newTestManager(t)
  344. room := m.CreateRoom("alice", "", "")
  345. host, _ := room.AddParticipant("alice")
  346. guest, _ := room.AddParticipant("bob")
  347. room.RemoveParticipant(guest.PeerID)
  348. records := room.Attendance()
  349. if len(records) != 2 {
  350. t.Fatalf("Attendance() returned %d records, want 2", len(records))
  351. }
  352. if records[0].Username != "alice" || records[0].PeerID != host.PeerID {
  353. t.Errorf("first record = %+v, want alice/%d", records[0], host.PeerID)
  354. }
  355. if !records[0].Present() {
  356. t.Errorf("host record marked as left")
  357. }
  358. if records[1].Username != "bob" {
  359. t.Errorf("second record username = %q, want bob", records[1].Username)
  360. }
  361. if records[1].Present() {
  362. t.Errorf("removed guest still marked as present")
  363. }
  364. if records[1].LeftAt.Before(records[1].JoinedAt) {
  365. t.Errorf("LeftAt %v before JoinedAt %v", records[1].LeftAt, records[1].JoinedAt)
  366. }
  367. //Rejoin appends a fresh record instead of reviving the old one
  368. room.AddParticipant("bob")
  369. records = room.Attendance()
  370. if len(records) != 3 {
  371. t.Fatalf("Attendance() after rejoin returned %d records, want 3", len(records))
  372. }
  373. if records[1].Present() || !records[2].Present() {
  374. t.Errorf("rejoin did not append a fresh present record")
  375. }
  376. if !room.HasParticipantUsername("bob") {
  377. t.Errorf("HasParticipantUsername(bob) = false, want true")
  378. }
  379. if room.HasParticipantUsername("mallory") {
  380. t.Errorf("HasParticipantUsername(mallory) = true, want false")
  381. }
  382. }
  383. func TestListRoomsByHost(t *testing.T) {
  384. m := newTestManager(t)
  385. m.CreateRoom("alice", "One", "")
  386. m.CreateRoom("alice", "Two", "")
  387. m.CreateRoom("bob", "Other", "")
  388. if got := len(m.ListRoomsByHost("alice")); got != 2 {
  389. t.Errorf("alice hosts %d rooms, want 2", got)
  390. }
  391. if got := len(m.ListRoomsByHost("carol")); got != 0 {
  392. t.Errorf("carol hosts %d rooms, want 0", got)
  393. }
  394. }
  395. func TestSpaceBoundRoomLifecycle(t *testing.T) {
  396. m, sm := newSpaceBoundManager(t)
  397. room := m.CreateRoom("alice", "Standup", "")
  398. if room.SpaceID == "" {
  399. t.Fatalf("space-bound room has no SpaceID")
  400. }
  401. space, ok := sm.GetSpace(room.SpaceID)
  402. if !ok {
  403. t.Fatalf("bound space %q not registered", room.SpaceID)
  404. }
  405. if space.Owner != "alice" || space.Name != "Standup" {
  406. t.Errorf("space owner/name = %q/%q, want alice/Standup", space.Owner, space.Name)
  407. }
  408. //Attachments are stored in the space and readable through both APIs
  409. att, err := m.SaveAttachment(room.ID, "photo.png", "bob", strings.NewReader("img-bytes"), 1024)
  410. if err != nil {
  411. t.Fatalf("SaveAttachment() error = %v", err)
  412. }
  413. item, ok := space.GetItem(att.ID)
  414. if !ok {
  415. t.Fatalf("attachment not stored as a space item")
  416. }
  417. if item.Type != sharedspace.ItemTypeImage {
  418. t.Errorf("png attachment item type = %q, want image", item.Type)
  419. }
  420. if item.Origin != OriginMeetRoom {
  421. t.Errorf("attachment origin = %q, want %q", item.Origin, OriginMeetRoom)
  422. }
  423. if got, ok := m.GetAttachment(room.ID, att.ID); !ok || got.DiskPath != item.DiskPath {
  424. t.Errorf("GetAttachment did not resolve the space-backed file")
  425. }
  426. //Chat mirrors into the space with the meetroom origin
  427. m.LogChat(room.ID, "alice", "hello world")
  428. items := space.Items()
  429. if len(items) != 2 {
  430. t.Fatalf("space holds %d items, want 2", len(items))
  431. }
  432. if items[1].Type != sharedspace.ItemTypeText || items[1].Text != "hello world" || items[1].Origin != OriginMeetRoom {
  433. t.Errorf("mirrored chat item = %+v", items[1])
  434. }
  435. //Oversized uploads map back to the meetroom error
  436. if _, err := m.SaveAttachment(room.ID, "big.bin", "bob", strings.NewReader(strings.Repeat("A", 100)), 10); err != ErrAttachmentTooLarge {
  437. t.Errorf("oversized upload error = %v, want ErrAttachmentTooLarge", err)
  438. }
  439. //Closing the room deletes the bound space and its blobs
  440. m.CloseRoom(room.ID)
  441. if _, ok := sm.GetSpace(room.SpaceID); ok {
  442. t.Errorf("bound space still registered after CloseRoom")
  443. }
  444. if _, err := os.Stat(item.DiskPath); !os.IsNotExist(err) {
  445. t.Errorf("space blob still on disk after CloseRoom: %v", err)
  446. }
  447. }
  448. func TestSpaceItemBridge(t *testing.T) {
  449. m, sm := newSpaceBoundManager(t)
  450. var bridged []*sharedspace.Item
  451. var bridgedRoom *Room
  452. m.SetSpaceItemHandler(func(room *Room, item *sharedspace.Item) {
  453. bridgedRoom = room
  454. bridged = append(bridged, item)
  455. })
  456. room := m.CreateRoom("alice", "", "")
  457. space, _ := sm.GetSpace(room.SpaceID)
  458. //Items posted by the room itself must not echo back through the bridge
  459. m.LogChat(room.ID, "alice", "own message")
  460. m.SaveAttachment(room.ID, "notes.txt", "alice", strings.NewReader("data"), 1024)
  461. if len(bridged) != 0 {
  462. t.Fatalf("bridge fired %d times for meetroom-origin items, want 0", len(bridged))
  463. }
  464. //External (AGI) items flow through the bridge
  465. agiItem, err := space.AddText("scriptbot", "posted from AGI", "agi")
  466. if err != nil {
  467. t.Fatalf("AddText() error = %v", err)
  468. }
  469. if len(bridged) != 1 || bridged[0] != agiItem {
  470. t.Fatalf("bridge did not deliver the AGI item (fired %d times)", len(bridged))
  471. }
  472. if bridgedRoom != room {
  473. t.Errorf("bridge delivered wrong room")
  474. }
  475. //AGI-posted files resolve as room attachments for the download endpoint
  476. blob, _ := space.SaveBlob(sharedspace.ItemTypeFile, "report.pdf", "scriptbot", "agi", strings.NewReader("pdf"), 1024)
  477. if len(bridged) != 2 {
  478. t.Fatalf("bridge fired %d times, want 2", len(bridged))
  479. }
  480. if att, ok := m.GetAttachment(room.ID, blob.ID); !ok || att.Name != "report.pdf" {
  481. t.Errorf("AGI-posted file not resolvable via GetAttachment")
  482. }
  483. }
  484. func TestRoomRidesSpaceChannel(t *testing.T) {
  485. //The meeting's realtime transport is the bound space's channel: meeting
  486. //participants appear as channel subscribers, and generic subscribers on
  487. //the same space receive meeting frames live.
  488. m, sm := newSpaceBoundManager(t)
  489. room := m.CreateRoom("alice", "", "")
  490. space, ok := sm.GetSpace(room.SpaceID)
  491. if !ok {
  492. t.Fatalf("bound space not found")
  493. }
  494. channel := space.Channel()
  495. host, _ := room.AddParticipant("alice")
  496. guest, _ := room.AddParticipant("bob")
  497. if channel.Count() != room.ParticipantCount() {
  498. t.Errorf("channel has %d subscribers, room has %d participants", channel.Count(), room.ParticipantCount())
  499. }
  500. if _, ok := channel.Get(host.PeerID); !ok {
  501. t.Errorf("host peer ID %d not a channel subscriber", host.PeerID)
  502. }
  503. //A generic space subscriber (e.g. a sharedspace WebSocket client)
  504. //receives room broadcasts without being a meeting participant
  505. watcher, err := channel.Join("watcher")
  506. if err != nil {
  507. t.Fatalf("generic Join() error = %v", err)
  508. }
  509. if room.ParticipantCount() != 2 {
  510. t.Errorf("generic subscriber leaked into the meeting roster")
  511. }
  512. room.Broadcast([]byte(`{"type":"chat","text":"hi"}`), host.PeerID)
  513. select {
  514. case msg := <-watcher.Send:
  515. if string(msg) != `{"type":"chat","text":"hi"}` {
  516. t.Errorf("watcher received %q", msg)
  517. }
  518. default:
  519. t.Errorf("generic space subscriber did not receive the room broadcast")
  520. }
  521. //Targeted sends reach meeting participants through the channel
  522. if !room.SendTo(guest.PeerID, []byte("direct")) {
  523. t.Errorf("SendTo(guest) = false")
  524. }
  525. //Closing the room tears down the shared channel: everyone drops
  526. m.CloseRoom(room.ID)
  527. if _, open := <-watcher.Send; open {
  528. t.Errorf("generic subscriber still connected after CloseRoom")
  529. }
  530. if _, err := channel.Join("late"); err == nil {
  531. t.Errorf("channel still accepts subscribers after CloseRoom")
  532. }
  533. }
  534. func TestUnboundRoomStandaloneChannel(t *testing.T) {
  535. //Rooms without a space manager run on a standalone channel with the
  536. //same transport semantics.
  537. m := newTestManager(t)
  538. room := m.CreateRoom("alice", "", "")
  539. a, _ := room.AddParticipant("alice")
  540. b, _ := room.AddParticipant("bob")
  541. room.Broadcast([]byte("frame"), a.PeerID)
  542. if msg := <-b.Send; string(msg) != "frame" {
  543. t.Errorf("b received %q, want frame", msg)
  544. }
  545. room.RemoveParticipant(b.PeerID)
  546. if room.ParticipantCount() != 1 {
  547. t.Errorf("ParticipantCount() = %d, want 1", room.ParticipantCount())
  548. }
  549. }
  550. func TestParticipantMessageIsValidJSONFrame(t *testing.T) {
  551. //Guards the wire contract: frames pushed by the transport layer are
  552. //opaque bytes; make sure Broadcast does not mutate or alias them.
  553. m := newTestManager(t)
  554. room := m.CreateRoom("alice", "", "")
  555. a, _ := room.AddParticipant("alice")
  556. original := []byte(`{"type":"chat","text":"hi"}`)
  557. room.Broadcast(original, -1)
  558. original[2] = 'X' //mutate the caller's buffer after broadcast
  559. got := <-a.Send
  560. var decoded map[string]interface{}
  561. if err := json.Unmarshal(got, &decoded); err != nil {
  562. t.Fatalf("broadcast frame corrupted by caller mutation: %v", err)
  563. }
  564. if decoded["type"] != "chat" {
  565. t.Errorf("frame type = %v, want chat", decoded["type"])
  566. }
  567. }