response.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. package httpexpect
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "io/ioutil"
  6. "mime"
  7. "net/http"
  8. "reflect"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/ajg/form"
  14. )
  15. // StatusRange is enum for response status ranges.
  16. type StatusRange int
  17. const (
  18. // Status1xx defines "Informational" status codes.
  19. Status1xx StatusRange = 100
  20. // Status2xx defines "Success" status codes.
  21. Status2xx StatusRange = 200
  22. // Status3xx defines "Redirection" status codes.
  23. Status3xx StatusRange = 300
  24. // Status4xx defines "Client Error" status codes.
  25. Status4xx StatusRange = 400
  26. // Status5xx defines "Server Error" status codes.
  27. Status5xx StatusRange = 500
  28. )
  29. // Response provides methods to inspect attached http.Response object.
  30. type Response struct {
  31. chain chain
  32. resp *http.Response
  33. // Content "eaten" on makeResponse, so we can't manually read the `Expect().Raw().Body`
  34. // therefore we just export this for now, we have a solution like we do on Iris
  35. // to use a noop reader to not "eat" it but we don't need it here.
  36. // Usage: `Expect().Content`.
  37. Content []byte
  38. cookies []*http.Cookie
  39. time time.Duration
  40. }
  41. // NewResponse returns a new Response given a reporter used to report
  42. // failures and http.Response to be inspected.
  43. //
  44. // Both reporter and response should not be nil. If response is nil,
  45. // failure is reported.
  46. //
  47. // If duration is given, it defines response time to be reported by
  48. // response.Duration().
  49. func NewResponse(
  50. reporter Reporter, response *http.Response, duration ...time.Duration) *Response {
  51. var dr time.Duration
  52. if len(duration) > 0 {
  53. dr = duration[0]
  54. }
  55. return makeResponse(makeChain(reporter), response, dr)
  56. }
  57. func makeResponse(
  58. chain chain, response *http.Response, duration time.Duration) *Response {
  59. var content []byte
  60. var cookies []*http.Cookie
  61. if response != nil {
  62. content = getContent(&chain, response)
  63. cookies = response.Cookies()
  64. } else {
  65. chain.fail("expected non-nil response")
  66. }
  67. return &Response{
  68. chain: chain,
  69. resp: response,
  70. Content: content,
  71. cookies: cookies,
  72. time: duration,
  73. }
  74. }
  75. func getContent(chain *chain, resp *http.Response) []byte {
  76. if resp.Body == nil {
  77. return []byte{}
  78. }
  79. content, err := ioutil.ReadAll(resp.Body)
  80. if err != nil {
  81. chain.fail(err.Error())
  82. return nil
  83. }
  84. return content
  85. }
  86. // Raw returns underlying http.Response object.
  87. // This is the value originally passed to NewResponse.
  88. func (r *Response) Raw() *http.Response {
  89. return r.resp
  90. }
  91. // Duration returns a new Number object that may be used to inspect
  92. // response time, in nanoseconds.
  93. //
  94. // Response time is a time interval starting just before request is sent
  95. // and ending right after response is received, retrieved from monotonic
  96. // clock source.
  97. //
  98. // Example:
  99. // resp := NewResponse(t, response, time.Duration(10000000))
  100. // resp.Duration().Equal(10 * time.Millisecond)
  101. func (r *Response) Duration() *Number {
  102. return &Number{r.chain, float64(r.time)}
  103. }
  104. // Status succeeds if response contains given status code.
  105. //
  106. // Example:
  107. // resp := NewResponse(t, response)
  108. // resp.Status(http.StatusOK)
  109. func (r *Response) Status(status int) *Response {
  110. if r.chain.failed() {
  111. return r
  112. }
  113. r.checkEqual("status", statusCodeText(status), statusCodeText(r.resp.StatusCode))
  114. return r
  115. }
  116. // StatusRange succeeds if response status belongs to given range.
  117. //
  118. // Supported ranges:
  119. // - Status1xx - Informational
  120. // - Status2xx - Success
  121. // - Status3xx - Redirection
  122. // - Status4xx - Client Error
  123. // - Status5xx - Server Error
  124. //
  125. // See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.
  126. //
  127. // Example:
  128. // resp := NewResponse(t, response)
  129. // resp.StatusRange(Status2xx)
  130. func (r *Response) StatusRange(rn StatusRange) *Response {
  131. if r.chain.failed() {
  132. return r
  133. }
  134. status := statusCodeText(r.resp.StatusCode)
  135. actual := statusRangeText(r.resp.StatusCode)
  136. expected := statusRangeText(int(rn))
  137. if actual == "" || actual != expected {
  138. if actual == "" {
  139. r.chain.fail("\nexpected status from range:\n %q\n\nbut got:\n %q",
  140. expected, status)
  141. } else {
  142. r.chain.fail(
  143. "\nexpected status from range:\n %q\n\nbut got:\n %q (%q)",
  144. expected, actual, status)
  145. }
  146. }
  147. return r
  148. }
  149. func statusCodeText(code int) string {
  150. if s := http.StatusText(code); s != "" {
  151. return strconv.Itoa(code) + " " + s
  152. }
  153. return strconv.Itoa(code)
  154. }
  155. func statusRangeText(code int) string {
  156. switch {
  157. case code >= 100 && code < 200:
  158. return "1xx Informational"
  159. case code >= 200 && code < 300:
  160. return "2xx Success"
  161. case code >= 300 && code < 400:
  162. return "3xx Redirection"
  163. case code >= 400 && code < 500:
  164. return "4xx Client Error"
  165. case code >= 500 && code < 600:
  166. return "5xx Server Error"
  167. default:
  168. return ""
  169. }
  170. }
  171. // Headers returns a new Object that may be used to inspect header map.
  172. //
  173. // Example:
  174. // resp := NewResponse(t, response)
  175. // resp.Headers().Value("Content-Type").String().Equal("application-json")
  176. func (r *Response) Headers() *Object {
  177. var value map[string]interface{}
  178. if !r.chain.failed() {
  179. value, _ = canonMap(&r.chain, r.resp.Header)
  180. }
  181. return &Object{r.chain, value}
  182. }
  183. // Header returns a new String object that may be used to inspect given header.
  184. //
  185. // Example:
  186. // resp := NewResponse(t, response)
  187. // resp.Header("Content-Type").Equal("application-json")
  188. // resp.Header("Date").DateTime().Le(time.Now())
  189. func (r *Response) Header(header string) *String {
  190. value := ""
  191. if !r.chain.failed() {
  192. value = r.resp.Header.Get(header)
  193. }
  194. return &String{r.chain, value}
  195. }
  196. // Cookies returns a new Array object with all cookie names set by this response.
  197. // Returned Array contains a String value for every cookie name.
  198. //
  199. // Note that this returns only cookies set by Set-Cookie headers of this response.
  200. // It doesn't return session cookies from previous responses, which may be stored
  201. // in a cookie jar.
  202. //
  203. // Example:
  204. // resp := NewResponse(t, response)
  205. // resp.Cookies().Contains("session")
  206. func (r *Response) Cookies() *Array {
  207. if r.chain.failed() {
  208. return &Array{r.chain, nil}
  209. }
  210. names := []interface{}{}
  211. for _, c := range r.cookies {
  212. names = append(names, c.Name)
  213. }
  214. return &Array{r.chain, names}
  215. }
  216. // Cookie returns a new Cookie object that may be used to inspect given cookie
  217. // set by this response.
  218. //
  219. // Note that this returns only cookies set by Set-Cookie headers of this response.
  220. // It doesn't return session cookies from previous responses, which may be stored
  221. // in a cookie jar.
  222. //
  223. // Example:
  224. // resp := NewResponse(t, response)
  225. // resp.Cookie("session").Domain().Equal("example.com")
  226. func (r *Response) Cookie(name string) *Cookie {
  227. if r.chain.failed() {
  228. return &Cookie{r.chain, nil}
  229. }
  230. names := []string{}
  231. for _, c := range r.cookies {
  232. if c.Name == name {
  233. return &Cookie{r.chain, c}
  234. }
  235. names = append(names, c.Name)
  236. }
  237. r.chain.fail("\nexpected response with cookie:\n %q\n\nbut got only cookies:\n%s",
  238. name, dumpValue(names))
  239. return &Cookie{r.chain, nil}
  240. }
  241. // Body returns a new String object that may be used to inspect response body.
  242. //
  243. // Example:
  244. // resp := NewResponse(t, response)
  245. // resp.Body().NotEmpty()
  246. // resp.Body().Length().Equal(100)
  247. func (r *Response) Body() *String {
  248. return &String{r.chain, string(r.Content)}
  249. }
  250. // NoContent succeeds if response contains empty Content-Type header and
  251. // empty body.
  252. func (r *Response) NoContent() *Response {
  253. if r.chain.failed() {
  254. return r
  255. }
  256. contentType := r.resp.Header.Get("Content-Type")
  257. r.checkEqual("\"Content-Type\" header", "", contentType)
  258. r.checkEqual("body", "", string(r.Content))
  259. return r
  260. }
  261. // ContentType succeeds if response contains Content-Type header with given
  262. // media type and charset.
  263. //
  264. // If charset is omitted, and mediaType is non-empty, Content-Type header
  265. // should contain empty or utf-8 charset.
  266. //
  267. // If charset is omitted, and mediaType is also empty, Content-Type header
  268. // should contain no charset.
  269. func (r *Response) ContentType(mediaType string, charset ...string) *Response {
  270. r.checkContentType(mediaType, charset...)
  271. return r
  272. }
  273. // ContentEncoding succeeds if response has exactly given Content-Encoding list.
  274. // Common values are empty, "gzip", "compress", "deflate", "identity" and "br".
  275. func (r *Response) ContentEncoding(encoding ...string) *Response {
  276. if r.chain.failed() {
  277. return r
  278. }
  279. r.checkEqual("\"Content-Encoding\" header", encoding, r.resp.Header["Content-Encoding"])
  280. return r
  281. }
  282. // TransferEncoding succeeds if response contains given Transfer-Encoding list.
  283. // Common values are empty, "chunked" and "identity".
  284. func (r *Response) TransferEncoding(encoding ...string) *Response {
  285. if r.chain.failed() {
  286. return r
  287. }
  288. r.checkEqual("\"Transfer-Encoding\" header", encoding, r.resp.TransferEncoding)
  289. return r
  290. }
  291. // Text returns a new String object that may be used to inspect response body.
  292. //
  293. // Text succeeds if response contains "text/plain" Content-Type header
  294. // with empty or "utf-8" charset.
  295. //
  296. // Example:
  297. // resp := NewResponse(t, response)
  298. // resp.Text().Equal("hello, world!")
  299. func (r *Response) Text() *String {
  300. var content string
  301. if !r.chain.failed() && r.checkContentType("text/plain") {
  302. content = string(r.Content)
  303. }
  304. return &String{r.chain, content}
  305. }
  306. // Form returns a new Object that may be used to inspect form contents
  307. // of response.
  308. //
  309. // Form succeeds if response contains "application/x-www-form-urlencoded"
  310. // Content-Type header and if form may be decoded from response body.
  311. // Decoding is performed using https://github.com/ajg/form.
  312. //
  313. // Example:
  314. // resp := NewResponse(t, response)
  315. // resp.Form().Value("foo").Equal("bar")
  316. func (r *Response) Form() *Object {
  317. object := r.getForm()
  318. return &Object{r.chain, object}
  319. }
  320. func (r *Response) getForm() map[string]interface{} {
  321. if r.chain.failed() {
  322. return nil
  323. }
  324. if !r.checkContentType("application/x-www-form-urlencoded", "") {
  325. return nil
  326. }
  327. decoder := form.NewDecoder(bytes.NewReader(r.Content))
  328. var object map[string]interface{}
  329. if err := decoder.Decode(&object); err != nil {
  330. r.chain.fail(err.Error())
  331. return nil
  332. }
  333. return object
  334. }
  335. // JSON returns a new Value object that may be used to inspect JSON contents
  336. // of response.
  337. //
  338. // JSON succeeds if response contains "application/json" Content-Type header
  339. // with empty or "utf-8" charset and if JSON may be decoded from response body.
  340. //
  341. // Example:
  342. // resp := NewResponse(t, response)
  343. // resp.JSON().Array().Elements("foo", "bar")
  344. func (r *Response) JSON() *Value {
  345. value := r.getJSON()
  346. return &Value{r.chain, value}
  347. }
  348. func (r *Response) getJSON() interface{} {
  349. if r.chain.failed() {
  350. return nil
  351. }
  352. if !r.checkContentType("application/json") {
  353. return nil
  354. }
  355. var value interface{}
  356. if err := json.Unmarshal(r.Content, &value); err != nil {
  357. r.chain.fail(err.Error())
  358. return nil
  359. }
  360. return value
  361. }
  362. // JSONP returns a new Value object that may be used to inspect JSONP contents
  363. // of response.
  364. //
  365. // JSONP succeeds if response contains "application/javascript" Content-Type
  366. // header with empty or "utf-8" charset and response body of the following form:
  367. // callback(<valid json>);
  368. // or:
  369. // callback(<valid json>)
  370. //
  371. // Whitespaces are allowed.
  372. //
  373. // Example:
  374. // resp := NewResponse(t, response)
  375. // resp.JSONP("myCallback").Array().Elements("foo", "bar")
  376. func (r *Response) JSONP(callback string) *Value {
  377. value := r.getJSONP(callback)
  378. return &Value{r.chain, value}
  379. }
  380. var (
  381. jsonp = regexp.MustCompile(`^\s*([^\s(]+)\s*\((.*)\)\s*;*\s*$`)
  382. )
  383. func (r *Response) getJSONP(callback string) interface{} {
  384. if r.chain.failed() {
  385. return nil
  386. }
  387. if !r.checkContentType("application/javascript") {
  388. return nil
  389. }
  390. m := jsonp.FindSubmatch(r.Content)
  391. if len(m) != 3 || string(m[1]) != callback {
  392. r.chain.fail(
  393. "\nexpected JSONP body in form of:\n \"%s(<valid json>)\"\n\nbut got:\n %q\n",
  394. callback,
  395. string(r.Content))
  396. return nil
  397. }
  398. var value interface{}
  399. if err := json.Unmarshal(m[2], &value); err != nil {
  400. r.chain.fail(err.Error())
  401. return nil
  402. }
  403. return value
  404. }
  405. func (r *Response) checkContentType(expectedType string, expectedCharset ...string) bool {
  406. if r.chain.failed() {
  407. return false
  408. }
  409. contentType := r.resp.Header.Get("Content-Type")
  410. if expectedType == "" && len(expectedCharset) == 0 {
  411. if contentType == "" {
  412. return true
  413. }
  414. }
  415. mediaType, params, err := mime.ParseMediaType(contentType)
  416. if err != nil {
  417. r.chain.fail("\ngot invalid \"Content-Type\" header %q", contentType)
  418. return false
  419. }
  420. if mediaType != expectedType {
  421. r.chain.fail(
  422. "\nexpected \"Content-Type\" header with %q media type,"+
  423. "\nbut got %q", expectedType, mediaType)
  424. return false
  425. }
  426. charset := params["charset"]
  427. if len(expectedCharset) == 0 {
  428. if charset != "" && !strings.EqualFold(charset, "utf-8") {
  429. r.chain.fail(
  430. "\nexpected \"Content-Type\" header with \"utf-8\" or empty charset,"+
  431. "\nbut got %q", charset)
  432. return false
  433. }
  434. } else {
  435. if !strings.EqualFold(charset, expectedCharset[0]) {
  436. r.chain.fail(
  437. "\nexpected \"Content-Type\" header with %q charset,"+
  438. "\nbut got %q", expectedCharset[0], charset)
  439. return false
  440. }
  441. }
  442. return true
  443. }
  444. func (r *Response) checkEqual(what string, expected, actual interface{}) {
  445. if !reflect.DeepEqual(expected, actual) {
  446. r.chain.fail("\nexpected %s equal to:\n%s\n\nbut got:\n%s", what,
  447. dumpValue(expected), dumpValue(actual))
  448. }
  449. }