user_handler_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package api_test
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "reflect"
  7. "strings"
  8. "testing"
  9. "time"
  10. "github.com/go-chi/chi"
  11. "github.com/porter-dev/porter/internal/config"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/internal/repository"
  14. "github.com/porter-dev/porter/internal/repository/test"
  15. "github.com/porter-dev/porter/server/api"
  16. "github.com/porter-dev/porter/server/router"
  17. sessionstore "github.com/porter-dev/porter/internal/auth"
  18. lr "github.com/porter-dev/porter/internal/logger"
  19. vr "github.com/porter-dev/porter/internal/validator"
  20. )
  21. type tester struct {
  22. app *api.App
  23. repo *repository.Repository
  24. store *sessionstore.PGStore
  25. router *chi.Mux
  26. req *http.Request
  27. rr *httptest.ResponseRecorder
  28. cookie *http.Cookie
  29. }
  30. type userTest struct {
  31. initializers []func(t *tester)
  32. msg string
  33. method string
  34. endpoint string
  35. body string
  36. expStatus int
  37. expBody string
  38. useCookie bool
  39. validators []func(c *userTest, tester *tester, t *testing.T)
  40. }
  41. func (t *tester) execute() {
  42. t.router.ServeHTTP(t.rr, t.req)
  43. }
  44. func (t *tester) reset() {
  45. t.rr = httptest.NewRecorder()
  46. t.req = nil
  47. }
  48. func (t *tester) createUserSession(email string, pw string) {
  49. req, _ := http.NewRequest(
  50. "POST",
  51. "/api/users",
  52. strings.NewReader(`{"email":"`+email+`","password":"`+pw+`"}`),
  53. )
  54. t.req = req
  55. t.execute()
  56. if cookies := t.rr.Result().Cookies(); len(cookies) > 0 {
  57. t.cookie = cookies[0]
  58. }
  59. t.reset()
  60. }
  61. func initUserDefault(tester *tester) {
  62. tester.createUserSession("belanger@getporter.dev", "hello")
  63. }
  64. func initUserWithClusters(tester *tester) {
  65. initUserDefault(tester)
  66. user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
  67. user.Clusters = []models.ClusterConfig{
  68. models.ClusterConfig{
  69. Name: "cluster-test",
  70. Server: "https://localhost",
  71. Context: "context-test",
  72. User: "test-admin",
  73. },
  74. }
  75. user.RawKubeConfig = []byte("apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n server: https://localhost\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\nusers:\n- name: test-admin")
  76. tester.repo.User.UpdateUser(user)
  77. }
  78. func newTester(canQuery bool) *tester {
  79. appConf := config.Conf{
  80. Debug: true,
  81. Server: config.ServerConf{
  82. Port: 8080,
  83. CookieName: "porter",
  84. CookieSecrets: [][]byte{[]byte("secret")},
  85. TimeoutRead: time.Second * 5,
  86. TimeoutWrite: time.Second * 10,
  87. TimeoutIdle: time.Second * 15,
  88. },
  89. // unimportant here
  90. Db: config.DBConf{},
  91. }
  92. logger := lr.NewConsole(appConf.Debug)
  93. validator := vr.New()
  94. repo := test.NewRepository(canQuery)
  95. store, _ := sessionstore.NewStore(repo, appConf.Server)
  96. app := api.New(logger, repo, validator, store, appConf.Server.CookieName)
  97. r := router.New(app, store, appConf.Server.CookieName)
  98. return &tester{
  99. app: app,
  100. repo: repo,
  101. store: store,
  102. router: r,
  103. req: nil,
  104. rr: httptest.NewRecorder(),
  105. cookie: nil,
  106. }
  107. }
  108. func testUserRequests(t *testing.T, tests []*userTest, canQuery bool) {
  109. for _, c := range tests {
  110. // create a new tester
  111. tester := newTester(canQuery)
  112. // if there's an initializer, call it
  113. for _, init := range c.initializers {
  114. init(tester)
  115. }
  116. req, err := http.NewRequest(
  117. c.method,
  118. c.endpoint,
  119. strings.NewReader(c.body),
  120. )
  121. tester.req = req
  122. if c.useCookie {
  123. req.AddCookie(tester.cookie)
  124. }
  125. if err != nil {
  126. t.Fatal(err)
  127. }
  128. tester.execute()
  129. rr := tester.rr
  130. // first, check that the status matches
  131. if status := rr.Code; status != c.expStatus {
  132. t.Errorf("%s, handler returned wrong status code: got %v want %v",
  133. c.msg, status, c.expStatus)
  134. }
  135. // if there's a validator, call it
  136. for _, validate := range c.validators {
  137. validate(c, tester, t)
  138. }
  139. }
  140. }
  141. var createUserTests = []*userTest{
  142. &userTest{
  143. msg: "Create user",
  144. method: "POST",
  145. endpoint: "/api/users",
  146. body: `{
  147. "email": "belanger@getporter.dev",
  148. "password": "hello"
  149. }`,
  150. expStatus: http.StatusCreated,
  151. expBody: "",
  152. },
  153. &userTest{
  154. msg: "Create user invalid email",
  155. method: "POST",
  156. endpoint: "/api/users",
  157. body: `{
  158. "email": "notanemail",
  159. "password": "hello"
  160. }`,
  161. expStatus: http.StatusUnprocessableEntity,
  162. expBody: `{"code":601,"errors":["email validation failed"]}`,
  163. validators: []func(c *userTest, tester *tester, t *testing.T){
  164. BasicBodyValidator,
  165. },
  166. },
  167. &userTest{
  168. msg: "Create user missing field",
  169. method: "POST",
  170. endpoint: "/api/users",
  171. body: `{
  172. "password": "hello"
  173. }`,
  174. expStatus: http.StatusUnprocessableEntity,
  175. expBody: `{"code":601,"errors":["required validation failed"]}`,
  176. validators: []func(c *userTest, tester *tester, t *testing.T){
  177. BasicBodyValidator,
  178. },
  179. },
  180. &userTest{
  181. initializers: []func(tester *tester){
  182. initUserDefault,
  183. },
  184. msg: "Create user same email",
  185. method: "POST",
  186. endpoint: "/api/users",
  187. body: `{
  188. "email": "belanger@getporter.dev",
  189. "password": "hello"
  190. }`,
  191. expStatus: http.StatusUnprocessableEntity,
  192. expBody: `{"code":601,"errors":["email already taken"]}`,
  193. validators: []func(c *userTest, tester *tester, t *testing.T){
  194. BasicBodyValidator,
  195. },
  196. },
  197. &userTest{
  198. msg: "Create user invalid field type",
  199. method: "POST",
  200. endpoint: "/api/users",
  201. body: `{
  202. "email": "belanger@getporter.dev",
  203. "password": 0
  204. }`,
  205. expStatus: http.StatusBadRequest,
  206. expBody: `{"code":600,"errors":["could not process request"]}`,
  207. validators: []func(c *userTest, tester *tester, t *testing.T){
  208. BasicBodyValidator,
  209. },
  210. },
  211. }
  212. func TestHandleCreateUser(t *testing.T) {
  213. testUserRequests(t, createUserTests, true)
  214. }
  215. var createUserTestsWriteFail = []*userTest{
  216. &userTest{
  217. msg: "Create user db connection down",
  218. method: "POST",
  219. endpoint: "/api/users",
  220. body: `{
  221. "email": "belanger@getporter.dev",
  222. "password": "hello"
  223. }`,
  224. expStatus: http.StatusInternalServerError,
  225. expBody: `{"code":500,"errors":["could not read from database"]}`,
  226. validators: []func(c *userTest, tester *tester, t *testing.T){
  227. BasicBodyValidator,
  228. },
  229. },
  230. }
  231. func TestHandleCreateUserWriteFail(t *testing.T) {
  232. testUserRequests(t, createUserTestsWriteFail, false)
  233. }
  234. var loginUserTests = []*userTest{
  235. &userTest{
  236. initializers: []func(tester *tester){
  237. initUserDefault,
  238. },
  239. msg: "Login user successful",
  240. method: "POST",
  241. endpoint: "/api/login",
  242. body: `{
  243. "email": "belanger@getporter.dev",
  244. "password": "hello"
  245. }`,
  246. expStatus: http.StatusOK,
  247. expBody: ``,
  248. validators: []func(c *userTest, tester *tester, t *testing.T){
  249. BasicBodyValidator,
  250. },
  251. },
  252. &userTest{
  253. initializers: []func(tester *tester){
  254. initUserDefault,
  255. },
  256. msg: "Login user already logged in",
  257. method: "POST",
  258. endpoint: "/api/login",
  259. body: `{
  260. "email": "belanger@getporter.dev",
  261. "password": "hello"
  262. }`,
  263. expStatus: http.StatusOK,
  264. expBody: ``,
  265. useCookie: true,
  266. validators: []func(c *userTest, tester *tester, t *testing.T){
  267. BasicBodyValidator,
  268. },
  269. },
  270. &userTest{
  271. msg: "Login user unregistered email",
  272. method: "POST",
  273. endpoint: "/api/login",
  274. body: `{
  275. "email": "belanger@getporter.dev",
  276. "password": "hello"
  277. }`,
  278. expStatus: http.StatusUnauthorized,
  279. expBody: `{"code":401,"errors":["email not registered"]}`,
  280. validators: []func(c *userTest, tester *tester, t *testing.T){
  281. BasicBodyValidator,
  282. },
  283. },
  284. &userTest{
  285. initializers: []func(tester *tester){
  286. initUserDefault,
  287. },
  288. msg: "Login user incorrect password",
  289. method: "POST",
  290. endpoint: "/api/login",
  291. body: `{
  292. "email": "belanger@getporter.dev",
  293. "password": "notthepassword"
  294. }`,
  295. expStatus: http.StatusUnauthorized,
  296. expBody: `{"code":401,"errors":["incorrect password"]}`,
  297. useCookie: true,
  298. validators: []func(c *userTest, tester *tester, t *testing.T){
  299. BasicBodyValidator,
  300. },
  301. },
  302. }
  303. func TestHandleLoginUser(t *testing.T) {
  304. testUserRequests(t, loginUserTests, true)
  305. }
  306. var readUserTests = []*userTest{
  307. &userTest{
  308. initializers: []func(tester *tester){
  309. initUserWithClusters,
  310. },
  311. msg: "Read user successful",
  312. method: "GET",
  313. endpoint: "/api/users/1",
  314. body: "",
  315. expStatus: http.StatusOK,
  316. expBody: `{"id":1,"email":"belanger@getporter.dev","clusters":[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n server: https://localhost\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\nusers:\n- name: test-admin"}`,
  317. useCookie: true,
  318. validators: []func(c *userTest, tester *tester, t *testing.T){
  319. UserModelBodyValidator,
  320. },
  321. },
  322. &userTest{
  323. initializers: []func(tester *tester){
  324. initUserDefault,
  325. },
  326. msg: "Read user unauthorized",
  327. method: "GET",
  328. endpoint: "/api/users/2",
  329. body: "",
  330. expStatus: http.StatusForbidden,
  331. expBody: http.StatusText(http.StatusForbidden) + "\n",
  332. validators: []func(c *userTest, tester *tester, t *testing.T){
  333. BasicBodyValidator,
  334. },
  335. },
  336. }
  337. func TestHandleReadUser(t *testing.T) {
  338. testUserRequests(t, readUserTests, true)
  339. }
  340. var readUserClustersTests = []*userTest{
  341. &userTest{
  342. initializers: []func(tester *tester){
  343. initUserWithClusters,
  344. },
  345. msg: "Read user successful",
  346. method: "GET",
  347. endpoint: "/api/users/1/clusters",
  348. body: "",
  349. expStatus: http.StatusOK,
  350. useCookie: true,
  351. expBody: `[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}]`,
  352. validators: []func(c *userTest, tester *tester, t *testing.T){
  353. ClusterBodyValidator,
  354. },
  355. },
  356. }
  357. func TestHandleReadUserClusters(t *testing.T) {
  358. testUserRequests(t, readUserClustersTests, true)
  359. }
  360. var readUserClustersAllTests = []*userTest{
  361. &userTest{
  362. initializers: []func(tester *tester){
  363. initUserWithClusters,
  364. },
  365. msg: "Read user successful",
  366. method: "GET",
  367. endpoint: "/api/users/1/clusters/all",
  368. body: "",
  369. expStatus: http.StatusOK,
  370. useCookie: true,
  371. expBody: `[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}]`,
  372. validators: []func(c *userTest, tester *tester, t *testing.T){
  373. ClusterBodyValidator,
  374. },
  375. },
  376. &userTest{
  377. initializers: []func(tester *tester){
  378. initUserWithClusters,
  379. func(tester *tester) {
  380. initUserDefault(tester)
  381. user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
  382. user.Clusters = []models.ClusterConfig{}
  383. user.RawKubeConfig = []byte("apiVersion: \xc5\n")
  384. tester.repo.User.UpdateUser(user)
  385. },
  386. },
  387. msg: "Read user with invalid utf-8 \xc5 in kubeconfig",
  388. method: "GET",
  389. endpoint: "/api/users/1/clusters/all",
  390. body: "",
  391. expStatus: http.StatusBadRequest,
  392. useCookie: true,
  393. expBody: `{"code":600,"errors":["could not process request"]}`,
  394. validators: []func(c *userTest, tester *tester, t *testing.T){
  395. ClusterBodyValidator,
  396. },
  397. },
  398. }
  399. func TestHandleReadUserClustersAll(t *testing.T) {
  400. testUserRequests(t, readUserClustersAllTests, true)
  401. }
  402. var updateUserTests = []*userTest{
  403. &userTest{
  404. initializers: []func(tester *tester){
  405. initUserDefault,
  406. },
  407. msg: "Update user successful",
  408. method: "PUT",
  409. endpoint: "/api/users/1",
  410. body: `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n server: https://localhost\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\nusers:\n- name: test-admin", "allowedClusters":[]}`,
  411. expStatus: http.StatusNoContent,
  412. expBody: "",
  413. useCookie: true,
  414. validators: []func(c *userTest, tester *tester, t *testing.T){
  415. func(c *userTest, tester *tester, t *testing.T) {
  416. req, err := http.NewRequest(
  417. "GET",
  418. "/api/users/1",
  419. strings.NewReader(""),
  420. )
  421. req.AddCookie(tester.cookie)
  422. if err != nil {
  423. t.Fatal(err)
  424. }
  425. rr2 := httptest.NewRecorder()
  426. tester.router.ServeHTTP(rr2, req)
  427. gotBody := &models.UserExternal{}
  428. expBody := &models.UserExternal{}
  429. json.Unmarshal(rr2.Body.Bytes(), gotBody)
  430. json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev","clusters":[],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n server: https://localhost\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\nusers:\n- name: test-admin"}`), expBody)
  431. if !reflect.DeepEqual(gotBody, expBody) {
  432. t.Errorf("%s, handler returned wrong body: got %v want %v",
  433. "validator failed", gotBody, expBody)
  434. }
  435. },
  436. },
  437. },
  438. &userTest{
  439. initializers: []func(tester *tester){
  440. initUserDefault,
  441. },
  442. msg: "Update user invalid id",
  443. method: "PUT",
  444. endpoint: "/api/users/alsdfjk",
  445. body: `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: context-test\nclusters:\n- cluster:\n server: https://localhost\n name: cluster-test\ncontexts:\n- context:\n cluster: cluster-test\n user: test-admin\n name: context-test\nusers:\n- name: test-admin", "allowedClusters":[]}`,
  446. expStatus: http.StatusForbidden,
  447. expBody: http.StatusText(http.StatusForbidden) + "\n",
  448. validators: []func(c *userTest, tester *tester, t *testing.T){
  449. BasicBodyValidator,
  450. },
  451. },
  452. &userTest{
  453. initializers: []func(tester *tester){
  454. initUserDefault,
  455. },
  456. msg: "Update user bad kubeconfig",
  457. method: "PUT",
  458. endpoint: "/api/users/1",
  459. body: `{"rawKubeConfig":"notvalidyaml", "allowedClusters":[]}`,
  460. expStatus: http.StatusBadRequest,
  461. expBody: `{"code":600,"errors":["could not process request"]}`,
  462. useCookie: true,
  463. validators: []func(c *userTest, tester *tester, t *testing.T){
  464. BasicBodyValidator,
  465. },
  466. },
  467. }
  468. func TestHandleUpdateUser(t *testing.T) {
  469. testUserRequests(t, updateUserTests, true)
  470. }
  471. var deleteUserTests = []*userTest{
  472. &userTest{
  473. initializers: []func(tester *tester){
  474. initUserDefault,
  475. },
  476. msg: "Delete user successful",
  477. method: "DELETE",
  478. endpoint: "/api/users/1",
  479. body: `{"password":"hello"}`,
  480. expStatus: http.StatusNoContent,
  481. expBody: "",
  482. useCookie: true,
  483. validators: []func(c *userTest, tester *tester, t *testing.T){
  484. func(c *userTest, tester *tester, t *testing.T) {
  485. req, err := http.NewRequest(
  486. "GET",
  487. "/api/users/1",
  488. strings.NewReader(""),
  489. )
  490. req.AddCookie(tester.cookie)
  491. if err != nil {
  492. t.Fatal(err)
  493. }
  494. rr2 := httptest.NewRecorder()
  495. tester.router.ServeHTTP(rr2, req)
  496. gotBody := &models.UserExternal{}
  497. expBody := &models.UserExternal{}
  498. if status := rr2.Code; status != 404 {
  499. t.Errorf("DELETE user validation, handler returned wrong status code: got %v want %v",
  500. status, 404)
  501. }
  502. json.Unmarshal(rr2.Body.Bytes(), gotBody)
  503. json.Unmarshal([]byte(`{"code":602,"errors":["could not find requested object"]}`), expBody)
  504. if !reflect.DeepEqual(gotBody, expBody) {
  505. t.Errorf("%s, handler returned wrong body: got %v want %v",
  506. "validator failed", gotBody, expBody)
  507. }
  508. },
  509. },
  510. },
  511. &userTest{
  512. initializers: []func(tester *tester){
  513. initUserDefault,
  514. },
  515. msg: "Delete user invalid id",
  516. method: "DELETE",
  517. endpoint: "/api/users/aldkjf",
  518. body: `{"password":"hello"}`,
  519. expStatus: http.StatusForbidden,
  520. expBody: http.StatusText(http.StatusForbidden) + "\n",
  521. validators: []func(c *userTest, tester *tester, t *testing.T){
  522. BasicBodyValidator,
  523. },
  524. },
  525. &userTest{
  526. initializers: []func(tester *tester){
  527. initUserDefault,
  528. },
  529. msg: "Delete user missing password",
  530. method: "DELETE",
  531. endpoint: "/api/users/1",
  532. body: `{}`,
  533. expStatus: http.StatusUnprocessableEntity,
  534. expBody: `{"code":601,"errors":["required validation failed"]}`,
  535. useCookie: true,
  536. validators: []func(c *userTest, tester *tester, t *testing.T){
  537. BasicBodyValidator,
  538. },
  539. },
  540. }
  541. func TestHandleDeleteUser(t *testing.T) {
  542. testUserRequests(t, deleteUserTests, true)
  543. }
  544. func BasicBodyValidator(c *userTest, tester *tester, t *testing.T) {
  545. if body := tester.rr.Body.String(); body != c.expBody {
  546. t.Errorf("%s, handler returned wrong body: got %v want %v",
  547. c.msg, body, c.expBody)
  548. }
  549. }
  550. func UserModelBodyValidator(c *userTest, tester *tester, t *testing.T) {
  551. gotBody := &models.UserExternal{}
  552. expBody := &models.UserExternal{}
  553. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  554. json.Unmarshal([]byte(c.expBody), expBody)
  555. if !reflect.DeepEqual(gotBody, expBody) {
  556. t.Errorf("%s, handler returned wrong body: got %v want %v",
  557. c.msg, gotBody, expBody)
  558. }
  559. }
  560. func ClusterBodyValidator(c *userTest, tester *tester, t *testing.T) {
  561. // if status is expected to be 200, parse the body for UserExternal
  562. gotBody := &[]models.ClusterConfigExternal{}
  563. expBody := &[]models.ClusterConfigExternal{}
  564. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  565. json.Unmarshal([]byte(c.expBody), expBody)
  566. if !reflect.DeepEqual(gotBody, expBody) {
  567. t.Errorf("%s, handler returned wrong body: got %v want %v",
  568. c.msg, gotBody, expBody)
  569. }
  570. }