binding_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. // Copyright 2013 The Martini Contrib Authors. All rights reserved.
  2. // Copyright 2014 The Gogs Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package middleware
  6. import (
  7. "bytes"
  8. "mime/multipart"
  9. "net/http"
  10. "net/http/httptest"
  11. "strconv"
  12. "strings"
  13. "testing"
  14. "github.com/codegangsta/martini"
  15. )
  16. func TestBind(t *testing.T) {
  17. testBind(t, false)
  18. }
  19. func TestBindWithInterface(t *testing.T) {
  20. testBind(t, true)
  21. }
  22. func TestMultipartBind(t *testing.T) {
  23. index := 0
  24. for test, expectStatus := range bindMultipartTests {
  25. handler := func(post BlogPost, errors Errors) {
  26. handle(test, t, index, post, errors)
  27. }
  28. recorder := testMultipart(t, test, Bind(BlogPost{}), handler, index)
  29. if recorder.Code != expectStatus {
  30. t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus)
  31. }
  32. index++
  33. }
  34. }
  35. func TestForm(t *testing.T) {
  36. testForm(t, false)
  37. }
  38. func TestFormWithInterface(t *testing.T) {
  39. testForm(t, true)
  40. }
  41. func TestEmptyForm(t *testing.T) {
  42. testEmptyForm(t)
  43. }
  44. func TestMultipartForm(t *testing.T) {
  45. for index, test := range multipartformTests {
  46. handler := func(post BlogPost, errors Errors) {
  47. handle(test, t, index, post, errors)
  48. }
  49. testMultipart(t, test, MultipartForm(BlogPost{}), handler, index)
  50. }
  51. }
  52. func TestMultipartFormWithInterface(t *testing.T) {
  53. for index, test := range multipartformTests {
  54. handler := func(post Modeler, errors Errors) {
  55. post.Create(test, t, index)
  56. }
  57. testMultipart(t, test, MultipartForm(BlogPost{}, (*Modeler)(nil)), handler, index)
  58. }
  59. }
  60. func TestJson(t *testing.T) {
  61. testJson(t, false)
  62. }
  63. func TestJsonWithInterface(t *testing.T) {
  64. testJson(t, true)
  65. }
  66. func TestEmptyJson(t *testing.T) {
  67. testEmptyJson(t)
  68. }
  69. func TestValidate(t *testing.T) {
  70. handlerMustErr := func(errors Errors) {
  71. if errors.Count() == 0 {
  72. t.Error("Expected at least one error, got 0")
  73. }
  74. }
  75. handlerNoErr := func(errors Errors) {
  76. if errors.Count() > 0 {
  77. t.Error("Expected no errors, got", errors.Count())
  78. }
  79. }
  80. performValidationTest(&BlogPost{"", "...", 0, 0, []int{}}, handlerMustErr, t)
  81. performValidationTest(&BlogPost{"Good Title", "Good content", 0, 0, []int{}}, handlerNoErr, t)
  82. performValidationTest(&User{Name: "Jim", Home: Address{"", ""}}, handlerMustErr, t)
  83. performValidationTest(&User{Name: "Jim", Home: Address{"required", ""}}, handlerNoErr, t)
  84. }
  85. func handle(test testCase, t *testing.T, index int, post BlogPost, errors Errors) {
  86. assertEqualField(t, "Title", index, test.ref.Title, post.Title)
  87. assertEqualField(t, "Content", index, test.ref.Content, post.Content)
  88. assertEqualField(t, "Views", index, test.ref.Views, post.Views)
  89. for i := range test.ref.Multiple {
  90. if i >= len(post.Multiple) {
  91. t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", post.Multiple, len(post.Multiple), test.ref.Multiple, len(test.ref.Multiple))
  92. break
  93. }
  94. if test.ref.Multiple[i] != post.Multiple[i] {
  95. t.Errorf("Expected: %v to deep equal: %v", post.Multiple, test.ref.Multiple)
  96. break
  97. }
  98. }
  99. if test.ok && errors.Count() > 0 {
  100. t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors)
  101. } else if !test.ok && errors.Count() == 0 {
  102. t.Errorf("%+v should have errors, but was OK (0 errors)", test)
  103. }
  104. }
  105. func handleEmpty(test emptyPayloadTestCase, t *testing.T, index int, section BlogSection, errors Errors) {
  106. assertEqualField(t, "Title", index, test.ref.Title, section.Title)
  107. assertEqualField(t, "Content", index, test.ref.Content, section.Content)
  108. if test.ok && errors.Count() > 0 {
  109. t.Errorf("%+v should be OK (0 errors), but had errors: %+v", test, errors)
  110. } else if !test.ok && errors.Count() == 0 {
  111. t.Errorf("%+v should have errors, but was OK (0 errors)", test)
  112. }
  113. }
  114. func testBind(t *testing.T, withInterface bool) {
  115. index := 0
  116. for test, expectStatus := range bindTests {
  117. m := martini.Classic()
  118. recorder := httptest.NewRecorder()
  119. handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
  120. binding := Bind(BlogPost{})
  121. if withInterface {
  122. handler = func(post BlogPost, errors Errors) {
  123. post.Create(test, t, index)
  124. }
  125. binding = Bind(BlogPost{}, (*Modeler)(nil))
  126. }
  127. switch test.method {
  128. case "GET":
  129. m.Get(route, binding, handler)
  130. case "POST":
  131. m.Post(route, binding, handler)
  132. }
  133. req, err := http.NewRequest(test.method, test.path, strings.NewReader(test.payload))
  134. req.Header.Add("Content-Type", test.contentType)
  135. if err != nil {
  136. t.Error(err)
  137. }
  138. m.ServeHTTP(recorder, req)
  139. if recorder.Code != expectStatus {
  140. t.Errorf("On test case %v, got status code %d but expected %d", test, recorder.Code, expectStatus)
  141. }
  142. index++
  143. }
  144. }
  145. func testJson(t *testing.T, withInterface bool) {
  146. for index, test := range jsonTests {
  147. recorder := httptest.NewRecorder()
  148. handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
  149. binding := Json(BlogPost{})
  150. if withInterface {
  151. handler = func(post BlogPost, errors Errors) {
  152. post.Create(test, t, index)
  153. }
  154. binding = Bind(BlogPost{}, (*Modeler)(nil))
  155. }
  156. m := martini.Classic()
  157. switch test.method {
  158. case "GET":
  159. m.Get(route, binding, handler)
  160. case "POST":
  161. m.Post(route, binding, handler)
  162. case "PUT":
  163. m.Put(route, binding, handler)
  164. case "DELETE":
  165. m.Delete(route, binding, handler)
  166. }
  167. req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload))
  168. if err != nil {
  169. t.Error(err)
  170. }
  171. m.ServeHTTP(recorder, req)
  172. }
  173. }
  174. func testEmptyJson(t *testing.T) {
  175. for index, test := range emptyPayloadTests {
  176. recorder := httptest.NewRecorder()
  177. handler := func(section BlogSection, errors Errors) { handleEmpty(test, t, index, section, errors) }
  178. binding := Json(BlogSection{})
  179. m := martini.Classic()
  180. switch test.method {
  181. case "GET":
  182. m.Get(route, binding, handler)
  183. case "POST":
  184. m.Post(route, binding, handler)
  185. case "PUT":
  186. m.Put(route, binding, handler)
  187. case "DELETE":
  188. m.Delete(route, binding, handler)
  189. }
  190. req, err := http.NewRequest(test.method, route, strings.NewReader(test.payload))
  191. if err != nil {
  192. t.Error(err)
  193. }
  194. m.ServeHTTP(recorder, req)
  195. }
  196. }
  197. func testForm(t *testing.T, withInterface bool) {
  198. for index, test := range formTests {
  199. recorder := httptest.NewRecorder()
  200. handler := func(post BlogPost, errors Errors) { handle(test, t, index, post, errors) }
  201. binding := Form(BlogPost{})
  202. if withInterface {
  203. handler = func(post BlogPost, errors Errors) {
  204. post.Create(test, t, index)
  205. }
  206. binding = Form(BlogPost{}, (*Modeler)(nil))
  207. }
  208. m := martini.Classic()
  209. switch test.method {
  210. case "GET":
  211. m.Get(route, binding, handler)
  212. case "POST":
  213. m.Post(route, binding, handler)
  214. }
  215. req, err := http.NewRequest(test.method, test.path, nil)
  216. if err != nil {
  217. t.Error(err)
  218. }
  219. m.ServeHTTP(recorder, req)
  220. }
  221. }
  222. func testEmptyForm(t *testing.T) {
  223. for index, test := range emptyPayloadTests {
  224. recorder := httptest.NewRecorder()
  225. handler := func(section BlogSection, errors Errors) { handleEmpty(test, t, index, section, errors) }
  226. binding := Form(BlogSection{})
  227. m := martini.Classic()
  228. switch test.method {
  229. case "GET":
  230. m.Get(route, binding, handler)
  231. case "POST":
  232. m.Post(route, binding, handler)
  233. }
  234. req, err := http.NewRequest(test.method, test.path, nil)
  235. if err != nil {
  236. t.Error(err)
  237. }
  238. m.ServeHTTP(recorder, req)
  239. }
  240. }
  241. func testMultipart(t *testing.T, test testCase, middleware martini.Handler, handler martini.Handler, index int) *httptest.ResponseRecorder {
  242. recorder := httptest.NewRecorder()
  243. m := martini.Classic()
  244. m.Post(route, middleware, handler)
  245. body := &bytes.Buffer{}
  246. writer := multipart.NewWriter(body)
  247. writer.WriteField("title", test.ref.Title)
  248. writer.WriteField("content", test.ref.Content)
  249. writer.WriteField("views", strconv.Itoa(test.ref.Views))
  250. if len(test.ref.Multiple) != 0 {
  251. for _, value := range test.ref.Multiple {
  252. writer.WriteField("multiple", strconv.Itoa(value))
  253. }
  254. }
  255. req, err := http.NewRequest(test.method, test.path, body)
  256. req.Header.Add("Content-Type", writer.FormDataContentType())
  257. if err != nil {
  258. t.Error(err)
  259. }
  260. err = writer.Close()
  261. if err != nil {
  262. t.Error(err)
  263. }
  264. m.ServeHTTP(recorder, req)
  265. return recorder
  266. }
  267. func assertEqualField(t *testing.T, fieldname string, testcasenumber int, expected interface{}, got interface{}) {
  268. if expected != got {
  269. t.Errorf("%s: expected=%s, got=%s in test case %d\n", fieldname, expected, got, testcasenumber)
  270. }
  271. }
  272. func performValidationTest(data interface{}, handler func(Errors), t *testing.T) {
  273. recorder := httptest.NewRecorder()
  274. m := martini.Classic()
  275. m.Get(route, Validate(data), handler)
  276. req, err := http.NewRequest("GET", route, nil)
  277. if err != nil {
  278. t.Error("HTTP error:", err)
  279. }
  280. m.ServeHTTP(recorder, req)
  281. }
  282. func (self BlogPost) Validate(errors *Errors, req *http.Request) {
  283. if len(self.Title) < 4 {
  284. errors.Fields["Title"] = "Too short; minimum 4 characters"
  285. }
  286. if len(self.Content) > 1024 {
  287. errors.Fields["Content"] = "Too long; maximum 1024 characters"
  288. }
  289. if len(self.Content) < 5 {
  290. errors.Fields["Content"] = "Too short; minimum 5 characters"
  291. }
  292. }
  293. func (self BlogPost) Create(test testCase, t *testing.T, index int) {
  294. assertEqualField(t, "Title", index, test.ref.Title, self.Title)
  295. assertEqualField(t, "Content", index, test.ref.Content, self.Content)
  296. assertEqualField(t, "Views", index, test.ref.Views, self.Views)
  297. for i := range test.ref.Multiple {
  298. if i >= len(self.Multiple) {
  299. t.Errorf("Expected: %v (size %d) to have same size as: %v (size %d)", self.Multiple, len(self.Multiple), test.ref.Multiple, len(test.ref.Multiple))
  300. break
  301. }
  302. if test.ref.Multiple[i] != self.Multiple[i] {
  303. t.Errorf("Expected: %v to deep equal: %v", self.Multiple, test.ref.Multiple)
  304. break
  305. }
  306. }
  307. }
  308. func (self BlogSection) Create(test emptyPayloadTestCase, t *testing.T, index int) {
  309. // intentionally left empty
  310. }
  311. type (
  312. testCase struct {
  313. method string
  314. path string
  315. payload string
  316. contentType string
  317. ok bool
  318. ref *BlogPost
  319. }
  320. emptyPayloadTestCase struct {
  321. method string
  322. path string
  323. payload string
  324. contentType string
  325. ok bool
  326. ref *BlogSection
  327. }
  328. Modeler interface {
  329. Create(test testCase, t *testing.T, index int)
  330. }
  331. BlogPost struct {
  332. Title string `form:"title" json:"title" binding:"required"`
  333. Content string `form:"content" json:"content"`
  334. Views int `form:"views" json:"views"`
  335. internal int `form:"-"`
  336. Multiple []int `form:"multiple"`
  337. }
  338. BlogSection struct {
  339. Title string `form:"title" json:"title"`
  340. Content string `form:"content" json:"content"`
  341. }
  342. User struct {
  343. Name string `json:"name" binding:"required"`
  344. Home Address `json:"address" binding:"required"`
  345. }
  346. Address struct {
  347. Street1 string `json:"street1" binding:"required"`
  348. Street2 string `json:"street2"`
  349. }
  350. )
  351. var (
  352. bindTests = map[testCase]int{
  353. // These should bail at the deserialization/binding phase
  354. testCase{
  355. "POST",
  356. path,
  357. `{ bad JSON `,
  358. "application/json",
  359. false,
  360. new(BlogPost),
  361. }: http.StatusBadRequest,
  362. testCase{
  363. "POST",
  364. path,
  365. `not multipart but has content-type`,
  366. "multipart/form-data",
  367. false,
  368. new(BlogPost),
  369. }: http.StatusBadRequest,
  370. testCase{
  371. "POST",
  372. path,
  373. `no content-type and not URL-encoded or JSON"`,
  374. "",
  375. false,
  376. new(BlogPost),
  377. }: http.StatusBadRequest,
  378. // These should deserialize, then bail at the validation phase
  379. testCase{
  380. "POST",
  381. path + "?title= This is wrong ",
  382. `not URL-encoded but has content-type`,
  383. "x-www-form-urlencoded",
  384. false,
  385. new(BlogPost),
  386. }: 422, // according to comments in Form() -> although the request is not url encoded, ParseForm does not complain
  387. testCase{
  388. "GET",
  389. path + "?content=This+is+the+content",
  390. ``,
  391. "x-www-form-urlencoded",
  392. false,
  393. &BlogPost{Title: "", Content: "This is the content"},
  394. }: 422,
  395. testCase{
  396. "GET",
  397. path + "",
  398. `{"content":"", "title":"Blog Post Title"}`,
  399. "application/json",
  400. false,
  401. &BlogPost{Title: "Blog Post Title", Content: ""},
  402. }: 422,
  403. // These should succeed
  404. testCase{
  405. "GET",
  406. path + "",
  407. `{"content":"This is the content", "title":"Blog Post Title"}`,
  408. "application/json",
  409. true,
  410. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  411. }: http.StatusOK,
  412. testCase{
  413. "GET",
  414. path + "?content=This+is+the+content&title=Blog+Post+Title",
  415. ``,
  416. "",
  417. true,
  418. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  419. }: http.StatusOK,
  420. testCase{
  421. "GET",
  422. path + "?content=This is the content&title=Blog+Post+Title",
  423. `{"content":"This is the content", "title":"Blog Post Title"}`,
  424. "",
  425. true,
  426. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  427. }: http.StatusOK,
  428. testCase{
  429. "GET",
  430. path + "",
  431. `{"content":"This is the content", "title":"Blog Post Title"}`,
  432. "",
  433. true,
  434. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  435. }: http.StatusOK,
  436. }
  437. bindMultipartTests = map[testCase]int{
  438. // This should deserialize, then bail at the validation phase
  439. testCase{
  440. "POST",
  441. path,
  442. "",
  443. "multipart/form-data",
  444. false,
  445. &BlogPost{Title: "", Content: "This is the content"},
  446. }: 422,
  447. // This should succeed
  448. testCase{
  449. "POST",
  450. path,
  451. "",
  452. "multipart/form-data",
  453. true,
  454. &BlogPost{Title: "This is the Title", Content: "This is the content"},
  455. }: http.StatusOK,
  456. }
  457. formTests = []testCase{
  458. {
  459. "GET",
  460. path + "?content=This is the content",
  461. "",
  462. "",
  463. false,
  464. &BlogPost{Title: "", Content: "This is the content"},
  465. },
  466. {
  467. "POST",
  468. path + "?content=This+is+the+content&title=Blog+Post+Title&views=3",
  469. "",
  470. "",
  471. false, // false because POST requests should have a body, not just a query string
  472. &BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3},
  473. },
  474. {
  475. "GET",
  476. path + "?content=This+is+the+content&title=Blog+Post+Title&views=3&multiple=5&multiple=10&multiple=15&multiple=20",
  477. "",
  478. "",
  479. true,
  480. &BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}},
  481. },
  482. }
  483. multipartformTests = []testCase{
  484. {
  485. "POST",
  486. path,
  487. "",
  488. "multipart/form-data",
  489. false,
  490. &BlogPost{Title: "", Content: "This is the content"},
  491. },
  492. {
  493. "POST",
  494. path,
  495. "",
  496. "multipart/form-data",
  497. false,
  498. &BlogPost{Title: "Blog Post Title", Views: 3},
  499. },
  500. {
  501. "POST",
  502. path,
  503. "",
  504. "multipart/form-data",
  505. true,
  506. &BlogPost{Title: "Blog Post Title", Content: "This is the content", Views: 3, Multiple: []int{5, 10, 15, 20}},
  507. },
  508. }
  509. emptyPayloadTests = []emptyPayloadTestCase{
  510. {
  511. "GET",
  512. "",
  513. "",
  514. "",
  515. true,
  516. &BlogSection{},
  517. },
  518. {
  519. "POST",
  520. "",
  521. "",
  522. "",
  523. true,
  524. &BlogSection{},
  525. },
  526. {
  527. "PUT",
  528. "",
  529. "",
  530. "",
  531. true,
  532. &BlogSection{},
  533. },
  534. {
  535. "DELETE",
  536. "",
  537. "",
  538. "",
  539. true,
  540. &BlogSection{},
  541. },
  542. }
  543. jsonTests = []testCase{
  544. // bad requests
  545. {
  546. "GET",
  547. "",
  548. `{blah blah blah}`,
  549. "",
  550. false,
  551. &BlogPost{},
  552. },
  553. {
  554. "POST",
  555. "",
  556. `{asdf}`,
  557. "",
  558. false,
  559. &BlogPost{},
  560. },
  561. {
  562. "PUT",
  563. "",
  564. `{blah blah blah}`,
  565. "",
  566. false,
  567. &BlogPost{},
  568. },
  569. {
  570. "DELETE",
  571. "",
  572. `{;sdf _SDf- }`,
  573. "",
  574. false,
  575. &BlogPost{},
  576. },
  577. // Valid-JSON requests
  578. {
  579. "GET",
  580. "",
  581. `{"content":"This is the content"}`,
  582. "",
  583. false,
  584. &BlogPost{Title: "", Content: "This is the content"},
  585. },
  586. {
  587. "POST",
  588. "",
  589. `{}`,
  590. "application/json",
  591. false,
  592. &BlogPost{Title: "", Content: ""},
  593. },
  594. {
  595. "POST",
  596. "",
  597. `{"content":"This is the content", "title":"Blog Post Title"}`,
  598. "",
  599. true,
  600. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  601. },
  602. {
  603. "PUT",
  604. "",
  605. `{"content":"This is the content", "title":"Blog Post Title"}`,
  606. "",
  607. true,
  608. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  609. },
  610. {
  611. "DELETE",
  612. "",
  613. `{"content":"This is the content", "title":"Blog Post Title"}`,
  614. "",
  615. true,
  616. &BlogPost{Title: "Blog Post Title", Content: "This is the content"},
  617. },
  618. }
  619. )
  620. const (
  621. route = "/blogposts/create"
  622. path = "http://localhost:3000" + route
  623. )