user_handler_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  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: default\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)
  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: default\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. validators: []func(c *userTest, tester *tester, t *testing.T){
  318. UserModelBodyValidator,
  319. },
  320. },
  321. &userTest{
  322. initializers: []func(tester *tester){
  323. initUserDefault,
  324. },
  325. msg: "Read user bad id field",
  326. method: "GET",
  327. endpoint: "/api/users/aldkfjas",
  328. body: "",
  329. expStatus: http.StatusBadRequest,
  330. expBody: `{"code":600,"errors":["could not process request"]}`,
  331. validators: []func(c *userTest, tester *tester, t *testing.T){
  332. BasicBodyValidator,
  333. },
  334. },
  335. &userTest{
  336. initializers: []func(tester *tester){
  337. initUserDefault,
  338. },
  339. msg: "Read user not found",
  340. method: "GET",
  341. endpoint: "/api/users/2",
  342. body: "",
  343. expStatus: http.StatusNotFound,
  344. expBody: `{"code":602,"errors":["could not find requested object"]}`,
  345. validators: []func(c *userTest, tester *tester, t *testing.T){
  346. BasicBodyValidator,
  347. },
  348. },
  349. }
  350. func TestHandleReadUser(t *testing.T) {
  351. testUserRequests(t, readUserTests, true)
  352. }
  353. var readUserClustersTests = []*userTest{
  354. &userTest{
  355. initializers: []func(tester *tester){
  356. initUserWithClusters,
  357. },
  358. msg: "Read user successful",
  359. method: "GET",
  360. endpoint: "/api/users/1/clusters",
  361. body: "",
  362. expStatus: http.StatusOK,
  363. expBody: `[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}]`,
  364. validators: []func(c *userTest, tester *tester, t *testing.T){
  365. ClusterBodyValidator,
  366. },
  367. },
  368. }
  369. func TestHandleReadUserClusters(t *testing.T) {
  370. testUserRequests(t, readUserClustersTests, true)
  371. }
  372. var readUserClustersAllTests = []*userTest{
  373. &userTest{
  374. initializers: []func(tester *tester){
  375. initUserWithClusters,
  376. },
  377. msg: "Read user successful",
  378. method: "GET",
  379. endpoint: "/api/users/1/clusters/all",
  380. body: "",
  381. expStatus: http.StatusOK,
  382. expBody: `[{"name":"cluster-test","server":"https://localhost","context":"context-test","user":"test-admin"}]`,
  383. validators: []func(c *userTest, tester *tester, t *testing.T){
  384. ClusterBodyValidator,
  385. },
  386. },
  387. &userTest{
  388. initializers: []func(tester *tester){
  389. initUserWithClusters,
  390. func(tester *tester) {
  391. initUserDefault(tester)
  392. user, _ := tester.repo.User.ReadUserByEmail("belanger@getporter.dev")
  393. user.Clusters = []models.ClusterConfig{}
  394. user.RawKubeConfig = []byte("apiVersion: \xc5\n")
  395. tester.repo.User.UpdateUser(user)
  396. },
  397. },
  398. msg: "Read user with invalid utf-8 \xc5 in kubeconfig",
  399. method: "GET",
  400. endpoint: "/api/users/1/clusters/all",
  401. body: "",
  402. expStatus: http.StatusBadRequest,
  403. expBody: `{"code":600,"errors":["could not process request"]}`,
  404. validators: []func(c *userTest, tester *tester, t *testing.T){
  405. ClusterBodyValidator,
  406. },
  407. },
  408. }
  409. func TestHandleReadUserClustersAll(t *testing.T) {
  410. testUserRequests(t, readUserClustersAllTests, true)
  411. }
  412. var updateUserTests = []*userTest{
  413. &userTest{
  414. initializers: []func(tester *tester){
  415. initUserDefault,
  416. },
  417. msg: "Update user successful",
  418. method: "PUT",
  419. endpoint: "/api/users/1",
  420. body: `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\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":[]}`,
  421. expStatus: http.StatusNoContent,
  422. expBody: "",
  423. validators: []func(c *userTest, tester *tester, t *testing.T){
  424. func(c *userTest, tester *tester, t *testing.T) {
  425. req, err := http.NewRequest(
  426. "GET",
  427. "/api/users/1",
  428. strings.NewReader(""),
  429. )
  430. if err != nil {
  431. t.Fatal(err)
  432. }
  433. rr2 := httptest.NewRecorder()
  434. tester.router.ServeHTTP(rr2, req)
  435. gotBody := &models.UserExternal{}
  436. expBody := &models.UserExternal{}
  437. json.Unmarshal(rr2.Body.Bytes(), gotBody)
  438. json.Unmarshal([]byte(`{"id":1,"email":"belanger@getporter.dev","clusters":[],"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\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)
  439. if !reflect.DeepEqual(gotBody, expBody) {
  440. t.Errorf("%s, handler returned wrong body: got %v want %v",
  441. "validator failed", gotBody, expBody)
  442. }
  443. },
  444. },
  445. },
  446. &userTest{
  447. initializers: []func(tester *tester){
  448. initUserDefault,
  449. },
  450. msg: "Update user invalid id",
  451. method: "PUT",
  452. endpoint: "/api/users/alsdfjk",
  453. body: `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\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":[]}`,
  454. expStatus: http.StatusBadRequest,
  455. expBody: `{"code":600,"errors":["could not process request"]}`,
  456. validators: []func(c *userTest, tester *tester, t *testing.T){
  457. BasicBodyValidator,
  458. },
  459. },
  460. &userTest{
  461. initializers: []func(tester *tester){
  462. initUserDefault,
  463. },
  464. msg: "Update user bad kubeconfig",
  465. method: "PUT",
  466. endpoint: "/api/users/1",
  467. body: `{"rawKubeConfig":"notvalidyaml", "allowedClusters":[]}`,
  468. expStatus: http.StatusBadRequest,
  469. expBody: `{"code":600,"errors":["could not process request"]}`,
  470. validators: []func(c *userTest, tester *tester, t *testing.T){
  471. BasicBodyValidator,
  472. },
  473. },
  474. }
  475. func TestHandleUpdateUser(t *testing.T) {
  476. testUserRequests(t, updateUserTests, true)
  477. }
  478. var updateUserTestsWriteFail = []*userTest{
  479. &userTest{
  480. initializers: []func(tester *tester){
  481. initUserDefault,
  482. },
  483. msg: "Update user db connection down",
  484. method: "PUT",
  485. endpoint: "/api/users/1",
  486. body: `{"rawKubeConfig":"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: default\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":[]}`,
  487. expStatus: http.StatusInternalServerError,
  488. expBody: `{"code":500,"errors":["could not write to database"]}`,
  489. validators: []func(c *userTest, tester *tester, t *testing.T){
  490. BasicBodyValidator,
  491. },
  492. },
  493. }
  494. func TestHandleUpdateUserWriteFail(t *testing.T) {
  495. testUserRequests(t, updateUserTestsWriteFail, false)
  496. }
  497. var deleteUserTests = []*userTest{
  498. &userTest{
  499. initializers: []func(tester *tester){
  500. initUserDefault,
  501. },
  502. msg: "Delete user successful",
  503. method: "DELETE",
  504. endpoint: "/api/users/1",
  505. body: `{"password":"hello"}`,
  506. expStatus: http.StatusNoContent,
  507. expBody: "",
  508. validators: []func(c *userTest, tester *tester, t *testing.T){
  509. func(c *userTest, tester *tester, t *testing.T) {
  510. req, err := http.NewRequest(
  511. "GET",
  512. "/api/users/1",
  513. strings.NewReader(""),
  514. )
  515. if err != nil {
  516. t.Fatal(err)
  517. }
  518. rr2 := httptest.NewRecorder()
  519. tester.router.ServeHTTP(rr2, req)
  520. gotBody := &models.UserExternal{}
  521. expBody := &models.UserExternal{}
  522. if status := rr2.Code; status != 404 {
  523. t.Errorf("DELETE user validation, handler returned wrong status code: got %v want %v",
  524. status, 404)
  525. }
  526. json.Unmarshal(rr2.Body.Bytes(), gotBody)
  527. json.Unmarshal([]byte(`{"code":602,"errors":["could not find requested object"]}`), expBody)
  528. if !reflect.DeepEqual(gotBody, expBody) {
  529. t.Errorf("%s, handler returned wrong body: got %v want %v",
  530. "validator failed", gotBody, expBody)
  531. }
  532. },
  533. },
  534. },
  535. &userTest{
  536. initializers: []func(tester *tester){
  537. initUserDefault,
  538. },
  539. msg: "Delete user invalid id",
  540. method: "DELETE",
  541. endpoint: "/api/users/aldkjf",
  542. body: `{"password":"hello"}`,
  543. expStatus: http.StatusBadRequest,
  544. expBody: `{"code":600,"errors":["could not process request"]}`,
  545. validators: []func(c *userTest, tester *tester, t *testing.T){
  546. BasicBodyValidator,
  547. },
  548. },
  549. &userTest{
  550. initializers: []func(tester *tester){
  551. initUserDefault,
  552. },
  553. msg: "Delete user missing password",
  554. method: "DELETE",
  555. endpoint: "/api/users/1",
  556. body: `{}`,
  557. expStatus: http.StatusUnprocessableEntity,
  558. expBody: `{"code":601,"errors":["required validation failed"]}`,
  559. validators: []func(c *userTest, tester *tester, t *testing.T){
  560. BasicBodyValidator,
  561. },
  562. },
  563. }
  564. func TestHandleDeleteUser(t *testing.T) {
  565. testUserRequests(t, deleteUserTests, true)
  566. }
  567. func BasicBodyValidator(c *userTest, tester *tester, t *testing.T) {
  568. if body := tester.rr.Body.String(); body != c.expBody {
  569. t.Errorf("%s, handler returned wrong body: got %v want %v",
  570. c.msg, body, c.expBody)
  571. }
  572. }
  573. func UserModelBodyValidator(c *userTest, tester *tester, t *testing.T) {
  574. gotBody := &models.UserExternal{}
  575. expBody := &models.UserExternal{}
  576. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  577. json.Unmarshal([]byte(c.expBody), expBody)
  578. if !reflect.DeepEqual(gotBody, expBody) {
  579. t.Errorf("%s, handler returned wrong body: got %v want %v",
  580. c.msg, gotBody, expBody)
  581. }
  582. }
  583. func ClusterBodyValidator(c *userTest, tester *tester, t *testing.T) {
  584. // if status is expected to be 200, parse the body for UserExternal
  585. gotBody := &[]models.ClusterConfigExternal{}
  586. expBody := &[]models.ClusterConfigExternal{}
  587. json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
  588. json.Unmarshal([]byte(c.expBody), expBody)
  589. if !reflect.DeepEqual(gotBody, expBody) {
  590. t.Errorf("%s, handler returned wrong body: got %v want %v",
  591. c.msg, gotBody, expBody)
  592. }
  593. }