mgmt_api_test.go 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  1. package http
  2. import (
  3. "io"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/mail"
  8. "strings"
  9. "testing"
  10. "time"
  11. "github.com/google/uuid"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/mock"
  14. "github.com/mk6i/retro-aim-server/state"
  15. "github.com/mk6i/retro-aim-server/wire"
  16. )
  17. func TestSessionHandler_GET(t *testing.T) {
  18. fnNewSess := func(screenName string, uin uint32) *state.Session {
  19. sess := state.NewSession()
  20. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  21. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  22. sess.SetUIN(uin)
  23. return sess
  24. }
  25. tt := []struct {
  26. name string
  27. want string
  28. statusCode int
  29. timeSinceFunc func(t time.Time) time.Duration
  30. mockParams mockParams
  31. }{
  32. {
  33. name: "without sessions",
  34. want: `{"count":0,"sessions":[]}`,
  35. statusCode: http.StatusOK,
  36. mockParams: mockParams{
  37. sessionRetrieverParams: sessionRetrieverParams{
  38. sessionRetrieverAllSessionsParams: sessionRetrieverAllSessionsParams{
  39. {
  40. result: []*state.Session{},
  41. },
  42. },
  43. },
  44. },
  45. },
  46. {
  47. name: "with sessions",
  48. want: `{"count":3,"sessions":[{"id":"usera","screen_name":"userA","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":false},{"id":"userb","screen_name":"userB","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":false},{"id":"100003","screen_name":"100003","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":true}]}`,
  49. statusCode: http.StatusOK,
  50. timeSinceFunc: func(t time.Time) time.Duration { t0 := time.Now(); return t0.Sub(t0) },
  51. mockParams: mockParams{
  52. sessionRetrieverParams: sessionRetrieverParams{
  53. sessionRetrieverAllSessionsParams: sessionRetrieverAllSessionsParams{
  54. {
  55. result: []*state.Session{
  56. fnNewSess("userA", 0),
  57. fnNewSess("userB", 0),
  58. fnNewSess("100003", 100003),
  59. },
  60. },
  61. },
  62. },
  63. },
  64. },
  65. }
  66. for _, tc := range tt {
  67. t.Run(tc.name, func(t *testing.T) {
  68. request := httptest.NewRequest(http.MethodGet, "/session", nil)
  69. responseRecorder := httptest.NewRecorder()
  70. sessionRetriever := newMockSessionRetriever(t)
  71. for _, params := range tc.mockParams.sessionRetrieverParams.sessionRetrieverAllSessionsParams {
  72. sessionRetriever.EXPECT().
  73. AllSessions().
  74. Return(params.result)
  75. }
  76. getSessionHandler(responseRecorder, request, sessionRetriever, tc.timeSinceFunc)
  77. if responseRecorder.Code != tc.statusCode {
  78. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  79. }
  80. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  81. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  82. }
  83. })
  84. }
  85. }
  86. func TestSessionHandlerScreenname_GET(t *testing.T) {
  87. fnNewSess := func(screenName string, uin uint32) *state.Session {
  88. sess := state.NewSession()
  89. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  90. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  91. sess.SetUIN(uin)
  92. return sess
  93. }
  94. tt := []struct {
  95. name string
  96. sessions []*state.Session
  97. requestScreenName state.IdentScreenName
  98. want string
  99. statusCode int
  100. timeSinceFunc func(t time.Time) time.Duration
  101. mockParams mockParams
  102. }{
  103. {
  104. name: "no session for screenname",
  105. sessions: []*state.Session{},
  106. requestScreenName: state.NewIdentScreenName("userA"),
  107. want: `session not found`,
  108. statusCode: http.StatusNotFound,
  109. mockParams: mockParams{
  110. sessionRetrieverParams: sessionRetrieverParams{
  111. retrieveSessionByNameParams: retrieveSessionByNameParams{
  112. {
  113. screenName: state.NewIdentScreenName("userA"),
  114. result: nil,
  115. },
  116. },
  117. },
  118. },
  119. },
  120. {
  121. name: "active session found for screenname",
  122. requestScreenName: state.NewIdentScreenName("userA"),
  123. want: `{"count":1,"sessions":[{"id":"usera","screen_name":"userA","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":false}]}`,
  124. statusCode: http.StatusOK,
  125. timeSinceFunc: func(t time.Time) time.Duration { t0 := time.Now(); return t0.Sub(t0) },
  126. mockParams: mockParams{
  127. sessionRetrieverParams: sessionRetrieverParams{
  128. retrieveSessionByNameParams: retrieveSessionByNameParams{
  129. {
  130. screenName: state.NewIdentScreenName("userA"),
  131. result: fnNewSess("userA", 0),
  132. },
  133. },
  134. },
  135. },
  136. },
  137. }
  138. for _, tc := range tt {
  139. t.Run(tc.name, func(t *testing.T) {
  140. request := httptest.NewRequest(http.MethodGet, "/session/"+tc.requestScreenName.String(), nil)
  141. request.SetPathValue("screenname", tc.requestScreenName.String())
  142. responseRecorder := httptest.NewRecorder()
  143. sessionRetriever := newMockSessionRetriever(t)
  144. for _, params := range tc.mockParams.sessionRetrieverParams.retrieveSessionByNameParams {
  145. sessionRetriever.EXPECT().
  146. RetrieveByScreenName(params.screenName).
  147. Return(params.result)
  148. }
  149. getSessionHandler(responseRecorder, request, sessionRetriever, tc.timeSinceFunc)
  150. if responseRecorder.Code != tc.statusCode {
  151. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  152. }
  153. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  154. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  155. }
  156. })
  157. }
  158. }
  159. func TestUserAccountHandler_GET(t *testing.T) {
  160. tt := []struct {
  161. name string
  162. requestScreenName state.IdentScreenName
  163. want string
  164. statusCode int
  165. mockParams mockParams
  166. }{
  167. {
  168. name: "invalid account",
  169. requestScreenName: state.NewIdentScreenName("userA"),
  170. want: `user not found`,
  171. statusCode: http.StatusNotFound,
  172. mockParams: mockParams{
  173. userManagerParams: userManagerParams{
  174. getUserParams: getUserParams{
  175. {
  176. screenName: state.NewIdentScreenName("userA"),
  177. result: nil,
  178. },
  179. },
  180. },
  181. },
  182. },
  183. {
  184. name: "valid aim account",
  185. requestScreenName: state.NewIdentScreenName("userA"),
  186. want: `{"id":"usera","screen_name":"userA","profile":"My Profile Text","email_address":"\u003cuserA@aol.com\u003e","reg_status":2,"confirmed":true,"is_icq":false}`,
  187. statusCode: http.StatusOK,
  188. mockParams: mockParams{
  189. userManagerParams: userManagerParams{
  190. getUserParams: getUserParams{
  191. {
  192. screenName: state.NewIdentScreenName("userA"),
  193. result: &state.User{
  194. DisplayScreenName: "userA",
  195. IdentScreenName: state.NewIdentScreenName("userA"),
  196. },
  197. },
  198. },
  199. },
  200. accountRetrieverParams: accountRetrieverParams{
  201. emailAddressByNameParams: emailAddressByNameParams{
  202. {
  203. screenName: state.NewIdentScreenName("userA"),
  204. result: &mail.Address{
  205. Address: "userA@aol.com",
  206. },
  207. },
  208. },
  209. regStatusByNameParams: regStatusByNameParams{
  210. {
  211. screenName: state.NewIdentScreenName("userA"),
  212. result: uint16(0x02),
  213. },
  214. },
  215. confirmStatusByNameParams: confirmStatusByNameParams{
  216. {
  217. screenName: state.NewIdentScreenName("userA"),
  218. result: true,
  219. },
  220. },
  221. },
  222. profileRetrieverParams: profileRetrieverParams{
  223. retrieveProfileParams: retrieveProfileParams{
  224. {
  225. screenName: state.NewIdentScreenName("userA"),
  226. result: "My Profile Text",
  227. },
  228. },
  229. },
  230. },
  231. },
  232. }
  233. for _, tc := range tt {
  234. t.Run(tc.name, func(t *testing.T) {
  235. request := httptest.NewRequest(http.MethodGet, "/user/"+tc.requestScreenName.String()+"/account", nil)
  236. request.SetPathValue("screenname", tc.requestScreenName.String())
  237. responseRecorder := httptest.NewRecorder()
  238. userManager := newMockUserManager(t)
  239. for _, params := range tc.mockParams.userManagerParams.getUserParams {
  240. userManager.EXPECT().
  241. User(params.screenName).
  242. Return(params.result, params.err)
  243. }
  244. accountRetriever := newMockAccountRetriever(t)
  245. for _, params := range tc.mockParams.accountRetrieverParams.emailAddressByNameParams {
  246. accountRetriever.EXPECT().
  247. EmailAddressByName(params.screenName).
  248. Return(params.result, params.err)
  249. }
  250. for _, params := range tc.mockParams.accountRetrieverParams.regStatusByNameParams {
  251. accountRetriever.EXPECT().
  252. RegStatusByName(params.screenName).
  253. Return(params.result, params.err)
  254. }
  255. for _, params := range tc.mockParams.accountRetrieverParams.confirmStatusByNameParams {
  256. accountRetriever.EXPECT().
  257. ConfirmStatusByName(params.screenName).
  258. Return(params.result, params.err)
  259. }
  260. profileRetriever := newMockProfileRetriever(t)
  261. for _, params := range tc.mockParams.profileRetrieverParams.retrieveProfileParams {
  262. profileRetriever.EXPECT().
  263. Profile(params.screenName).
  264. Return(params.result, params.err)
  265. }
  266. getUserAccountHandler(responseRecorder, request, userManager, accountRetriever, profileRetriever, slog.Default())
  267. if responseRecorder.Code != tc.statusCode {
  268. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  269. }
  270. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  271. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  272. }
  273. })
  274. }
  275. }
  276. func TestUserBuddyIconHandler_GET(t *testing.T) {
  277. tt := []struct {
  278. name string
  279. requestScreenName state.IdentScreenName
  280. want string
  281. statusCode int
  282. contentType string
  283. mockParams mockParams
  284. }{
  285. {
  286. name: "invalid account",
  287. requestScreenName: state.NewIdentScreenName("userA"),
  288. want: `user not found`,
  289. statusCode: http.StatusNotFound,
  290. contentType: "text/plain; charset=utf-8",
  291. mockParams: mockParams{
  292. userManagerParams: userManagerParams{
  293. getUserParams: getUserParams{
  294. {
  295. screenName: state.NewIdentScreenName("userA"),
  296. result: nil,
  297. },
  298. },
  299. },
  300. },
  301. },
  302. {
  303. name: "account with buddy icon",
  304. requestScreenName: state.NewIdentScreenName("userA"),
  305. want: string([]byte{'i', 'c', 'o', 'n'}),
  306. statusCode: http.StatusOK,
  307. contentType: "image/gif",
  308. mockParams: mockParams{
  309. userManagerParams: userManagerParams{
  310. getUserParams: getUserParams{
  311. {
  312. screenName: state.NewIdentScreenName("userA"),
  313. result: &state.User{
  314. DisplayScreenName: "userA",
  315. IdentScreenName: state.NewIdentScreenName("userA"),
  316. },
  317. },
  318. },
  319. },
  320. feedBagRetrieverParams: feedBagRetrieverParams{
  321. buddyIconRefByNameParams: buddyIconRefByNameParams{
  322. {
  323. screenName: state.NewIdentScreenName("userA"),
  324. result: &wire.BARTID{
  325. Type: wire.BARTTypesBuddyIcon,
  326. BARTInfo: wire.BARTInfo{
  327. Flags: 0x00,
  328. Hash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  329. },
  330. },
  331. },
  332. },
  333. },
  334. bartRetrieverParams: bartRetrieverParams{
  335. bartRetrieveParams: bartRetrieveParams{
  336. {
  337. itemHash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  338. result: []byte{'i', 'c', 'o', 'n'},
  339. },
  340. },
  341. },
  342. },
  343. },
  344. {
  345. name: "account with cleared buddy icon",
  346. requestScreenName: state.NewIdentScreenName("userA"),
  347. want: "icon not found",
  348. statusCode: http.StatusNotFound,
  349. contentType: "text/plain; charset=utf-8",
  350. mockParams: mockParams{
  351. userManagerParams: userManagerParams{
  352. getUserParams: getUserParams{
  353. {
  354. screenName: state.NewIdentScreenName("userA"),
  355. result: &state.User{
  356. DisplayScreenName: "userA",
  357. IdentScreenName: state.NewIdentScreenName("userA"),
  358. },
  359. },
  360. },
  361. },
  362. feedBagRetrieverParams: feedBagRetrieverParams{
  363. buddyIconRefByNameParams: buddyIconRefByNameParams{
  364. {
  365. screenName: state.NewIdentScreenName("userA"),
  366. result: &wire.BARTID{
  367. Type: wire.BARTTypesBuddyIcon,
  368. BARTInfo: wire.BARTInfo{
  369. Flags: 0x00,
  370. Hash: wire.GetClearIconHash(),
  371. },
  372. },
  373. },
  374. },
  375. },
  376. },
  377. },
  378. {
  379. name: "account with no buddy icon",
  380. requestScreenName: state.NewIdentScreenName("userA"),
  381. want: "icon not found",
  382. statusCode: http.StatusNotFound,
  383. contentType: "text/plain; charset=utf-8",
  384. mockParams: mockParams{
  385. userManagerParams: userManagerParams{
  386. getUserParams: getUserParams{
  387. {
  388. screenName: state.NewIdentScreenName("userA"),
  389. result: &state.User{
  390. DisplayScreenName: "userA",
  391. IdentScreenName: state.NewIdentScreenName("userA"),
  392. },
  393. },
  394. },
  395. },
  396. feedBagRetrieverParams: feedBagRetrieverParams{
  397. buddyIconRefByNameParams: buddyIconRefByNameParams{
  398. {
  399. screenName: state.NewIdentScreenName("userA"),
  400. result: nil,
  401. },
  402. },
  403. },
  404. },
  405. },
  406. }
  407. for _, tc := range tt {
  408. t.Run(tc.name, func(t *testing.T) {
  409. request := httptest.NewRequest(http.MethodGet, "/user/"+tc.requestScreenName.String()+"/icon", nil)
  410. request.SetPathValue("screenname", tc.requestScreenName.String())
  411. responseRecorder := httptest.NewRecorder()
  412. userManager := newMockUserManager(t)
  413. for _, params := range tc.mockParams.userManagerParams.getUserParams {
  414. userManager.EXPECT().
  415. User(params.screenName).
  416. Return(params.result, params.err)
  417. }
  418. feedbagRetriever := newMockFeedBagRetriever(t)
  419. for _, params := range tc.mockParams.feedBagRetrieverParams.buddyIconRefByNameParams {
  420. feedbagRetriever.EXPECT().
  421. BuddyIconRefByName(params.screenName).
  422. Return(params.result, params.err)
  423. }
  424. bartRetriever := newMockBARTRetriever(t)
  425. for _, params := range tc.mockParams.bartRetrieverParams.bartRetrieveParams {
  426. bartRetriever.EXPECT().
  427. BARTRetrieve(params.itemHash).
  428. Return(params.result, params.err)
  429. }
  430. getUserBuddyIconHandler(responseRecorder, request, userManager, feedbagRetriever, bartRetriever, slog.Default())
  431. if responseRecorder.Code != tc.statusCode {
  432. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  433. }
  434. contentType := responseRecorder.Header().Get("Content-Type")
  435. if contentType != tc.contentType {
  436. t.Errorf("Want content type '%s', got '%s'", tc.contentType, contentType)
  437. }
  438. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  439. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  440. }
  441. })
  442. }
  443. }
  444. func TestUserHandler_GET(t *testing.T) {
  445. tt := []struct {
  446. name string
  447. want string
  448. statusCode int
  449. mockParams mockParams
  450. }{
  451. {
  452. name: "empty user store",
  453. want: `[]`,
  454. statusCode: http.StatusOK,
  455. mockParams: mockParams{
  456. userManagerParams: userManagerParams{
  457. allUsersParams: allUsersParams{
  458. {
  459. result: []state.User{},
  460. },
  461. },
  462. },
  463. },
  464. },
  465. {
  466. name: "user store containing 3 users",
  467. 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}]`,
  468. statusCode: http.StatusOK,
  469. mockParams: mockParams{
  470. userManagerParams: userManagerParams{
  471. allUsersParams: allUsersParams{
  472. {
  473. result: []state.User{
  474. {
  475. DisplayScreenName: "userA",
  476. IdentScreenName: state.NewIdentScreenName("userA"),
  477. },
  478. {
  479. DisplayScreenName: "userB",
  480. IdentScreenName: state.NewIdentScreenName("userB"),
  481. },
  482. {
  483. DisplayScreenName: "100003",
  484. IdentScreenName: state.NewIdentScreenName("100003"),
  485. IsICQ: true,
  486. },
  487. },
  488. },
  489. },
  490. },
  491. },
  492. },
  493. {
  494. name: "user handler error",
  495. want: `internal server error`,
  496. statusCode: http.StatusInternalServerError,
  497. mockParams: mockParams{
  498. userManagerParams: userManagerParams{
  499. allUsersParams: allUsersParams{
  500. {
  501. result: []state.User{},
  502. err: io.EOF,
  503. },
  504. },
  505. },
  506. },
  507. },
  508. }
  509. for _, tc := range tt {
  510. t.Run(tc.name, func(t *testing.T) {
  511. responseRecorder := httptest.NewRecorder()
  512. userManager := newMockUserManager(t)
  513. for _, params := range tc.mockParams.userManagerParams.allUsersParams {
  514. userManager.EXPECT().
  515. AllUsers().
  516. Return(params.result, params.err)
  517. }
  518. getUserHandler(responseRecorder, userManager, slog.Default())
  519. if responseRecorder.Code != tc.statusCode {
  520. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  521. }
  522. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  523. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  524. }
  525. })
  526. }
  527. }
  528. func TestUserHandler_POST(t *testing.T) {
  529. tt := []struct {
  530. name string
  531. body string
  532. UUID uuid.UUID
  533. want string
  534. password string
  535. statusCode int
  536. mockParams mockParams
  537. }{
  538. {
  539. name: "with valid AIM user",
  540. body: `{"screen_name":"userA", "password":"thepassword"}`,
  541. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  542. want: `User account created successfully.`,
  543. password: "thepassword",
  544. statusCode: http.StatusCreated,
  545. mockParams: mockParams{
  546. userManagerParams: userManagerParams{
  547. insertUserParams: insertUserParams{
  548. {
  549. u: state.User{
  550. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  551. DisplayScreenName: "userA",
  552. IdentScreenName: state.NewIdentScreenName("userA"),
  553. },
  554. err: nil,
  555. },
  556. },
  557. },
  558. },
  559. },
  560. {
  561. name: "with valid ICQ user",
  562. body: `{"screen_name":"100003", "password":"thepass"}`,
  563. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  564. want: `User account created successfully.`,
  565. password: "thepass",
  566. statusCode: http.StatusCreated,
  567. mockParams: mockParams{
  568. userManagerParams: userManagerParams{
  569. insertUserParams: insertUserParams{
  570. {
  571. u: state.User{
  572. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  573. DisplayScreenName: "100003",
  574. IdentScreenName: state.NewIdentScreenName("100003"),
  575. IsICQ: true,
  576. },
  577. err: nil,
  578. },
  579. },
  580. },
  581. },
  582. },
  583. {
  584. name: "with malformed body",
  585. body: `{"screen_name":"userA", "password":"thepassword"`, // missing closing }
  586. want: `malformed input`,
  587. statusCode: http.StatusBadRequest,
  588. },
  589. {
  590. name: "user handler error",
  591. body: `{"screen_name":"userA", "password":"thepassword"}`,
  592. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  593. want: `internal server error`,
  594. password: "thepassword",
  595. statusCode: http.StatusInternalServerError,
  596. mockParams: mockParams{
  597. userManagerParams: userManagerParams{
  598. insertUserParams: insertUserParams{
  599. {
  600. u: state.User{
  601. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  602. DisplayScreenName: "userA",
  603. IdentScreenName: state.NewIdentScreenName("userA"),
  604. },
  605. err: io.EOF,
  606. },
  607. },
  608. },
  609. },
  610. },
  611. {
  612. name: "duplicate user",
  613. body: `{"screen_name":"userA", "password":"thepassword"}`,
  614. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  615. want: `user already exists`,
  616. password: "thepassword",
  617. statusCode: http.StatusConflict,
  618. mockParams: mockParams{
  619. userManagerParams: userManagerParams{
  620. insertUserParams: insertUserParams{
  621. {
  622. u: state.User{
  623. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  624. DisplayScreenName: "userA",
  625. IdentScreenName: state.NewIdentScreenName("userA"),
  626. },
  627. err: state.ErrDupUser,
  628. },
  629. },
  630. },
  631. },
  632. },
  633. {
  634. name: "invalid AIM screen name",
  635. body: `{"screen_name":"a", "password":"thepassword"}`,
  636. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  637. want: `invalid screen name: screen name must be between 3 and 16 characters`,
  638. statusCode: http.StatusBadRequest,
  639. },
  640. {
  641. name: "invalid AIM password",
  642. body: `{"screen_name":"userA", "password":"1"}`,
  643. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  644. want: `invalid password: invalid password length: password length must be between 4-16 characters`,
  645. statusCode: http.StatusBadRequest,
  646. },
  647. {
  648. name: "invalid ICQ UIN",
  649. body: `{"screen_name":"1000", "password":"thepass"}`,
  650. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  651. want: `invalid uin: uin must be a number in the range 10000-2147483646`,
  652. statusCode: http.StatusBadRequest,
  653. },
  654. {
  655. name: "invalid ICQ password",
  656. body: `{"screen_name":"100003", "password":"thelongpassword"}`,
  657. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  658. want: `invalid password: invalid password length: password must be between 6-8 characters`,
  659. statusCode: http.StatusBadRequest,
  660. },
  661. }
  662. for _, tc := range tt {
  663. t.Run(tc.name, func(t *testing.T) {
  664. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  665. responseRecorder := httptest.NewRecorder()
  666. userManager := newMockUserManager(t)
  667. for _, params := range tc.mockParams.userManagerParams.insertUserParams {
  668. assert.NoError(t, params.u.HashPassword(tc.password))
  669. userManager.EXPECT().
  670. InsertUser(params.u).
  671. Return(params.err)
  672. }
  673. newUUID := func() uuid.UUID { return tc.UUID }
  674. postUserHandler(responseRecorder, request, userManager, newUUID, slog.Default())
  675. if responseRecorder.Code != tc.statusCode {
  676. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  677. }
  678. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  679. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  680. }
  681. })
  682. }
  683. }
  684. func TestUserHandler_DELETE(t *testing.T) {
  685. tt := []struct {
  686. name string
  687. body string
  688. want string
  689. statusCode int
  690. mockParams mockParams
  691. }{
  692. {
  693. name: "with valid user",
  694. body: `{"screen_name":"userA"}`,
  695. want: `User account successfully deleted.`,
  696. statusCode: http.StatusNoContent,
  697. mockParams: mockParams{
  698. userManagerParams: userManagerParams{
  699. deleteUserParams: deleteUserParams{
  700. {
  701. screenName: state.NewIdentScreenName("userA"),
  702. },
  703. },
  704. },
  705. },
  706. },
  707. {
  708. name: "with non-existent user",
  709. body: `{"screen_name":"userA"}`,
  710. want: `user does not exist`,
  711. statusCode: http.StatusNotFound,
  712. mockParams: mockParams{
  713. userManagerParams: userManagerParams{
  714. deleteUserParams: deleteUserParams{
  715. {
  716. screenName: state.NewIdentScreenName("userA"),
  717. err: state.ErrNoUser,
  718. },
  719. },
  720. },
  721. },
  722. },
  723. {
  724. name: "with malformed body",
  725. body: `{"screen_name":"userA"`, // missing closing }
  726. want: `malformed input`,
  727. statusCode: http.StatusBadRequest,
  728. },
  729. {
  730. name: "user handler error",
  731. body: `{"screen_name":"userA"}`,
  732. want: `internal server error`,
  733. statusCode: http.StatusInternalServerError,
  734. mockParams: mockParams{
  735. userManagerParams: userManagerParams{
  736. deleteUserParams: deleteUserParams{
  737. {
  738. screenName: state.NewIdentScreenName("userA"),
  739. err: io.EOF,
  740. },
  741. },
  742. },
  743. },
  744. },
  745. }
  746. for _, tc := range tt {
  747. t.Run(tc.name, func(t *testing.T) {
  748. request := httptest.NewRequest(http.MethodDelete, "/user", strings.NewReader(tc.body))
  749. responseRecorder := httptest.NewRecorder()
  750. userManager := newMockUserManager(t)
  751. for _, params := range tc.mockParams.userManagerParams.deleteUserParams {
  752. userManager.EXPECT().
  753. DeleteUser(params.screenName).
  754. Return(params.err)
  755. }
  756. deleteUserHandler(responseRecorder, request, userManager, slog.Default())
  757. if responseRecorder.Code != tc.statusCode {
  758. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  759. }
  760. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  761. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  762. }
  763. })
  764. }
  765. }
  766. func TestUserPasswordHandler_PUT(t *testing.T) {
  767. tt := []struct {
  768. name string
  769. body string
  770. want string
  771. statusCode int
  772. mockParams mockParams
  773. }{
  774. {
  775. name: "user with valid password",
  776. body: `{"screen_name":"userA", "password":"thenewpassword"}`,
  777. want: `Password successfully reset.`,
  778. statusCode: http.StatusNoContent,
  779. mockParams: mockParams{
  780. userManagerParams: userManagerParams{
  781. setUserPasswordParams: setUserPasswordParams{
  782. {
  783. screenName: state.NewIdentScreenName("userA"),
  784. newPassword: "thenewpassword",
  785. },
  786. },
  787. },
  788. },
  789. },
  790. {
  791. name: "user with invalid password",
  792. body: `{"screen_name":"userA", "password":"a"}`,
  793. want: `invalid password length`,
  794. statusCode: http.StatusBadRequest,
  795. mockParams: mockParams{
  796. userManagerParams: userManagerParams{
  797. setUserPasswordParams: setUserPasswordParams{
  798. {
  799. screenName: state.NewIdentScreenName("userA"),
  800. newPassword: "a",
  801. err: state.ErrPasswordInvalid,
  802. },
  803. },
  804. },
  805. },
  806. },
  807. {
  808. name: "with malformed body",
  809. body: `{"screen_name":"userA", "password":"thepassword"`, // missing closing }
  810. want: `malformed input`,
  811. statusCode: http.StatusBadRequest,
  812. },
  813. {
  814. name: "password updater returns runtime error",
  815. body: `{"screen_name":"userA", "password":"thepassword"}`,
  816. want: `internal server error`,
  817. statusCode: http.StatusInternalServerError,
  818. mockParams: mockParams{
  819. userManagerParams: userManagerParams{
  820. setUserPasswordParams: setUserPasswordParams{
  821. {
  822. screenName: state.NewIdentScreenName("userA"),
  823. newPassword: "thepassword",
  824. err: io.EOF,
  825. },
  826. },
  827. },
  828. },
  829. },
  830. {
  831. name: "user doesn't exist",
  832. body: `{"screen_name":"userA", "password":"thepassword"}`,
  833. want: `user does not exist`,
  834. statusCode: http.StatusNotFound,
  835. mockParams: mockParams{
  836. userManagerParams: userManagerParams{
  837. setUserPasswordParams: setUserPasswordParams{
  838. {
  839. screenName: state.NewIdentScreenName("userA"),
  840. newPassword: "thepassword",
  841. err: state.ErrNoUser,
  842. },
  843. },
  844. },
  845. },
  846. },
  847. }
  848. for _, tc := range tt {
  849. t.Run(tc.name, func(t *testing.T) {
  850. request := httptest.NewRequest(http.MethodPut, "/user", strings.NewReader(tc.body))
  851. responseRecorder := httptest.NewRecorder()
  852. userManager := newMockUserManager(t)
  853. for _, params := range tc.mockParams.userManagerParams.setUserPasswordParams {
  854. userManager.EXPECT().
  855. SetUserPassword(params.screenName, params.newPassword).
  856. Return(params.err)
  857. }
  858. putUserPasswordHandler(responseRecorder, request, userManager, slog.Default())
  859. if responseRecorder.Code != tc.statusCode {
  860. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  861. }
  862. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  863. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  864. }
  865. })
  866. }
  867. }
  868. func TestPublicChatHandler_GET(t *testing.T) {
  869. fnNewSess := func(screenName string) *state.Session {
  870. sess := state.NewSession()
  871. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  872. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  873. return sess
  874. }
  875. chatRoom1 := state.NewChatRoom("chat-room-1-name", state.NewIdentScreenName("chat-room-1-creator"), state.PublicExchange)
  876. chatRoom2 := state.NewChatRoom("chat-room-2-name", state.NewIdentScreenName("chat-room-1-creator"), state.PublicExchange)
  877. tt := []struct {
  878. name string
  879. want string
  880. statusCode int
  881. mockParams mockParams
  882. }{
  883. {
  884. name: "multiple chat rooms with participants",
  885. want: `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?roomname=chat-room-1-name\u0026exchange=5","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?roomname=chat-room-2-name\u0026exchange=5","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
  886. statusCode: http.StatusOK,
  887. mockParams: mockParams{
  888. chatRoomRetrieverParams: chatRoomRetrieverParams{
  889. allChatRoomsParams: allChatRoomsParams{
  890. {
  891. exchange: state.PublicExchange,
  892. result: []state.ChatRoom{
  893. chatRoom1,
  894. chatRoom2,
  895. },
  896. },
  897. },
  898. },
  899. chatSessionRetrieverParams: chatSessionRetrieverParams{
  900. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  901. {
  902. cookie: chatRoom1.Cookie(),
  903. result: []*state.Session{
  904. fnNewSess("userA"),
  905. fnNewSess("userB"),
  906. },
  907. },
  908. {
  909. cookie: chatRoom2.Cookie(),
  910. result: []*state.Session{
  911. fnNewSess("userC"),
  912. fnNewSess("userD"),
  913. },
  914. },
  915. },
  916. },
  917. },
  918. },
  919. {
  920. name: "chat room without participants",
  921. want: `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?roomname=chat-room-1-name\u0026exchange=5","participants":[]}]`,
  922. statusCode: http.StatusOK,
  923. mockParams: mockParams{
  924. chatRoomRetrieverParams: chatRoomRetrieverParams{
  925. allChatRoomsParams: allChatRoomsParams{
  926. {
  927. exchange: state.PublicExchange,
  928. result: []state.ChatRoom{
  929. chatRoom1,
  930. },
  931. },
  932. },
  933. },
  934. chatSessionRetrieverParams: chatSessionRetrieverParams{
  935. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  936. {
  937. cookie: chatRoom1.Cookie(),
  938. result: []*state.Session{},
  939. },
  940. },
  941. },
  942. },
  943. },
  944. {
  945. name: "no chat rooms",
  946. want: `[]`,
  947. statusCode: http.StatusOK,
  948. mockParams: mockParams{
  949. chatRoomRetrieverParams: chatRoomRetrieverParams{
  950. allChatRoomsParams: allChatRoomsParams{
  951. {
  952. exchange: state.PublicExchange,
  953. result: []state.ChatRoom{},
  954. },
  955. },
  956. },
  957. },
  958. },
  959. }
  960. for _, tc := range tt {
  961. t.Run(tc.name, func(t *testing.T) {
  962. request := httptest.NewRequest(http.MethodGet, "/chat/room/public", nil)
  963. responseRecorder := httptest.NewRecorder()
  964. chatRoomRetriever := newMockChatRoomRetriever(t)
  965. for _, params := range tc.mockParams.chatRoomRetrieverParams.allChatRoomsParams {
  966. chatRoomRetriever.EXPECT().
  967. AllChatRooms(params.exchange).
  968. Return(params.result, params.err)
  969. }
  970. chatSessionRetriever := newMockChatSessionRetriever(t)
  971. for _, params := range tc.mockParams.chatSessionRetrieverParams.chatSessionRetrieverAllSessionsParams {
  972. chatSessionRetriever.EXPECT().
  973. AllSessions(params.cookie).
  974. Return(params.result)
  975. }
  976. getPublicChatHandler(responseRecorder, request, chatRoomRetriever, chatSessionRetriever, slog.Default())
  977. if responseRecorder.Code != tc.statusCode {
  978. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  979. }
  980. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  981. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  982. }
  983. })
  984. }
  985. }
  986. func TestPrivateChatHandler_GET(t *testing.T) {
  987. fnNewSess := func(screenName string) *state.Session {
  988. sess := state.NewSession()
  989. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  990. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  991. return sess
  992. }
  993. chatRoom1 := state.NewChatRoom("chat-room-1-name", state.NewIdentScreenName("chat-room-1-creator"), state.PrivateExchange)
  994. chatRoom2 := state.NewChatRoom("chat-room-2-name", state.NewIdentScreenName("chat-room-2-creator"), state.PrivateExchange)
  995. tt := []struct {
  996. name string
  997. want string
  998. statusCode int
  999. mockParams mockParams
  1000. }{
  1001. {
  1002. name: "multiple chat rooms with participants",
  1003. want: `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","creator_id":"chat-room-1-creator","url":"aim:gochat?roomname=chat-room-1-name\u0026exchange=4","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?roomname=chat-room-2-name\u0026exchange=4","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
  1004. statusCode: http.StatusOK,
  1005. mockParams: mockParams{
  1006. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1007. allChatRoomsParams: allChatRoomsParams{
  1008. {
  1009. exchange: state.PrivateExchange,
  1010. result: []state.ChatRoom{
  1011. chatRoom1,
  1012. chatRoom2,
  1013. },
  1014. },
  1015. },
  1016. },
  1017. chatSessionRetrieverParams: chatSessionRetrieverParams{
  1018. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  1019. {
  1020. cookie: chatRoom1.Cookie(),
  1021. result: []*state.Session{
  1022. fnNewSess("userA"),
  1023. fnNewSess("userB"),
  1024. },
  1025. },
  1026. {
  1027. cookie: chatRoom2.Cookie(),
  1028. result: []*state.Session{
  1029. fnNewSess("userC"),
  1030. fnNewSess("userD"),
  1031. },
  1032. },
  1033. },
  1034. },
  1035. },
  1036. },
  1037. {
  1038. name: "chat room without participants",
  1039. want: `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","creator_id":"chat-room-1-creator","url":"aim:gochat?roomname=chat-room-1-name\u0026exchange=4","participants":[]}]`,
  1040. statusCode: http.StatusOK,
  1041. mockParams: mockParams{
  1042. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1043. allChatRoomsParams: allChatRoomsParams{
  1044. {
  1045. exchange: state.PrivateExchange,
  1046. result: []state.ChatRoom{
  1047. chatRoom1,
  1048. },
  1049. },
  1050. },
  1051. },
  1052. chatSessionRetrieverParams: chatSessionRetrieverParams{
  1053. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  1054. {
  1055. cookie: chatRoom1.Cookie(),
  1056. result: []*state.Session{},
  1057. },
  1058. },
  1059. },
  1060. },
  1061. },
  1062. {
  1063. name: "no chat rooms",
  1064. want: `[]`,
  1065. statusCode: http.StatusOK,
  1066. mockParams: mockParams{
  1067. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1068. allChatRoomsParams: allChatRoomsParams{
  1069. {
  1070. exchange: state.PrivateExchange,
  1071. result: []state.ChatRoom{},
  1072. },
  1073. },
  1074. },
  1075. },
  1076. },
  1077. }
  1078. for _, tc := range tt {
  1079. t.Run(tc.name, func(t *testing.T) {
  1080. request := httptest.NewRequest(http.MethodGet, "/chat/room/private", nil)
  1081. responseRecorder := httptest.NewRecorder()
  1082. chatRoomRetriever := newMockChatRoomRetriever(t)
  1083. for _, params := range tc.mockParams.chatRoomRetrieverParams.allChatRoomsParams {
  1084. chatRoomRetriever.EXPECT().
  1085. AllChatRooms(params.exchange).
  1086. Return(params.result, params.err)
  1087. }
  1088. chatSessionRetriever := newMockChatSessionRetriever(t)
  1089. for _, params := range tc.mockParams.chatSessionRetrieverParams.chatSessionRetrieverAllSessionsParams {
  1090. chatSessionRetriever.EXPECT().
  1091. AllSessions(params.cookie).
  1092. Return(params.result)
  1093. }
  1094. getPrivateChatHandler(responseRecorder, request, chatRoomRetriever, chatSessionRetriever, slog.Default())
  1095. if responseRecorder.Code != tc.statusCode {
  1096. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1097. }
  1098. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1099. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1100. }
  1101. })
  1102. }
  1103. }
  1104. func TestInstantMessageHandler_POST(t *testing.T) {
  1105. type relayToScreenNameInputs struct {
  1106. sender state.IdentScreenName
  1107. recipient state.IdentScreenName
  1108. msg string
  1109. }
  1110. tt := []struct {
  1111. name string
  1112. relayToScreenNameInputs []relayToScreenNameInputs
  1113. body string
  1114. want string
  1115. statusCode int
  1116. }{
  1117. {
  1118. name: "send an instant message",
  1119. relayToScreenNameInputs: []relayToScreenNameInputs{
  1120. {
  1121. sender: state.NewIdentScreenName("sender_sn"),
  1122. recipient: state.NewIdentScreenName("recip_sn"),
  1123. msg: "hello world!",
  1124. },
  1125. },
  1126. body: `{"from":"sender_sn","to":"recip_sn","text":"hello world!"}`,
  1127. want: `Message sent successfully.`,
  1128. statusCode: http.StatusOK,
  1129. },
  1130. {
  1131. name: "with malformed body",
  1132. body: `{"screen_name":"userA", "password":"thepassword"`,
  1133. want: `malformed input`,
  1134. statusCode: http.StatusBadRequest,
  1135. },
  1136. }
  1137. for _, tc := range tt {
  1138. t.Run(tc.name, func(t *testing.T) {
  1139. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  1140. responseRecorder := httptest.NewRecorder()
  1141. messageRelayer := newMockMessageRelayer(t)
  1142. for _, params := range tc.relayToScreenNameInputs {
  1143. validateSNAC := func(msg wire.SNACMessage) bool {
  1144. body := msg.Body.(wire.SNAC_0x04_0x07_ICBMChannelMsgToClient)
  1145. assert.Equal(t, params.sender.String(), body.TLVUserInfo.ScreenName)
  1146. b, ok := body.Slice(wire.ICBMTLVAOLIMData)
  1147. assert.True(t, ok)
  1148. txt, err := wire.UnmarshalICBMMessageText(b)
  1149. assert.NoError(t, err)
  1150. assert.Equal(t, params.msg, txt)
  1151. return true
  1152. }
  1153. messageRelayer.EXPECT().
  1154. RelayToScreenName(mock.Anything, params.recipient, mock.MatchedBy(validateSNAC))
  1155. }
  1156. postInstantMessageHandler(responseRecorder, request, messageRelayer, slog.Default())
  1157. if responseRecorder.Code != tc.statusCode {
  1158. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1159. }
  1160. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1161. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  1162. }
  1163. })
  1164. }
  1165. }