mgmt_api_test.go 22 KB

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