mgmt_api_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. package http
  2. import (
  3. "io"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/google/uuid"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/mk6i/retro-aim-server/state"
  12. )
  13. func TestSessionHandler_GET(t *testing.T) {
  14. fnNewSess := func(screenName string) *state.Session {
  15. sess := state.NewSession()
  16. sess.SetIdentScreenName(state.NewIdentScreenName(screenName))
  17. sess.SetDisplayScreenName(state.DisplayScreenName(screenName))
  18. return sess
  19. }
  20. tt := []struct {
  21. name string
  22. sessions []*state.Session
  23. userHandlerErr error
  24. want string
  25. statusCode int
  26. }{
  27. {
  28. name: "without sessions",
  29. sessions: []*state.Session{},
  30. want: `{"count":0,"sessions":[]}`,
  31. statusCode: http.StatusOK,
  32. },
  33. {
  34. name: "with sessions",
  35. sessions: []*state.Session{
  36. fnNewSess("userA"),
  37. fnNewSess("userB"),
  38. },
  39. want: `{"count":2,"sessions":[{"screen_name":"userA"},{"screen_name":"userB"}]}`,
  40. statusCode: http.StatusOK,
  41. },
  42. }
  43. for _, tc := range tt {
  44. t.Run(tc.name, func(t *testing.T) {
  45. request := httptest.NewRequest(http.MethodGet, "/session", nil)
  46. responseRecorder := httptest.NewRecorder()
  47. sessionRetriever := newMockSessionRetriever(t)
  48. sessionRetriever.EXPECT().
  49. AllSessions().
  50. Return(tc.sessions)
  51. sessionHandler(responseRecorder, request, sessionRetriever)
  52. if responseRecorder.Code != tc.statusCode {
  53. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  54. }
  55. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  56. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  57. }
  58. })
  59. }
  60. }
  61. func TestSessionHandler_DisallowedMethod(t *testing.T) {
  62. request := httptest.NewRequest(http.MethodPut, "/session", nil)
  63. responseRecorder := httptest.NewRecorder()
  64. sessionHandler(responseRecorder, request, nil)
  65. wantCode := http.StatusMethodNotAllowed
  66. if responseRecorder.Code != wantCode {
  67. t.Errorf("want status '%d', got '%d'", http.StatusMethodNotAllowed, responseRecorder.Code)
  68. }
  69. wantBody := `method not allowed`
  70. if strings.TrimSpace(responseRecorder.Body.String()) != wantBody {
  71. t.Errorf("want '%s', got '%s'", wantBody, responseRecorder.Body)
  72. }
  73. }
  74. func TestUserHandler_GET(t *testing.T) {
  75. tt := []struct {
  76. name string
  77. users []state.User
  78. userHandlerErr error
  79. want string
  80. statusCode int
  81. }{
  82. {
  83. name: "empty user store",
  84. users: []state.User{},
  85. want: `[]`,
  86. statusCode: http.StatusOK,
  87. },
  88. {
  89. name: "user store containing 2 users",
  90. users: []state.User{
  91. {
  92. DisplayScreenName: "userA",
  93. IdentScreenName: state.NewIdentScreenName("userA"),
  94. },
  95. {
  96. DisplayScreenName: "userB",
  97. IdentScreenName: state.NewIdentScreenName("userB"),
  98. },
  99. },
  100. want: `[{"screen_name":"userA"},{"screen_name":"userB"}]`,
  101. statusCode: http.StatusOK,
  102. },
  103. {
  104. name: "user handler error",
  105. users: []state.User{},
  106. userHandlerErr: io.EOF,
  107. want: `internal server error`,
  108. statusCode: http.StatusInternalServerError,
  109. },
  110. }
  111. for _, tc := range tt {
  112. t.Run(tc.name, func(t *testing.T) {
  113. request := httptest.NewRequest(http.MethodGet, "/user", nil)
  114. responseRecorder := httptest.NewRecorder()
  115. userManager := newMockUserManager(t)
  116. userManager.EXPECT().
  117. AllUsers().
  118. Return(tc.users, tc.userHandlerErr)
  119. userHandler(responseRecorder, request, userManager, nil, slog.Default())
  120. if responseRecorder.Code != tc.statusCode {
  121. t.Errorf("Want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  122. }
  123. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  124. t.Errorf("Want '%s', got '%s'", tc.want, responseRecorder.Body)
  125. }
  126. })
  127. }
  128. }
  129. func TestUserHandler_POST(t *testing.T) {
  130. tt := []struct {
  131. name string
  132. body string
  133. UUID uuid.UUID
  134. user state.User
  135. userHandlerErr error
  136. want string
  137. statusCode int
  138. }{
  139. {
  140. name: "with valid user",
  141. body: `{"screen_name":"userA", "password":"thepassword"}`,
  142. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  143. user: func() state.User {
  144. user := state.User{
  145. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  146. DisplayScreenName: "userA",
  147. IdentScreenName: state.NewIdentScreenName("userA"),
  148. }
  149. assert.NoError(t, user.HashPassword("thepassword"))
  150. return user
  151. }(),
  152. want: `User account created successfully.`,
  153. statusCode: http.StatusCreated,
  154. },
  155. {
  156. name: "with malformed body",
  157. body: `{"screen_name":"userA", "password":"thepassword"`,
  158. user: state.User{},
  159. want: `malformed input`,
  160. statusCode: http.StatusBadRequest,
  161. },
  162. {
  163. name: "user handler error",
  164. body: `{"screen_name":"userA", "password":"thepassword"}`,
  165. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  166. user: func() state.User {
  167. user := state.User{
  168. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  169. DisplayScreenName: "userA",
  170. IdentScreenName: state.NewIdentScreenName("userA"),
  171. }
  172. assert.NoError(t, user.HashPassword("thepassword"))
  173. return user
  174. }(),
  175. userHandlerErr: io.EOF,
  176. want: `internal server error`,
  177. statusCode: http.StatusInternalServerError,
  178. },
  179. {
  180. name: "duplicate user",
  181. body: `{"screen_name":"userA", "password":"thepassword"}`,
  182. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  183. user: func() state.User {
  184. user := state.User{
  185. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  186. DisplayScreenName: "userA",
  187. IdentScreenName: state.NewIdentScreenName("userA"),
  188. }
  189. assert.NoError(t, user.HashPassword("thepassword"))
  190. return user
  191. }(),
  192. userHandlerErr: state.ErrDupUser,
  193. want: `user already exists`,
  194. statusCode: http.StatusConflict,
  195. },
  196. }
  197. for _, tc := range tt {
  198. t.Run(tc.name, func(t *testing.T) {
  199. request := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(tc.body))
  200. responseRecorder := httptest.NewRecorder()
  201. userManager := newMockUserManager(t)
  202. if tc.user.IdentScreenName.String() != "" {
  203. userManager.EXPECT().
  204. InsertUser(tc.user).
  205. Return(tc.userHandlerErr)
  206. }
  207. newUUID := func() uuid.UUID { return tc.UUID }
  208. userHandler(responseRecorder, request, userManager, newUUID, slog.Default())
  209. if responseRecorder.Code != tc.statusCode {
  210. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  211. }
  212. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  213. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  214. }
  215. })
  216. }
  217. }
  218. func TestUserHandler_DELETE(t *testing.T) {
  219. tt := []struct {
  220. name string
  221. body string
  222. user state.User
  223. userHandlerErr error
  224. want string
  225. statusCode int
  226. }{
  227. {
  228. name: "with valid user",
  229. body: `{"screen_name":"userA"}`,
  230. user: state.User{
  231. IdentScreenName: state.NewIdentScreenName("userA"),
  232. },
  233. want: `User account successfully deleted.`,
  234. statusCode: http.StatusNoContent,
  235. },
  236. {
  237. name: "with non-existent user",
  238. body: `{"screen_name":"userA"}`,
  239. user: state.User{
  240. IdentScreenName: state.NewIdentScreenName("userA"),
  241. },
  242. userHandlerErr: state.ErrNoUser,
  243. want: `user does not exist`,
  244. statusCode: http.StatusNotFound,
  245. },
  246. {
  247. name: "with malformed body",
  248. body: `{"screen_name":"userA"`,
  249. user: state.User{},
  250. want: `malformed input`,
  251. statusCode: http.StatusBadRequest,
  252. },
  253. {
  254. name: "user handler error",
  255. body: `{"screen_name":"userA"}`,
  256. user: state.User{
  257. IdentScreenName: state.NewIdentScreenName("userA"),
  258. },
  259. userHandlerErr: io.EOF,
  260. want: `internal server error`,
  261. statusCode: http.StatusInternalServerError,
  262. },
  263. }
  264. for _, tc := range tt {
  265. t.Run(tc.name, func(t *testing.T) {
  266. request := httptest.NewRequest(http.MethodDelete, "/user", strings.NewReader(tc.body))
  267. responseRecorder := httptest.NewRecorder()
  268. userManager := newMockUserManager(t)
  269. if tc.user.IdentScreenName.String() != "" {
  270. userManager.EXPECT().
  271. DeleteUser(tc.user.IdentScreenName).
  272. Return(tc.userHandlerErr)
  273. }
  274. userHandler(responseRecorder, request, userManager, nil, slog.Default())
  275. if responseRecorder.Code != tc.statusCode {
  276. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  277. }
  278. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  279. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  280. }
  281. })
  282. }
  283. }
  284. func TestUserPasswordHandler_PUT(t *testing.T) {
  285. tt := []struct {
  286. name string
  287. body string
  288. user state.User
  289. UUID uuid.UUID
  290. userHandlerErr error
  291. want string
  292. statusCode int
  293. }{
  294. {
  295. name: "with valid password",
  296. body: `{"screen_name":"userA", "password":"thepassword"}`,
  297. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  298. user: func() state.User {
  299. user := state.User{
  300. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  301. DisplayScreenName: "userA",
  302. IdentScreenName: state.NewIdentScreenName("userA"),
  303. }
  304. assert.NoError(t, user.HashPassword("thepassword"))
  305. return user
  306. }(),
  307. want: ``,
  308. statusCode: http.StatusNoContent,
  309. },
  310. {
  311. name: "with malformed body",
  312. body: `{"screen_name":"userA", "password":"thepassword"`,
  313. user: state.User{},
  314. want: `malformed input`,
  315. statusCode: http.StatusBadRequest,
  316. },
  317. {
  318. name: "user password handler error",
  319. body: `{"screen_name":"userA", "password":"thepassword"}`,
  320. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  321. user: func() state.User {
  322. user := state.User{
  323. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  324. DisplayScreenName: "userA",
  325. IdentScreenName: state.NewIdentScreenName("userA"),
  326. }
  327. assert.NoError(t, user.HashPassword("thepassword"))
  328. return user
  329. }(),
  330. userHandlerErr: io.EOF,
  331. want: `internal server error`,
  332. statusCode: http.StatusInternalServerError,
  333. },
  334. {
  335. name: "user doesn't exist",
  336. body: `{"screen_name":"userA", "password":"thepassword"}`,
  337. UUID: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b"),
  338. user: func() state.User {
  339. user := state.User{
  340. AuthKey: uuid.MustParse("07c70701-ba68-49a9-9f9b-67a53816e37b").String(),
  341. DisplayScreenName: "userA",
  342. IdentScreenName: state.NewIdentScreenName("userA"),
  343. }
  344. assert.NoError(t, user.HashPassword("thepassword"))
  345. return user
  346. }(),
  347. userHandlerErr: state.ErrNoUser,
  348. want: `user does not exist`,
  349. statusCode: http.StatusNotFound,
  350. },
  351. }
  352. for _, tc := range tt {
  353. t.Run(tc.name, func(t *testing.T) {
  354. request := httptest.NewRequest(http.MethodPut, "/user", strings.NewReader(tc.body))
  355. responseRecorder := httptest.NewRecorder()
  356. userManager := newMockUserManager(t)
  357. if tc.user.IdentScreenName.String() != "" {
  358. userManager.EXPECT().
  359. SetUserPassword(tc.user).
  360. Return(tc.userHandlerErr)
  361. }
  362. newUUID := func() uuid.UUID { return tc.UUID }
  363. userPasswordHandler(responseRecorder, request, userManager, newUUID, slog.Default())
  364. if responseRecorder.Code != tc.statusCode {
  365. t.Errorf("want status '%d', got '%d'", tc.statusCode, responseRecorder.Code)
  366. }
  367. if strings.TrimSpace(responseRecorder.Body.String()) != tc.want {
  368. t.Errorf("want '%s', got '%s'", tc.want, responseRecorder.Body)
  369. }
  370. })
  371. }
  372. }
  373. func TestUserHandler_DisallowedMethod(t *testing.T) {
  374. request := httptest.NewRequest(http.MethodPut, "/user", nil)
  375. responseRecorder := httptest.NewRecorder()
  376. userHandler(responseRecorder, request, nil, nil, nil)
  377. wantCode := http.StatusMethodNotAllowed
  378. if responseRecorder.Code != wantCode {
  379. t.Errorf("want status '%d', got '%d'", http.StatusMethodNotAllowed, responseRecorder.Code)
  380. }
  381. wantBody := `method not allowed`
  382. if strings.TrimSpace(responseRecorder.Body.String()) != wantBody {
  383. t.Errorf("want '%s', got '%s'", wantBody, responseRecorder.Body)
  384. }
  385. }