mgmt_api_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. package http
  2. import (
  3. "io"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "time"
  10. "github.com/google/uuid"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/mock"
  13. "github.com/mk6i/retro-aim-server/state"
  14. "github.com/mk6i/retro-aim-server/wire"
  15. )
  16. func TestSessionHandler_GET(t *testing.T) {
  17. fnNewSess := func(screenName string) *state.Session {
  18. sess := state.NewSession()
  19. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  20. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  21. return sess
  22. }
  23. tt := []struct {
  24. name string
  25. sessions []*state.Session
  26. userHandlerErr error
  27. want string
  28. statusCode int
  29. }{
  30. {
  31. name: "without sessions",
  32. sessions: []*state.Session{},
  33. want: `{"count":0,"sessions":[]}`,
  34. statusCode: http.StatusOK,
  35. },
  36. {
  37. name: "with sessions",
  38. sessions: []*state.Session{
  39. fnNewSess("userA"),
  40. fnNewSess("userB"),
  41. },
  42. want: `{"count":2,"sessions":[{"screen_name":"userA"},{"screen_name":"userB"}]}`,
  43. statusCode: http.StatusOK,
  44. },
  45. }
  46. for _, tc := range tt {
  47. t.Run(tc.name, func(t *testing.T) {
  48. request := httptest.NewRequest(http.MethodGet, "/session", nil)
  49. responseRecorder := httptest.NewRecorder()
  50. sessionRetriever := newMockSessionRetriever(t)
  51. sessionRetriever.EXPECT().
  52. AllSessions().
  53. Return(tc.sessions)
  54. sessionHandler(responseRecorder, request, sessionRetriever)
  55. if responseRecorder.Code != tc.statusCode {
  56. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  57. }
  58. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  59. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  60. }
  61. })
  62. }
  63. }
  64. func TestSessionHandler_DisallowedMethod(t *testing.T) {
  65. request := httptest.NewRequest(http.MethodPut, "/session", nil)
  66. responseRecorder := httptest.NewRecorder()
  67. sessionHandler(responseRecorder, request, nil)
  68. wantCode := http.StatusMethodNotAllowed
  69. if responseRecorder.Code != wantCode {
  70. t.Errorf("want status '%d', got '%d'", http.StatusMethodNotAllowed, responseRecorder.Code)
  71. }
  72. wantBody := `method not allowed`
  73. if strings.TrimSpace(responseRecorder.Body.String()) != wantBody {
  74. t.Errorf("want '%s', got '%s'", wantBody, responseRecorder.Body)
  75. }
  76. }
  77. func TestUserHandler_GET(t *testing.T) {
  78. tt := []struct {
  79. name string
  80. users []state.User
  81. userHandlerErr error
  82. want string
  83. statusCode int
  84. }{
  85. {
  86. name: "empty user store",
  87. users: []state.User{},
  88. want: `[]`,
  89. statusCode: http.StatusOK,
  90. },
  91. {
  92. name: "user store containing 2 users",
  93. users: []state.User{
  94. {
  95. DisplayScreenName: "userA",
  96. IdentScreenName: state.NewIdentScreenName("userA"),
  97. },
  98. {
  99. DisplayScreenName: "userB",
  100. IdentScreenName: state.NewIdentScreenName("userB"),
  101. },
  102. },
  103. want: `[{"screen_name":"userA"},{"screen_name":"userB"}]`,
  104. statusCode: http.StatusOK,
  105. },
  106. {
  107. name: "user handler error",
  108. users: []state.User{},
  109. userHandlerErr: io.EOF,
  110. want: `internal server error`,
  111. statusCode: http.StatusInternalServerError,
  112. },
  113. }
  114. for _, tc := range tt {
  115. t.Run(tc.name, func(t *testing.T) {
  116. request := httptest.NewRequest(http.MethodGet, "/user", nil)
  117. responseRecorder := httptest.NewRecorder()
  118. userManager := newMockUserManager(t)
  119. userManager.EXPECT().
  120. AllUsers().
  121. Return(tc.users, tc.userHandlerErr)
  122. userHandler(responseRecorder, request, userManager, nil, slog.Default())
  123. if responseRecorder.Code != tc.statusCode {
  124. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  125. }
  126. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  127. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  128. }
  129. })
  130. }
  131. }
  132. func TestUserHandler_POST(t *testing.T) {
  133. tt := []struct {
  134. name string
  135. body string
  136. UUID uuid.UUID
  137. user state.User
  138. userHandlerErr error
  139. want string
  140. statusCode int
  141. }{
  142. {
  143. name: "with valid user",
  144. body: `{"screen_name":"userA", "password":"thepassword"}`,
  145. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  146. user: func() state.User {
  147. user := state.User{
  148. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  149. DisplayScreenName: "userA",
  150. IdentScreenName: state.NewIdentScreenName("userA"),
  151. }
  152. assert.NoError(t, user.HashPassword("thepassword"))
  153. return user
  154. }(),
  155. want: `User account created successfully.`,
  156. statusCode: http.StatusCreated,
  157. },
  158. {
  159. name: "with malformed body",
  160. body: `{"screen_name":"userA", "password":"thepassword"`,
  161. user: state.User{},
  162. want: `malformed input`,
  163. statusCode: http.StatusBadRequest,
  164. },
  165. {
  166. name: "user handler error",
  167. body: `{"screen_name":"userA", "password":"thepassword"}`,
  168. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  169. user: func() state.User {
  170. user := state.User{
  171. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  172. DisplayScreenName: "userA",
  173. IdentScreenName: state.NewIdentScreenName("userA"),
  174. }
  175. assert.NoError(t, user.HashPassword("thepassword"))
  176. return user
  177. }(),
  178. userHandlerErr: io.EOF,
  179. want: `internal server error`,
  180. statusCode: http.StatusInternalServerError,
  181. },
  182. {
  183. name: "duplicate user",
  184. body: `{"screen_name":"userA", "password":"thepassword"}`,
  185. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  186. user: func() state.User {
  187. user := state.User{
  188. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  189. DisplayScreenName: "userA",
  190. IdentScreenName: state.NewIdentScreenName("userA"),
  191. }
  192. assert.NoError(t, user.HashPassword("thepassword"))
  193. return user
  194. }(),
  195. userHandlerErr: state.ErrDupUser,
  196. want: `user already exists`,
  197. statusCode: http.StatusConflict,
  198. },
  199. }
  200. for _, tc := range tt {
  201. t.Run(tc.name, func(t *testing.T) {
  202. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  203. responseRecorder := httptest.NewRecorder()
  204. userManager := newMockUserManager(t)
  205. if tc.user.IdentScreenName.String() != "" {
  206. userManager.EXPECT().
  207. InsertUser(tc.user).
  208. Return(tc.userHandlerErr)
  209. }
  210. newUUID := func() uuid.UUID { return tc.UUID }
  211. userHandler(responseRecorder, request, userManager, newUUID, slog.Default())
  212. if responseRecorder.Code != tc.statusCode {
  213. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  214. }
  215. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  216. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  217. }
  218. })
  219. }
  220. }
  221. func TestUserHandler_DELETE(t *testing.T) {
  222. tt := []struct {
  223. name string
  224. body string
  225. user state.User
  226. userHandlerErr error
  227. want string
  228. statusCode int
  229. }{
  230. {
  231. name: "with valid user",
  232. body: `{"screen_name":"userA"}`,
  233. user: state.User{
  234. IdentScreenName: state.NewIdentScreenName("userA"),
  235. },
  236. want: `User account successfully deleted.`,
  237. statusCode: http.StatusNoContent,
  238. },
  239. {
  240. name: "with non-existent user",
  241. body: `{"screen_name":"userA"}`,
  242. user: state.User{
  243. IdentScreenName: state.NewIdentScreenName("userA"),
  244. },
  245. userHandlerErr: state.ErrNoUser,
  246. want: `user does not exist`,
  247. statusCode: http.StatusNotFound,
  248. },
  249. {
  250. name: "with malformed body",
  251. body: `{"screen_name":"userA"`,
  252. user: state.User{},
  253. want: `malformed input`,
  254. statusCode: http.StatusBadRequest,
  255. },
  256. {
  257. name: "user handler error",
  258. body: `{"screen_name":"userA"}`,
  259. user: state.User{
  260. IdentScreenName: state.NewIdentScreenName("userA"),
  261. },
  262. userHandlerErr: io.EOF,
  263. want: `internal server error`,
  264. statusCode: http.StatusInternalServerError,
  265. },
  266. }
  267. for _, tc := range tt {
  268. t.Run(tc.name, func(t *testing.T) {
  269. request := httptest.NewRequest(http.MethodDelete, "/user", strings.NewReader(tc.body))
  270. responseRecorder := httptest.NewRecorder()
  271. userManager := newMockUserManager(t)
  272. if tc.user.IdentScreenName.String() != "" {
  273. userManager.EXPECT().
  274. DeleteUser(tc.user.IdentScreenName).
  275. Return(tc.userHandlerErr)
  276. }
  277. userHandler(responseRecorder, request, userManager, nil, slog.Default())
  278. if responseRecorder.Code != tc.statusCode {
  279. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  280. }
  281. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  282. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  283. }
  284. })
  285. }
  286. }
  287. func TestUserPasswordHandler_PUT(t *testing.T) {
  288. tt := []struct {
  289. name string
  290. body string
  291. user state.User
  292. UUID uuid.UUID
  293. userHandlerErr error
  294. want string
  295. statusCode int
  296. }{
  297. {
  298. name: "with valid password",
  299. body: `{"screen_name":"userA", "password":"thepassword"}`,
  300. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  301. user: func() state.User {
  302. user := state.User{
  303. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  304. DisplayScreenName: "userA",
  305. IdentScreenName: state.NewIdentScreenName("userA"),
  306. }
  307. assert.NoError(t, user.HashPassword("thepassword"))
  308. return user
  309. }(),
  310. want: ``,
  311. statusCode: http.StatusNoContent,
  312. },
  313. {
  314. name: "with malformed body",
  315. body: `{"screen_name":"userA", "password":"thepassword"`,
  316. user: state.User{},
  317. want: `malformed input`,
  318. statusCode: http.StatusBadRequest,
  319. },
  320. {
  321. name: "user password handler error",
  322. body: `{"screen_name":"userA", "password":"thepassword"}`,
  323. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  324. user: func() state.User {
  325. user := state.User{
  326. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  327. DisplayScreenName: "userA",
  328. IdentScreenName: state.NewIdentScreenName("userA"),
  329. }
  330. assert.NoError(t, user.HashPassword("thepassword"))
  331. return user
  332. }(),
  333. userHandlerErr: io.EOF,
  334. want: `internal server error`,
  335. statusCode: http.StatusInternalServerError,
  336. },
  337. {
  338. name: "user doesn't exist",
  339. body: `{"screen_name":"userA", "password":"thepassword"}`,
  340. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  341. user: func() state.User {
  342. user := state.User{
  343. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  344. DisplayScreenName: "userA",
  345. IdentScreenName: state.NewIdentScreenName("userA"),
  346. }
  347. assert.NoError(t, user.HashPassword("thepassword"))
  348. return user
  349. }(),
  350. userHandlerErr: state.ErrNoUser,
  351. want: `user does not exist`,
  352. statusCode: http.StatusNotFound,
  353. },
  354. }
  355. for _, tc := range tt {
  356. t.Run(tc.name, func(t *testing.T) {
  357. request := httptest.NewRequest(http.MethodPut, "/user", strings.NewReader(tc.body))
  358. responseRecorder := httptest.NewRecorder()
  359. userManager := newMockUserManager(t)
  360. if tc.user.IdentScreenName.String() != "" {
  361. userManager.EXPECT().
  362. SetUserPassword(tc.user).
  363. Return(tc.userHandlerErr)
  364. }
  365. newUUID := func() uuid.UUID { return tc.UUID }
  366. userPasswordHandler(responseRecorder, request, userManager, newUUID, slog.Default())
  367. if responseRecorder.Code != tc.statusCode {
  368. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  369. }
  370. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  371. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  372. }
  373. })
  374. }
  375. }
  376. func TestUserHandler_DisallowedMethod(t *testing.T) {
  377. request := httptest.NewRequest(http.MethodPut, "/user", nil)
  378. responseRecorder := httptest.NewRecorder()
  379. userHandler(responseRecorder, request, nil, nil, nil)
  380. wantCode := http.StatusMethodNotAllowed
  381. if responseRecorder.Code != wantCode {
  382. t.Errorf("want status '%d', got '%d'", http.StatusMethodNotAllowed, responseRecorder.Code)
  383. }
  384. wantBody := `method not allowed`
  385. if strings.TrimSpace(responseRecorder.Body.String()) != wantBody {
  386. t.Errorf("want '%s', got '%s'", wantBody, responseRecorder.Body)
  387. }
  388. }
  389. func TestPublicChatHandler_GET(t *testing.T) {
  390. fnNewSess := func(screenName string) *state.Session {
  391. sess := state.NewSession()
  392. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  393. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  394. return sess
  395. }
  396. type allChatRoomsParams struct {
  397. exchange uint16
  398. result []state.ChatRoom
  399. err error
  400. }
  401. type allSessionsParams struct {
  402. cookie string
  403. result []*state.Session
  404. }
  405. tt := []struct {
  406. name string
  407. allChatRoomsParams allChatRoomsParams
  408. allSessionsParams []allSessionsParams
  409. userHandlerErr error
  410. want string
  411. statusCode int
  412. }{
  413. {
  414. name: "multiple chat rooms with participants",
  415. allChatRoomsParams: allChatRoomsParams{
  416. exchange: state.PublicExchange,
  417. result: []state.ChatRoom{
  418. {
  419. Cookie: "chat-room-1-cookie",
  420. Creator: state.NewIdentScreenName("chat-room-1-creator"),
  421. Name: "chat-room-1-name",
  422. CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
  423. },
  424. {
  425. Cookie: "chat-room-2-cookie",
  426. Creator: state.NewIdentScreenName("chat-room-2-creator"),
  427. Name: "chat-room-2-name",
  428. CreateTime: time.Date(2022, 01, 04, 6, 8, 1, 2, time.UTC),
  429. },
  430. },
  431. },
  432. allSessionsParams: []allSessionsParams{
  433. {
  434. cookie: "chat-room-1-cookie",
  435. result: []*state.Session{
  436. fnNewSess("userA"),
  437. fnNewSess("userB"),
  438. },
  439. },
  440. {
  441. cookie: "chat-room-2-cookie",
  442. result: []*state.Session{
  443. fnNewSess("userC"),
  444. fnNewSess("userD"),
  445. },
  446. },
  447. },
  448. want: `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]},{"name":"chat-room-2-name","create_time":"2022-01-04T06:08:01.000000002Z","url":"aim:gochat?exchange=0\u0026roomname=chat-room-2-name","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
  449. statusCode: http.StatusOK,
  450. },
  451. {
  452. name: "chat room without participants",
  453. allChatRoomsParams: allChatRoomsParams{
  454. exchange: state.PublicExchange,
  455. result: []state.ChatRoom{
  456. {
  457. Cookie: "chat-room-1-cookie",
  458. Creator: state.NewIdentScreenName("chat-room-1-creator"),
  459. Name: "chat-room-1-name",
  460. CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
  461. },
  462. },
  463. },
  464. allSessionsParams: []allSessionsParams{
  465. {
  466. cookie: "chat-room-1-cookie",
  467. result: []*state.Session{},
  468. },
  469. },
  470. want: `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[]}]`,
  471. statusCode: http.StatusOK,
  472. },
  473. {
  474. name: "no chat rooms",
  475. allChatRoomsParams: allChatRoomsParams{
  476. exchange: state.PublicExchange,
  477. result: []state.ChatRoom{},
  478. },
  479. allSessionsParams: []allSessionsParams{},
  480. want: `[]`,
  481. statusCode: http.StatusOK,
  482. },
  483. }
  484. for _, tc := range tt {
  485. t.Run(tc.name, func(t *testing.T) {
  486. request := httptest.NewRequest(http.MethodGet, "/chat/room/public", nil)
  487. responseRecorder := httptest.NewRecorder()
  488. chatRoomRetriever := newMockChatRoomRetriever(t)
  489. chatRoomRetriever.EXPECT().
  490. AllChatRooms(tc.allChatRoomsParams.exchange).
  491. Return(tc.allChatRoomsParams.result, tc.allChatRoomsParams.err)
  492. chatSessionRetriever := newMockChatSessionRetriever(t)
  493. for _, params := range tc.allSessionsParams {
  494. chatSessionRetriever.EXPECT().
  495. AllSessions(params.cookie).
  496. Return(params.result)
  497. }
  498. getPublicChatHandler(responseRecorder, request, chatRoomRetriever, chatSessionRetriever, slog.Default())
  499. if responseRecorder.Code != tc.statusCode {
  500. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  501. }
  502. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  503. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  504. }
  505. })
  506. }
  507. }
  508. func TestPrivateChatHandler_GET(t *testing.T) {
  509. fnNewSess := func(screenName string) *state.Session {
  510. sess := state.NewSession()
  511. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  512. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  513. return sess
  514. }
  515. type allChatRoomsParams struct {
  516. exchange uint16
  517. result []state.ChatRoom
  518. err error
  519. }
  520. type allSessionsParams struct {
  521. cookie string
  522. result []*state.Session
  523. }
  524. tt := []struct {
  525. name string
  526. allChatRoomsParams allChatRoomsParams
  527. allSessionsParams []allSessionsParams
  528. userHandlerErr error
  529. want string
  530. statusCode int
  531. }{
  532. {
  533. name: "multiple chat rooms with participants",
  534. allChatRoomsParams: allChatRoomsParams{
  535. exchange: state.PrivateExchange,
  536. result: []state.ChatRoom{
  537. {
  538. Cookie: "chat-room-1-cookie",
  539. Creator: state.NewIdentScreenName("chat-room-1-creator"),
  540. Name: "chat-room-1-name",
  541. CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
  542. },
  543. {
  544. Cookie: "chat-room-2-cookie",
  545. Creator: state.NewIdentScreenName("chat-room-2-creator"),
  546. Name: "chat-room-2-name",
  547. CreateTime: time.Date(2022, 01, 04, 6, 8, 1, 2, time.UTC),
  548. },
  549. },
  550. },
  551. allSessionsParams: []allSessionsParams{
  552. {
  553. cookie: "chat-room-1-cookie",
  554. result: []*state.Session{
  555. fnNewSess("userA"),
  556. fnNewSess("userB"),
  557. },
  558. },
  559. {
  560. cookie: "chat-room-2-cookie",
  561. result: []*state.Session{
  562. fnNewSess("userC"),
  563. fnNewSess("userD"),
  564. },
  565. },
  566. },
  567. want: `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","creator_id":"chat-room-1-creator","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]},{"name":"chat-room-2-name","create_time":"2022-01-04T06:08:01.000000002Z","creator_id":"chat-room-2-creator","url":"aim:gochat?exchange=0\u0026roomname=chat-room-2-name","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
  568. statusCode: http.StatusOK,
  569. },
  570. {
  571. name: "chat room without participants",
  572. allChatRoomsParams: allChatRoomsParams{
  573. exchange: state.PrivateExchange,
  574. result: []state.ChatRoom{
  575. {
  576. Cookie: "chat-room-1-cookie",
  577. Creator: state.NewIdentScreenName("chat-room-1-creator"),
  578. Name: "chat-room-1-name",
  579. CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
  580. },
  581. },
  582. },
  583. allSessionsParams: []allSessionsParams{
  584. {
  585. cookie: "chat-room-1-cookie",
  586. result: []*state.Session{},
  587. },
  588. },
  589. want: `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","creator_id":"chat-room-1-creator","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[]}]`,
  590. statusCode: http.StatusOK,
  591. },
  592. {
  593. name: "no chat rooms",
  594. allChatRoomsParams: allChatRoomsParams{
  595. exchange: state.PrivateExchange,
  596. result: []state.ChatRoom{},
  597. },
  598. allSessionsParams: []allSessionsParams{},
  599. want: `[]`,
  600. statusCode: http.StatusOK,
  601. },
  602. }
  603. for _, tc := range tt {
  604. t.Run(tc.name, func(t *testing.T) {
  605. request := httptest.NewRequest(http.MethodGet, "/chat/room/private", nil)
  606. responseRecorder := httptest.NewRecorder()
  607. chatRoomRetriever := newMockChatRoomRetriever(t)
  608. chatRoomRetriever.EXPECT().
  609. AllChatRooms(tc.allChatRoomsParams.exchange).
  610. Return(tc.allChatRoomsParams.result, tc.allChatRoomsParams.err)
  611. chatSessionRetriever := newMockChatSessionRetriever(t)
  612. for _, params := range tc.allSessionsParams {
  613. chatSessionRetriever.EXPECT().
  614. AllSessions(params.cookie).
  615. Return(params.result)
  616. }
  617. getPrivateChatHandler(responseRecorder, request, chatRoomRetriever, chatSessionRetriever, slog.Default())
  618. if responseRecorder.Code != tc.statusCode {
  619. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  620. }
  621. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  622. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  623. }
  624. })
  625. }
  626. }
  627. func TestInstantMessageHandler_POST(t *testing.T) {
  628. type relayToScreenNameParams struct {
  629. sender state.IdentScreenName
  630. recipient state.IdentScreenName
  631. msg string
  632. }
  633. tt := []struct {
  634. name string
  635. relayToScreenNameParams []relayToScreenNameParams
  636. body string
  637. want string
  638. statusCode int
  639. }{
  640. {
  641. name: "send an instant message",
  642. relayToScreenNameParams: []relayToScreenNameParams{
  643. {
  644. sender: state.NewIdentScreenName("sender_sn"),
  645. recipient: state.NewIdentScreenName("recip_sn"),
  646. msg: "hello world!",
  647. },
  648. },
  649. body: `{"from":"sender_sn","to":"recip_sn","text":"hello world!"}`,
  650. want: `Message sent successfully.`,
  651. statusCode: http.StatusOK,
  652. },
  653. {
  654. name: "with malformed body",
  655. body: `{"screen_name":"userA", "password":"thepassword"`,
  656. want: `malformed input`,
  657. statusCode: http.StatusBadRequest,
  658. },
  659. }
  660. for _, tc := range tt {
  661. t.Run(tc.name, func(t *testing.T) {
  662. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  663. responseRecorder := httptest.NewRecorder()
  664. messageRelayer := newMockMessageRelayer(t)
  665. for _, params := range tc.relayToScreenNameParams {
  666. validateSNAC := func(msg wire.SNACMessage) bool {
  667. body := msg.Body.(wire.SNAC_0x04_0x07_ICBMChannelMsgToClient)
  668. assert.Equal(t, params.sender.String(), body.TLVUserInfo.ScreenName)
  669. b, ok := body.Slice(wire.ICBMTLVAOLIMData)
  670. assert.True(t, ok)
  671. txt, err := wire.UnmarshalICBMMessageText(b)
  672. assert.NoError(t, err)
  673. assert.Equal(t, params.msg, txt)
  674. return true
  675. }
  676. messageRelayer.EXPECT().
  677. RelayToScreenName(mock.Anything, params.recipient, mock.MatchedBy(validateSNAC))
  678. }
  679. postInstantMessageHandler(responseRecorder, request, messageRelayer, slog.Default())
  680. if responseRecorder.Code != tc.statusCode {
  681. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  682. }
  683. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  684. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  685. }
  686. })
  687. }
  688. }