mgmt_api_test.go 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301
  1. package http
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "log/slog"
  7. "net/http"
  8. "net/http/httptest"
  9. "net/mail"
  10. "net/netip"
  11. "strings"
  12. "testing"
  13. "time"
  14. "github.com/google/uuid"
  15. "github.com/stretchr/testify/assert"
  16. "github.com/stretchr/testify/mock"
  17. "github.com/mk6i/retro-aim-server/config"
  18. "github.com/mk6i/retro-aim-server/state"
  19. "github.com/mk6i/retro-aim-server/wire"
  20. )
  21. func TestSessionHandler_GET(t *testing.T) {
  22. fnNewSess := func(screenName string, uin uint32) *state.Session {
  23. sess := state.NewSession()
  24. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  25. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  26. sess.SetUIN(uin)
  27. ip, _ := netip.ParseAddrPort("1.2.3.4:1234")
  28. sess.SetRemoteAddr(&ip)
  29. return sess
  30. }
  31. tt := []struct {
  32. name string
  33. want string
  34. statusCode int
  35. timeSinceFunc func(t time.Time) time.Duration
  36. mockParams mockParams
  37. }{
  38. {
  39. name: "without sessions",
  40. want: `{"count":0,"sessions":[]}`,
  41. statusCode: http.StatusOK,
  42. mockParams: mockParams{
  43. sessionRetrieverParams: sessionRetrieverParams{
  44. sessionRetrieverAllSessionsParams: sessionRetrieverAllSessionsParams{
  45. {
  46. result: []*state.Session{},
  47. },
  48. },
  49. },
  50. },
  51. },
  52. {
  53. name: "with sessions",
  54. want: `{"count":3,"sessions":[{"id":"usera","screen_name":"userA","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":false,"remote_addr":"1.2.3.4","remote_port":1234},{"id":"userb","screen_name":"userB","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":false,"remote_addr":"1.2.3.4","remote_port":1234},{"id":"100003","screen_name":"100003","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":true,"remote_addr":"1.2.3.4","remote_port":1234}]}`,
  55. statusCode: http.StatusOK,
  56. timeSinceFunc: func(t time.Time) time.Duration { t0 := time.Now(); return t0.Sub(t0) },
  57. mockParams: mockParams{
  58. sessionRetrieverParams: sessionRetrieverParams{
  59. sessionRetrieverAllSessionsParams: sessionRetrieverAllSessionsParams{
  60. {
  61. result: []*state.Session{
  62. fnNewSess("userA", 0),
  63. fnNewSess("userB", 0),
  64. fnNewSess("100003", 100003),
  65. },
  66. },
  67. },
  68. },
  69. },
  70. },
  71. }
  72. for _, tc := range tt {
  73. t.Run(tc.name, func(t *testing.T) {
  74. request := httptest.NewRequest(http.MethodGet, "/session", nil)
  75. responseRecorder := httptest.NewRecorder()
  76. sessionRetriever := newMockSessionRetriever(t)
  77. for _, params := range tc.mockParams.sessionRetrieverParams.sessionRetrieverAllSessionsParams {
  78. sessionRetriever.EXPECT().
  79. AllSessions().
  80. Return(params.result)
  81. }
  82. getSessionHandler(responseRecorder, request, sessionRetriever, tc.timeSinceFunc)
  83. if responseRecorder.Code != tc.statusCode {
  84. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  85. }
  86. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  87. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  88. }
  89. })
  90. }
  91. }
  92. func TestSessionHandlerScreenname_GET(t *testing.T) {
  93. fnNewSess := func(screenName string, uin uint32) *state.Session {
  94. sess := state.NewSession()
  95. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  96. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  97. sess.SetUIN(uin)
  98. ip, _ := netip.ParseAddrPort("1.2.3.4:1234")
  99. sess.SetRemoteAddr(&ip)
  100. return sess
  101. }
  102. tt := []struct {
  103. name string
  104. sessions []*state.Session
  105. requestScreenName state.IdentScreenName
  106. want string
  107. statusCode int
  108. timeSinceFunc func(t time.Time) time.Duration
  109. mockParams mockParams
  110. }{
  111. {
  112. name: "no session for screenname",
  113. sessions: []*state.Session{},
  114. requestScreenName: state.NewIdentScreenName("userA"),
  115. want: `session not found`,
  116. statusCode: http.StatusNotFound,
  117. mockParams: mockParams{
  118. sessionRetrieverParams: sessionRetrieverParams{
  119. retrieveSessionByNameParams: retrieveSessionByNameParams{
  120. {
  121. screenName: state.NewIdentScreenName("userA"),
  122. result: nil,
  123. },
  124. },
  125. },
  126. },
  127. },
  128. {
  129. name: "active session found for screenname",
  130. requestScreenName: state.NewIdentScreenName("userA"),
  131. want: `{"count":1,"sessions":[{"id":"usera","screen_name":"userA","online_seconds":0,"away_message":"","idle_seconds":0,"is_icq":false,"remote_addr":"1.2.3.4","remote_port":1234}]}`,
  132. statusCode: http.StatusOK,
  133. timeSinceFunc: func(t time.Time) time.Duration { t0 := time.Now(); return t0.Sub(t0) },
  134. mockParams: mockParams{
  135. sessionRetrieverParams: sessionRetrieverParams{
  136. retrieveSessionByNameParams: retrieveSessionByNameParams{
  137. {
  138. screenName: state.NewIdentScreenName("userA"),
  139. result: fnNewSess("userA", 0),
  140. },
  141. },
  142. },
  143. },
  144. },
  145. }
  146. for _, tc := range tt {
  147. t.Run(tc.name, func(t *testing.T) {
  148. request := httptest.NewRequest(http.MethodGet, "/session/"+tc.requestScreenName.String(), nil)
  149. request.SetPathValue("screenname", tc.requestScreenName.String())
  150. responseRecorder := httptest.NewRecorder()
  151. sessionRetriever := newMockSessionRetriever(t)
  152. for _, params := range tc.mockParams.sessionRetrieverParams.retrieveSessionByNameParams {
  153. sessionRetriever.EXPECT().
  154. RetrieveSession(params.screenName).
  155. Return(params.result)
  156. }
  157. getSessionHandler(responseRecorder, request, sessionRetriever, tc.timeSinceFunc)
  158. if responseRecorder.Code != tc.statusCode {
  159. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  160. }
  161. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  162. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  163. }
  164. })
  165. }
  166. }
  167. func TestSessionHandlerScreenname_DELETE(t *testing.T) {
  168. fnNewSess := func(screenName string) *state.Session {
  169. sess := state.NewSession()
  170. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  171. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  172. ip, _ := netip.ParseAddrPort("1.2.3.4:1234")
  173. sess.SetRemoteAddr(&ip)
  174. return sess
  175. }
  176. tt := []struct {
  177. name string
  178. session *state.Session
  179. requestScreenName state.IdentScreenName
  180. statusCode int
  181. mockParams mockParams
  182. }{
  183. {
  184. name: "delete an active session",
  185. requestScreenName: state.NewIdentScreenName("userA"),
  186. statusCode: http.StatusNoContent,
  187. mockParams: mockParams{
  188. sessionRetrieverParams: sessionRetrieverParams{
  189. retrieveSessionByNameParams: retrieveSessionByNameParams{
  190. {
  191. screenName: state.NewIdentScreenName("userA"),
  192. result: fnNewSess("userA"),
  193. },
  194. },
  195. },
  196. },
  197. },
  198. {
  199. name: "delete a non-existent session",
  200. requestScreenName: state.NewIdentScreenName("userA"),
  201. statusCode: http.StatusNotFound,
  202. mockParams: mockParams{
  203. sessionRetrieverParams: sessionRetrieverParams{
  204. retrieveSessionByNameParams: retrieveSessionByNameParams{
  205. {
  206. screenName: state.NewIdentScreenName("userA"),
  207. result: nil,
  208. },
  209. },
  210. },
  211. },
  212. },
  213. }
  214. for _, tc := range tt {
  215. t.Run(tc.name, func(t *testing.T) {
  216. request := httptest.NewRequest(http.MethodDelete, "/session/"+tc.requestScreenName.String(), nil)
  217. request.SetPathValue("screenname", tc.requestScreenName.String())
  218. responseRecorder := httptest.NewRecorder()
  219. sessionRetriever := newMockSessionRetriever(t)
  220. for _, params := range tc.mockParams.sessionRetrieverParams.retrieveSessionByNameParams {
  221. sessionRetriever.EXPECT().
  222. RetrieveSession(params.screenName).
  223. Return(params.result)
  224. }
  225. deleteSessionHandler(responseRecorder, request, sessionRetriever)
  226. if responseRecorder.Code != tc.statusCode {
  227. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  228. }
  229. })
  230. }
  231. }
  232. func TestUserAccountHandler_GET(t *testing.T) {
  233. tt := []struct {
  234. name string
  235. requestScreenName state.IdentScreenName
  236. want string
  237. statusCode int
  238. mockParams mockParams
  239. }{
  240. {
  241. name: "invalid account",
  242. requestScreenName: state.NewIdentScreenName("userA"),
  243. want: `user not found`,
  244. statusCode: http.StatusNotFound,
  245. mockParams: mockParams{
  246. userManagerParams: userManagerParams{
  247. getUserParams: getUserParams{
  248. {
  249. screenName: state.NewIdentScreenName("userA"),
  250. result: nil,
  251. },
  252. },
  253. },
  254. },
  255. },
  256. {
  257. name: "valid aim account",
  258. requestScreenName: state.NewIdentScreenName("userA"),
  259. want: `{"id":"usera","screen_name":"userA","profile":"My Profile Text","email_address":"\u003cuserA@aol.com\u003e","reg_status":2,"confirmed":true,"is_icq":false,"suspended_status":""}`,
  260. statusCode: http.StatusOK,
  261. mockParams: mockParams{
  262. userManagerParams: userManagerParams{
  263. getUserParams: getUserParams{
  264. {
  265. screenName: state.NewIdentScreenName("userA"),
  266. result: &state.User{
  267. DisplayScreenName: "userA",
  268. IdentScreenName: state.NewIdentScreenName("userA"),
  269. SuspendedStatus: 0x0,
  270. },
  271. },
  272. },
  273. },
  274. accountManagerParams: accountManagerParams{
  275. emailAddressByNameParams: emailAddressByNameParams{
  276. {
  277. screenName: state.NewIdentScreenName("userA"),
  278. result: &mail.Address{
  279. Address: "userA@aol.com",
  280. },
  281. },
  282. },
  283. regStatusByNameParams: regStatusByNameParams{
  284. {
  285. screenName: state.NewIdentScreenName("userA"),
  286. result: uint16(0x02),
  287. },
  288. },
  289. confirmStatusByNameParams: confirmStatusByNameParams{
  290. {
  291. screenName: state.NewIdentScreenName("userA"),
  292. result: true,
  293. },
  294. },
  295. },
  296. profileRetrieverParams: profileRetrieverParams{
  297. retrieveProfileParams: retrieveProfileParams{
  298. {
  299. screenName: state.NewIdentScreenName("userA"),
  300. result: "My Profile Text",
  301. },
  302. },
  303. },
  304. },
  305. },
  306. {
  307. name: "suspended aim account",
  308. requestScreenName: state.NewIdentScreenName("userB"),
  309. want: `{"id":"userb","screen_name":"userB","profile":"My Profile Text","email_address":"\u003cuserB@aol.com\u003e","reg_status":2,"confirmed":true,"is_icq":false,"suspended_status":"suspended"}`,
  310. statusCode: http.StatusOK,
  311. mockParams: mockParams{
  312. userManagerParams: userManagerParams{
  313. getUserParams: getUserParams{
  314. {
  315. screenName: state.NewIdentScreenName("userB"),
  316. result: &state.User{
  317. DisplayScreenName: "userB",
  318. IdentScreenName: state.NewIdentScreenName("userB"),
  319. SuspendedStatus: wire.LoginErrSuspendedAccount,
  320. },
  321. },
  322. },
  323. },
  324. accountManagerParams: accountManagerParams{
  325. emailAddressByNameParams: emailAddressByNameParams{
  326. {
  327. screenName: state.NewIdentScreenName("userB"),
  328. result: &mail.Address{
  329. Address: "userB@aol.com",
  330. },
  331. },
  332. },
  333. regStatusByNameParams: regStatusByNameParams{
  334. {
  335. screenName: state.NewIdentScreenName("userB"),
  336. result: uint16(0x02),
  337. },
  338. },
  339. confirmStatusByNameParams: confirmStatusByNameParams{
  340. {
  341. screenName: state.NewIdentScreenName("userB"),
  342. result: true,
  343. },
  344. },
  345. },
  346. profileRetrieverParams: profileRetrieverParams{
  347. retrieveProfileParams: retrieveProfileParams{
  348. {
  349. screenName: state.NewIdentScreenName("userB"),
  350. result: "My Profile Text",
  351. },
  352. },
  353. },
  354. },
  355. },
  356. }
  357. for _, tc := range tt {
  358. t.Run(tc.name, func(t *testing.T) {
  359. request := httptest.NewRequest(http.MethodGet, "/user/"+tc.requestScreenName.String()+"/account", nil)
  360. request.SetPathValue("screenname", tc.requestScreenName.String())
  361. responseRecorder := httptest.NewRecorder()
  362. userManager := newMockUserManager(t)
  363. for _, params := range tc.mockParams.userManagerParams.getUserParams {
  364. userManager.EXPECT().
  365. User(params.screenName).
  366. Return(params.result, params.err)
  367. }
  368. accountManager := newMockAccountManager(t)
  369. for _, params := range tc.mockParams.accountManagerParams.emailAddressByNameParams {
  370. accountManager.EXPECT().
  371. EmailAddressByName(params.screenName).
  372. Return(params.result, params.err)
  373. }
  374. for _, params := range tc.mockParams.accountManagerParams.regStatusByNameParams {
  375. accountManager.EXPECT().
  376. RegStatusByName(params.screenName).
  377. Return(params.result, params.err)
  378. }
  379. for _, params := range tc.mockParams.accountManagerParams.confirmStatusByNameParams {
  380. accountManager.EXPECT().
  381. ConfirmStatusByName(params.screenName).
  382. Return(params.result, params.err)
  383. }
  384. profileRetriever := newMockProfileRetriever(t)
  385. for _, params := range tc.mockParams.profileRetrieverParams.retrieveProfileParams {
  386. profileRetriever.EXPECT().
  387. Profile(params.screenName).
  388. Return(params.result, params.err)
  389. }
  390. getUserAccountHandler(responseRecorder, request, userManager, accountManager, profileRetriever, 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 TestUserAccountHandler_PATCH(t *testing.T) {
  401. tt := []struct {
  402. name string
  403. requestScreenName state.IdentScreenName
  404. want string
  405. body string
  406. statusCode int
  407. mockParams mockParams
  408. }{
  409. {
  410. name: "suspending a non-existent account",
  411. requestScreenName: state.NewIdentScreenName("userA"),
  412. body: `{"suspended_status":"suspended"}`,
  413. want: `user not found`,
  414. statusCode: http.StatusNotFound,
  415. mockParams: mockParams{
  416. userManagerParams: userManagerParams{
  417. getUserParams: getUserParams{
  418. {
  419. screenName: state.NewIdentScreenName("userA"),
  420. result: nil,
  421. },
  422. },
  423. },
  424. },
  425. },
  426. {
  427. name: "patching with invalid suspended_status value",
  428. requestScreenName: state.NewIdentScreenName("userA"),
  429. body: `{"suspended_status":"thisisinvalid"}`,
  430. want: `{"message":"suspended_status must be empty str or one of deleted,expired,suspended,suspended_age"}`,
  431. statusCode: http.StatusBadRequest,
  432. mockParams: mockParams{
  433. userManagerParams: userManagerParams{
  434. getUserParams: getUserParams{
  435. {
  436. screenName: state.NewIdentScreenName("userA"),
  437. result: &state.User{},
  438. },
  439. },
  440. },
  441. },
  442. },
  443. {
  444. name: "suspending an active aim account",
  445. requestScreenName: state.NewIdentScreenName("userA"),
  446. statusCode: http.StatusNoContent,
  447. body: `{"suspended_status":"suspended"}`,
  448. mockParams: mockParams{
  449. userManagerParams: userManagerParams{
  450. getUserParams: getUserParams{
  451. {
  452. screenName: state.NewIdentScreenName("userA"),
  453. result: &state.User{
  454. DisplayScreenName: "userA",
  455. IdentScreenName: state.NewIdentScreenName("userA"),
  456. SuspendedStatus: 0x0,
  457. },
  458. },
  459. },
  460. },
  461. accountManagerParams: accountManagerParams{
  462. updateSuspendedStatusParams: updateSuspendedStatusParams{
  463. {
  464. suspendedStatus: wire.LoginErrSuspendedAccount,
  465. screenName: state.NewIdentScreenName("userA"),
  466. err: nil,
  467. },
  468. },
  469. },
  470. },
  471. },
  472. {
  473. name: "unsuspending a suspended aim account",
  474. requestScreenName: state.NewIdentScreenName("userA"),
  475. statusCode: http.StatusNoContent,
  476. body: `{"suspended_status":""}`,
  477. mockParams: mockParams{
  478. userManagerParams: userManagerParams{
  479. getUserParams: getUserParams{
  480. {
  481. screenName: state.NewIdentScreenName("userA"),
  482. result: &state.User{
  483. DisplayScreenName: "userA",
  484. IdentScreenName: state.NewIdentScreenName("userA"),
  485. SuspendedStatus: wire.LoginErrSuspendedAccount,
  486. },
  487. },
  488. },
  489. },
  490. accountManagerParams: accountManagerParams{
  491. updateSuspendedStatusParams: updateSuspendedStatusParams{
  492. {
  493. suspendedStatus: 0x0,
  494. screenName: state.NewIdentScreenName("userA"),
  495. err: nil,
  496. },
  497. },
  498. },
  499. },
  500. },
  501. {
  502. name: "suspending an already suspended aim account",
  503. requestScreenName: state.NewIdentScreenName("userA"),
  504. statusCode: http.StatusNotModified,
  505. body: `{"suspended_status":"suspended"}`,
  506. mockParams: mockParams{
  507. userManagerParams: userManagerParams{
  508. getUserParams: getUserParams{
  509. {
  510. screenName: state.NewIdentScreenName("userA"),
  511. result: &state.User{
  512. DisplayScreenName: "userA",
  513. IdentScreenName: state.NewIdentScreenName("userA"),
  514. SuspendedStatus: wire.LoginErrSuspendedAccount,
  515. },
  516. },
  517. },
  518. },
  519. },
  520. },
  521. }
  522. for _, tc := range tt {
  523. t.Run(tc.name, func(t *testing.T) {
  524. request := httptest.NewRequest(http.MethodPatch, "/user/"+tc.requestScreenName.String()+"/account", strings.NewReader(tc.body))
  525. request.SetPathValue("screenname", tc.requestScreenName.String())
  526. responseRecorder := httptest.NewRecorder()
  527. userManager := newMockUserManager(t)
  528. for _, params := range tc.mockParams.userManagerParams.getUserParams {
  529. userManager.EXPECT().
  530. User(params.screenName).
  531. Return(params.result, params.err)
  532. }
  533. accountManager := newMockAccountManager(t)
  534. for _, params := range tc.mockParams.accountManagerParams.updateSuspendedStatusParams {
  535. accountManager.EXPECT().
  536. UpdateSuspendedStatus(params.suspendedStatus, params.screenName).
  537. Return(params.err)
  538. }
  539. patchUserAccountHandler(responseRecorder, request, userManager, accountManager, slog.Default())
  540. if responseRecorder.Code != tc.statusCode {
  541. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  542. }
  543. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  544. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  545. }
  546. })
  547. }
  548. }
  549. func TestUserBuddyIconHandler_GET(t *testing.T) {
  550. sampleGIF := []byte{
  551. 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x32, 0x00, 0x32, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
  552. 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
  553. 0x32, 0x00, 0x32, 0x00, 0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b}
  554. sampleJPG := []byte{0xFF, 0xD8, 0xFF, 0x43, 0x13, 0x37}
  555. tt := []struct {
  556. name string
  557. requestScreenName state.IdentScreenName
  558. want string
  559. statusCode int
  560. contentType string
  561. mockParams mockParams
  562. }{
  563. {
  564. name: "invalid account",
  565. requestScreenName: state.NewIdentScreenName("userA"),
  566. want: `user not found`,
  567. statusCode: http.StatusNotFound,
  568. contentType: "text/plain; charset=utf-8",
  569. mockParams: mockParams{
  570. userManagerParams: userManagerParams{
  571. getUserParams: getUserParams{
  572. {
  573. screenName: state.NewIdentScreenName("userA"),
  574. result: nil,
  575. },
  576. },
  577. },
  578. },
  579. },
  580. {
  581. name: "account with gif buddy icon",
  582. requestScreenName: state.NewIdentScreenName("userA"),
  583. want: string(sampleGIF),
  584. statusCode: http.StatusOK,
  585. contentType: "image/gif",
  586. mockParams: mockParams{
  587. userManagerParams: userManagerParams{
  588. getUserParams: getUserParams{
  589. {
  590. screenName: state.NewIdentScreenName("userA"),
  591. result: &state.User{
  592. DisplayScreenName: "userA",
  593. IdentScreenName: state.NewIdentScreenName("userA"),
  594. },
  595. },
  596. },
  597. },
  598. feedBagRetrieverParams: feedBagRetrieverParams{
  599. buddyIconRefByNameParams: buddyIconRefByNameParams{
  600. {
  601. screenName: state.NewIdentScreenName("userA"),
  602. result: &wire.BARTID{
  603. Type: wire.BARTTypesBuddyIcon,
  604. BARTInfo: wire.BARTInfo{
  605. Flags: 0x00,
  606. Hash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  607. },
  608. },
  609. },
  610. },
  611. },
  612. bartRetrieverParams: bartRetrieverParams{
  613. bartRetrieveParams: bartRetrieveParams{
  614. {
  615. itemHash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  616. result: sampleGIF,
  617. },
  618. },
  619. },
  620. },
  621. },
  622. {
  623. name: "account with jpg buddy icon",
  624. requestScreenName: state.NewIdentScreenName("userA"),
  625. want: string(sampleJPG),
  626. statusCode: http.StatusOK,
  627. contentType: "image/jpeg",
  628. mockParams: mockParams{
  629. userManagerParams: userManagerParams{
  630. getUserParams: getUserParams{
  631. {
  632. screenName: state.NewIdentScreenName("userA"),
  633. result: &state.User{
  634. DisplayScreenName: "userA",
  635. IdentScreenName: state.NewIdentScreenName("userA"),
  636. },
  637. },
  638. },
  639. },
  640. feedBagRetrieverParams: feedBagRetrieverParams{
  641. buddyIconRefByNameParams: buddyIconRefByNameParams{
  642. {
  643. screenName: state.NewIdentScreenName("userA"),
  644. result: &wire.BARTID{
  645. Type: wire.BARTTypesBuddyIcon,
  646. BARTInfo: wire.BARTInfo{
  647. Flags: 0x00,
  648. Hash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  649. },
  650. },
  651. },
  652. },
  653. },
  654. bartRetrieverParams: bartRetrieverParams{
  655. bartRetrieveParams: bartRetrieveParams{
  656. {
  657. itemHash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  658. result: sampleJPG,
  659. },
  660. },
  661. },
  662. },
  663. },
  664. {
  665. name: "account with unknown format buddy icon",
  666. requestScreenName: state.NewIdentScreenName("userA"),
  667. want: string([]byte{0x13, 0x37, 0x13, 0x37, 0x13, 0x37}),
  668. statusCode: http.StatusOK,
  669. contentType: "application/octet-stream",
  670. mockParams: mockParams{
  671. userManagerParams: userManagerParams{
  672. getUserParams: getUserParams{
  673. {
  674. screenName: state.NewIdentScreenName("userA"),
  675. result: &state.User{
  676. DisplayScreenName: "userA",
  677. IdentScreenName: state.NewIdentScreenName("userA"),
  678. },
  679. },
  680. },
  681. },
  682. feedBagRetrieverParams: feedBagRetrieverParams{
  683. buddyIconRefByNameParams: buddyIconRefByNameParams{
  684. {
  685. screenName: state.NewIdentScreenName("userA"),
  686. result: &wire.BARTID{
  687. Type: wire.BARTTypesBuddyIcon,
  688. BARTInfo: wire.BARTInfo{
  689. Flags: 0x00,
  690. Hash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  691. },
  692. },
  693. },
  694. },
  695. },
  696. bartRetrieverParams: bartRetrieverParams{
  697. bartRetrieveParams: bartRetrieveParams{
  698. {
  699. itemHash: []byte{'t', 'h', 'e', 'h', 'a', 's', 'h'},
  700. result: []byte{0x13, 0x37, 0x13, 0x37, 0x13, 0x37},
  701. },
  702. },
  703. },
  704. },
  705. },
  706. {
  707. name: "account with cleared buddy icon",
  708. requestScreenName: state.NewIdentScreenName("userA"),
  709. want: "icon not found",
  710. statusCode: http.StatusNotFound,
  711. contentType: "text/plain; charset=utf-8",
  712. mockParams: mockParams{
  713. userManagerParams: userManagerParams{
  714. getUserParams: getUserParams{
  715. {
  716. screenName: state.NewIdentScreenName("userA"),
  717. result: &state.User{
  718. DisplayScreenName: "userA",
  719. IdentScreenName: state.NewIdentScreenName("userA"),
  720. },
  721. },
  722. },
  723. },
  724. feedBagRetrieverParams: feedBagRetrieverParams{
  725. buddyIconRefByNameParams: buddyIconRefByNameParams{
  726. {
  727. screenName: state.NewIdentScreenName("userA"),
  728. result: &wire.BARTID{
  729. Type: wire.BARTTypesBuddyIcon,
  730. BARTInfo: wire.BARTInfo{
  731. Flags: 0x00,
  732. Hash: wire.GetClearIconHash(),
  733. },
  734. },
  735. },
  736. },
  737. },
  738. },
  739. },
  740. {
  741. name: "account with no buddy icon",
  742. requestScreenName: state.NewIdentScreenName("userA"),
  743. want: "icon not found",
  744. statusCode: http.StatusNotFound,
  745. contentType: "text/plain; charset=utf-8",
  746. mockParams: mockParams{
  747. userManagerParams: userManagerParams{
  748. getUserParams: getUserParams{
  749. {
  750. screenName: state.NewIdentScreenName("userA"),
  751. result: &state.User{
  752. DisplayScreenName: "userA",
  753. IdentScreenName: state.NewIdentScreenName("userA"),
  754. },
  755. },
  756. },
  757. },
  758. feedBagRetrieverParams: feedBagRetrieverParams{
  759. buddyIconRefByNameParams: buddyIconRefByNameParams{
  760. {
  761. screenName: state.NewIdentScreenName("userA"),
  762. result: nil,
  763. },
  764. },
  765. },
  766. },
  767. },
  768. }
  769. for _, tc := range tt {
  770. t.Run(tc.name, func(t *testing.T) {
  771. request := httptest.NewRequest(http.MethodGet, "/user/"+tc.requestScreenName.String()+"/icon", nil)
  772. request.SetPathValue("screenname", tc.requestScreenName.String())
  773. responseRecorder := httptest.NewRecorder()
  774. userManager := newMockUserManager(t)
  775. for _, params := range tc.mockParams.userManagerParams.getUserParams {
  776. userManager.EXPECT().
  777. User(params.screenName).
  778. Return(params.result, params.err)
  779. }
  780. feedbagRetriever := newMockFeedBagRetriever(t)
  781. for _, params := range tc.mockParams.feedBagRetrieverParams.buddyIconRefByNameParams {
  782. feedbagRetriever.EXPECT().
  783. BuddyIconRefByName(params.screenName).
  784. Return(params.result, params.err)
  785. }
  786. bartRetriever := newMockBARTRetriever(t)
  787. for _, params := range tc.mockParams.bartRetrieverParams.bartRetrieveParams {
  788. bartRetriever.EXPECT().
  789. BARTRetrieve(params.itemHash).
  790. Return(params.result, params.err)
  791. }
  792. getUserBuddyIconHandler(responseRecorder, request, userManager, feedbagRetriever, bartRetriever, slog.Default())
  793. if responseRecorder.Code != tc.statusCode {
  794. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  795. }
  796. contentType := responseRecorder.Header().Get("Content-Type")
  797. if contentType != tc.contentType {
  798. t.Errorf("Want content type '%s', got '%s'", tc.contentType, contentType)
  799. }
  800. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  801. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  802. }
  803. })
  804. }
  805. }
  806. func TestUserHandler_GET(t *testing.T) {
  807. tt := []struct {
  808. name string
  809. want string
  810. statusCode int
  811. mockParams mockParams
  812. }{
  813. {
  814. name: "empty user store",
  815. want: `[]`,
  816. statusCode: http.StatusOK,
  817. mockParams: mockParams{
  818. userManagerParams: userManagerParams{
  819. allUsersParams: allUsersParams{
  820. {
  821. result: []state.User{},
  822. },
  823. },
  824. },
  825. },
  826. },
  827. {
  828. name: "user store containing 3 users",
  829. want: `[{"id":"usera","screen_name":"userA","is_icq":false,"suspended_status":""},{"id":"userb","screen_name":"userB","is_icq":false,"suspended_status":""},{"id":"100003","screen_name":"100003","is_icq":true,"suspended_status":""}]`,
  830. statusCode: http.StatusOK,
  831. mockParams: mockParams{
  832. userManagerParams: userManagerParams{
  833. allUsersParams: allUsersParams{
  834. {
  835. result: []state.User{
  836. {
  837. DisplayScreenName: "userA",
  838. IdentScreenName: state.NewIdentScreenName("userA"),
  839. },
  840. {
  841. DisplayScreenName: "userB",
  842. IdentScreenName: state.NewIdentScreenName("userB"),
  843. },
  844. {
  845. DisplayScreenName: "100003",
  846. IdentScreenName: state.NewIdentScreenName("100003"),
  847. IsICQ: true,
  848. },
  849. },
  850. },
  851. },
  852. },
  853. },
  854. },
  855. {
  856. name: "user handler error",
  857. want: `internal server error`,
  858. statusCode: http.StatusInternalServerError,
  859. mockParams: mockParams{
  860. userManagerParams: userManagerParams{
  861. allUsersParams: allUsersParams{
  862. {
  863. result: []state.User{},
  864. err: io.EOF,
  865. },
  866. },
  867. },
  868. },
  869. },
  870. }
  871. for _, tc := range tt {
  872. t.Run(tc.name, func(t *testing.T) {
  873. responseRecorder := httptest.NewRecorder()
  874. userManager := newMockUserManager(t)
  875. for _, params := range tc.mockParams.userManagerParams.allUsersParams {
  876. userManager.EXPECT().
  877. AllUsers().
  878. Return(params.result, params.err)
  879. }
  880. getUserHandler(responseRecorder, userManager, slog.Default())
  881. if responseRecorder.Code != tc.statusCode {
  882. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  883. }
  884. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  885. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  886. }
  887. })
  888. }
  889. }
  890. func TestUserHandler_POST(t *testing.T) {
  891. tt := []struct {
  892. name string
  893. body string
  894. UUID uuid.UUID
  895. want string
  896. password string
  897. statusCode int
  898. mockParams mockParams
  899. }{
  900. {
  901. name: "with valid AIM user",
  902. body: `{"screen_name":"userA", "password":"thepassword"}`,
  903. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  904. want: `User account created successfully.`,
  905. password: "thepassword",
  906. statusCode: http.StatusCreated,
  907. mockParams: mockParams{
  908. userManagerParams: userManagerParams{
  909. insertUserParams: insertUserParams{
  910. {
  911. u: state.User{
  912. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  913. DisplayScreenName: "userA",
  914. IdentScreenName: state.NewIdentScreenName("userA"),
  915. },
  916. err: nil,
  917. },
  918. },
  919. },
  920. },
  921. },
  922. {
  923. name: "with valid ICQ user",
  924. body: `{"screen_name":"100003", "password":"thepass"}`,
  925. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  926. want: `User account created successfully.`,
  927. password: "thepass",
  928. statusCode: http.StatusCreated,
  929. mockParams: mockParams{
  930. userManagerParams: userManagerParams{
  931. insertUserParams: insertUserParams{
  932. {
  933. u: state.User{
  934. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  935. DisplayScreenName: "100003",
  936. IdentScreenName: state.NewIdentScreenName("100003"),
  937. IsICQ: true,
  938. },
  939. err: nil,
  940. },
  941. },
  942. },
  943. },
  944. },
  945. {
  946. name: "with malformed body",
  947. body: `{"screen_name":"userA", "password":"thepassword"`, // missing closing }
  948. want: `malformed input`,
  949. statusCode: http.StatusBadRequest,
  950. },
  951. {
  952. name: "user handler error",
  953. body: `{"screen_name":"userA", "password":"thepassword"}`,
  954. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  955. want: `internal server error`,
  956. password: "thepassword",
  957. statusCode: http.StatusInternalServerError,
  958. mockParams: mockParams{
  959. userManagerParams: userManagerParams{
  960. insertUserParams: insertUserParams{
  961. {
  962. u: state.User{
  963. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  964. DisplayScreenName: "userA",
  965. IdentScreenName: state.NewIdentScreenName("userA"),
  966. },
  967. err: io.EOF,
  968. },
  969. },
  970. },
  971. },
  972. },
  973. {
  974. name: "duplicate user",
  975. body: `{"screen_name":"userA", "password":"thepassword"}`,
  976. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  977. want: `user already exists`,
  978. password: "thepassword",
  979. statusCode: http.StatusConflict,
  980. mockParams: mockParams{
  981. userManagerParams: userManagerParams{
  982. insertUserParams: insertUserParams{
  983. {
  984. u: state.User{
  985. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  986. DisplayScreenName: "userA",
  987. IdentScreenName: state.NewIdentScreenName("userA"),
  988. },
  989. err: state.ErrDupUser,
  990. },
  991. },
  992. },
  993. },
  994. },
  995. {
  996. name: "invalid AIM screen name",
  997. body: `{"screen_name":"a", "password":"thepassword"}`,
  998. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  999. want: `invalid screen name: screen name must be between 3 and 16 characters`,
  1000. statusCode: http.StatusBadRequest,
  1001. },
  1002. {
  1003. name: "invalid AIM password",
  1004. body: `{"screen_name":"userA", "password":"1"}`,
  1005. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  1006. want: `invalid password: invalid password length: password length must be between 4-16 characters`,
  1007. statusCode: http.StatusBadRequest,
  1008. },
  1009. {
  1010. name: "invalid ICQ UIN",
  1011. body: `{"screen_name":"1000", "password":"thepass"}`,
  1012. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  1013. want: `invalid uin: uin must be a number in the range 10000-2147483646`,
  1014. statusCode: http.StatusBadRequest,
  1015. },
  1016. {
  1017. name: "invalid ICQ password",
  1018. body: `{"screen_name":"100003", "password":"thelongpassword"}`,
  1019. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  1020. want: `invalid password: invalid password length: password must be between 6-8 characters`,
  1021. statusCode: http.StatusBadRequest,
  1022. },
  1023. }
  1024. for _, tc := range tt {
  1025. t.Run(tc.name, func(t *testing.T) {
  1026. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  1027. responseRecorder := httptest.NewRecorder()
  1028. userManager := newMockUserManager(t)
  1029. for _, params := range tc.mockParams.userManagerParams.insertUserParams {
  1030. assert.NoError(t, params.u.HashPassword(tc.password))
  1031. userManager.EXPECT().
  1032. InsertUser(params.u).
  1033. Return(params.err)
  1034. }
  1035. newUUID := func() uuid.UUID { return tc.UUID }
  1036. postUserHandler(responseRecorder, request, userManager, newUUID, slog.Default())
  1037. if responseRecorder.Code != tc.statusCode {
  1038. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1039. }
  1040. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1041. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  1042. }
  1043. })
  1044. }
  1045. }
  1046. func TestUserHandler_DELETE(t *testing.T) {
  1047. tt := []struct {
  1048. name string
  1049. body string
  1050. want string
  1051. statusCode int
  1052. mockParams mockParams
  1053. }{
  1054. {
  1055. name: "with valid user",
  1056. body: `{"screen_name":"userA"}`,
  1057. want: `User account successfully deleted.`,
  1058. statusCode: http.StatusNoContent,
  1059. mockParams: mockParams{
  1060. userManagerParams: userManagerParams{
  1061. deleteUserParams: deleteUserParams{
  1062. {
  1063. screenName: state.NewIdentScreenName("userA"),
  1064. },
  1065. },
  1066. },
  1067. },
  1068. },
  1069. {
  1070. name: "with non-existent user",
  1071. body: `{"screen_name":"userA"}`,
  1072. want: `user does not exist`,
  1073. statusCode: http.StatusNotFound,
  1074. mockParams: mockParams{
  1075. userManagerParams: userManagerParams{
  1076. deleteUserParams: deleteUserParams{
  1077. {
  1078. screenName: state.NewIdentScreenName("userA"),
  1079. err: state.ErrNoUser,
  1080. },
  1081. },
  1082. },
  1083. },
  1084. },
  1085. {
  1086. name: "with malformed body",
  1087. body: `{"screen_name":"userA"`, // missing closing }
  1088. want: `malformed input`,
  1089. statusCode: http.StatusBadRequest,
  1090. },
  1091. {
  1092. name: "user handler error",
  1093. body: `{"screen_name":"userA"}`,
  1094. want: `internal server error`,
  1095. statusCode: http.StatusInternalServerError,
  1096. mockParams: mockParams{
  1097. userManagerParams: userManagerParams{
  1098. deleteUserParams: deleteUserParams{
  1099. {
  1100. screenName: state.NewIdentScreenName("userA"),
  1101. err: io.EOF,
  1102. },
  1103. },
  1104. },
  1105. },
  1106. },
  1107. }
  1108. for _, tc := range tt {
  1109. t.Run(tc.name, func(t *testing.T) {
  1110. request := httptest.NewRequest(http.MethodDelete, "/user", strings.NewReader(tc.body))
  1111. responseRecorder := httptest.NewRecorder()
  1112. userManager := newMockUserManager(t)
  1113. for _, params := range tc.mockParams.userManagerParams.deleteUserParams {
  1114. userManager.EXPECT().
  1115. DeleteUser(params.screenName).
  1116. Return(params.err)
  1117. }
  1118. deleteUserHandler(responseRecorder, request, userManager, slog.Default())
  1119. if responseRecorder.Code != tc.statusCode {
  1120. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1121. }
  1122. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1123. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  1124. }
  1125. })
  1126. }
  1127. }
  1128. func TestUserPasswordHandler_PUT(t *testing.T) {
  1129. tt := []struct {
  1130. name string
  1131. body string
  1132. want string
  1133. statusCode int
  1134. mockParams mockParams
  1135. }{
  1136. {
  1137. name: "user with valid password",
  1138. body: `{"screen_name":"userA", "password":"thenewpassword"}`,
  1139. want: `Password successfully reset.`,
  1140. statusCode: http.StatusNoContent,
  1141. mockParams: mockParams{
  1142. userManagerParams: userManagerParams{
  1143. setUserPasswordParams: setUserPasswordParams{
  1144. {
  1145. screenName: state.NewIdentScreenName("userA"),
  1146. newPassword: "thenewpassword",
  1147. },
  1148. },
  1149. },
  1150. },
  1151. },
  1152. {
  1153. name: "user with invalid password",
  1154. body: `{"screen_name":"userA", "password":"a"}`,
  1155. want: `invalid password length`,
  1156. statusCode: http.StatusBadRequest,
  1157. mockParams: mockParams{
  1158. userManagerParams: userManagerParams{
  1159. setUserPasswordParams: setUserPasswordParams{
  1160. {
  1161. screenName: state.NewIdentScreenName("userA"),
  1162. newPassword: "a",
  1163. err: state.ErrPasswordInvalid,
  1164. },
  1165. },
  1166. },
  1167. },
  1168. },
  1169. {
  1170. name: "with malformed body",
  1171. body: `{"screen_name":"userA", "password":"thepassword"`, // missing closing }
  1172. want: `malformed input`,
  1173. statusCode: http.StatusBadRequest,
  1174. },
  1175. {
  1176. name: "password updater returns runtime error",
  1177. body: `{"screen_name":"userA", "password":"thepassword"}`,
  1178. want: `internal server error`,
  1179. statusCode: http.StatusInternalServerError,
  1180. mockParams: mockParams{
  1181. userManagerParams: userManagerParams{
  1182. setUserPasswordParams: setUserPasswordParams{
  1183. {
  1184. screenName: state.NewIdentScreenName("userA"),
  1185. newPassword: "thepassword",
  1186. err: io.EOF,
  1187. },
  1188. },
  1189. },
  1190. },
  1191. },
  1192. {
  1193. name: "user doesn't exist",
  1194. body: `{"screen_name":"userA", "password":"thepassword"}`,
  1195. want: `user does not exist`,
  1196. statusCode: http.StatusNotFound,
  1197. mockParams: mockParams{
  1198. userManagerParams: userManagerParams{
  1199. setUserPasswordParams: setUserPasswordParams{
  1200. {
  1201. screenName: state.NewIdentScreenName("userA"),
  1202. newPassword: "thepassword",
  1203. err: state.ErrNoUser,
  1204. },
  1205. },
  1206. },
  1207. },
  1208. },
  1209. }
  1210. for _, tc := range tt {
  1211. t.Run(tc.name, func(t *testing.T) {
  1212. request := httptest.NewRequest(http.MethodPut, "/user", strings.NewReader(tc.body))
  1213. responseRecorder := httptest.NewRecorder()
  1214. userManager := newMockUserManager(t)
  1215. for _, params := range tc.mockParams.userManagerParams.setUserPasswordParams {
  1216. userManager.EXPECT().
  1217. SetUserPassword(params.screenName, params.newPassword).
  1218. Return(params.err)
  1219. }
  1220. putUserPasswordHandler(responseRecorder, request, userManager, slog.Default())
  1221. if responseRecorder.Code != tc.statusCode {
  1222. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1223. }
  1224. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1225. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  1226. }
  1227. })
  1228. }
  1229. }
  1230. func TestPublicChatHandler_GET(t *testing.T) {
  1231. fnNewSess := func(screenName string) *state.Session {
  1232. sess := state.NewSession()
  1233. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  1234. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  1235. return sess
  1236. }
  1237. chatRoom1 := state.NewChatRoom("chat-room-1-name", state.NewIdentScreenName("chat-room-1-creator"), state.PublicExchange)
  1238. chatRoom2 := state.NewChatRoom("chat-room-2-name", state.NewIdentScreenName("chat-room-1-creator"), state.PublicExchange)
  1239. tt := []struct {
  1240. name string
  1241. want string
  1242. statusCode int
  1243. mockParams mockParams
  1244. }{
  1245. {
  1246. name: "multiple chat rooms with participants",
  1247. want: `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?roomname=chat-room-1-name&exchange=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&exchange=5","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
  1248. statusCode: http.StatusOK,
  1249. mockParams: mockParams{
  1250. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1251. allChatRoomsParams: allChatRoomsParams{
  1252. {
  1253. exchange: state.PublicExchange,
  1254. result: []state.ChatRoom{
  1255. chatRoom1,
  1256. chatRoom2,
  1257. },
  1258. },
  1259. },
  1260. },
  1261. chatSessionRetrieverParams: chatSessionRetrieverParams{
  1262. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  1263. {
  1264. cookie: chatRoom1.Cookie(),
  1265. result: []*state.Session{
  1266. fnNewSess("userA"),
  1267. fnNewSess("userB"),
  1268. },
  1269. },
  1270. {
  1271. cookie: chatRoom2.Cookie(),
  1272. result: []*state.Session{
  1273. fnNewSess("userC"),
  1274. fnNewSess("userD"),
  1275. },
  1276. },
  1277. },
  1278. },
  1279. },
  1280. },
  1281. {
  1282. name: "chat room without participants",
  1283. want: `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?roomname=chat-room-1-name&exchange=5","participants":[]}]`,
  1284. statusCode: http.StatusOK,
  1285. mockParams: mockParams{
  1286. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1287. allChatRoomsParams: allChatRoomsParams{
  1288. {
  1289. exchange: state.PublicExchange,
  1290. result: []state.ChatRoom{
  1291. chatRoom1,
  1292. },
  1293. },
  1294. },
  1295. },
  1296. chatSessionRetrieverParams: chatSessionRetrieverParams{
  1297. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  1298. {
  1299. cookie: chatRoom1.Cookie(),
  1300. result: []*state.Session{},
  1301. },
  1302. },
  1303. },
  1304. },
  1305. },
  1306. {
  1307. name: "no chat rooms",
  1308. want: `[]`,
  1309. statusCode: http.StatusOK,
  1310. mockParams: mockParams{
  1311. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1312. allChatRoomsParams: allChatRoomsParams{
  1313. {
  1314. exchange: state.PublicExchange,
  1315. result: []state.ChatRoom{},
  1316. },
  1317. },
  1318. },
  1319. },
  1320. },
  1321. }
  1322. for _, tc := range tt {
  1323. t.Run(tc.name, func(t *testing.T) {
  1324. request := httptest.NewRequest(http.MethodGet, "/chat/room/public", nil)
  1325. responseRecorder := httptest.NewRecorder()
  1326. chatRoomRetriever := newMockChatRoomRetriever(t)
  1327. for _, params := range tc.mockParams.chatRoomRetrieverParams.allChatRoomsParams {
  1328. chatRoomRetriever.EXPECT().
  1329. AllChatRooms(params.exchange).
  1330. Return(params.result, params.err)
  1331. }
  1332. chatSessionRetriever := newMockChatSessionRetriever(t)
  1333. for _, params := range tc.mockParams.chatSessionRetrieverParams.chatSessionRetrieverAllSessionsParams {
  1334. chatSessionRetriever.EXPECT().
  1335. AllSessions(params.cookie).
  1336. Return(params.result)
  1337. }
  1338. getPublicChatHandler(responseRecorder, request, chatRoomRetriever, chatSessionRetriever, slog.Default())
  1339. if responseRecorder.Code != tc.statusCode {
  1340. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1341. }
  1342. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1343. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1344. }
  1345. })
  1346. }
  1347. }
  1348. func TestPrivateChatHandler_GET(t *testing.T) {
  1349. fnNewSess := func(screenName string) *state.Session {
  1350. sess := state.NewSession()
  1351. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  1352. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  1353. return sess
  1354. }
  1355. chatRoom1 := state.NewChatRoom("chat-room-1-name", state.NewIdentScreenName("chat-room-1-creator"), state.PrivateExchange)
  1356. chatRoom2 := state.NewChatRoom("chat-room-2-name", state.NewIdentScreenName("chat-room-2-creator"), state.PrivateExchange)
  1357. tt := []struct {
  1358. name string
  1359. want string
  1360. statusCode int
  1361. mockParams mockParams
  1362. }{
  1363. {
  1364. name: "multiple chat rooms with participants",
  1365. 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&exchange=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&exchange=4","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
  1366. statusCode: http.StatusOK,
  1367. mockParams: mockParams{
  1368. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1369. allChatRoomsParams: allChatRoomsParams{
  1370. {
  1371. exchange: state.PrivateExchange,
  1372. result: []state.ChatRoom{
  1373. chatRoom1,
  1374. chatRoom2,
  1375. },
  1376. },
  1377. },
  1378. },
  1379. chatSessionRetrieverParams: chatSessionRetrieverParams{
  1380. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  1381. {
  1382. cookie: chatRoom1.Cookie(),
  1383. result: []*state.Session{
  1384. fnNewSess("userA"),
  1385. fnNewSess("userB"),
  1386. },
  1387. },
  1388. {
  1389. cookie: chatRoom2.Cookie(),
  1390. result: []*state.Session{
  1391. fnNewSess("userC"),
  1392. fnNewSess("userD"),
  1393. },
  1394. },
  1395. },
  1396. },
  1397. },
  1398. },
  1399. {
  1400. name: "chat room without participants",
  1401. 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&exchange=4","participants":[]}]`,
  1402. statusCode: http.StatusOK,
  1403. mockParams: mockParams{
  1404. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1405. allChatRoomsParams: allChatRoomsParams{
  1406. {
  1407. exchange: state.PrivateExchange,
  1408. result: []state.ChatRoom{
  1409. chatRoom1,
  1410. },
  1411. },
  1412. },
  1413. },
  1414. chatSessionRetrieverParams: chatSessionRetrieverParams{
  1415. chatSessionRetrieverAllSessionsParams: chatSessionRetrieverAllSessionsParams{
  1416. {
  1417. cookie: chatRoom1.Cookie(),
  1418. result: []*state.Session{},
  1419. },
  1420. },
  1421. },
  1422. },
  1423. },
  1424. {
  1425. name: "no chat rooms",
  1426. want: `[]`,
  1427. statusCode: http.StatusOK,
  1428. mockParams: mockParams{
  1429. chatRoomRetrieverParams: chatRoomRetrieverParams{
  1430. allChatRoomsParams: allChatRoomsParams{
  1431. {
  1432. exchange: state.PrivateExchange,
  1433. result: []state.ChatRoom{},
  1434. },
  1435. },
  1436. },
  1437. },
  1438. },
  1439. }
  1440. for _, tc := range tt {
  1441. t.Run(tc.name, func(t *testing.T) {
  1442. request := httptest.NewRequest(http.MethodGet, "/chat/room/private", nil)
  1443. responseRecorder := httptest.NewRecorder()
  1444. chatRoomRetriever := newMockChatRoomRetriever(t)
  1445. for _, params := range tc.mockParams.chatRoomRetrieverParams.allChatRoomsParams {
  1446. chatRoomRetriever.EXPECT().
  1447. AllChatRooms(params.exchange).
  1448. Return(params.result, params.err)
  1449. }
  1450. chatSessionRetriever := newMockChatSessionRetriever(t)
  1451. for _, params := range tc.mockParams.chatSessionRetrieverParams.chatSessionRetrieverAllSessionsParams {
  1452. chatSessionRetriever.EXPECT().
  1453. AllSessions(params.cookie).
  1454. Return(params.result)
  1455. }
  1456. getPrivateChatHandler(responseRecorder, request, chatRoomRetriever, chatSessionRetriever, slog.Default())
  1457. if responseRecorder.Code != tc.statusCode {
  1458. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1459. }
  1460. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1461. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1462. }
  1463. })
  1464. }
  1465. }
  1466. func TestInstantMessageHandler_POST(t *testing.T) {
  1467. type relayToScreenNameInputs struct {
  1468. sender state.IdentScreenName
  1469. recipient state.IdentScreenName
  1470. msg string
  1471. }
  1472. tt := []struct {
  1473. name string
  1474. relayToScreenNameInputs []relayToScreenNameInputs
  1475. body string
  1476. want string
  1477. statusCode int
  1478. }{
  1479. {
  1480. name: "send an instant message",
  1481. relayToScreenNameInputs: []relayToScreenNameInputs{
  1482. {
  1483. sender: state.NewIdentScreenName("sender_sn"),
  1484. recipient: state.NewIdentScreenName("recip_sn"),
  1485. msg: "hello world!",
  1486. },
  1487. },
  1488. body: `{"from":"sender_sn","to":"recip_sn","text":"hello world!"}`,
  1489. want: `Message sent successfully.`,
  1490. statusCode: http.StatusOK,
  1491. },
  1492. {
  1493. name: "with malformed body",
  1494. body: `{"screen_name":"userA", "password":"thepassword"`,
  1495. want: `malformed input`,
  1496. statusCode: http.StatusBadRequest,
  1497. },
  1498. }
  1499. for _, tc := range tt {
  1500. t.Run(tc.name, func(t *testing.T) {
  1501. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  1502. responseRecorder := httptest.NewRecorder()
  1503. messageRelayer := newMockMessageRelayer(t)
  1504. for _, params := range tc.relayToScreenNameInputs {
  1505. validateSNAC := func(msg wire.SNACMessage) bool {
  1506. body := msg.Body.(wire.SNAC_0x04_0x07_ICBMChannelMsgToClient)
  1507. assert.Equal(t, params.sender.String(), body.TLVUserInfo.ScreenName)
  1508. b, ok := body.Bytes(wire.ICBMTLVAOLIMData)
  1509. assert.True(t, ok)
  1510. txt, err := wire.UnmarshalICBMMessageText(b)
  1511. assert.NoError(t, err)
  1512. assert.Equal(t, params.msg, txt)
  1513. return true
  1514. }
  1515. messageRelayer.EXPECT().
  1516. RelayToScreenName(mock.Anything, params.recipient, mock.MatchedBy(validateSNAC))
  1517. }
  1518. postInstantMessageHandler(responseRecorder, request, messageRelayer, slog.Default())
  1519. if responseRecorder.Code != tc.statusCode {
  1520. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1521. }
  1522. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1523. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  1524. }
  1525. })
  1526. }
  1527. }
  1528. func TestVersionHandler_GET(t *testing.T) {
  1529. tt := []struct {
  1530. name string
  1531. want string
  1532. statusCode int
  1533. buildInfo config.Build
  1534. }{
  1535. {
  1536. name: "get ras version",
  1537. want: `{"version":"13.3.7","commit":"asdfASDF12345678","date":"2024-03-01"}`,
  1538. statusCode: http.StatusOK,
  1539. buildInfo: config.Build{
  1540. Version: "13.3.7",
  1541. Commit: "asdfASDF12345678",
  1542. Date: "2024-03-01",
  1543. },
  1544. },
  1545. }
  1546. for _, tc := range tt {
  1547. t.Run(tc.name, func(t *testing.T) {
  1548. responseRecorder := httptest.NewRecorder()
  1549. getVersionHandler(responseRecorder, tc.buildInfo)
  1550. if responseRecorder.Code != tc.statusCode {
  1551. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1552. }
  1553. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1554. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1555. }
  1556. })
  1557. }
  1558. }
  1559. func TestDirectoryCategoryHandler_GET(t *testing.T) {
  1560. tt := []struct {
  1561. name string
  1562. want string
  1563. statusCode int
  1564. mockParams mockParams
  1565. }{
  1566. {
  1567. name: "no categories",
  1568. want: `[]`,
  1569. statusCode: http.StatusOK,
  1570. mockParams: mockParams{
  1571. directoryManagerParams: directoryManagerParams{
  1572. categoriesParams: categoriesParams{
  1573. {
  1574. result: nil,
  1575. },
  1576. },
  1577. },
  1578. },
  1579. },
  1580. {
  1581. name: "error fetching categories",
  1582. want: `{"message":"internal server error"}`,
  1583. statusCode: http.StatusInternalServerError,
  1584. mockParams: mockParams{
  1585. directoryManagerParams: directoryManagerParams{
  1586. categoriesParams: categoriesParams{
  1587. {
  1588. result: nil,
  1589. err: errors.New("error fetching categories"),
  1590. },
  1591. },
  1592. },
  1593. },
  1594. },
  1595. {
  1596. name: "fetch some categories",
  1597. want: `[{"id":1,"name":"category-1"},{"id":2,"name":"category-2"}]`,
  1598. statusCode: http.StatusOK,
  1599. mockParams: mockParams{
  1600. directoryManagerParams: directoryManagerParams{
  1601. categoriesParams: categoriesParams{
  1602. {
  1603. result: []state.Category{
  1604. {
  1605. ID: 1,
  1606. Name: "category-1",
  1607. },
  1608. {
  1609. ID: 2,
  1610. Name: "category-2",
  1611. },
  1612. },
  1613. },
  1614. },
  1615. },
  1616. },
  1617. },
  1618. }
  1619. for _, tc := range tt {
  1620. t.Run(tc.name, func(t *testing.T) {
  1621. responseRecorder := httptest.NewRecorder()
  1622. directoryManager := newMockDirectoryManager(t)
  1623. for _, params := range tc.mockParams.categoriesParams {
  1624. directoryManager.EXPECT().
  1625. Categories().
  1626. Return(params.result, params.err)
  1627. }
  1628. getDirectoryCategoryHandler(responseRecorder, directoryManager, slog.Default())
  1629. if responseRecorder.Code != tc.statusCode {
  1630. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1631. }
  1632. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1633. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1634. }
  1635. })
  1636. }
  1637. }
  1638. func TestDirectoryCategoryKeywordHandler_GET(t *testing.T) {
  1639. tt := []struct {
  1640. name string
  1641. categoryID int
  1642. want string
  1643. statusCode int
  1644. mockParams mockParams
  1645. }{
  1646. {
  1647. name: "category not found",
  1648. categoryID: 1,
  1649. want: `{"message":"category not found"}`,
  1650. statusCode: http.StatusNotFound,
  1651. mockParams: mockParams{
  1652. directoryManagerParams: directoryManagerParams{
  1653. keywordsByCategoryParams: keywordsByCategoryParams{
  1654. {
  1655. categoryID: 1,
  1656. result: nil,
  1657. err: state.ErrKeywordCategoryNotFound,
  1658. },
  1659. },
  1660. },
  1661. },
  1662. },
  1663. {
  1664. name: "error fetching keywords by category",
  1665. categoryID: 1,
  1666. want: `{"message":"internal server error"}`,
  1667. statusCode: http.StatusInternalServerError,
  1668. mockParams: mockParams{
  1669. directoryManagerParams: directoryManagerParams{
  1670. keywordsByCategoryParams: keywordsByCategoryParams{
  1671. {
  1672. categoryID: 1,
  1673. result: nil,
  1674. err: errors.New("error fetching keywords by category"),
  1675. },
  1676. },
  1677. },
  1678. },
  1679. },
  1680. {
  1681. name: "invalid category ID",
  1682. categoryID: -1,
  1683. want: `{"message":"invalid category ID"}`,
  1684. statusCode: http.StatusBadRequest,
  1685. mockParams: mockParams{
  1686. directoryManagerParams: directoryManagerParams{
  1687. keywordsByCategoryParams: keywordsByCategoryParams{},
  1688. },
  1689. },
  1690. },
  1691. {
  1692. name: "no keywords",
  1693. categoryID: 1,
  1694. want: `[]`,
  1695. statusCode: http.StatusOK,
  1696. mockParams: mockParams{
  1697. directoryManagerParams: directoryManagerParams{
  1698. keywordsByCategoryParams: keywordsByCategoryParams{
  1699. {
  1700. categoryID: 1,
  1701. result: nil,
  1702. },
  1703. },
  1704. },
  1705. },
  1706. },
  1707. {
  1708. name: "fetch some keywords by category",
  1709. categoryID: 1,
  1710. want: `[{"id":1,"name":"keyword-1"},{"id":2,"name":"keyword-2"}]`,
  1711. statusCode: http.StatusOK,
  1712. mockParams: mockParams{
  1713. directoryManagerParams: directoryManagerParams{
  1714. keywordsByCategoryParams: keywordsByCategoryParams{
  1715. {
  1716. categoryID: 1,
  1717. result: []state.Keyword{
  1718. {
  1719. ID: 1,
  1720. Name: "keyword-1",
  1721. },
  1722. {
  1723. ID: 2,
  1724. Name: "keyword-2",
  1725. },
  1726. },
  1727. },
  1728. },
  1729. },
  1730. },
  1731. },
  1732. }
  1733. for _, tc := range tt {
  1734. t.Run(tc.name, func(t *testing.T) {
  1735. request := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/directory/category/%d/keyword", tc.categoryID), nil)
  1736. request.SetPathValue("id", fmt.Sprintf("%d", tc.categoryID))
  1737. responseRecorder := httptest.NewRecorder()
  1738. directoryManager := newMockDirectoryManager(t)
  1739. for _, params := range tc.mockParams.keywordsByCategoryParams {
  1740. directoryManager.EXPECT().
  1741. KeywordsByCategory(params.categoryID).
  1742. Return(params.result, params.err)
  1743. }
  1744. getDirectoryCategoryKeywordHandler(responseRecorder, request, directoryManager, slog.Default())
  1745. if responseRecorder.Code != tc.statusCode {
  1746. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1747. }
  1748. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1749. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1750. }
  1751. })
  1752. }
  1753. }
  1754. func TestDirectoryCategoryHandler_DELETE(t *testing.T) {
  1755. tt := []struct {
  1756. name string
  1757. categoryID int
  1758. want string
  1759. statusCode int
  1760. mockParams mockParams
  1761. }{
  1762. {
  1763. name: "category not found",
  1764. categoryID: 1,
  1765. want: `{"message":"category not found"}`,
  1766. statusCode: http.StatusNotFound,
  1767. mockParams: mockParams{
  1768. directoryManagerParams: directoryManagerParams{
  1769. deleteCategoryParams: deleteCategoryParams{
  1770. {
  1771. categoryID: 1,
  1772. err: state.ErrKeywordCategoryNotFound,
  1773. },
  1774. },
  1775. },
  1776. },
  1777. },
  1778. {
  1779. name: "keyword in use by user",
  1780. categoryID: 1,
  1781. want: `{"message":"can't delete because category in use by a user"}`,
  1782. statusCode: http.StatusConflict,
  1783. mockParams: mockParams{
  1784. directoryManagerParams: directoryManagerParams{
  1785. deleteCategoryParams: deleteCategoryParams{
  1786. {
  1787. categoryID: 1,
  1788. err: state.ErrKeywordInUse,
  1789. },
  1790. },
  1791. },
  1792. },
  1793. },
  1794. {
  1795. name: "runtime error",
  1796. categoryID: 1,
  1797. want: `{"message":"internal server error"}`,
  1798. statusCode: http.StatusInternalServerError,
  1799. mockParams: mockParams{
  1800. directoryManagerParams: directoryManagerParams{
  1801. deleteCategoryParams: deleteCategoryParams{
  1802. {
  1803. categoryID: 1,
  1804. err: errors.New("error deleting keyword"),
  1805. },
  1806. },
  1807. },
  1808. },
  1809. },
  1810. {
  1811. name: "successful deletion",
  1812. categoryID: 1,
  1813. want: ``,
  1814. statusCode: http.StatusNoContent,
  1815. mockParams: mockParams{
  1816. directoryManagerParams: directoryManagerParams{
  1817. deleteCategoryParams: deleteCategoryParams{
  1818. {
  1819. categoryID: 1,
  1820. },
  1821. },
  1822. },
  1823. },
  1824. },
  1825. {
  1826. name: "invalid category ID",
  1827. categoryID: -1,
  1828. want: `invalid category ID`,
  1829. statusCode: http.StatusBadRequest,
  1830. mockParams: mockParams{
  1831. directoryManagerParams: directoryManagerParams{
  1832. deleteCategoryParams: deleteCategoryParams{},
  1833. },
  1834. },
  1835. },
  1836. }
  1837. for _, tc := range tt {
  1838. t.Run(tc.name, func(t *testing.T) {
  1839. request := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/directory/category/%d/keyword", tc.categoryID), nil)
  1840. request.SetPathValue("id", fmt.Sprintf("%d", tc.categoryID))
  1841. responseRecorder := httptest.NewRecorder()
  1842. directoryManager := newMockDirectoryManager(t)
  1843. for _, params := range tc.mockParams.deleteCategoryParams {
  1844. directoryManager.EXPECT().
  1845. DeleteCategory(params.categoryID).
  1846. Return(params.err)
  1847. }
  1848. deleteDirectoryCategoryHandler(responseRecorder, request, directoryManager, slog.Default())
  1849. if responseRecorder.Code != tc.statusCode {
  1850. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1851. }
  1852. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1853. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1854. }
  1855. })
  1856. }
  1857. }
  1858. func TestDirectoryCategoryHandler_POST(t *testing.T) {
  1859. tt := []struct {
  1860. name string
  1861. body string
  1862. want string
  1863. statusCode int
  1864. mockParams mockParams
  1865. }{
  1866. {
  1867. name: "category already exists",
  1868. body: `{"name":"the_category"}`,
  1869. want: `{"message":"category already exists"}`,
  1870. statusCode: http.StatusConflict,
  1871. mockParams: mockParams{
  1872. directoryManagerParams: directoryManagerParams{
  1873. createCategoryParams: createCategoryParams{
  1874. {
  1875. name: "the_category",
  1876. err: state.ErrKeywordCategoryExists,
  1877. },
  1878. },
  1879. },
  1880. },
  1881. },
  1882. {
  1883. name: "runtime error",
  1884. body: `{"name":"the_category"}`,
  1885. want: `{"message":"internal server error"}`,
  1886. statusCode: http.StatusInternalServerError,
  1887. mockParams: mockParams{
  1888. directoryManagerParams: directoryManagerParams{
  1889. createCategoryParams: createCategoryParams{
  1890. {
  1891. name: "the_category",
  1892. err: errors.New("error creating category"),
  1893. },
  1894. },
  1895. },
  1896. },
  1897. },
  1898. {
  1899. name: "bad input",
  1900. body: `{"name":"the_category"`,
  1901. want: `{"message":"malformed input"}`,
  1902. statusCode: http.StatusBadRequest,
  1903. mockParams: mockParams{
  1904. directoryManagerParams: directoryManagerParams{
  1905. createCategoryParams: createCategoryParams{},
  1906. },
  1907. },
  1908. },
  1909. {
  1910. name: "successful creation",
  1911. body: `{"name":"the_category"}`,
  1912. want: `{"id":1,"name":"the_category"}`,
  1913. statusCode: http.StatusCreated,
  1914. mockParams: mockParams{
  1915. directoryManagerParams: directoryManagerParams{
  1916. createCategoryParams: createCategoryParams{
  1917. {
  1918. name: "the_category",
  1919. result: state.Category{
  1920. ID: 1,
  1921. Name: "the_category",
  1922. },
  1923. },
  1924. },
  1925. },
  1926. },
  1927. },
  1928. }
  1929. for _, tc := range tt {
  1930. t.Run(tc.name, func(t *testing.T) {
  1931. request := httptest.NewRequest(http.MethodPost, "/directory/category", strings.NewReader(tc.body))
  1932. responseRecorder := httptest.NewRecorder()
  1933. directoryManager := newMockDirectoryManager(t)
  1934. for _, params := range tc.mockParams.createCategoryParams {
  1935. directoryManager.EXPECT().
  1936. CreateCategory(params.name).
  1937. Return(params.result, params.err)
  1938. }
  1939. postDirectoryCategoryHandler(responseRecorder, request, directoryManager, slog.Default())
  1940. if responseRecorder.Code != tc.statusCode {
  1941. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  1942. }
  1943. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  1944. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  1945. }
  1946. })
  1947. }
  1948. }
  1949. func TestDirectoryKeywordHandler_POST(t *testing.T) {
  1950. tt := []struct {
  1951. name string
  1952. body string
  1953. want string
  1954. statusCode int
  1955. mockParams mockParams
  1956. }{
  1957. {
  1958. name: "keyword already exists",
  1959. body: `{"category_id":1,"name":"the_keyword"}`,
  1960. want: `{"message":"keyword already exists"}`,
  1961. statusCode: http.StatusConflict,
  1962. mockParams: mockParams{
  1963. directoryManagerParams: directoryManagerParams{
  1964. createKeywordParams: createKeywordParams{
  1965. {
  1966. name: "the_keyword",
  1967. categoryID: 1,
  1968. err: state.ErrKeywordExists,
  1969. },
  1970. },
  1971. },
  1972. },
  1973. },
  1974. {
  1975. name: "category not found",
  1976. body: `{"category_id":1,"name":"the_keyword"}`,
  1977. want: `{"message":"category not found"}`,
  1978. statusCode: http.StatusNotFound,
  1979. mockParams: mockParams{
  1980. directoryManagerParams: directoryManagerParams{
  1981. createKeywordParams: createKeywordParams{
  1982. {
  1983. name: "the_keyword",
  1984. categoryID: 1,
  1985. err: state.ErrKeywordCategoryNotFound,
  1986. },
  1987. },
  1988. },
  1989. },
  1990. },
  1991. {
  1992. name: "runtime error",
  1993. body: `{"category_id":1,"name":"the_keyword"}`,
  1994. want: `{"message":"internal server error"}`,
  1995. statusCode: http.StatusInternalServerError,
  1996. mockParams: mockParams{
  1997. directoryManagerParams: directoryManagerParams{
  1998. createKeywordParams: createKeywordParams{
  1999. {
  2000. name: "the_keyword",
  2001. categoryID: 1,
  2002. err: errors.New("error creating keyword"),
  2003. },
  2004. },
  2005. },
  2006. },
  2007. },
  2008. {
  2009. name: "bad input",
  2010. body: `{"category_id":1,"name":"the_keyword"`,
  2011. want: `{"message":"malformed input"}`,
  2012. statusCode: http.StatusBadRequest,
  2013. mockParams: mockParams{
  2014. directoryManagerParams: directoryManagerParams{
  2015. createKeywordParams: createKeywordParams{},
  2016. },
  2017. },
  2018. },
  2019. {
  2020. name: "successful creation",
  2021. body: `{"category_id":1,"name":"the_keyword"}`,
  2022. want: `{"id":1,"name":"the_keyword"}`,
  2023. statusCode: http.StatusCreated,
  2024. mockParams: mockParams{
  2025. directoryManagerParams: directoryManagerParams{
  2026. createKeywordParams: createKeywordParams{
  2027. {
  2028. name: "the_keyword",
  2029. categoryID: 1,
  2030. result: state.Keyword{
  2031. ID: 1,
  2032. Name: "the_keyword",
  2033. },
  2034. },
  2035. },
  2036. },
  2037. },
  2038. },
  2039. }
  2040. for _, tc := range tt {
  2041. t.Run(tc.name, func(t *testing.T) {
  2042. request := httptest.NewRequest(http.MethodPost, "/directory/keyword", strings.NewReader(tc.body))
  2043. responseRecorder := httptest.NewRecorder()
  2044. directoryManager := newMockDirectoryManager(t)
  2045. for _, params := range tc.mockParams.createKeywordParams {
  2046. directoryManager.EXPECT().
  2047. CreateKeyword(params.name, params.categoryID).
  2048. Return(params.result, params.err)
  2049. }
  2050. postDirectoryKeywordHandler(responseRecorder, request, directoryManager, slog.Default())
  2051. if responseRecorder.Code != tc.statusCode {
  2052. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  2053. }
  2054. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  2055. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  2056. }
  2057. })
  2058. }
  2059. }
  2060. func TestDirectoryKeywordHandler_DELETE(t *testing.T) {
  2061. tt := []struct {
  2062. name string
  2063. categoryID int
  2064. want string
  2065. statusCode int
  2066. mockParams mockParams
  2067. }{
  2068. {
  2069. name: "keyword not found",
  2070. categoryID: 1,
  2071. want: `{"message":"keyword not found"}`,
  2072. statusCode: http.StatusNotFound,
  2073. mockParams: mockParams{
  2074. directoryManagerParams: directoryManagerParams{
  2075. deleteKeywordParams: deleteKeywordParams{
  2076. {
  2077. id: 1,
  2078. err: state.ErrKeywordNotFound,
  2079. },
  2080. },
  2081. },
  2082. },
  2083. },
  2084. {
  2085. name: "keyword in use by user",
  2086. categoryID: 1,
  2087. want: `{"message":"can't delete because category in use by a user"}`,
  2088. statusCode: http.StatusConflict,
  2089. mockParams: mockParams{
  2090. directoryManagerParams: directoryManagerParams{
  2091. deleteKeywordParams: deleteKeywordParams{
  2092. {
  2093. id: 1,
  2094. err: state.ErrKeywordInUse,
  2095. },
  2096. },
  2097. },
  2098. },
  2099. },
  2100. {
  2101. name: "runtime error",
  2102. categoryID: 1,
  2103. want: `{"message":"internal server error"}`,
  2104. statusCode: http.StatusInternalServerError,
  2105. mockParams: mockParams{
  2106. directoryManagerParams: directoryManagerParams{
  2107. deleteKeywordParams: deleteKeywordParams{
  2108. {
  2109. id: 1,
  2110. err: errors.New("error deleting keyword"),
  2111. },
  2112. },
  2113. },
  2114. },
  2115. },
  2116. {
  2117. name: "successful deletion",
  2118. categoryID: 1,
  2119. want: ``,
  2120. statusCode: http.StatusNoContent,
  2121. mockParams: mockParams{
  2122. directoryManagerParams: directoryManagerParams{
  2123. deleteKeywordParams: deleteKeywordParams{
  2124. {
  2125. id: 1,
  2126. },
  2127. },
  2128. },
  2129. },
  2130. },
  2131. {
  2132. name: "invalid keyword ID",
  2133. categoryID: -1,
  2134. want: `{"message":"invalid keyword ID"}`,
  2135. statusCode: http.StatusBadRequest,
  2136. mockParams: mockParams{
  2137. directoryManagerParams: directoryManagerParams{
  2138. deleteKeywordParams: deleteKeywordParams{},
  2139. },
  2140. },
  2141. },
  2142. }
  2143. for _, tc := range tt {
  2144. t.Run(tc.name, func(t *testing.T) {
  2145. request := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/directory/keyword/%d", tc.categoryID), nil)
  2146. request.SetPathValue("id", fmt.Sprintf("%d", tc.categoryID))
  2147. responseRecorder := httptest.NewRecorder()
  2148. directoryManager := newMockDirectoryManager(t)
  2149. for _, params := range tc.mockParams.deleteKeywordParams {
  2150. directoryManager.EXPECT().
  2151. DeleteKeyword(params.id).
  2152. Return(params.err)
  2153. }
  2154. deleteDirectoryKeywordHandler(responseRecorder, request, directoryManager, slog.Default())
  2155. if responseRecorder.Code != tc.statusCode {
  2156. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  2157. }
  2158. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  2159. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  2160. }
  2161. })
  2162. }
  2163. }