mgmt_api_test.go 24 KB

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