Update dependencies (#32)

This commit is contained in:
Joona Hoikkala
2018-01-22 11:19:33 +02:00
committed by GitHub
parent 733245fb3d
commit c5337fc841
1727 changed files with 697926 additions and 12716 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"Deadline": "30s",
"LineLength": 90,
"Enable": [
"gotype",
"goimports",
"golint",
"gosimple",
"gofmt",
"vet",
"lll",
"staticcheck",
"ineffassign",
"deadcode",
"unused",
"unconvert",
"misspell",
"test",
"testify"
]
}
+19
View File
@@ -0,0 +1,19 @@
language: go
sudo: false
go:
- 1.7
- 1.8
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- go get github.com/alecthomas/gometalinter
- ${GOPATH}/bin/gometalinter --install
script:
- go get -v -t . ./_examples
- go test -coverprofile profile.cov .
- go test ./_examples
- ${GOPATH}/bin/gometalinter --config .gometalinter
- ${GOPATH}/bin/goveralls -coverprofile profile.cov -service=travis-ci
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Victor Gaydov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+15
View File
@@ -0,0 +1,15 @@
all: update test check
update:
go get -u -t . ./_examples
test:
go test . ./_examples
check:
go get -u github.com/alecthomas/gometalinter
gometalinter --install
gometalinter --config .gometalinter
fmt:
gofmt -s -w . ./_examples
+455
View File
@@ -0,0 +1,455 @@
# httpexpect [![GoDoc](https://godoc.org/github.com/gavv/httpexpect?status.svg)](https://godoc.org/github.com/gavv/httpexpect) [![Gitter](https://badges.gitter.im/gavv/httpexpect.svg)](https://gitter.im/gavv/httpexpect?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Travis](https://img.shields.io/travis/gavv/httpexpect.svg)](https://travis-ci.org/gavv/httpexpect) [![Coveralls](https://coveralls.io/repos/github/gavv/httpexpect/badge.svg?branch=master)](https://coveralls.io/github/gavv/httpexpect?branch=master)
Concise, declarative, and easy to use end-to-end HTTP and REST API testing for Go (golang).
Basically, httpexpect is a set of chainable *builders* for HTTP requests and *assertions* for HTTP responses and payload, on top of net/http and several utility packages.
Workflow:
* Incrementally build HTTP requests.
* Inspect HTTP responses.
* Inspect response payload recursively.
## Features
##### Request builder
* URL path construction, with simple string interpolation provided by [`go-interpol`](https://github.com/imkira/go-interpol) package.
* URL query parameters (encoding using [`go-querystring`](https://github.com/google/go-querystring) package).
* Headers, cookies, payload: JSON, urlencoded or multipart forms (encoding using [`form`](https://github.com/ajg/form) package), plain text.
* Create custom [request builders](#reusable-builders) that can be reused.
##### Response assertions
* Response status, predefined status ranges.
* Headers, cookies, payload: JSON, JSONP, forms, text.
* Round-trip time.
##### Payload assertions
* Type-specific assertions, supported types: object, array, string, number, boolean, null, datetime.
* Regular expressions.
* Simple JSON queries (using subset of [JSONPath](http://goessner.net/articles/JsonPath/)), provided by [`jsonpath`](https://github.com/yalp/jsonpath) package.
* [JSON Schema](http://json-schema.org/) validation, provided by [`gojsonschema`](https://github.com/xeipuuv/gojsonschema) package.
##### Pretty printing
* Verbose error messages.
* JSON diff is produced on failure using [`gojsondiff`](https://github.com/yudai/gojsondiff/) package.
* Failures are reported using [`testify`](https://github.com/stretchr/testify/) (`assert` or `require` package) or standard `testing` package.
* Dumping requests and responses in various formats, using [`httputil`](https://golang.org/pkg/net/http/httputil/), [`http2curl`](https://github.com/moul/http2curl), or simple compact logger.
##### Tuning
* Tests can communicate with server via real HTTP client or invoke `net/http` or [`fasthttp`](https://github.com/valyala/fasthttp/) handler directly.
* Custom HTTP client, logger, printer, and failure reporter may be provided by user.
* Custom HTTP request factory may be provided, e.g. from the Google App Engine testing.
## Status
Stable branches are available on [`gopkg.in`](http://labix.org/gopkg.in) and will not introduce backward-incompatible changes.
Current stable branch is [`v1`](http://gopkg.in/gavv/httpexpect.v1):
```go
import "gopkg.in/gavv/httpexpect.v1"
```
Development is done in `master` branch on github:
```go
import "github.com/gavv/httpexpect"
```
## Documentation
Documentation is available on [GoDoc](https://godoc.org/github.com/gavv/httpexpect). It contains an overview and reference.
## Examples
See [`_examples`](_examples) directory for complete standalone examples.
* [`fruits_test.go`](_examples/fruits_test.go)
Testing a simple CRUD server made with bare `net/http`.
* [`iris_test.go`](_examples/iris_test.go)
Testing a server made with [`iris`](https://github.com/kataras/iris/) framework. Example includes JSON queries and validation, URL and form parameters, basic auth, sessions, and streaming. Tests invoke the `http.Handler` directly.
* [`echo_test.go`](_examples/echo_test.go)
Testing a server with JWT authentication made with [`echo`](https://github.com/labstack/echo/) framework. Tests use either HTTP client or invoke the `http.Handler` directly.
* [`fasthttp_test.go`](_examples/fasthttp_test.go)
Testing a server made with [`fasthttp`](https://github.com/valyala/fasthttp) package. Tests invoke the `fasthttp.RequestHandler` directly.
* [`gae_test.go`](_examples/gae_test.go)
Testing a server running under the [Google App Engine](https://en.wikipedia.org/wiki/Google_App_Engine).
## Quick start
##### Hello, world!
```go
package example
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect"
)
func TestFruits(t *testing.T) {
// create http.Handler
handler := FruitsHandler()
// run server using httptest
server := httptest.NewServer(handler)
defer server.Close()
// create httpexpect instance
e := httpexpect.New(t, server.URL)
// is it working?
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Empty()
}
```
##### JSON
```go
orange := map[string]interface{}{
"weight": 100,
}
e.PUT("/fruits/orange").WithJSON(orange).
Expect().
Status(http.StatusNoContent).NoContent()
e.GET("/fruits/orange").
Expect().
Status(http.StatusOK).
JSON().Object().ContainsKey("weight").ValueEqual("weight", 100)
apple := map[string]interface{}{
"colors": []interface{}{"green", "red"},
"weight": 200,
}
e.PUT("/fruits/apple").WithJSON(apple).
Expect().
Status(http.StatusNoContent).NoContent()
obj := e.GET("/fruits/apple").
Expect().
Status(http.StatusOK).JSON().Object()
obj.Keys().ContainsOnly("colors", "weight")
obj.Value("colors").Array().Elements("green", "red")
obj.Value("colors").Array().Element(0).String().Equal("green")
obj.Value("colors").Array().Element(1).String().Equal("red")
obj.Value("colors").Array().First().String().Equal("green")
obj.Value("colors").Array().Last().String().Equal("red")
```
##### JSON Schema and JSON Path
```go
schema := `{
"type": "array",
"items": {
"type": "object",
"properties": {
...
"private": {
"type": "boolean"
}
}
}
}`
repos := e.GET("/repos/octocat").
Expect().
Status(http.StatusOK).JSON()
// validate JSON schema
repos.Schema(schema)
// run JSONPath query and iterate results
for _, private := range repos.Path("$..private").Array().Iter() {
private.Boolean().False()
}
```
##### Forms
```go
// post form encoded from struct or map
e.POST("/form").WithForm(structOrMap).
Expect().
Status(http.StatusOK)
// set individual fields
e.POST("/form").WithFormField("foo", "hello").WithFormField("bar", 123).
Expect().
Status(http.StatusOK)
// multipart form
e.POST("/form").WithMultipart().
WithFile("avatar", "./john.png").WithFormField("username", "john").
Expect().
Status(http.StatusOK)
```
##### URL construction
```go
// construct path using ordered parameters
e.GET("/repos/{user}/{repo}", "octocat", "hello-world").
Expect().
Status(http.StatusOK)
// construct path using named parameters
e.GET("/repos/{user}/{repo}").
WithPath("user", "octocat").WithPath("repo", "hello-world").
Expect().
Status(http.StatusOK)
// set query parameters
e.GET("/repos/{user}", "octocat").WithQuery("sort", "asc").
Expect().
Status(http.StatusOK) // "/repos/octocat?sort=asc"
```
##### Headers
```go
// set If-Match
e.POST("/users/john").WithHeader("If-Match", etag).WithJSON(john).
Expect().
Status(http.StatusOK)
// check ETag
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("ETag").NotEmpty()
// check Date
t := time.Now()
e.GET("/users/john").
Expect().
Status(http.StatusOK).Header("Date").DateTime().InRange(t, time.Now())
```
##### Cookies
```go
// set cookie
t := time.Now()
e.POST("/users/john").WithCookie("session", sessionID).WithJSON(john).
Expect().
Status(http.StatusOK)
// check cookies
c := e.GET("/users/john").
Expect().
Status(http.StatusOK).Cookie("session")
c.Value().Equal(sessionID)
c.Domain().Equal("example.com")
c.Path().Equal("/")
c.Expires().InRange(t, t.Add(time.Hour * 24))
```
##### Regular expressions
```go
// simple match
e.GET("/users/john").
Expect().
Header("Location").
Match("http://(.+)/users/(.+)").Values("example.com", "john")
// check capture groups by index or name
m := e.GET("/users/john").
Expect().
Header("Location").Match("http://(?P<host>.+)/users/(?P<user>.+)")
m.Index(0).Equal("http://example.com/users/john")
m.Index(1).Equal("example.com")
m.Index(2).Equal("john")
m.Name("host").Equal("example.com")
m.Name("user").Equal("john")
```
##### Subdomains and per-request URL
```go
e.GET("/path").WithURL("http://example.com").
Expect().
Status(http.StatusOK)
e.GET("/path").WithURL("http://subdomain.example.com").
Expect().
Status(http.StatusOK)
```
##### Reusable builders
```go
e := httpexpect.New(t, "http://example.com")
r := e.POST("/login").WithForm(Login{"ford", "betelgeuse7"}).
Expect().
Status(http.StatusOK).JSON().Object()
token := r.Value("token").String().Raw()
auth := e.Builder(func (req *httpexpect.Request) {
req.WithHeader("Authorization", "Bearer "+token)
})
auth.GET("/restricted").
Expect().
Status(http.StatusOK)
e.GET("/restricted").
Expect().
Status(http.StatusUnauthorized)
```
##### Custom config
```go
e := httpexpect.WithConfig(httpexpect.Config{
// prepend this url to all requests
BaseURL: "http://example.com",
// use http.Client with a cookie jar and timeout
Client: &http.Client{
Jar: httpexpect.NewJar(),
Timeout: time.Second * 30,
},
// use fatal failures
Reporter: httpexpect.NewRequireReporter(t),
// use verbose logging
Printers: []httpexpect.Printer{
httpexpect.NewCurlPrinter(t),
httpexpect.NewDebugPrinter(t, true),
},
})
```
##### Session support
```go
// cookie jar is used to store cookies from server
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: httpexpect.NewJar(), // used by default if Client is nil
},
})
// cookies are disabled
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})
```
##### Use HTTP handler directly
```go
// invoke http.Handler directly using httpexpect.Binder
var handler http.Handler = MyHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewJar(),
},
})
// invoke fasthttp.RequestHandler directly using httpexpect.FastBinder
var handler fasthttp.RequestHandler = myHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewFastBinder(handler),
Jar: httpexpect.NewJar(),
},
})
```
##### Per-request client or handler
```go
e := httpexpect.New(t, server.URL)
client := &http.Client{
Transport: &http.Transport{
DisableCompression: true,
},
}
// overwrite client
e.GET("/path").WithClient(client).
Expect().
Status(http.StatusOK)
// construct client that invokes a handler directly and overwrite client
e.GET("/path").WithHandler(handler).
Expect().
Status(http.StatusOK)
```
## Similar packages
* [`gorequest`](https://github.com/parnurzeal/gorequest)
* [`gabs`](https://github.com/Jeffail/gabs)
* [`gofight`](https://github.com/appleboy/gofight)
* [`baloo`](https://github.com/h2non/baloo)
* [`frisby`](https://github.com/verdverm/frisby)
* [`forest`](https://github.com/emicklei/forest)
* [`restit`](https://github.com/go-restit/restit)
* [`supertest`](https://github.com/haoxins/supertest)
* [`http-test`](https://github.com/vsco/http-test)
* [`go-json-rest/rest/test`](https://godoc.org/github.com/ant0ine/go-json-rest/rest/test)
## Contributing
Feel free to report bugs, suggest improvements, and send pull requests! Please add documentation and tests for new features.
Update dependencies, build code, and run tests and linters:
```
$ make
```
Format code:
```
$ make fmt
```
## License
[MIT](LICENSE)
+2
View File
@@ -0,0 +1,2 @@
// Package examples demonstrates httpexpect usage.
package examples
+49
View File
@@ -0,0 +1,49 @@
package examples
import (
"net/http"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
// EchoHandler creates http.Handler using echo framework.
//
// Routes:
// GET /login authenticate user and return JWT token
// GET /restricted/hello return "hello, world!" (requires authentication)
func EchoHandler() http.Handler {
e := echo.New()
e.POST("/login", func(ctx echo.Context) error {
username := ctx.FormValue("username")
password := ctx.FormValue("password")
if username == "ford" && password == "betelgeuse7" {
// create token
token := jwt.New(jwt.SigningMethodHS256)
// generate encoded token and send it as response
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return ctx.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
return echo.ErrUnauthorized
})
r := e.Group("/restricted")
r.Use(middleware.JWT([]byte("secret")))
r.GET("/hello", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "hello, world!")
})
return e
}
+87
View File
@@ -0,0 +1,87 @@
package examples
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect"
)
// Echo JWT token authentication tests.
//
// This test is executed for the EchoHandler() in two modes:
// - via http client
// - via http.Handler
func testEcho(e *httpexpect.Expect) {
type Login struct {
Username string `form:"username"`
Password string `form:"password"`
}
e.POST("/login").WithForm(Login{"ford", "<bad password>"}).
Expect().
Status(http.StatusUnauthorized)
r := e.POST("/login").WithForm(Login{"ford", "betelgeuse7"}).
Expect().
Status(http.StatusOK).JSON().Object()
r.Keys().ContainsOnly("token")
token := r.Value("token").String().Raw()
e.GET("/restricted/hello").
Expect().
Status(http.StatusBadRequest)
e.GET("/restricted/hello").WithHeader("Authorization", "Bearer <bad token>").
Expect().
Status(http.StatusUnauthorized)
e.GET("/restricted/hello").WithHeader("Authorization", "Bearer "+token).
Expect().
Status(http.StatusOK).Body().Equal("hello, world!")
auth := e.Builder(func(req *httpexpect.Request) {
req.WithHeader("Authorization", "Bearer "+token)
})
auth.GET("/restricted/hello").
Expect().
Status(http.StatusOK).Body().Equal("hello, world!")
}
func TestEchoClient(t *testing.T) {
handler := EchoHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: server.URL,
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})
testEcho(e)
}
func TestEchoHandler(t *testing.T) {
handler := EchoHandler()
e := httpexpect.WithConfig(httpexpect.Config{
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewJar(),
},
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})
testEcho(e)
}
+23
View File
@@ -0,0 +1,23 @@
package examples
import (
"github.com/valyala/fasthttp"
)
// FastHTTPHandler creates fasthttp.RequestHandler.
//
// Routes:
// GET /ping return "pong"
func FastHTTPHandler() fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/ping":
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetContentType("text/plain")
ctx.SetBody([]byte("pong"))
default:
panic("unsupported path")
}
}
}
+29
View File
@@ -0,0 +1,29 @@
package examples
import (
"net/http"
"testing"
"github.com/gavv/httpexpect"
)
// fastHTTPTester returns a new Expect instance to test FastHTTPHandler().
func fastHTTPTester(t *testing.T) *httpexpect.Expect {
return httpexpect.WithConfig(httpexpect.Config{
// Pass requests directly to FastHTTPHandler.
Client: &http.Client{
Transport: httpexpect.NewFastBinder(FastHTTPHandler()),
Jar: httpexpect.NewJar(),
},
// Report errors using testify.
Reporter: httpexpect.NewAssertReporter(t),
})
}
func TestFastHTTP(t *testing.T) {
e := fastHTTPTester(t)
e.GET("/ping").Expect().
Status(200).
Text().Equal("pong")
}
+85
View File
@@ -0,0 +1,85 @@
package examples
import (
"encoding/json"
"net/http"
"path"
)
type (
fruitMap map[string]interface{}
)
// FruitsHandler creates http.Handler for the fruits server.
//
// Routes:
// GET /fruits get fruit list
// GET /fruits/{name} get fruit
// PUT /fruits/{name} add or update fruit
func FruitsHandler() http.Handler {
fruits := fruitMap{}
mux := http.NewServeMux()
mux.HandleFunc("/fruits", func(w http.ResponseWriter, r *http.Request) {
handleFruitList(fruits, w, r)
})
mux.HandleFunc("/fruits/", func(w http.ResponseWriter, r *http.Request) {
handleFruit(fruits, w, r)
})
return mux
}
func handleFruitList(fruits fruitMap, w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
ret := []string{}
for k := range fruits {
ret = append(ret, k)
}
b, err := json.Marshal(ret)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func handleFruit(fruits fruitMap, w http.ResponseWriter, r *http.Request) {
_, name := path.Split(r.URL.Path)
switch r.Method {
case "GET":
if data, ok := fruits[name]; ok {
b, err := json.Marshal(data)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
} else {
w.WriteHeader(http.StatusNotFound)
}
case "PUT":
var data map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
panic(err)
}
fruits[name] = data
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
+68
View File
@@ -0,0 +1,68 @@
package examples
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gavv/httpexpect"
)
func TestFruits(t *testing.T) {
handler := FruitsHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := httpexpect.New(t, server.URL)
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().Empty()
orange := map[string]interface{}{
"weight": 100,
}
e.PUT("/fruits/orange").WithJSON(orange).
Expect().
Status(http.StatusNoContent).NoContent()
apple := map[string]interface{}{
"colors": []interface{}{"green", "red"},
"weight": 200,
}
e.PUT("/fruits/apple").WithJSON(apple).
Expect().
Status(http.StatusNoContent).NoContent()
e.GET("/fruits").
Expect().
Status(http.StatusOK).JSON().Array().ContainsOnly("orange", "apple")
e.GET("/fruits/orange").
Expect().
Status(http.StatusOK).JSON().Object().Equal(orange).NotEqual(apple)
e.GET("/fruits/orange").
Expect().
Status(http.StatusOK).
JSON().Object().ContainsKey("weight").ValueEqual("weight", 100)
obj := e.GET("/fruits/apple").
Expect().
Status(http.StatusOK).JSON().Object()
obj.Keys().ContainsOnly("colors", "weight")
obj.Value("colors").Array().Elements("green", "red")
obj.Value("colors").Array().Element(0).String().Equal("green")
obj.Value("colors").Array().Element(1).String().Equal("red")
obj.Value("weight").Number().Equal(200)
e.GET("/fruits/melon").
Expect().
Status(http.StatusNotFound)
}
+22
View File
@@ -0,0 +1,22 @@
package examples
import (
"net/http"
"google.golang.org/appengine"
)
// GaeHandler creates http.Handler to run in the Google App Engine.
//
// Routes:
// GET /ping return "pong"
func GaeHandler() http.Handler {
m := http.NewServeMux()
m.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
_ = appengine.NewContext(r)
w.Write([]byte("pong"))
})
return m
}
+64
View File
@@ -0,0 +1,64 @@
package examples
import (
"net/http"
"os"
"testing"
"github.com/gavv/httpexpect"
"google.golang.org/appengine/aetest"
)
// init() is used by GAE to start serving the app
// added here for illustration purposes
//
// func init() {
// http.Handle("/", GaeHandler())
// }
// gaeInstance is our global dev_appserver instance.
var gaeInstance aetest.Instance
// TestMain is called first to create the gaeInstance.
func TestMain(m *testing.M) {
// INFO: Remove the return to actually run the tests.
// Requires installed Google Appengine SDK.
// https://cloud.google.com/appengine/downloads
return
var err error
gaeInstance, err = aetest.NewInstance(nil)
if err != nil {
panic(err)
}
c := m.Run() // call all actual tests
gaeInstance.Close()
os.Exit(c)
}
// gaeTester returns a new Expect instance to test GaeHandler().
func gaeTester(t *testing.T) *httpexpect.Expect {
return httpexpect.WithConfig(httpexpect.Config{
// Use gaeInstance to create requests.
// aetest.Instance is compatible with httpexpect.RequestFactory.
RequestFactory: gaeInstance,
// Pass requests directly to GaeHandler.
Client: &http.Client{
Transport: httpexpect.NewBinder(GaeHandler()),
Jar: httpexpect.NewJar(),
},
// Report errors using testify.
Reporter: httpexpect.NewAssertReporter(t),
})
}
func TestGae(t *testing.T) {
e := gaeTester(t)
e.GET("/ping").Expect().
Status(200).
Text().Equal("pong")
}
+111
View File
@@ -0,0 +1,111 @@
// +build go1.8
package examples
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"gopkg.in/kataras/iris.v6"
"gopkg.in/kataras/iris.v6/adaptors/httprouter"
"gopkg.in/kataras/iris.v6/adaptors/sessions"
"gopkg.in/kataras/iris.v6/middleware/basicauth"
)
// IrisHandler tests iris v6's handler
func IrisHandler() http.Handler {
app := iris.New(iris.Configuration{VHost: "example.com"})
app.Adapt(iris.DevLogger())
app.Adapt(httprouter.New())
app.Adapt(sessions.New(sessions.Config{Cookie: "irissessionid"}))
app.Get("/things", func(c *iris.Context) {
c.JSON(iris.StatusOK, []interface{}{
iris.Map{
"name": "foo",
"description": "foo thing",
},
iris.Map{
"name": "bar",
"description": "bar thing",
},
})
})
app.Post("/redirect", func(c *iris.Context) {
c.Redirect("/things", iris.StatusFound)
})
app.Post("/params/:x/:y", func(c *iris.Context) {
c.JSON(iris.StatusOK, iris.Map{
"x": c.Param("x"),
"y": c.Param("y"),
"q": c.URLParam("q"),
"p1": c.FormValue("p1"),
"p2": c.FormValue("p2"),
})
})
auth := basicauth.Default(map[string]string{
"ford": "betelgeuse7",
})
app.Get("/auth", auth, func(c *iris.Context) {
c.Writef("authenticated!")
})
app.Post("/session/set", func(c *iris.Context) {
sess := iris.Map{}
if err := c.ReadJSON(&sess); err != nil {
panic(err.Error())
}
c.Session().Set("name", sess["name"])
})
app.Get("/session/get", func(c *iris.Context) {
name := c.Session().GetString("name")
c.JSON(iris.StatusOK, iris.Map{
"name": name,
})
})
app.Get("/stream", func(c *iris.Context) {
c.StreamWriter(func(w io.Writer) bool {
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "%d", i)
}
// return true to continue, return false to stop and flush
return false
})
// if we had to write here then the StreamWriter callback should
// return true
})
app.Post("/stream", func(c *iris.Context) {
body, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.EmitError(iris.StatusBadRequest)
return
}
c.Write(body)
})
sub := app.Party("subdomain.")
sub.Post("/set", func(c *iris.Context) {
c.Session().Set("message", "hello from subdomain")
})
sub.Get("/get", func(c *iris.Context) {
c.Writef(c.Session().GetString("message"))
})
app.Boot()
return app.Router
}
+161
View File
@@ -0,0 +1,161 @@
// +build go1.8
package examples
import (
"net/http"
"strings"
"testing"
"github.com/gavv/httpexpect"
)
func irisTester(t *testing.T) *httpexpect.Expect {
handler := IrisHandler()
return httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Client: &http.Client{
Transport: httpexpect.NewBinder(handler),
Jar: httpexpect.NewJar(),
},
Reporter: httpexpect.NewAssertReporter(t),
Printers: []httpexpect.Printer{
httpexpect.NewDebugPrinter(t, true),
},
})
}
func TestIrisThings(t *testing.T) {
e := irisTester(t)
schema := `{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"description": {"type": "string"}
},
"required": ["name", "description"]
}
}`
things := e.GET("/things").
Expect().
Status(http.StatusOK).JSON()
things.Schema(schema)
names := things.Path("$[*].name").Array()
names.Elements("foo", "bar")
for n, desc := range things.Path("$..description").Array().Iter() {
m := desc.String().Match("(.+) (.+)")
m.Index(1).Equal(names.Element(n).String().Raw())
m.Index(2).Equal("thing")
}
}
func TestIrisRedirect(t *testing.T) {
e := irisTester(t)
things := e.POST("/redirect").
Expect().
Status(http.StatusOK).JSON().Array()
things.Length().Equal(2)
things.Element(0).Object().ValueEqual("name", "foo")
things.Element(1).Object().ValueEqual("name", "bar")
}
func TestIrisParams(t *testing.T) {
e := irisTester(t)
type Form struct {
P1 string `form:"p1"`
P2 string `form:"p2"`
}
// POST /params/xxx/yyy?q=qqq
// Form: p1=P1&p2=P2
r := e.POST("/params/{x}/{y}", "xxx", "yyy").
WithQuery("q", "qqq").WithForm(Form{P1: "P1", P2: "P2"}).
Expect().
Status(http.StatusOK).JSON().Object()
r.Value("x").Equal("xxx")
r.Value("y").Equal("yyy")
r.Value("q").Equal("qqq")
r.ValueEqual("p1", "P1")
r.ValueEqual("p2", "P2")
}
func TestIrisAuth(t *testing.T) {
e := irisTester(t)
e.GET("/auth").
Expect().
Status(http.StatusUnauthorized)
e.GET("/auth").WithBasicAuth("ford", "<bad password>").
Expect().
Status(http.StatusUnauthorized)
e.GET("/auth").WithBasicAuth("ford", "betelgeuse7").
Expect().
Status(http.StatusOK).Body().Equal("authenticated!")
}
func TestIrisSession(t *testing.T) {
e := irisTester(t)
e.POST("/session/set").WithJSON(map[string]string{"name": "test"}).
Expect().
Status(http.StatusOK).Cookies().NotEmpty()
r := e.GET("/session/get").
Expect().
Status(http.StatusOK).JSON().Object()
r.Equal(map[string]string{
"name": "test",
})
}
func TestIrisStream(t *testing.T) {
e := irisTester(t)
e.GET("/stream").
Expect().
Status(http.StatusOK).
TransferEncoding("chunked"). // ensure server sent chunks
Body().Equal("0123456789")
// send chunks to server
e.POST("/stream").WithChunked(strings.NewReader("<long text>")).
Expect().
Status(http.StatusOK).Body().Equal("<long text>")
}
func TestIrisSubdomain(t *testing.T) {
e := irisTester(t)
sub := e.Builder(func(req *httpexpect.Request) {
req.WithURL("http://subdomain.127.0.0.1")
})
sub.POST("/set").
Expect().
Status(http.StatusOK)
sub.GET("/get").
Expect().
Status(http.StatusOK).
Body().Equal("hello from subdomain")
}
+299
View File
@@ -0,0 +1,299 @@
package httpexpect
import (
"reflect"
)
// Array provides methods to inspect attached []interface{} object
// (Go representation of JSON array).
type Array struct {
chain chain
value []interface{}
}
// NewArray returns a new Array given a reporter used to report failures
// and value to be inspected.
//
// Both reporter and value should not be nil. If value is nil, failure is
// reported.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
func NewArray(reporter Reporter, value []interface{}) *Array {
chain := makeChain(reporter)
if value == nil {
chain.fail("expected non-nil array value")
} else {
value, _ = canonArray(&chain, value)
}
return &Array{chain, value}
}
// Raw returns underlying value attached to Array.
// This is the value originally passed to NewArray, converted to canonical form.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// assert.Equal(t, []interface{}{"foo", 123.0}, array.Raw())
func (a *Array) Raw() []interface{} {
return a.value
}
// Path is similar to Value.Path.
func (a *Array) Path(path string) *Value {
return getPath(&a.chain, a.value, path)
}
// Schema is similar to Value.Schema.
func (a *Array) Schema(schema interface{}) *Array {
checkSchema(&a.chain, a.value, schema)
return a
}
// Length returns a new Number object that may be used to inspect array length.
//
// Example:
// array := NewArray(t, []interface{}{1, 2, 3})
// array.Length().Equal(3)
func (a *Array) Length() *Number {
return &Number{a.chain, float64(len(a.value))}
}
// Element returns a new Value object that may be used to inspect array element
// for given index.
//
// If index is out of array bounds, Element reports failure and returns empty
// (but non-nil) value.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.Element(0).String().Equal("foo")
// array.Element(1).Number().Equal(123)
func (a *Array) Element(index int) *Value {
if index < 0 || index >= len(a.value) {
a.chain.fail(
"\narray index out of bounds:\n index %d\n\n bounds [%d; %d)",
index,
0,
len(a.value))
return &Value{a.chain, nil}
}
return &Value{a.chain, a.value[index]}
}
// First returns a new Value object that may be used to inspect first element
// of given array.
//
// If given array is empty, First reports failure and returns empty
// (but non-nil) value.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.First().String().Equal("foo")
func (a *Array) First() *Value {
if len(a.value) < 1 {
a.chain.fail("\narray is empty")
return &Value{a.chain, nil}
}
return &Value{a.chain, a.value[0]}
}
// Last returns a new Value object that may be used to inspect last element
// of given array.
//
// If given array is empty, Last reports failure and returns empty
// (but non-nil) value.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.Last().Number().Equal(123)
func (a *Array) Last() *Value {
if len(a.value) < 1 {
a.chain.fail("\narray is empty")
return &Value{a.chain, nil}
}
return &Value{a.chain, a.value[len(a.value)-1]}
}
// Iter returns a new slice of Values attached to array elements.
//
// Example:
// strings := []interface{}{"foo", "bar"}
// array := NewArray(t, strings)
//
// for n, val := range array.Iter() {
// val.String().Equal(strings[n])
// }
func (a *Array) Iter() []Value {
if a.chain.failed() {
return []Value{}
}
ret := []Value{}
for n := range a.value {
ret = append(ret, Value{a.chain, a.value[n]})
}
return ret
}
// Empty succeeds if array is empty.
//
// Example:
// array := NewArray(t, []interface{}{})
// array.Empty()
func (a *Array) Empty() *Array {
return a.Equal([]interface{}{})
}
// NotEmpty succeeds if array is non-empty.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.NotEmpty()
func (a *Array) NotEmpty() *Array {
return a.NotEqual([]interface{}{})
}
// Equal succeeds if array is equal to another array.
// Before comparison, both arrays are converted to canonical form.
//
// value should be slice of any type.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.Equal([]interface{}{"foo", 123})
//
// array := NewArray(t, []interface{}{"foo", "bar"})
// array.Equal([]string{}{"foo", "bar"})
//
// array := NewArray(t, []interface{}{123, 456})
// array.Equal([]int{}{123, 456})
func (a *Array) Equal(value interface{}) *Array {
expected, ok := canonArray(&a.chain, value)
if !ok {
return a
}
if !reflect.DeepEqual(expected, a.value) {
a.chain.fail("\nexpected array equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
dumpValue(expected),
dumpValue(a.value),
diffValues(expected, a.value))
}
return a
}
// NotEqual succeeds if array is not equal to another array.
// Before comparison, both arrays are converted to canonical form.
//
// value should be slice of any type.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.NotEqual([]interface{}{123, "foo"})
func (a *Array) NotEqual(value interface{}) *Array {
expected, ok := canonArray(&a.chain, value)
if !ok {
return a
}
if reflect.DeepEqual(expected, a.value) {
a.chain.fail("\nexpected array not equal to:\n%s",
dumpValue(expected))
}
return a
}
// Elements succeeds if array contains all given elements, in given order, and only them.
// Before comparison, array and all elements are converted to canonical form.
//
// For partial or unordered comparison, see Contains and ContainsOnly.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.Elements("foo", 123)
//
// This calls are equivalent:
// array.Elelems("a", "b")
// array.Equal([]interface{}{"a", "b"})
func (a *Array) Elements(values ...interface{}) *Array {
return a.Equal(values)
}
// Contains succeeds if array contains all given elements (in any order).
// Before comparison, array and all elements are converted to canonical form.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.Contains(123, "foo")
func (a *Array) Contains(values ...interface{}) *Array {
elements, ok := canonArray(&a.chain, values)
if !ok {
return a
}
for _, e := range elements {
if !a.containsElement(e) {
a.chain.fail("\nexpected array containing element:\n%s\n\nbut got:\n%s",
dumpValue(e), dumpValue(a.value))
}
}
return a
}
// NotContains succeeds if array contains none of given elements.
// Before comparison, array and all elements are converted to canonical form.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.NotContains("bar") // success
// array.NotContains("bar", "foo") // failure (array contains "foo")
func (a *Array) NotContains(values ...interface{}) *Array {
elements, ok := canonArray(&a.chain, values)
if !ok {
return a
}
for _, e := range elements {
if a.containsElement(e) {
a.chain.fail("\nexpected array not containing element:\n%s\n\nbut got:\n%s",
dumpValue(e), dumpValue(a.value))
}
}
return a
}
// ContainsOnly succeeds if array contains all given elements, in any order, and only
// them. Before comparison, array and all elements are converted to canonical form.
//
// Example:
// array := NewArray(t, []interface{}{"foo", 123})
// array.ContainsOnly(123, "foo")
//
// This calls are equivalent:
// array.ContainsOnly("a", "b")
// array.ContainsOnly("b", "a")
func (a *Array) ContainsOnly(values ...interface{}) *Array {
elements, ok := canonArray(&a.chain, values)
if !ok {
return a
}
if len(elements) != len(a.value) {
a.chain.fail("\nexpected array of length == %d:\n%s\n\n"+
"but got array of length %d:\n%s",
len(elements), dumpValue(elements),
len(a.value), dumpValue(a.value))
return a
}
for _, e := range elements {
if !a.containsElement(e) {
a.chain.fail("\nexpected array containing element:\n%s\n\nbut got:\n%s",
dumpValue(e), dumpValue(a.value))
}
}
return a
}
func (a *Array) containsElement(expected interface{}) bool {
for _, e := range a.value {
if reflect.DeepEqual(expected, e) {
return true
}
}
return false
}
+454
View File
@@ -0,0 +1,454 @@
package httpexpect
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestArrayFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Array{chain, nil}
value.chain.assertFailed(t)
value.Path("$").chain.assertFailed(t)
value.Schema("")
assert.False(t, value.Length() == nil)
assert.False(t, value.Element(0) == nil)
assert.False(t, value.Iter() == nil)
assert.True(t, len(value.Iter()) == 0)
value.Length().chain.assertFailed(t)
value.Element(0).chain.assertFailed(t)
value.First().chain.assertFailed(t)
value.Last().chain.assertFailed(t)
value.Empty()
value.NotEmpty()
value.Equal(nil)
value.NotEqual(nil)
value.Elements("foo")
value.Contains("foo")
value.NotContains("foo")
value.ContainsOnly("foo")
}
func TestArrayGetters(t *testing.T) {
reporter := newMockReporter(t)
a := []interface{}{"foo", 123.0}
value := NewArray(reporter, a)
assert.Equal(t, a, value.Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, a, value.Path("$").Raw())
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "array"}`)
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "object"}`)
value.chain.assertFailed(t)
value.chain.reset()
assert.Equal(t, 2.0, value.Length().Raw())
assert.Equal(t, "foo", value.Element(0).Raw())
assert.Equal(t, 123.0, value.Element(1).Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, nil, value.Element(2).Raw())
value.chain.assertFailed(t)
value.chain.reset()
it := value.Iter()
assert.Equal(t, 2, len(it))
assert.Equal(t, "foo", it[0].Raw())
assert.Equal(t, 123.0, it[1].Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, "foo", value.First().Raw())
assert.Equal(t, 123.0, value.Last().Raw())
value.chain.assertOK(t)
value.chain.reset()
}
func TestArrayEmpty(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewArray(reporter, nil)
_ = value1
value1.chain.assertFailed(t)
value1.chain.reset()
value2 := NewArray(reporter, []interface{}{})
value2.Empty()
value2.chain.assertOK(t)
value2.chain.reset()
value2.NotEmpty()
value2.chain.assertFailed(t)
value2.chain.reset()
value3 := NewArray(reporter, []interface{}{""})
value3.Empty()
value3.chain.assertFailed(t)
value3.chain.reset()
value3.NotEmpty()
value3.chain.assertOK(t)
value3.chain.reset()
}
func TestArrayEqualEmpty(t *testing.T) {
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{})
assert.Equal(t, []interface{}{}, value.Raw())
value.Equal([]interface{}{})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual([]interface{}{})
value.chain.assertFailed(t)
value.chain.reset()
value.Equal([]interface{}{""})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual([]interface{}{""})
value.chain.assertOK(t)
value.chain.reset()
}
func TestArrayEqualNotEmpty(t *testing.T) {
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{"foo", "bar"})
assert.Equal(t, []interface{}{"foo", "bar"}, value.Raw())
value.Equal([]interface{}{})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual([]interface{}{})
value.chain.assertOK(t)
value.chain.reset()
value.Equal([]interface{}{"foo"})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual([]interface{}{"foo"})
value.chain.assertOK(t)
value.chain.reset()
value.Equal([]interface{}{"bar", "foo"})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual([]interface{}{"bar", "foo"})
value.chain.assertOK(t)
value.chain.reset()
value.Equal([]interface{}{"foo", "bar"})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual([]interface{}{"foo", "bar"})
value.chain.assertFailed(t)
value.chain.reset()
}
func TestArrayEqualTypes(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewArray(reporter, []interface{}{"foo", "bar"})
value2 := NewArray(reporter, []interface{}{123, 456})
value3 := NewArray(reporter, []interface{}{
map[string]interface{}{
"foo": 123,
},
map[string]interface{}{
"foo": 456,
},
})
value1.Equal([]string{"foo", "bar"})
value1.chain.assertOK(t)
value1.chain.reset()
value1.Equal([]string{"bar", "foo"})
value1.chain.assertFailed(t)
value1.chain.reset()
value1.NotEqual([]string{"foo", "bar"})
value1.chain.assertFailed(t)
value1.chain.reset()
value1.NotEqual([]string{"bar", "foo"})
value1.chain.assertOK(t)
value1.chain.reset()
value2.Equal([]int{123, 456})
value2.chain.assertOK(t)
value2.chain.reset()
value2.Equal([]int{456, 123})
value2.chain.assertFailed(t)
value2.chain.reset()
value2.NotEqual([]int{123, 456})
value2.chain.assertFailed(t)
value2.chain.reset()
value2.NotEqual([]int{456, 123})
value2.chain.assertOK(t)
value2.chain.reset()
type S struct {
Foo int `json:"foo"`
}
value3.Equal([]S{{123}, {456}})
value3.chain.assertOK(t)
value3.chain.reset()
value3.Equal([]S{{456}, {123}})
value3.chain.assertFailed(t)
value3.chain.reset()
value3.NotEqual([]S{{123}, {456}})
value3.chain.assertFailed(t)
value3.chain.reset()
value3.NotEqual([]S{{456}, {123}})
value3.chain.assertOK(t)
value3.chain.reset()
}
func TestArrayElements(t *testing.T) {
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{123, "foo"})
value.Elements(123)
value.chain.assertFailed(t)
value.chain.reset()
value.Elements("foo")
value.chain.assertFailed(t)
value.chain.reset()
value.Elements("foo", 123)
value.chain.assertFailed(t)
value.chain.reset()
value.Elements(123, "foo", "foo")
value.chain.assertFailed(t)
value.chain.reset()
value.Elements(123, "foo")
value.chain.assertOK(t)
value.chain.reset()
}
func TestArrayContains(t *testing.T) {
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{123, "foo"})
value.Contains(123)
value.chain.assertOK(t)
value.chain.reset()
value.NotContains(123)
value.chain.assertFailed(t)
value.chain.reset()
value.Contains("foo", 123)
value.chain.assertOK(t)
value.chain.reset()
value.NotContains("foo", 123)
value.chain.assertFailed(t)
value.chain.reset()
value.Contains("foo", "foo")
value.chain.assertOK(t)
value.chain.reset()
value.NotContains("foo", "foo")
value.chain.assertFailed(t)
value.chain.reset()
value.Contains(123, "foo", "FOO")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContains(123, "foo", "FOO")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContains("FOO")
value.chain.assertOK(t)
value.chain.reset()
value.Contains([]interface{}{123, "foo"})
value.chain.assertFailed(t)
value.chain.reset()
value.NotContains([]interface{}{123, "foo"})
value.chain.assertOK(t)
value.chain.reset()
}
func TestArrayContainsOnly(t *testing.T) {
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{123, "foo"})
value.ContainsOnly(123)
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsOnly("foo")
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsOnly(123, "foo", "foo")
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsOnly(123, "foo")
value.chain.assertOK(t)
value.chain.reset()
value.ContainsOnly("foo", 123)
value.chain.assertOK(t)
value.chain.reset()
}
func TestArrayConvertEqual(t *testing.T) {
type (
myArray []interface{}
myInt int
)
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{123, 456})
assert.Equal(t, []interface{}{123.0, 456.0}, value.Raw())
value.Equal(myArray{myInt(123), 456.0})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(myArray{myInt(123), 456.0})
value.chain.assertFailed(t)
value.chain.reset()
value.Equal([]interface{}{"123", "456"})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual([]interface{}{"123", "456"})
value.chain.assertOK(t)
value.chain.reset()
value.Equal(nil)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(nil)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestArrayConvertElements(t *testing.T) {
type (
myInt int
)
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{123, 456})
assert.Equal(t, []interface{}{123.0, 456.0}, value.Raw())
value.Elements(myInt(123), 456.0)
value.chain.assertOK(t)
value.chain.reset()
value.Elements(func() {})
value.chain.assertFailed(t)
value.chain.reset()
}
func TestArrayConvertContains(t *testing.T) {
type (
myInt int
)
reporter := newMockReporter(t)
value := NewArray(reporter, []interface{}{123, 456})
assert.Equal(t, []interface{}{123.0, 456.0}, value.Raw())
value.Contains(myInt(123), 456.0)
value.chain.assertOK(t)
value.chain.reset()
value.NotContains(myInt(123), 456.0)
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsOnly(myInt(123), 456.0)
value.chain.assertOK(t)
value.chain.reset()
value.Contains("123")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContains("123")
value.chain.assertOK(t)
value.chain.reset()
value.ContainsOnly("123.0", "456.0")
value.chain.assertFailed(t)
value.chain.reset()
value.Contains(func() {})
value.chain.assertFailed(t)
value.chain.reset()
value.NotContains(func() {})
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsOnly(func() {})
value.chain.assertFailed(t)
value.chain.reset()
}
+214
View File
@@ -0,0 +1,214 @@
package httpexpect
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"github.com/valyala/fasthttp"
)
// Binder implements networkless http.RoundTripper attached directly to
// http.Handler.
//
// Binder emulates network communication by invoking given http.Handler
// directly. It passes httptest.ResponseRecorder as http.ResponseWriter
// to the handler, and then constructs http.Response from recorded data.
type Binder struct {
// HTTP handler invoked for every request.
Handler http.Handler
// TLS connection state used for https:// requests.
TLS *tls.ConnectionState
}
// NewBinder returns a new Binder given a http.Handler.
//
// Example:
// client := &http.Client{
// Transport: NewBinder(handler),
// }
func NewBinder(handler http.Handler) Binder {
return Binder{Handler: handler}
}
// RoundTrip implements http.RoundTripper.RoundTrip.
func (binder Binder) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Proto == "" {
req.Proto = fmt.Sprintf("HTTP/%d.%d", req.ProtoMajor, req.ProtoMinor)
}
if req.Body != nil {
if req.ContentLength == -1 {
req.TransferEncoding = []string{"chunked"}
}
} else {
req.Body = ioutil.NopCloser(bytes.NewReader(nil))
}
if req.URL != nil && req.URL.Scheme == "https" && binder.TLS != nil {
req.TLS = binder.TLS
}
if req.RequestURI == "" {
req.RequestURI = req.URL.RequestURI()
}
recorder := httptest.NewRecorder()
binder.Handler.ServeHTTP(recorder, req)
resp := http.Response{
Request: req,
StatusCode: recorder.Code,
Status: http.StatusText(recorder.Code),
Header: recorder.HeaderMap,
}
if recorder.Flushed {
resp.TransferEncoding = []string{"chunked"}
}
if recorder.Body != nil {
resp.Body = ioutil.NopCloser(recorder.Body)
}
return &resp, nil
}
// FastBinder implements networkless http.RoundTripper attached directly
// to fasthttp.RequestHandler.
//
// FastBinder emulates network communication by invoking given fasthttp.RequestHandler
// directly. It converts http.Request to fasthttp.Request, invokes handler, and then
// converts fasthttp.Response to http.Response.
type FastBinder struct {
// FastHTTP handler invoked for every request.
Handler fasthttp.RequestHandler
// TLS connection state used for https:// requests.
TLS *tls.ConnectionState
}
// NewFastBinder returns a new FastBinder given a fasthttp.RequestHandler.
//
// Example:
// client := &http.Client{
// Transport: NewFastBinder(fasthandler),
// }
func NewFastBinder(handler fasthttp.RequestHandler) FastBinder {
return FastBinder{Handler: handler}
}
// RoundTrip implements http.RoundTripper.RoundTrip.
func (binder FastBinder) RoundTrip(stdreq *http.Request) (*http.Response, error) {
fastreq := std2fast(stdreq)
var conn net.Conn
if stdreq.URL != nil && stdreq.URL.Scheme == "https" && binder.TLS != nil {
conn = connTLS{state: binder.TLS}
} else {
conn = connNonTLS{}
}
ctx := fasthttp.RequestCtx{}
ctx.Init2(conn, fastLogger{}, true)
fastreq.CopyTo(&ctx.Request)
if stdreq.ContentLength >= 0 {
ctx.Request.Header.SetContentLength(int(stdreq.ContentLength))
} else {
ctx.Request.Header.Add("Transfer-Encoding", "chunked")
}
if stdreq.Body != nil {
b, err := ioutil.ReadAll(stdreq.Body)
if err == nil {
ctx.Request.SetBody(b)
}
}
binder.Handler(&ctx)
return fast2std(stdreq, &ctx.Response), nil
}
func std2fast(stdreq *http.Request) *fasthttp.Request {
fastreq := &fasthttp.Request{}
fastreq.SetRequestURI(stdreq.URL.String())
fastreq.Header.SetMethod(stdreq.Method)
for k, a := range stdreq.Header {
for n, v := range a {
if n == 0 {
fastreq.Header.Set(k, v)
} else {
fastreq.Header.Add(k, v)
}
}
}
return fastreq
}
func fast2std(stdreq *http.Request, fastresp *fasthttp.Response) *http.Response {
status := fastresp.Header.StatusCode()
body := fastresp.Body()
stdresp := &http.Response{
Request: stdreq,
StatusCode: status,
Status: http.StatusText(status),
}
fastresp.Header.VisitAll(func(k, v []byte) {
sk := string(k)
sv := string(v)
if stdresp.Header == nil {
stdresp.Header = make(http.Header)
}
stdresp.Header.Add(sk, sv)
})
if fastresp.Header.ContentLength() == -1 {
stdresp.TransferEncoding = []string{"chunked"}
}
if body != nil {
stdresp.Body = ioutil.NopCloser(bytes.NewReader(body))
} else {
stdresp.Body = ioutil.NopCloser(bytes.NewReader(nil))
}
return stdresp
}
type fastLogger struct{}
func (fastLogger) Printf(format string, args ...interface{}) {
_, _ = format, args
}
type connNonTLS struct {
net.Conn
}
func (connNonTLS) RemoteAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4zero}
}
func (connNonTLS) LocalAddr() net.Addr {
return &net.TCPAddr{IP: net.IPv4zero}
}
type connTLS struct {
connNonTLS
state *tls.ConnectionState
}
func (c connTLS) ConnectionState() tls.ConnectionState {
return *c.state
}
+370
View File
@@ -0,0 +1,370 @@
package httpexpect
import (
"bufio"
"crypto/tls"
"io/ioutil"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
)
type mockHandler struct {
t *testing.T
https bool
http10 bool
chunked bool
}
func (c *mockHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
body, err := ioutil.ReadAll(req.Body)
assert.True(c.t, err == nil)
if c.http10 {
assert.Equal(c.t, "HTTP/1.0", req.Proto)
assert.Equal(c.t, 1, req.ProtoMajor)
assert.Equal(c.t, 0, req.ProtoMinor)
} else {
assert.Equal(c.t, "HTTP/1.1", req.Proto)
assert.Equal(c.t, 1, req.ProtoMajor)
assert.Equal(c.t, 1, req.ProtoMinor)
}
if c.https {
assert.Equal(c.t, "https://example.com/path", req.URL.String())
} else {
assert.Equal(c.t, "http://example.com/path", req.URL.String())
}
assert.Equal(c.t, "/path", req.URL.RequestURI())
assert.Equal(c.t, "/path", req.RequestURI)
assert.Equal(c.t, "GET", req.Method)
assert.Equal(c.t, "body", string(body))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(`{"hello":"world"}`))
if c.chunked {
w.(http.Flusher).Flush()
}
assert.True(c.t, err == nil)
}
func TestBinder(t *testing.T) {
handler := &mockHandler{
t: t,
http10: true,
chunked: false,
}
client := &http.Client{
Transport: NewBinder(handler),
}
req, err := http.NewRequest("GET", "http://example.com/path", strings.NewReader("body"))
if err != nil {
t.Fatal(err)
}
req.ProtoMajor = 1
req.ProtoMinor = 0
req.Proto = ""
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
header := http.Header{
"Content-Type": {"application/json"},
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, header, resp.Header)
assert.Equal(t, `{"hello":"world"}`, string(b))
assert.Equal(t, []string(nil), resp.TransferEncoding)
}
func TestBinderTLS(t *testing.T) {
handler := &mockHandler{t: t}
httpClient := &http.Client{
Transport: &Binder{
Handler: handler,
TLS: nil,
},
}
tlsState := &tls.ConnectionState{}
httpsClient := &http.Client{
Transport: &Binder{
Handler: handler,
TLS: tlsState,
},
}
handler.https = false
req, _ := http.NewRequest("GET", "http://example.com/path", strings.NewReader("body"))
resp, err := httpClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.Nil(t, req.TLS)
handler.https = true
req, _ = http.NewRequest("GET", "https://example.com/path", strings.NewReader("body"))
resp, err = httpClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.Nil(t, req.TLS)
handler.https = false
req, _ = http.NewRequest("GET", "http://example.com/path", strings.NewReader("body"))
resp, err = httpsClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.Nil(t, req.TLS)
handler.https = true
req, _ = http.NewRequest("GET", "https://example.com/path", strings.NewReader("body"))
resp, err = httpsClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.NotNil(t, req.TLS)
assert.Equal(t, tlsState, req.TLS)
}
func TestBinderChunked(t *testing.T) {
handler := &mockHandler{
t: t,
chunked: true,
}
client := &http.Client{
Transport: NewBinder(handler),
}
req, err := http.NewRequest("GET", "http://example.com/path", strings.NewReader("body"))
if err != nil {
t.Fatal(err)
}
req.ContentLength = -1
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, []string{"chunked"}, req.TransferEncoding)
assert.Equal(t, []string{"chunked"}, resp.TransferEncoding)
}
func TestFastBinder(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {
assert.Equal(t, "POST", string(ctx.Request.Header.Method()))
assert.Equal(t, "http://example.com/path", string(ctx.Request.Header.RequestURI()))
assert.Equal(t, "application/x-www-form-urlencoded",
string(ctx.Request.Header.ContentType()))
headers := map[string][]string{}
ctx.Request.Header.VisitAll(func(k, v []byte) {
headers[string(k)] = append(headers[string(k)], string(v))
})
expected := map[string][]string{
"Content-Type": {"application/x-www-form-urlencoded"},
"Content-Length": {"7"},
"Some-Header": {"foo", "bar"},
}
assert.Equal(t, expected, headers)
assert.Equal(t, "bar", string(ctx.FormValue("foo")))
assert.Equal(t, "foo=bar", string(ctx.Request.Body()))
ctx.Response.Header.Set("Content-Type", "application/json")
ctx.Response.SetBody([]byte(`{"hello":"world"}`))
}
client := &http.Client{
Transport: NewFastBinder(handler),
}
req, err := http.NewRequest(
"POST", "http://example.com/path", strings.NewReader("foo=bar"))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Some-Header", "foo")
req.Header.Add("Some-Header", "bar")
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
header := http.Header{
"Content-Type": {"application/json"},
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, header, resp.Header)
assert.Equal(t, `{"hello":"world"}`, string(b))
assert.Equal(t, []string(nil), resp.TransferEncoding)
}
func TestFastBinderTLS(t *testing.T) {
var isHTTPS, isTLS bool
tlsState := &tls.ConnectionState{}
handler := func(ctx *fasthttp.RequestCtx) {
isHTTPS = strings.HasPrefix(string(ctx.Request.Header.RequestURI()), "https://")
isTLS = ctx.IsTLS()
if isTLS {
assert.Equal(t, *tlsState, *ctx.TLSConnectionState())
}
}
httpClient := &http.Client{
Transport: &FastBinder{
Handler: handler,
TLS: nil,
},
}
httpsClient := &http.Client{
Transport: &FastBinder{
Handler: handler,
TLS: tlsState,
},
}
req, _ := http.NewRequest("GET", "http://example.com/path", strings.NewReader("body"))
resp, err := httpClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.False(t, isHTTPS)
assert.False(t, isTLS)
req, _ = http.NewRequest("GET", "https://example.com/path", strings.NewReader("body"))
resp, err = httpClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.True(t, isHTTPS)
assert.False(t, isTLS)
req, _ = http.NewRequest("GET", "http://example.com/path", strings.NewReader("body"))
resp, err = httpsClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.False(t, isHTTPS)
assert.False(t, isTLS)
req, _ = http.NewRequest("GET", "https://example.com/path", strings.NewReader("body"))
resp, err = httpsClient.Do(req)
assert.Nil(t, err)
assert.NotNil(t, resp)
assert.True(t, isHTTPS)
assert.True(t, isTLS)
}
func TestFastBinderChunked(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {
assert.Equal(t, "POST", string(ctx.Request.Header.Method()))
assert.Equal(t, "http://example.com/path", string(ctx.Request.Header.RequestURI()))
assert.Equal(t, "application/x-www-form-urlencoded",
string(ctx.Request.Header.ContentType()))
headers := map[string][]string{}
ctx.Request.Header.VisitAll(func(k, v []byte) {
headers[string(k)] = append(headers[string(k)], string(v))
})
expected := map[string][]string{
"Content-Type": {"application/x-www-form-urlencoded"},
"Transfer-Encoding": {"chunked"},
}
assert.Equal(t, expected, headers)
assert.Equal(t, "bar", string(ctx.FormValue("foo")))
assert.Equal(t, "foo=bar", string(ctx.Request.Body()))
ctx.Response.Header.Set("Content-Type", "application/json")
ctx.Response.SetBodyStreamWriter(func(w *bufio.Writer) {
w.WriteString(`[1, `)
w.Flush()
w.WriteString(`2]`)
})
}
client := &http.Client{
Transport: NewFastBinder(handler),
}
req, err := http.NewRequest(
"POST", "http://example.com/path", strings.NewReader("foo=bar"))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.ContentLength = -1
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, []string{"chunked"}, resp.TransferEncoding)
}
func TestFastBinderEmptyResponse(t *testing.T) {
handler := func(*fasthttp.RequestCtx) {}
client := &http.Client{
Transport: NewFastBinder(handler),
}
req, err := http.NewRequest("POST", "http://example.com/path", nil)
if err != nil {
t.Fatal(err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
assert.False(t, resp.Body == nil)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", string(b))
}
+82
View File
@@ -0,0 +1,82 @@
package httpexpect
// Boolean provides methods to inspect attached bool value
// (Go representation of JSON boolean).
type Boolean struct {
chain chain
value bool
}
// NewBoolean returns a new Boolean given a reporter used to report
// failures and value to be inspected.
//
// reporter should not be nil.
//
// Example:
// boolean := NewBoolean(t, true)
func NewBoolean(reporter Reporter, value bool) *Boolean {
return &Boolean{makeChain(reporter), value}
}
// Raw returns underlying value attached to Boolean.
// This is the value originally passed to NewBoolean.
//
// Example:
// boolean := NewBoolean(t, true)
// assert.Equal(t, true, boolean.Raw())
func (b *Boolean) Raw() bool {
return b.value
}
// Path is similar to Value.Path.
func (b *Boolean) Path(path string) *Value {
return getPath(&b.chain, b.value, path)
}
// Schema is similar to Value.Schema.
func (b *Boolean) Schema(schema interface{}) *Boolean {
checkSchema(&b.chain, b.value, schema)
return b
}
// Equal succeeds if boolean is equal to given value.
//
// Example:
// boolean := NewBoolean(t, true)
// boolean.Equal(true)
func (b *Boolean) Equal(value bool) *Boolean {
if !(b.value == value) {
b.chain.fail("expected boolean == %v, but got %v", value, b.value)
}
return b
}
// NotEqual succeeds if boolean is not equal to given value.
//
// Example:
// boolean := NewBoolean(t, true)
// boolean.NotEqual(false)
func (b *Boolean) NotEqual(value bool) *Boolean {
if !(b.value != value) {
b.chain.fail("expected boolean != %v, but got %v", value, b.value)
}
return b
}
// True succeeds if boolean is true.
//
// Example:
// boolean := NewBoolean(t, true)
// boolean.True()
func (b *Boolean) True() *Boolean {
return b.Equal(true)
}
// False succeeds if boolean is false.
//
// Example:
// boolean := NewBoolean(t, false)
// boolean.False()
func (b *Boolean) False() *Boolean {
return b.Equal(false)
}
+111
View File
@@ -0,0 +1,111 @@
package httpexpect
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBooleanFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Boolean{chain, false}
value.chain.assertFailed(t)
value.Path("$").chain.assertFailed(t)
value.Schema("")
value.Equal(false)
value.NotEqual(false)
value.True()
value.False()
}
func TestBooleanGetters(t *testing.T) {
reporter := newMockReporter(t)
value := NewBoolean(reporter, true)
assert.Equal(t, true, value.Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, true, value.Path("$").Raw())
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "boolean"}`)
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "object"}`)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestBooleanTrue(t *testing.T) {
reporter := newMockReporter(t)
value := NewBoolean(reporter, true)
assert.Equal(t, true, value.Raw())
value.Equal(true)
value.chain.assertOK(t)
value.chain.reset()
value.Equal(false)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(false)
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(true)
value.chain.assertFailed(t)
value.chain.reset()
value.True()
value.chain.assertOK(t)
value.chain.reset()
value.False()
value.chain.assertFailed(t)
value.chain.reset()
}
func TestBooleanFalse(t *testing.T) {
reporter := newMockReporter(t)
value := NewBoolean(reporter, false)
assert.Equal(t, false, value.Raw())
value.Equal(true)
value.chain.assertFailed(t)
value.chain.reset()
value.Equal(false)
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(false)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(true)
value.chain.assertOK(t)
value.chain.reset()
value.True()
value.chain.assertFailed(t)
value.chain.reset()
value.False()
value.chain.assertOK(t)
value.chain.reset()
}
+38
View File
@@ -0,0 +1,38 @@
package httpexpect
type chain struct {
reporter Reporter
failbit bool
}
func makeChain(reporter Reporter) chain {
return chain{reporter, false}
}
func (c *chain) failed() bool {
return c.failbit
}
func (c *chain) fail(message string, args ...interface{}) {
if c.failbit {
return
}
c.failbit = true
c.reporter.Errorf(message, args...)
}
func (c *chain) reset() {
c.failbit = false
}
func (c *chain) assertFailed(r Reporter) {
if !c.failbit {
r.Errorf("expected chain is failed, but it's ok")
}
}
func (c *chain) assertOK(r Reporter) {
if c.failbit {
r.Errorf("expected chain is ok, but it's failed")
}
}
+64
View File
@@ -0,0 +1,64 @@
package httpexpect
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestChainFail(t *testing.T) {
chain := makeChain(newMockReporter(t))
assert.False(t, chain.failed())
chain.fail("fail")
assert.True(t, chain.failed())
chain.fail("fail")
assert.True(t, chain.failed())
}
func TestChainCopy(t *testing.T) {
chain1 := makeChain(newMockReporter(t))
chain2 := chain1
assert.False(t, chain1.failed())
assert.False(t, chain2.failed())
chain1.fail("fail")
assert.True(t, chain1.failed())
assert.False(t, chain2.failed())
chain2.fail("fail")
assert.True(t, chain1.failed())
assert.True(t, chain2.failed())
}
func TestChainReport(t *testing.T) {
r0 := newMockReporter(t)
chain := makeChain(r0)
r1 := newMockReporter(t)
chain.assertOK(r1)
assert.False(t, r1.reported)
chain.assertFailed(r1)
assert.True(t, r1.reported)
assert.False(t, chain.failed())
chain.fail("fail")
assert.True(t, r0.reported)
r2 := newMockReporter(t)
chain.assertFailed(r2)
assert.False(t, r2.reported)
chain.assertOK(r2)
assert.True(t, r2.reported)
}
+105
View File
@@ -0,0 +1,105 @@
package httpexpect
import (
"net/http"
"time"
)
// Cookie provides methods to inspect attached http.Cookie value.
type Cookie struct {
chain chain
value *http.Cookie
}
// NewCookie returns a new Cookie object given a reporter used to report
// failures and cookie value to be inspected.
//
// reporter and value should not be nil.
//
// Example:
// cookie := NewCookie(reporter, &http.Cookie{...})
// cookie.Domain().Equal("example.com")
// cookie.Path().Equal("/")
// cookie.Expires().InRange(time.Now(), time.Now().Add(time.Hour * 24))
func NewCookie(reporter Reporter, value *http.Cookie) *Cookie {
chain := makeChain(reporter)
if value == nil {
chain.fail("expected non-nil cookie")
}
return &Cookie{chain, value}
}
// Raw returns underlying http.Cookie value attached to Cookie.
// This is the value originally passed to NewCookie.
//
// Example:
// cookie := NewCookie(t, c)
// assert.Equal(t, c, cookie.Raw())
func (c *Cookie) Raw() *http.Cookie {
return c.value
}
// Name returns a new String object that may be used to inspect
// cookie name.
//
// Example:
// cookie := NewCookie(t, &http.Cookie{...})
// cookie.Name().Equal("session")
func (c *Cookie) Name() *String {
if c.chain.failed() {
return &String{c.chain, ""}
}
return &String{c.chain, c.value.Name}
}
// Value returns a new String object that may be used to inspect
// cookie value.
//
// Example:
// cookie := NewCookie(t, &http.Cookie{...})
// cookie.Value().Equal("gH6z7Y")
func (c *Cookie) Value() *String {
if c.chain.failed() {
return &String{c.chain, ""}
}
return &String{c.chain, c.value.Value}
}
// Domain returns a new String object that may be used to inspect
// cookie domain.
//
// Example:
// cookie := NewCookie(t, &http.Cookie{...})
// cookie.Domain().Equal("example.com")
func (c *Cookie) Domain() *String {
if c.chain.failed() {
return &String{c.chain, ""}
}
return &String{c.chain, c.value.Domain}
}
// Path returns a new String object that may be used to inspect
// cookie path.
//
// Example:
// cookie := NewCookie(t, &http.Cookie{...})
// cookie.Path().Equal("/foo")
func (c *Cookie) Path() *String {
if c.chain.failed() {
return &String{c.chain, ""}
}
return &String{c.chain, c.value.Path}
}
// Expires returns a new DateTime object that may be used to inspect
// cookie expiration date.
//
// Example:
// cookie := NewCookie(t, &http.Cookie{...})
// cookie.Expires().InRange(time.Now(), time.Now().Add(time.Hour * 24))
func (c *Cookie) Expires() *DateTime {
if c.chain.failed() {
return &DateTime{c.chain, time.Unix(0, 0)}
}
return &DateTime{c.chain, c.value.Expires}
}
+54
View File
@@ -0,0 +1,54 @@
package httpexpect
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCookieFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Cookie{chain, nil}
assert.True(t, value.Raw() == nil)
assert.True(t, value.Name() != nil)
assert.True(t, value.Value() != nil)
assert.True(t, value.Domain() != nil)
assert.True(t, value.Path() != nil)
assert.True(t, value.Expires() != nil)
}
func TestCookieGetters(t *testing.T) {
reporter := newMockReporter(t)
NewCookie(reporter, nil).chain.assertFailed(t)
value := NewCookie(reporter, &http.Cookie{
Name: "name",
Value: "value",
Domain: "example.com",
Path: "/path",
Expires: time.Unix(1234, 0),
})
value.chain.assertOK(t)
value.Name().chain.assertOK(t)
value.Value().chain.assertOK(t)
value.Domain().chain.assertOK(t)
value.Path().chain.assertOK(t)
value.Expires().chain.assertOK(t)
assert.Equal(t, "name", value.Name().Raw())
assert.Equal(t, "value", value.Value().Raw())
assert.Equal(t, "example.com", value.Domain().Raw())
assert.Equal(t, "/path", value.Path().Raw())
assert.True(t, time.Unix(1234, 0).Equal(value.Expires().Raw()))
value.chain.assertOK(t)
}
+129
View File
@@ -0,0 +1,129 @@
package httpexpect
import (
"time"
)
// DateTime provides methods to inspect attached time.Time value.
type DateTime struct {
chain chain
value time.Time
}
// NewDateTime returns a new DateTime object given a reporter used to report
// failures and time.Time value to be inspected.
//
// reporter should not be nil.
//
// Example:
// dt := NewDateTime(reporter, time.Now())
// dt.Le(time.Now())
//
// time.Sleep(time.Second)
// dt.Lt(time.Now())
func NewDateTime(reporter Reporter, value time.Time) *DateTime {
return &DateTime{makeChain(reporter), value}
}
// Raw returns underlying time.Time value attached to DateTime.
// This is the value originally passed to NewDateTime.
//
// Example:
// dt := NewDateTime(t, timestamp)
// assert.Equal(t, timestamp, dt.Raw())
func (dt *DateTime) Raw() time.Time {
return dt.value
}
// Equal succeeds if DateTime is equal to given value.
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 1))
// dt.Equal(time.Unix(0, 1))
func (dt *DateTime) Equal(value time.Time) *DateTime {
if !dt.value.Equal(value) {
dt.chain.fail("\nexpected datetime equal to:\n %s\n\nbut got:\n %s",
value, dt.value)
}
return dt
}
// NotEqual succeeds if DateTime is not equal to given value.
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 1))
// dt.NotEqual(time.Unix(0, 2))
func (dt *DateTime) NotEqual(value time.Time) *DateTime {
if dt.value.Equal(value) {
dt.chain.fail("\nexpected datetime not equal to:\n %s", value)
}
return dt
}
// Gt succeeds if DateTime is greater than given value.
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 2))
// dt.Gt(time.Unix(0, 1))
func (dt *DateTime) Gt(value time.Time) *DateTime {
if !dt.value.After(value) {
dt.chain.fail("\nexpected datetime > then:\n %s\n\nbut got:\n %s",
value, dt.value)
}
return dt
}
// Ge succeeds if DateTime is greater than or equal to given value.
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 2))
// dt.Ge(time.Unix(0, 1))
func (dt *DateTime) Ge(value time.Time) *DateTime {
if !(dt.value.After(value) || dt.value.Equal(value)) {
dt.chain.fail("\nexpected datetime >= then:\n %s\n\nbut got:\n %s",
value, dt.value)
}
return dt
}
// Lt succeeds if DateTime is lesser than given value.
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 1))
// dt.Lt(time.Unix(0, 2))
func (dt *DateTime) Lt(value time.Time) *DateTime {
if !dt.value.Before(value) {
dt.chain.fail("\nexpected datetime < then:\n %s\n\nbut got:\n %s",
value, dt.value)
}
return dt
}
// Le succeeds if DateTime is lesser than or equal to given value.
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 1))
// dt.Le(time.Unix(0, 2))
func (dt *DateTime) Le(value time.Time) *DateTime {
if !(dt.value.Before(value) || dt.value.Equal(value)) {
dt.chain.fail("\nexpected datetime <= then:\n %s\n\nbut got:\n %s",
value, dt.value)
}
return dt
}
// InRange succeeds if DateTime is in given range [min; max].
//
// Example:
// dt := NewDateTime(t, time.Unix(0, 2))
// dt.InRange(time.Unix(0, 1), time.Unix(0, 3))
// dt.InRange(time.Unix(0, 2), time.Unix(0, 2))
func (dt *DateTime) InRange(min, max time.Time) *DateTime {
if !((dt.value.After(min) || dt.value.Equal(min)) &&
(dt.value.Before(max) || dt.value.Equal(max))) {
dt.chain.fail(
"\nexpected datetime in range:\n min: %s\n max: %s\n\nbut got: %s",
min, max, dt.value)
}
return dt
}
+134
View File
@@ -0,0 +1,134 @@
package httpexpect
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDateTimeFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
ts := time.Unix(0, 0)
value := &DateTime{chain, ts}
value.chain.assertFailed(t)
value.Equal(ts)
value.NotEqual(ts)
value.Gt(ts)
value.Ge(ts)
value.Lt(ts)
value.Le(ts)
value.InRange(ts, ts)
}
func TestDateTimeEqual(t *testing.T) {
reporter := newMockReporter(t)
value := NewDateTime(reporter, time.Unix(0, 1234))
assert.True(t, time.Unix(0, 1234).Equal(value.Raw()))
value.Equal(time.Unix(0, 1234))
value.chain.assertOK(t)
value.chain.reset()
value.Equal(time.Unix(0, 4321))
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(time.Unix(0, 4321))
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(time.Unix(0, 1234))
value.chain.assertFailed(t)
value.chain.reset()
}
func TestDateTimeGreater(t *testing.T) {
reporter := newMockReporter(t)
value := NewDateTime(reporter, time.Unix(0, 1234))
value.Gt(time.Unix(0, 1234-1))
value.chain.assertOK(t)
value.chain.reset()
value.Gt(time.Unix(0, 1234))
value.chain.assertFailed(t)
value.chain.reset()
value.Ge(time.Unix(0, 1234-1))
value.chain.assertOK(t)
value.chain.reset()
value.Ge(time.Unix(0, 1234))
value.chain.assertOK(t)
value.chain.reset()
value.Ge(time.Unix(0, 1234+1))
value.chain.assertFailed(t)
value.chain.reset()
}
func TestDateTimeLesser(t *testing.T) {
reporter := newMockReporter(t)
value := NewDateTime(reporter, time.Unix(0, 1234))
value.Lt(time.Unix(0, 1234+1))
value.chain.assertOK(t)
value.chain.reset()
value.Lt(time.Unix(0, 1234))
value.chain.assertFailed(t)
value.chain.reset()
value.Le(time.Unix(0, 1234+1))
value.chain.assertOK(t)
value.chain.reset()
value.Le(time.Unix(0, 1234))
value.chain.assertOK(t)
value.chain.reset()
value.Le(time.Unix(0, 1234-1))
value.chain.assertFailed(t)
value.chain.reset()
}
func TestDateTimeInRange(t *testing.T) {
reporter := newMockReporter(t)
value := NewDateTime(reporter, time.Unix(0, 1234))
value.InRange(time.Unix(0, 1234), time.Unix(0, 1234))
value.chain.assertOK(t)
value.chain.reset()
value.InRange(time.Unix(0, 1234-1), time.Unix(0, 1234))
value.chain.assertOK(t)
value.chain.reset()
value.InRange(time.Unix(0, 1234), time.Unix(0, 1234+1))
value.chain.assertOK(t)
value.chain.reset()
value.InRange(time.Unix(0, 1234+1), time.Unix(0, 1234+2))
value.chain.assertFailed(t)
value.chain.reset()
value.InRange(time.Unix(0, 1234-2), time.Unix(0, 1234-1))
value.chain.assertFailed(t)
value.chain.reset()
value.InRange(time.Unix(0, 1234+1), time.Unix(0, 1234-1))
value.chain.assertFailed(t)
value.chain.reset()
}
+376
View File
@@ -0,0 +1,376 @@
// Package httpexpect helps with end-to-end HTTP and REST API testing.
//
// Usage examples
//
// See example directory:
// - https://godoc.org/github.com/gavv/httpexpect/_examples
// - https://github.com/gavv/httpexpect/tree/master/_examples
//
// Communication mode
//
// There are two common ways to test API with httpexpect:
// - start HTTP server and instruct httpexpect to use HTTP client for communication
// - don't start server and instruct httpexpect to invoke http handler directly
//
// The second approach works only if the server is a Go module and its handler can
// be imported in tests.
//
// Concrete behaviour is determined by Client implementation passed to Config struct.
// If you're using http.Client, set its Transport field (http.RoundTriper) to one of
// the following:
// 1. default (nil) - use HTTP transport from net/http (you should start server)
// 2. httpexpect.Binder - invoke given http.Handler directly
// 3. httpexpect.FastBinder - invoke given fasthttp.RequestHandler directly
//
// Note that http handler can be usually obtained from http framework you're using.
// E.g., echo framework provides either http.Handler or fasthttp.RequestHandler.
//
// You can also provide your own implementation of RequestFactory (creates http.Request),
// or Client (gets http.Request and returns http.Response).
//
// If you're starting server from tests, it's very handy to use net/http/httptest.
//
// Value equality
//
// Whenever values are checked for equality in httpexpect, they are converted
// to "canonical form":
// - structs are converted to map[string]interface{}
// - type aliases are removed
// - numeric types are converted to float64
// - non-nil interfaces pointing to nil slices and maps are replaced with
// nil interfaces
//
// This is equivalent to subsequently json.Marshal() and json.Unmarshal() the value
// and currently is implemented so.
//
// Failure handling
//
// When some check fails, failure is reported. If non-fatal failures are used
// (see Reporter interface), execution is continued and instance that was checked
// is marked as failed.
//
// If specific instance is marked as failed, all subsequent checks are ignored
// for this instance and for any child instances retrieved after failure.
//
// Example:
// array := NewArray(NewAssertReporter(t), []interface{}{"foo", 123})
//
// e0 := array.Element(0) // success
// e1 := array.Element(1) // success
//
// s0 := e0.String() // success
// s1 := e1.String() // failure; e1 and s1 are marked as failed, e0 and s0 are not
//
// s0.Equal("foo") // success
// s1.Equal("bar") // this check is ignored because s1 is marked as failed
package httpexpect
import (
"io"
"net/http"
"net/http/cookiejar"
"time"
"golang.org/x/net/publicsuffix"
)
// Expect is a toplevel object that contains user Config and allows
// to construct Request objects.
type Expect struct {
config Config
builders []func(*Request)
}
// Config contains various settings.
type Config struct {
// BaseURL is a URL to prepended to all request. My be empty. If
// non-empty, trailing slash is allowed but not required and is
// appended automatically.
BaseURL string
// RequestFactory is used to pass in a custom *http.Request generation func.
// May be nil.
//
// You can use DefaultRequestFactory, or provide custom implementation.
// Useful for Google App Engine testing for example.
RequestFactory RequestFactory
// Client is used to send http.Request and receive http.Response.
// Should not be nil.
//
// You can use http.DefaultClient or http.Client, or provide
// custom implementation.
Client Client
// Reporter is used to report failures.
// Should not be nil.
//
// You can use AssertReporter, RequireReporter (they use testify),
// or testing.TB, or provide custom implementation.
Reporter Reporter
// Printers are used to print requests and responses.
// May be nil.
//
// You can use CompactPrinter, DebugPrinter, CurlPrinter, or provide
// custom implementation.
//
// You can also use builtin printers with alternative Logger if
// you're happy with their format, but want to send logs somewhere
// else instead of testing.TB.
Printers []Printer
}
// RequestFactory is used to create all http.Request objects.
// aetest.Instance from the Google App Engine implements this interface.
type RequestFactory interface {
NewRequest(method, urlStr string, body io.Reader) (*http.Request, error)
}
// Client is used to send http.Request and receive http.Response.
// http.Client, Binder, and FastBinder implement this interface.
type Client interface {
// Do sends request and returns response.
Do(*http.Request) (*http.Response, error)
}
// Printer is used to print requests and responses.
// CompactPrinter, DebugPrinter, and CurlPrinter implement this interface.
type Printer interface {
// Request is called before request is sent.
Request(*http.Request)
// Response is called after response is received.
Response(*http.Response, time.Duration)
}
// Logger is used as output backend for Printer.
// testing.TB implements this interface.
type Logger interface {
// Logf writes message to log.
Logf(fmt string, args ...interface{})
}
// Reporter is used to report failures.
// testing.TB, AssertReporter, and RequireReporter implement this interface.
type Reporter interface {
// Errorf reports failure.
// Allowed to return normally or terminate test using t.FailNow().
Errorf(message string, args ...interface{})
}
// LoggerReporter combines Logger and Reporter interfaces.
type LoggerReporter interface {
Logger
Reporter
}
// DefaultRequestFactory is the default RequestFactory implementation which just
// calls http.NewRequest.
type DefaultRequestFactory struct{}
// NewRequest implements RequestFactory.NewRequest.
func (DefaultRequestFactory) NewRequest(
method, urlStr string, body io.Reader) (*http.Request, error) {
return http.NewRequest(method, urlStr, body)
}
// New returns a new Expect object.
//
// baseURL specifies URL to prepended to all request. My be empty. If non-empty,
// trailing slash is allowed but not required and is appended automatically.
//
// New is a shorthand for WithConfig. It uses:
// - CompactPrinter as Printer, with testing.TB as Logger
// - AssertReporter as Reporter
// - DefaultRequestFactory as RequestFactory
//
// Client is set to a default client with a non-nil Jar:
// &http.Client{
// Jar: httpexpect.NewJar(),
// }
//
// Example:
// func TestSomething(t *testing.T) {
// e := httpexpect.New(t, "http://example.com/")
//
// e.GET("/path").
// Expect().
// Status(http.StatusOK)
// }
func New(t LoggerReporter, baseURL string) *Expect {
return WithConfig(Config{
BaseURL: baseURL,
Reporter: NewAssertReporter(t),
Printers: []Printer{
NewCompactPrinter(t),
},
})
}
// WithConfig returns a new Expect object with given config.
//
// Reporter should not be nil.
//
// If RequestFactory is nil, it's set to a DefaultRequestFactory instance.
//
// If Client is nil, it's set to a default client with a non-nil Jar:
// &http.Client{
// Jar: httpexpect.NewJar(),
// }
//
// Example:
// func TestSomething(t *testing.T) {
// e := httpexpect.WithConfig(httpexpect.Config{
// BaseURL: "http://example.com/",
// Client: &http.Client{
// Transport: httpexpect.NewBinder(myHandler()),
// Jar: httpexpect.NewJar(),
// },
// Reporter: httpexpect.NewAssertReporter(t),
// Printers: []httpexpect.Printer{
// httpexpect.NewCurlPrinter(t),
// httpexpect.NewDebugPrinter(t, true)
// },
// })
//
// e.GET("/path").
// Expect().
// Status(http.StatusOK)
// }
func WithConfig(config Config) *Expect {
if config.Reporter == nil {
panic("config.Reporter is nil")
}
if config.RequestFactory == nil {
config.RequestFactory = DefaultRequestFactory{}
}
if config.Client == nil {
config.Client = &http.Client{
Jar: NewJar(),
}
}
return &Expect{
config: config,
builders: nil,
}
}
// NewJar returns a new http.CookieJar.
//
// Returned jar is implemented in net/http/cookiejar. PublicSuffixList is
// implemented in golang.org/x/net/publicsuffix.
//
// Note that this jar ignores cookies when request url is empty.
func NewJar() http.CookieJar {
jar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
if err != nil {
panic(err)
}
return jar
}
// Builder returns a copy of Expect instance with given builder attached to it.
// Returned copy contains all previously attached builders plus a new one.
// Builders are invoked from Request method, after constructing every new request.
//
// Example:
// e := httpexpect.New(t, "http://example.com")
//
// token := e.POST("/login").WithForm(Login{"ford", "betelgeuse7"}).
// Expect().
// Status(http.StatusOK).JSON().Object().Value("token").String().Raw()
//
// auth := e.Builder(func (req *httpexpect.Request) {
// req.WithHeader("Authorization", "Bearer "+token)
// })
//
// auth.GET("/restricted").
// Expect().
// Status(http.StatusOK)
func (e *Expect) Builder(builder func(*Request)) *Expect {
ret := *e
ret.builders = append(e.builders, builder)
return &ret
}
// Request returns a new Request object.
// Arguments a similar to NewRequest.
// After creating request, all builders attached to Expect object are invoked.
// See Builder.
func (e *Expect) Request(method, path string, pathargs ...interface{}) *Request {
req := NewRequest(e.config, method, path, pathargs...)
for _, builder := range e.builders {
builder(req)
}
return req
}
// OPTIONS is a shorthand for e.Request("OPTIONS", path, pathargs...).
func (e *Expect) OPTIONS(path string, pathargs ...interface{}) *Request {
return e.Request("OPTIONS", path, pathargs...)
}
// HEAD is a shorthand for e.Request("HEAD", path, pathargs...).
func (e *Expect) HEAD(path string, pathargs ...interface{}) *Request {
return e.Request("HEAD", path, pathargs...)
}
// GET is a shorthand for e.Request("GET", path, pathargs...).
func (e *Expect) GET(path string, pathargs ...interface{}) *Request {
return e.Request("GET", path, pathargs...)
}
// POST is a shorthand for e.Request("POST", path, pathargs...).
func (e *Expect) POST(path string, pathargs ...interface{}) *Request {
return e.Request("POST", path, pathargs...)
}
// PUT is a shorthand for e.Request("PUT", path, pathargs...).
func (e *Expect) PUT(path string, pathargs ...interface{}) *Request {
return e.Request("PUT", path, pathargs...)
}
// PATCH is a shorthand for e.Request("PATCH", path, pathargs...).
func (e *Expect) PATCH(path string, pathargs ...interface{}) *Request {
return e.Request("PATCH", path, pathargs...)
}
// DELETE is a shorthand for e.Request("DELETE", path, pathargs...).
func (e *Expect) DELETE(path string, pathargs ...interface{}) *Request {
return e.Request("DELETE", path, pathargs...)
}
// Value is a shorthand for NewValue(e.config.Reporter, value).
func (e *Expect) Value(value interface{}) *Value {
return NewValue(e.config.Reporter, value)
}
// Object is a shorthand for NewObject(e.config.Reporter, value).
func (e *Expect) Object(value map[string]interface{}) *Object {
return NewObject(e.config.Reporter, value)
}
// Array is a shorthand for NewArray(e.config.Reporter, value).
func (e *Expect) Array(value []interface{}) *Array {
return NewArray(e.config.Reporter, value)
}
// String is a shorthand for NewString(e.config.Reporter, value).
func (e *Expect) String(value string) *String {
return NewString(e.config.Reporter, value)
}
// Number is a shorthand for NewNumber(e.config.Reporter, value).
func (e *Expect) Number(value float64) *Number {
return NewNumber(e.config.Reporter, value)
}
// Boolean is a shorthand for NewBoolean(e.config.Reporter, value).
func (e *Expect) Boolean(value bool) *Boolean {
return NewBoolean(e.config.Reporter, value)
}
+866
View File
@@ -0,0 +1,866 @@
package httpexpect
import (
"bufio"
"crypto/tls"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
)
func TestExpectMethods(t *testing.T) {
client := &mockClient{}
reporter := NewAssertReporter(t)
config := Config{
BaseURL: "http://example.com",
Client: client,
Reporter: reporter,
}
var reqs [8]*Request
e := WithConfig(config)
reqs[0] = e.Request("METHOD", "/url")
reqs[1] = e.OPTIONS("/url")
reqs[2] = e.HEAD("/url")
reqs[3] = e.GET("/url")
reqs[4] = e.POST("/url")
reqs[5] = e.PUT("/url")
reqs[6] = e.PATCH("/url")
reqs[7] = e.DELETE("/url")
assert.Equal(t, "METHOD", reqs[0].http.Method)
assert.Equal(t, "OPTIONS", reqs[1].http.Method)
assert.Equal(t, "HEAD", reqs[2].http.Method)
assert.Equal(t, "GET", reqs[3].http.Method)
assert.Equal(t, "POST", reqs[4].http.Method)
assert.Equal(t, "PUT", reqs[5].http.Method)
assert.Equal(t, "PATCH", reqs[6].http.Method)
assert.Equal(t, "DELETE", reqs[7].http.Method)
}
func TestExpectBuilders(t *testing.T) {
client := &mockClient{}
reporter := NewAssertReporter(t)
config := Config{
Client: client,
Reporter: reporter,
}
e := WithConfig(config)
var reqs1 []*Request
e1 := e.Builder(func(r *Request) {
reqs1 = append(reqs1, r)
})
var reqs2 []*Request
e2 := e1.Builder(func(r *Request) {
reqs2 = append(reqs2, r)
})
e.Request("METHOD", "/url")
r1 := e1.Request("METHOD", "/url")
r2 := e2.Request("METHOD", "/url")
assert.Equal(t, 2, int(len(reqs1)))
assert.Equal(t, 1, int(len(reqs2)))
assert.Equal(t, r1, reqs1[0])
assert.Equal(t, r2, reqs1[1])
assert.Equal(t, r1, reqs2[0])
}
func TestExpectValues(t *testing.T) {
client := &mockClient{}
r := NewAssertReporter(t)
config := Config{
Client: client,
Reporter: r,
}
e := WithConfig(config)
m := map[string]interface{}{}
a := []interface{}{}
s := ""
n := 0.0
b := false
assert.Equal(t, NewValue(r, m), e.Value(m))
assert.Equal(t, NewObject(r, m), e.Object(m))
assert.Equal(t, NewArray(r, a), e.Array(a))
assert.Equal(t, NewString(r, s), e.String(s))
assert.Equal(t, NewNumber(r, n), e.Number(n))
assert.Equal(t, NewBoolean(r, b), e.Boolean(b))
}
func TestExpectTraverse(t *testing.T) {
client := &mockClient{}
reporter := NewAssertReporter(t)
config := Config{
BaseURL: "http://example.com",
Client: client,
Reporter: reporter,
}
data := map[string]interface{}{
"foo": []interface{}{"bar", 123, false, nil},
"bar": "hello",
"baz": 456,
}
resp := WithConfig(config).GET("/url").WithJSON(data).Expect()
m := resp.JSON().Object()
m.Equal(data)
m.ContainsKey("foo")
m.ContainsKey("bar")
m.ContainsKey("foo")
m.ValueEqual("foo", data["foo"])
m.ValueEqual("bar", data["bar"])
m.ValueEqual("baz", data["baz"])
m.Keys().ContainsOnly("foo", "bar", "baz")
m.Values().ContainsOnly(data["foo"], data["bar"], data["baz"])
m.Value("foo").Array().Elements("bar", 123, false, nil)
m.Value("bar").String().Equal("hello")
m.Value("baz").Number().Equal(456)
m.Value("foo").Array().Element(2).Boolean().False()
m.Value("foo").Array().Element(3).Null()
}
func TestExpectBranches(t *testing.T) {
client := &mockClient{}
config := Config{
BaseURL: "http://example.com",
Client: client,
Reporter: newMockReporter(t),
}
data := map[string]interface{}{
"foo": []interface{}{"bar", 123, false, nil},
"bar": "hello",
"baz": 456,
}
resp := WithConfig(config).GET("/url").WithJSON(data).Expect()
m1 := resp.JSON().Array()
m2 := resp.JSON().Object()
e1 := m2.Value("foo").Object()
e2 := m2.Value("foo").Array().Element(999).String()
e3 := m2.Value("foo").Array().Element(0).Number()
e4 := m2.Value("foo").Array().Element(0).String()
e4.Equal("bar")
m1.chain.assertFailed(t)
m2.chain.assertOK(t)
e1.chain.assertFailed(t)
e2.chain.assertFailed(t)
e3.chain.assertFailed(t)
e4.chain.assertOK(t)
}
func TestExpectStdCompat(_ *testing.T) {
New(&testing.T{}, "")
New(&testing.B{}, "")
New(testing.TB(&testing.T{}), "")
}
type testRequestFactory struct {
lastreq *http.Request
fail bool
}
func (f *testRequestFactory) NewRequest(
method, urlStr string, body io.Reader) (*http.Request, error) {
if f.fail {
return nil, errors.New("testRequestFactory")
}
f.lastreq = httptest.NewRequest(method, urlStr, body)
return f.lastreq, nil
}
func TestExpectRequestFactory(t *testing.T) {
e1 := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
})
r1 := e1.Request("GET", "/")
r1.chain.assertOK(t)
assert.NotNil(t, r1.http)
f2 := &testRequestFactory{}
e2 := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
RequestFactory: f2,
})
r2 := e2.Request("GET", "/")
r2.chain.assertOK(t)
assert.NotNil(t, f2.lastreq)
assert.True(t, f2.lastreq == r2.http)
f3 := &testRequestFactory{
fail: true,
}
e3 := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: newMockReporter(t),
RequestFactory: f3,
})
r3 := e3.Request("GET", "/")
r3.chain.assertFailed(t)
assert.Nil(t, f3.lastreq)
}
func createBasicHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/foo", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"foo":123}`))
})
mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
w.Write([]byte(`field1=` + r.FormValue("field1")))
w.Write([]byte(`&field2=` + r.PostFormValue("field2")))
})
mux.HandleFunc("/baz/qux", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`[true, false]`))
case "PUT":
decoder := json.NewDecoder(r.Body)
var m map[string]interface{}
if err := decoder.Decode(&m); err != nil {
w.WriteHeader(http.StatusBadRequest)
} else if m["test"] != "ok" {
w.WriteHeader(http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`ok`))
}
}
})
mux.HandleFunc("/wee", func(w http.ResponseWriter, r *http.Request) {
if u, p, ok := r.BasicAuth(); ok {
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
w.Write([]byte(`username=` + u))
w.Write([]byte(`&password=` + p))
} else {
w.WriteHeader(http.StatusBadRequest)
}
})
return mux
}
func testBasicHandler(e *Expect) {
e.GET("/foo")
e.GET("/foo").Expect()
e.GET("/foo").Expect().Status(http.StatusOK)
e.GET("/foo").Expect().
Status(http.StatusOK).JSON().Object().ValueEqual("foo", 123)
e.PUT("/bar").WithQuery("field1", "hello").WithFormField("field2", "world").
Expect().
Status(http.StatusOK).
Form().ValueEqual("field1", "hello").ValueEqual("field2", "world")
e.GET("/{a}/{b}", "baz", "qux").
Expect().
Status(http.StatusOK).JSON().Array().Elements(true, false)
e.PUT("/{a}/{b}").
WithPath("a", "baz").
WithPath("b", "qux").
WithJSON(map[string]string{"test": "ok"}).
Expect().
Status(http.StatusOK).Body().Equal("ok")
auth := e.Builder(func(req *Request) {
req.WithBasicAuth("john", "secret")
})
auth.PUT("/wee").
Expect().
Status(http.StatusOK).
Form().ValueEqual("username", "john").ValueEqual("password", "secret")
}
func TestExpectBasicHandlerLiveDefault(t *testing.T) {
handler := createBasicHandler()
server := httptest.NewServer(handler)
defer server.Close()
testBasicHandler(New(t, server.URL))
}
func TestExpectBasicHandlerLiveConfig(t *testing.T) {
handler := createBasicHandler()
server := httptest.NewServer(handler)
defer server.Close()
testBasicHandler(WithConfig(Config{
BaseURL: server.URL,
Reporter: NewAssertReporter(t),
Printers: []Printer{
NewCurlPrinter(t),
NewDebugPrinter(t, true),
},
}))
}
func TestExpectBasicHandlerLiveTLS(t *testing.T) {
handler := createBasicHandler()
server := httptest.NewTLSServer(handler)
defer server.Close()
testBasicHandler(WithConfig(Config{
BaseURL: server.URL,
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
}))
}
func TestExpectBasicHandlerLiveLongRun(t *testing.T) {
if testing.Short() {
return
}
handler := createBasicHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := New(t, server.URL)
for i := 0; i < 2; i++ {
testBasicHandler(e)
}
}
func TestExpectBasicHandlerBinderStandard(t *testing.T) {
handler := createBasicHandler()
testBasicHandler(WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewBinder(handler),
},
}))
}
func TestExpectBasicHandlerBinderFast(t *testing.T) {
handler := fasthttpadaptor.NewFastHTTPHandler(createBasicHandler())
testBasicHandler(WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewFastBinder(handler),
},
}))
}
func createRedirectHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/foo", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(`hello`))
})
mux.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/foo", http.StatusFound)
})
return mux
}
func createRedirectFastHandler() fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/foo":
ctx.SetBody([]byte(`hello`))
case "/bar":
ctx.Redirect("/foo", http.StatusFound)
}
}
}
func testRedirectHandler(e *Expect) {
e.POST("/bar").
Expect().
Status(http.StatusOK).Body().Equal(`hello`)
}
func TestExpectRedirectHandlerLive(t *testing.T) {
handler := createRedirectHandler()
server := httptest.NewServer(handler)
defer server.Close()
testRedirectHandler(New(t, server.URL))
}
func TestExpectRedirectHandlerBinderStandard(t *testing.T) {
handler := createRedirectHandler()
testRedirectHandler(WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewBinder(handler),
},
}))
}
func TestExpectRedirectHandlerBinderFast(t *testing.T) {
handler := createRedirectFastHandler()
testRedirectHandler(WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewFastBinder(handler),
},
}))
}
func createAutoTLSHandler(https string) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/tls", func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
w.Write([]byte(`no`))
} else {
w.Write([]byte(`yes`))
}
})
mux.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
http.Redirect(w, r, https+r.RequestURI, http.StatusFound)
} else {
w.Write([]byte(`hello`))
}
})
return mux
}
func createAutoTLSFastHandler(https string) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Path()) {
case "/tls":
if !ctx.IsTLS() {
ctx.SetBody([]byte(`no`))
} else {
ctx.SetBody([]byte(`yes`))
}
case "/protected":
if !ctx.IsTLS() {
ctx.Redirect(https+string(ctx.Request.RequestURI()), http.StatusFound)
} else {
ctx.SetBody([]byte(`hello`))
}
}
}
}
func testAutoTLSHandler(config Config) {
e := WithConfig(config)
tls := e.POST("/tls").
Expect().
Status(http.StatusOK).Body()
if strings.HasPrefix(config.BaseURL, "https://") {
tls.Equal(`yes`)
} else {
tls.Equal(`no`)
}
e.POST("/protected").
Expect().
Status(http.StatusOK).Body().Equal(`hello`)
}
func TestExpectAutoTLSHandlerLive(t *testing.T) {
httpsServ := httptest.NewTLSServer(createAutoTLSHandler(""))
defer httpsServ.Close()
httpServ := httptest.NewServer(createAutoTLSHandler(httpsServ.URL))
defer httpServ.Close()
assert.True(t, strings.HasPrefix(httpsServ.URL, "https://"))
assert.True(t, strings.HasPrefix(httpServ.URL, "http://"))
for _, url := range []string{httpsServ.URL, httpServ.URL} {
testAutoTLSHandler(Config{
BaseURL: url,
Reporter: NewRequireReporter(t),
Printers: []Printer{
NewDebugPrinter(t, true),
},
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
})
}
}
func TestExpectAutoTLSHandlerBinderStandard(t *testing.T) {
handler := createAutoTLSHandler("https://example.com")
for _, url := range []string{"https://example.com", "http://example.com"} {
testAutoTLSHandler(Config{
BaseURL: url,
Reporter: NewRequireReporter(t),
Printers: []Printer{
NewDebugPrinter(t, true),
},
Client: &http.Client{
Transport: &Binder{
Handler: handler,
TLS: &tls.ConnectionState{},
},
},
})
}
}
func TestExpectAutoTLSHandlerBinderFast(t *testing.T) {
handler := createAutoTLSFastHandler("https://example.com")
for _, url := range []string{"https://example.com", "http://example.com"} {
testAutoTLSHandler(Config{
BaseURL: url,
Reporter: NewRequireReporter(t),
Printers: []Printer{
NewDebugPrinter(t, true),
},
Client: &http.Client{
Transport: &FastBinder{
Handler: handler,
TLS: &tls.ConnectionState{},
},
},
})
}
}
func createChunkedHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Proto != "HTTP/1.1" {
w.WriteHeader(http.StatusBadRequest)
} else if len(r.TransferEncoding) != 1 || r.TransferEncoding[0] != "chunked" {
w.WriteHeader(http.StatusBadRequest)
} else if r.PostFormValue("key") != "value" {
w.WriteHeader(http.StatusBadRequest)
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`[1, `))
w.(http.Flusher).Flush()
w.Write([]byte(`2]`))
}
})
return mux
}
func createChunkedFastHandler(t *testing.T) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
headers := map[string][]string{}
ctx.Request.Header.VisitAll(func(k, v []byte) {
headers[string(k)] = append(headers[string(k)], string(v))
})
assert.Equal(t, []string{"chunked"}, headers["Transfer-Encoding"])
assert.Equal(t, "value", string(ctx.FormValue("key")))
assert.Equal(t, "key=value", string(ctx.Request.Body()))
ctx.Response.Header.Set("Content-Type", "application/json")
ctx.Response.SetBodyStreamWriter(func(w *bufio.Writer) {
w.WriteString(`[1, `)
w.Flush()
w.WriteString(`2]`)
})
}
}
func testChunkedHandler(e *Expect) {
e.PUT("/").
WithHeader("Content-Type", "application/x-www-form-urlencoded").
WithChunked(strings.NewReader("key=value")).
Expect().
Status(http.StatusOK).
ContentType("application/json").
TransferEncoding("chunked").
JSON().Array().Elements(1, 2)
}
func TestExpectChunkedHandlerLive(t *testing.T) {
handler := createChunkedHandler()
server := httptest.NewServer(handler)
defer server.Close()
testChunkedHandler(New(t, server.URL))
}
func TestExpectChunkedHandlerBinderStandard(t *testing.T) {
handler := createChunkedHandler()
testChunkedHandler(WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewBinder(handler),
},
}))
}
func TestExpectChunkedHandlerBinderFast(t *testing.T) {
handler := createChunkedFastHandler(t)
testChunkedHandler(WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewFastBinder(handler),
},
}))
}
func createCookieHandler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "myname",
Value: "myvalue",
Path: "/",
Expires: time.Date(3000, 0, 0, 0, 0, 0, 0, time.UTC),
})
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("myname")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
} else {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(cookie.Value))
}
})
return mux
}
func testCookieHandler(e *Expect, enabled bool) {
r := e.PUT("/set").Expect().Status(http.StatusNoContent)
r.Cookies().ContainsOnly("myname")
c := r.Cookie("myname")
c.Value().Equal("myvalue")
c.Path().Equal("/")
c.Expires().Equal(time.Date(3000, 0, 0, 0, 0, 0, 0, time.UTC))
if enabled {
e.GET("/get").Expect().Status(http.StatusOK).Text().Equal("myvalue")
} else {
e.GET("/get").Expect().Status(http.StatusBadRequest)
}
}
func TestExpectCookieHandlerLiveDisabled(t *testing.T) {
handler := createCookieHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := WithConfig(Config{
BaseURL: server.URL,
Reporter: NewAssertReporter(t),
Client: &http.Client{
Jar: nil,
},
})
testCookieHandler(e, false)
}
func TestExpecCookieHandlerLiveEnabled(t *testing.T) {
handler := createCookieHandler()
server := httptest.NewServer(handler)
defer server.Close()
e := WithConfig(Config{
BaseURL: server.URL,
Reporter: NewAssertReporter(t),
Client: &http.Client{
Jar: NewJar(),
},
})
testCookieHandler(e, true)
}
func TestExpectCookieHandlerBinderStandardDisabled(t *testing.T) {
handler := createCookieHandler()
e := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewBinder(handler),
Jar: nil,
},
})
testCookieHandler(e, false)
}
func TestExpectCookieHandlerBinderStandardEnabled(t *testing.T) {
handler := createCookieHandler()
e := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewBinder(handler),
Jar: NewJar(),
},
})
testCookieHandler(e, true)
}
func TestExpectCookieHandlerBinderFastDisabled(t *testing.T) {
handler := fasthttpadaptor.NewFastHTTPHandler(createCookieHandler())
e := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewFastBinder(handler),
Jar: nil,
},
})
testCookieHandler(e, false)
}
func TestExpectCookieHandlerBinderFastEnabled(t *testing.T) {
handler := fasthttpadaptor.NewFastHTTPHandler(createCookieHandler())
e := WithConfig(Config{
BaseURL: "http://example.com",
Reporter: NewAssertReporter(t),
Client: &http.Client{
Transport: NewFastBinder(handler),
Jar: NewJar(),
},
})
testCookieHandler(e, true)
}
func TestExpectStaticFastBinder(t *testing.T) {
tempdir, err := ioutil.TempDir("", "httpexpect")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempdir)
if err := ioutil.WriteFile(
path.Join(tempdir, "hello"), []byte("hello, world!"), 0666); err != nil {
t.Fatal(err)
}
fs := &fasthttp.FS{
Root: tempdir,
}
handler := fs.NewRequestHandler()
e := WithConfig(Config{
Client: &http.Client{
Transport: NewFastBinder(handler),
Jar: NewJar(),
},
Reporter: NewAssertReporter(t),
Printers: []Printer{
NewDebugPrinter(t, true),
},
})
e.GET("/hello").
Expect().
Status(http.StatusOK).
Text().Equal("hello, world!")
}
+184
View File
@@ -0,0 +1,184 @@
package httpexpect
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"github.com/xeipuuv/gojsonschema"
"github.com/yalp/jsonpath"
"github.com/yudai/gojsondiff"
"github.com/yudai/gojsondiff/formatter"
)
func toString(str interface{}) (s string, ok bool) {
ok = true
defer func() {
if err := recover(); err != nil {
ok = false
}
}()
s = reflect.ValueOf(str).Convert(reflect.TypeOf("")).String()
return
}
func getPath(chain *chain, value interface{}, path string) *Value {
if chain.failed() {
return &Value{*chain, nil}
}
result, err := jsonpath.Read(value, path)
if err != nil {
chain.fail(err.Error())
return &Value{*chain, nil}
}
return &Value{*chain, result}
}
func checkSchema(chain *chain, value, schema interface{}) {
if chain.failed() {
return
}
valueLoader := gojsonschema.NewGoLoader(value)
var schemaLoader gojsonschema.JSONLoader
if str, ok := toString(schema); ok {
if ok, _ := regexp.MatchString(`^\w+://`, str); ok {
schemaLoader = gojsonschema.NewReferenceLoader(str)
} else {
schemaLoader = gojsonschema.NewStringLoader(str)
}
} else {
schemaLoader = gojsonschema.NewGoLoader(schema)
}
result, err := gojsonschema.Validate(schemaLoader, valueLoader)
if err != nil {
chain.fail("\n%s\n\nschema:\n%s\n\nvalue:\n%s",
err.Error(),
dumpSchema(schema),
dumpValue(value))
return
}
if !result.Valid() {
errors := ""
for _, err := range result.Errors() {
errors += fmt.Sprintf(" %s\n", err)
}
chain.fail(
"\njson schema validation failed, schema:\n%s\n\nvalue:%s\n\nerrors:\n%s",
dumpSchema(schema),
dumpValue(value),
errors)
return
}
}
func dumpSchema(schema interface{}) string {
if s, ok := toString(schema); ok {
schema = s
}
return regexp.MustCompile(`(?m:^)`).
ReplaceAllString(fmt.Sprintf("%v", schema), " ")
}
func canonNumber(chain *chain, number interface{}) (f float64, ok bool) {
ok = true
defer func() {
if err := recover(); err != nil {
chain.fail("%v", err)
ok = false
}
}()
f = reflect.ValueOf(number).Convert(reflect.TypeOf(float64(0))).Float()
return
}
func canonArray(chain *chain, in interface{}) ([]interface{}, bool) {
var out []interface{}
data, ok := canonValue(chain, in)
if ok {
out, ok = data.([]interface{})
if !ok {
chain.fail("expected array, got %v", out)
}
}
return out, ok
}
func canonMap(chain *chain, in interface{}) (map[string]interface{}, bool) {
var out map[string]interface{}
data, ok := canonValue(chain, in)
if ok {
out, ok = data.(map[string]interface{})
if !ok {
chain.fail("expected map, got %v", out)
}
}
return out, ok
}
func canonValue(chain *chain, in interface{}) (interface{}, bool) {
b, err := json.Marshal(in)
if err != nil {
chain.fail(err.Error())
return nil, false
}
var out interface{}
if err := json.Unmarshal(b, &out); err != nil {
chain.fail(err.Error())
return nil, false
}
return out, true
}
func dumpValue(value interface{}) string {
b, err := json.MarshalIndent(value, " ", " ")
if err != nil {
return " " + fmt.Sprintf("%#v", value)
}
return " " + string(b)
}
func diffValues(expected, actual interface{}) string {
differ := gojsondiff.New()
var diff gojsondiff.Diff
if ve, ok := expected.(map[string]interface{}); ok {
if va, ok := actual.(map[string]interface{}); ok {
diff = differ.CompareObjects(ve, va)
} else {
return " (unavailable)"
}
} else if ve, ok := expected.([]interface{}); ok {
if va, ok := actual.([]interface{}); ok {
diff = differ.CompareArrays(ve, va)
} else {
return " (unavailable)"
}
} else {
return " (unavailable)"
}
config := formatter.AsciiFormatterConfig{
ShowArrayIndex: true,
}
f := formatter.NewAsciiFormatter(expected, config)
str, err := f.Format(diff)
if err != nil {
return " (unavailable)"
}
return "--- expected\n+++ actual\n" + str
}
+137
View File
@@ -0,0 +1,137 @@
package httpexpect
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCanonNumber(t *testing.T) {
type (
myInt int
)
chain := makeChain(newMockReporter(t))
d1, ok := canonNumber(&chain, 123)
assert.True(t, ok)
assert.Equal(t, 123.0, d1)
chain.assertOK(t)
chain.reset()
d2, ok := canonNumber(&chain, 123.0)
assert.True(t, ok)
assert.Equal(t, 123.0, d2)
chain.assertOK(t)
chain.reset()
d3, ok := canonNumber(&chain, myInt(123))
assert.True(t, ok)
assert.Equal(t, 123.0, d3)
chain.assertOK(t)
chain.reset()
_, ok = canonNumber(&chain, "123")
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonNumber(&chain, nil)
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
}
func TestCanonArray(t *testing.T) {
type (
myArray []interface{}
myInt int
)
chain := makeChain(newMockReporter(t))
d1, ok := canonArray(&chain, []interface{}{123.0, 456.0})
assert.True(t, ok)
assert.Equal(t, []interface{}{123.0, 456.0}, d1)
chain.assertOK(t)
chain.reset()
d2, ok := canonArray(&chain, myArray{myInt(123), 456.0})
assert.True(t, ok)
assert.Equal(t, []interface{}{123.0, 456.0}, d2)
chain.assertOK(t)
chain.reset()
_, ok = canonArray(&chain, "123")
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonArray(&chain, func() {})
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonArray(&chain, nil)
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonArray(&chain, []interface{}(nil))
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
}
func TestCanonMap(t *testing.T) {
type (
myMap map[string]interface{}
myInt int
)
chain := makeChain(newMockReporter(t))
d1, ok := canonMap(&chain, map[string]interface{}{"foo": 123.0})
assert.True(t, ok)
assert.Equal(t, map[string]interface{}{"foo": 123.0}, d1)
chain.assertOK(t)
chain.reset()
d2, ok := canonMap(&chain, myMap{"foo": myInt(123)})
assert.True(t, ok)
assert.Equal(t, map[string]interface{}{"foo": 123.0}, d2)
chain.assertOK(t)
chain.reset()
_, ok = canonMap(&chain, "123")
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonMap(&chain, func() {})
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonMap(&chain, nil)
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
_, ok = canonMap(&chain, map[string]interface{}(nil))
assert.False(t, ok)
chain.assertFailed(t)
chain.reset()
}
func TestDiffErrors(t *testing.T) {
na := " (unavailable)"
assert.Equal(t, na, diffValues(map[string]interface{}{}, []interface{}{}))
assert.Equal(t, na, diffValues([]interface{}{}, map[string]interface{}{}))
assert.Equal(t, na, diffValues("foo", "bar"))
assert.Equal(t, na, diffValues(func() {}, func() {}))
assert.NotEqual(t, na, diffValues(map[string]interface{}{}, map[string]interface{}{}))
assert.NotEqual(t, na, diffValues([]interface{}{}, []interface{}{}))
}
+198
View File
@@ -0,0 +1,198 @@
package httpexpect
import (
"reflect"
)
// Match provides methods to inspect attached regexp match results.
type Match struct {
chain chain
submatches []string
names map[string]int
}
// NewMatch returns a new Match object given a reporter used to report
// failures and submatches to be inspected.
//
// reporter should not be nil. submatches and names may be nil.
//
// Example:
// s := "http://example.com/users/john"
// r := regexp.MustCompile(`http://(?P<host>.+)/users/(?P<user>.+)`)
// m := NewMatch(reporter, r.FindStringSubmatch(s), r.SubexpNames())
//
// m.NotEmpty()
// m.Length().Equal(3)
//
// m.Index(0).Equal("http://example.com/users/john")
// m.Index(1).Equal("example.com")
// m.Index(2).Equal("john")
//
// m.Name("host").Equal("example.com")
// m.Name("user").Equal("john")
func NewMatch(reporter Reporter, submatches []string, names []string) *Match {
return makeMatch(makeChain(reporter), submatches, names)
}
func makeMatch(chain chain, submatches []string, names []string) *Match {
if submatches == nil {
submatches = []string{}
}
namemap := map[string]int{}
for n, name := range names {
if name != "" {
namemap[name] = n
}
}
return &Match{chain, submatches, namemap}
}
// Raw returns underlying submatches attached to Match.
// This is the value originally passed to NewMatch.
//
// Example:
// m := NewMatch(t, submatches, names)
// assert.Equal(t, submatches, m.Raw())
func (m *Match) Raw() []string {
return m.submatches
}
// Length returns a new Number object that may be used to inspect
// number of submatches.
//
// Example:
// m := NewMatch(t, submatches, names)
// m.Length().Equal(len(submatches))
func (m *Match) Length() *Number {
return &Number{m.chain, float64(len(m.submatches))}
}
// Index returns a new String object that may be used to inspect submatch
// with given index.
//
// Note that submatch with index 0 contains the whole match. If index is out
// of bounds, Index reports failure and returns empty (but non-nil) value.
//
// Example:
// s := "http://example.com/users/john"
//
// r := regexp.MustCompile(`http://(.+)/users/(.+)`)
// m := NewMatch(t, r.FindStringSubmatch(s), nil)
//
// m.Index(0).Equal("http://example.com/users/john")
// m.Index(1).Equal("example.com")
// m.Index(2).Equal("john")
func (m *Match) Index(index int) *String {
if index < 0 || index >= len(m.submatches) {
m.chain.fail(
"\nsubmatch index out of bounds:\n index %d\n\n bounds [%d; %d)",
index,
0,
len(m.submatches))
return &String{m.chain, ""}
}
return &String{m.chain, m.submatches[index]}
}
// Name returns a new String object that may be used to inspect submatch
// with given name.
//
// If there is no submatch with given name, Name reports failure and returns
// empty (but non-nil) value.
//
// Example:
// s := "http://example.com/users/john"
//
// r := regexp.MustCompile(`http://(?P<host>.+)/users/(?P<user>.+)`)
// m := NewMatch(t, r.FindStringSubmatch(s), r.SubexpNames())
//
// m.Name("host").Equal("example.com")
// m.Name("user").Equal("john")
func (m *Match) Name(name string) *String {
index, ok := m.names[name]
if !ok {
m.chain.fail(
"\nsubmatch name not found:\n %q\n\navailable names:\n%s",
name,
dumpValue(m.names))
return &String{m.chain, ""}
}
return m.Index(index)
}
// Empty succeeds if submatches array is empty.
//
// Example:
// m := NewMatch(t, submatches, names)
// m.Empty()
func (m *Match) Empty() *Match {
if len(m.submatches) != 0 {
m.chain.fail("\nexpected zero submatches, but got:\n %s",
dumpValue(m.submatches))
}
return m
}
// NotEmpty succeeds if submatches array is non-empty.
//
// Example:
// m := NewMatch(t, submatches, names)
// m.NotEmpty()
func (m *Match) NotEmpty() *Match {
if len(m.submatches) == 0 {
m.chain.fail("expected non-zero submatches")
}
return m
}
// Values succeeds if submatches array, starting from index 1, is equal to
// given array.
//
// Note that submatch with index 0 contains the whole match and is not
// included into this check.
//
// Example:
// s := "http://example.com/users/john"
// r := regexp.MustCompile(`http://(.+)/users/(.+)`)
// m := NewMatch(t, r.FindStringSubmatch(s), nil)
// m.Values("example.com", "john")
func (m *Match) Values(values ...string) *Match {
if values == nil {
values = []string{}
}
if !reflect.DeepEqual(values, m.getValues()) {
m.chain.fail("\nexpected submatches equal to:\n%s\n\nbut got:\n%s",
dumpValue(values),
dumpValue(m.getValues()))
}
return m
}
// NotValues succeeds if submatches array, starting from index 1, is not
// equal to given array.
//
// Note that submatch with index 0 contains the whole match and is not
// included into this check.
//
// Example:
// s := "http://example.com/users/john"
// r := regexp.MustCompile(`http://(.+)/users/(.+)`)
// m := NewMatch(t, r.FindStringSubmatch(s), nil)
// m.NotValues("example.com", "bob")
func (m *Match) NotValues(values ...string) *Match {
if values == nil {
values = []string{}
}
if reflect.DeepEqual(values, m.getValues()) {
m.chain.fail("\nexpected submatches not equal to:\n%s",
dumpValue(values))
}
return m
}
func (m *Match) getValues() []string {
if len(m.submatches) > 1 {
return m.submatches[1:]
}
return []string{}
}
+173
View File
@@ -0,0 +1,173 @@
package httpexpect
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMatchFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Match{chain, nil, nil}
value.chain.assertFailed(t)
assert.False(t, value.Length() == nil)
assert.False(t, value.Index(0) == nil)
assert.False(t, value.Name("") == nil)
value.Length().chain.assertFailed(t)
value.Index(0).chain.assertFailed(t)
value.Name("").chain.assertFailed(t)
value.Empty()
value.NotEmpty()
value.Values("")
value.NotValues("")
}
func TestMatchGetters(t *testing.T) {
reporter := newMockReporter(t)
matches := []string{"m0", "m1", "m2"}
names := []string{"", "n1", "n2"}
value := NewMatch(reporter, matches, names)
assert.Equal(t, matches, value.Raw())
assert.Equal(t, 3.0, value.Length().Raw())
assert.Equal(t, "m0", value.Index(0).Raw())
assert.Equal(t, "m1", value.Index(1).Raw())
assert.Equal(t, "m2", value.Index(2).Raw())
value.chain.assertOK(t)
assert.Equal(t, "m1", value.Name("n1").Raw())
assert.Equal(t, "m2", value.Name("n2").Raw())
value.chain.assertOK(t)
assert.Equal(t, "", value.Index(-1).Raw())
value.chain.assertFailed(t)
value.chain.reset()
assert.Equal(t, "", value.Index(3).Raw())
value.chain.assertFailed(t)
value.chain.reset()
assert.Equal(t, "", value.Name("").Raw())
value.chain.assertFailed(t)
value.chain.reset()
assert.Equal(t, "", value.Name("bad").Raw())
value.chain.assertFailed(t)
value.chain.reset()
}
func TestMatchEmpty(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewMatch(reporter, []string{"m"}, nil)
value2 := NewMatch(reporter, []string{}, nil)
value3 := NewMatch(reporter, nil, nil)
assert.Equal(t, []string{}, value2.Raw())
assert.Equal(t, []string{}, value3.Raw())
value1.Empty()
value1.chain.assertFailed(t)
value1.chain.reset()
value1.NotEmpty()
value1.chain.assertOK(t)
value1.chain.reset()
value2.Empty()
value2.chain.assertOK(t)
value2.chain.reset()
value2.NotEmpty()
value2.chain.assertFailed(t)
value2.chain.reset()
value3.Empty()
value3.chain.assertOK(t)
value3.chain.reset()
value3.NotEmpty()
value3.chain.assertFailed(t)
value3.chain.reset()
}
func TestMatchValues(t *testing.T) {
reporter := newMockReporter(t)
value := NewMatch(reporter, []string{"m0", "m1", "m2"}, nil)
value.Values("m1", "m2")
value.chain.assertOK(t)
value.chain.reset()
value.Values("m2", "m1")
value.chain.assertFailed(t)
value.chain.reset()
value.Values("m1")
value.chain.assertFailed(t)
value.chain.reset()
value.Values()
value.chain.assertFailed(t)
value.chain.reset()
value.NotValues("m1", "m2")
value.chain.assertFailed(t)
value.chain.reset()
value.NotValues("m2", "m1")
value.chain.assertOK(t)
value.chain.reset()
value.NotValues("m1")
value.chain.assertOK(t)
value.chain.reset()
value.NotValues()
value.chain.assertOK(t)
value.chain.reset()
}
func TestMatchValuesEmpty(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewMatch(reporter, nil, nil)
value2 := NewMatch(reporter, []string{}, nil)
value3 := NewMatch(reporter, []string{"m0"}, nil)
value1.Values()
value1.chain.assertOK(t)
value1.chain.reset()
value1.Values("")
value1.chain.assertFailed(t)
value1.chain.reset()
value2.Values()
value2.chain.assertOK(t)
value2.chain.reset()
value2.Values("")
value2.chain.assertFailed(t)
value2.chain.reset()
value3.Values()
value3.chain.assertOK(t)
value3.chain.reset()
value3.Values("m0")
value3.chain.assertFailed(t)
value3.chain.reset()
}
+36
View File
@@ -0,0 +1,36 @@
package httpexpect
import (
"net/http"
"testing"
)
type mockClient struct {
req *http.Request
resp http.Response
err error
}
func (c *mockClient) Do(req *http.Request) (*http.Response, error) {
c.req = req
if c.err == nil {
c.resp.Header = c.req.Header
c.resp.Body = c.req.Body
return &c.resp, nil
}
return nil, c.err
}
type mockReporter struct {
testing *testing.T
reported bool
}
func newMockReporter(t *testing.T) *mockReporter {
return &mockReporter{t, false}
}
func (r *mockReporter) Errorf(message string, args ...interface{}) {
r.testing.Logf("Fail: "+message, args...)
r.reported = true
}
+244
View File
@@ -0,0 +1,244 @@
package httpexpect
import (
"math"
)
// Number provides methods to inspect attached float64 value
// (Go representation of JSON number).
type Number struct {
chain chain
value float64
}
// NewNumber returns a new Number given a reporter used to report
// failures and value to be inspected.
//
// reporter should not be nil.
//
// Example:
// number := NewNumber(t, 123.4)
func NewNumber(reporter Reporter, value float64) *Number {
return &Number{makeChain(reporter), value}
}
// Raw returns underlying value attached to Number.
// This is the value originally passed to NewNumber.
//
// Example:
// number := NewNumber(t, 123.4)
// assert.Equal(t, 123.4, number.Raw())
func (n *Number) Raw() float64 {
return n.value
}
// Path is similar to Value.Path.
func (n *Number) Path(path string) *Value {
return getPath(&n.chain, n.value, path)
}
// Schema is similar to Value.Schema.
func (n *Number) Schema(schema interface{}) *Number {
checkSchema(&n.chain, n.value, schema)
return n
}
// Equal succeeds if number is equal to given value.
//
// value should have numeric type convertible to float64. Before comparison,
// it is converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.Equal(float64(123))
// number.Equal(int32(123))
func (n *Number) Equal(value interface{}) *Number {
v, ok := canonNumber(&n.chain, value)
if !ok {
return n
}
if !(n.value == v) {
n.chain.fail("\nexpected number equal to:\n %v\n\nbut got:\n %v",
v, n.value)
}
return n
}
// NotEqual succeeds if number is not equal to given value.
//
// value should have numeric type convertible to float64. Before comparison,
// it is converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.NotEqual(float64(321))
// number.NotEqual(int32(321))
func (n *Number) NotEqual(value interface{}) *Number {
v, ok := canonNumber(&n.chain, value)
if !ok {
return n
}
if !(n.value != v) {
n.chain.fail("\nexpected number not equal to:\n %v\n\nbut got:\n %v",
v, n.value)
}
return n
}
// EqualDelta succeeds if two numerals are within delta of each other.
//
// Example:
// number := NewNumber(t, 123.0)
// number.EqualDelta(123.2, 0.3)
func (n *Number) EqualDelta(value, delta float64) *Number {
if math.IsNaN(n.value) || math.IsNaN(value) || math.IsNaN(delta) {
n.chain.fail("\nexpected number equal to:\n %v\n\nbut got:\n %v\n\ndelta:\n %v",
value, n.value, delta)
return n
}
diff := (n.value - value)
if diff < -delta || diff > delta {
n.chain.fail("\nexpected number equal to:\n %v\n\nbut got:\n %v\n\ndelta:\n %v",
value, n.value, delta)
return n
}
return n
}
// NotEqualDelta succeeds if two numerals are not within delta of each other.
//
// Example:
// number := NewNumber(t, 123.0)
// number.NotEqualDelta(123.2, 0.1)
func (n *Number) NotEqualDelta(value, delta float64) *Number {
if math.IsNaN(n.value) || math.IsNaN(value) || math.IsNaN(delta) {
n.chain.fail(
"\nexpected number not equal to:\n %v\n\nbut got:\n %v\n\ndelta:\n %v",
value, n.value, delta)
return n
}
diff := (n.value - value)
if !(diff < -delta || diff > delta) {
n.chain.fail(
"\nexpected number not equal to:\n %v\n\nbut got:\n %v\n\ndelta:\n %v",
value, n.value, delta)
return n
}
return n
}
// Gt succeeds if number is greater than given value.
//
// value should have numeric type convertible to float64. Before comparison,
// it is converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.Gt(float64(122))
// number.Gt(int32(122))
func (n *Number) Gt(value interface{}) *Number {
v, ok := canonNumber(&n.chain, value)
if !ok {
return n
}
if !(n.value > v) {
n.chain.fail("\nexpected number > then:\n %v\n\nbut got:\n %v",
v, n.value)
}
return n
}
// Ge succeeds if number is greater than or equal to given value.
//
// value should have numeric type convertible to float64. Before comparison,
// it is converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.Ge(float64(122))
// number.Ge(int32(122))
func (n *Number) Ge(value interface{}) *Number {
v, ok := canonNumber(&n.chain, value)
if !ok {
return n
}
if !(n.value >= v) {
n.chain.fail("\nexpected number >= then:\n %v\n\nbut got:\n %v",
v, n.value)
}
return n
}
// Lt succeeds if number is lesser than given value.
//
// value should have numeric type convertible to float64. Before comparison,
// it is converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.Lt(float64(124))
// number.Lt(int32(124))
func (n *Number) Lt(value interface{}) *Number {
v, ok := canonNumber(&n.chain, value)
if !ok {
return n
}
if !(n.value < v) {
n.chain.fail("\nexpected number < then:\n %v\n\nbut got:\n %v",
v, n.value)
}
return n
}
// Le succeeds if number is lesser than or equal to given value.
//
// value should have numeric type convertible to float64. Before comparison,
// it is converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.Le(float64(124))
// number.Le(int32(124))
func (n *Number) Le(value interface{}) *Number {
v, ok := canonNumber(&n.chain, value)
if !ok {
return n
}
if !(n.value <= v) {
n.chain.fail("\nexpected number <= then:\n %v\n\nbut got:\n %v",
v, n.value)
}
return n
}
// InRange succeeds if number is in given range [min; max].
//
// min and max should have numeric type convertible to float64. Before comparison,
// they are converted to float64.
//
// Example:
// number := NewNumber(t, 123)
// number.InRange(float32(100), int32(200)) // success
// number.InRange(100, 200) // success
// number.InRange(123, 123) // success
func (n *Number) InRange(min, max interface{}) *Number {
a, ok := canonNumber(&n.chain, min)
if !ok {
return n
}
b, ok := canonNumber(&n.chain, max)
if !ok {
return n
}
if !(n.value >= a && n.value <= b) {
n.chain.fail("\nexpected number in range:\n [%v; %v]\n\nbut got:\n %v",
a, b, n.value)
}
return n
}
+339
View File
@@ -0,0 +1,339 @@
package httpexpect
import (
"math"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNumberFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Number{chain, 0}
value.chain.assertFailed(t)
value.Path("$").chain.assertFailed(t)
value.Schema("")
value.Equal(0)
value.NotEqual(0)
value.Gt(0)
value.Ge(0)
value.Lt(0)
value.Le(0)
value.InRange(0, 0)
}
func TestNumberGetters(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 123.0)
assert.Equal(t, 123.0, value.Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, 123.0, value.Path("$").Raw())
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "number"}`)
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "object"}`)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberEqual(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
assert.Equal(t, 1234, int(value.Raw()))
value.Equal(1234)
value.chain.assertOK(t)
value.chain.reset()
value.Equal(4321)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(4321)
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(1234)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberEqualDelta(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234.5)
value.EqualDelta(1234.3, 0.3)
value.chain.assertOK(t)
value.chain.reset()
value.EqualDelta(1234.7, 0.3)
value.chain.assertOK(t)
value.chain.reset()
value.EqualDelta(1234.3, 0.1)
value.chain.assertFailed(t)
value.chain.reset()
value.EqualDelta(1234.7, 0.1)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqualDelta(1234.3, 0.3)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqualDelta(1234.7, 0.3)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqualDelta(1234.3, 0.1)
value.chain.assertOK(t)
value.chain.reset()
value.NotEqualDelta(1234.7, 0.1)
value.chain.assertOK(t)
value.chain.reset()
}
func TestNumberEqualNaN(t *testing.T) {
reporter := newMockReporter(t)
v1 := NewNumber(reporter, math.NaN())
v1.Equal(1234.5)
v1.chain.assertFailed(t)
v2 := NewNumber(reporter, 1234.5)
v2.Equal(math.NaN())
v2.chain.assertFailed(t)
v3 := NewNumber(reporter, math.NaN())
v3.EqualDelta(1234.0, 0.1)
v3.chain.assertFailed(t)
v4 := NewNumber(reporter, 1234.5)
v4.EqualDelta(math.NaN(), 0.1)
v4.chain.assertFailed(t)
v5 := NewNumber(reporter, 1234.5)
v5.EqualDelta(1234.5, math.NaN())
v5.chain.assertFailed(t)
v6 := NewNumber(reporter, math.NaN())
v6.NotEqualDelta(1234.0, 0.1)
v6.chain.assertFailed(t)
v7 := NewNumber(reporter, 1234.5)
v7.NotEqualDelta(math.NaN(), 0.1)
v7.chain.assertFailed(t)
v8 := NewNumber(reporter, 1234.5)
v8.NotEqualDelta(1234.5, math.NaN())
v8.chain.assertFailed(t)
}
func TestNumberGreater(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.Gt(1234 - 1)
value.chain.assertOK(t)
value.chain.reset()
value.Gt(1234)
value.chain.assertFailed(t)
value.chain.reset()
value.Ge(1234 - 1)
value.chain.assertOK(t)
value.chain.reset()
value.Ge(1234)
value.chain.assertOK(t)
value.chain.reset()
value.Ge(1234 + 1)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberLesser(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.Lt(1234 + 1)
value.chain.assertOK(t)
value.chain.reset()
value.Lt(1234)
value.chain.assertFailed(t)
value.chain.reset()
value.Le(1234 + 1)
value.chain.assertOK(t)
value.chain.reset()
value.Le(1234)
value.chain.assertOK(t)
value.chain.reset()
value.Le(1234 - 1)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberInRange(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.InRange(1234, 1234)
value.chain.assertOK(t)
value.chain.reset()
value.InRange(1234-1, 1234)
value.chain.assertOK(t)
value.chain.reset()
value.InRange(1234, 1234+1)
value.chain.assertOK(t)
value.chain.reset()
value.InRange(1234+1, 1234+2)
value.chain.assertFailed(t)
value.chain.reset()
value.InRange(1234-2, 1234-1)
value.chain.assertFailed(t)
value.chain.reset()
value.InRange(1234+1, 1234-1)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberConvertEqual(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.Equal(int64(1234))
value.chain.assertOK(t)
value.chain.reset()
value.Equal(float32(1234))
value.chain.assertOK(t)
value.chain.reset()
value.Equal("1234")
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(int64(4321))
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(float32(4321))
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual("4321")
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberConvertGreater(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.Gt(int64(1233))
value.chain.assertOK(t)
value.chain.reset()
value.Gt(float32(1233))
value.chain.assertOK(t)
value.chain.reset()
value.Gt("1233")
value.chain.assertFailed(t)
value.chain.reset()
value.Ge(int64(1233))
value.chain.assertOK(t)
value.chain.reset()
value.Ge(float32(1233))
value.chain.assertOK(t)
value.chain.reset()
value.Ge("1233")
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberConvertLesser(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.Lt(int64(1235))
value.chain.assertOK(t)
value.chain.reset()
value.Lt(float32(1235))
value.chain.assertOK(t)
value.chain.reset()
value.Lt("1235")
value.chain.assertFailed(t)
value.chain.reset()
value.Le(int64(1235))
value.chain.assertOK(t)
value.chain.reset()
value.Le(float32(1235))
value.chain.assertOK(t)
value.chain.reset()
value.Le("1235")
value.chain.assertFailed(t)
value.chain.reset()
}
func TestNumberConvertInRange(t *testing.T) {
reporter := newMockReporter(t)
value := NewNumber(reporter, 1234)
value.InRange(int64(1233), float32(1235))
value.chain.assertOK(t)
value.chain.reset()
value.InRange(int64(1233), "1235")
value.chain.assertFailed(t)
value.chain.reset()
value.InRange(nil, 1235)
value.chain.assertFailed(t)
value.chain.reset()
}
+329
View File
@@ -0,0 +1,329 @@
package httpexpect
import (
"reflect"
)
// Object provides methods to inspect attached map[string]interface{} object
// (Go representation of JSON object).
type Object struct {
chain chain
value map[string]interface{}
}
// NewObject returns a new Object given a reporter used to report failures
// and value to be inspected.
//
// Both reporter and value should not be nil. If value is nil, failure is
// reported.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
func NewObject(reporter Reporter, value map[string]interface{}) *Object {
chain := makeChain(reporter)
if value == nil {
chain.fail("expected non-nil map value")
} else {
value, _ = canonMap(&chain, value)
}
return &Object{chain, value}
}
// Raw returns underlying value attached to Object.
// This is the value originally passed to NewObject, converted to canonical form.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// assert.Equal(t, map[string]interface{}{"foo": 123.0}, object.Raw())
func (o *Object) Raw() map[string]interface{} {
return o.value
}
// Path is similar to Value.Path.
func (o *Object) Path(path string) *Value {
return getPath(&o.chain, o.value, path)
}
// Schema is similar to Value.Schema.
func (o *Object) Schema(schema interface{}) *Object {
checkSchema(&o.chain, o.value, schema)
return o
}
// Keys returns a new Array object that may be used to inspect objects keys.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
// object.Keys().ContainsOnly("foo", "bar")
func (o *Object) Keys() *Array {
keys := []interface{}{}
for k := range o.value {
keys = append(keys, k)
}
return &Array{o.chain, keys}
}
// Values returns a new Array object that may be used to inspect objects values.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
// object.Values().ContainsOnly(123, 456)
func (o *Object) Values() *Array {
values := []interface{}{}
for _, v := range o.value {
values = append(values, v)
}
return &Array{o.chain, values}
}
// Value returns a new Value object that may be used to inspect single value
// for given key.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.Value("foo").Number().Equal(123)
func (o *Object) Value(key string) *Value {
value, ok := o.value[key]
if !ok {
o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
key, dumpValue(o.value))
return &Value{o.chain, nil}
}
return &Value{o.chain, value}
}
// Empty succeeds if object is empty.
//
// Example:
// object := NewObject(t, map[string]interface{}{})
// object.Empty()
func (o *Object) Empty() *Object {
return o.Equal(map[string]interface{}{})
}
// NotEmpty succeeds if object is non-empty.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.NotEmpty()
func (o *Object) NotEmpty() *Object {
return o.NotEqual(map[string]interface{}{})
}
// Equal succeeds if object is equal to another object.
// Before comparison, both objects are converted to canonical form.
//
// value should map[string]interface{} or struct.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.Equal(map[string]interface{}{"foo": 123})
func (o *Object) Equal(value interface{}) *Object {
expected, ok := canonMap(&o.chain, value)
if !ok {
return o
}
if !reflect.DeepEqual(expected, o.value) {
o.chain.fail("\nexpected object equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
dumpValue(expected),
dumpValue(o.value),
diffValues(expected, o.value))
}
return o
}
// NotEqual succeeds if object is not equal to another object.
// Before comparison, both objects are converted to canonical form.
//
// value should map[string]interface{} or struct.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.Equal(map[string]interface{}{"bar": 123})
func (o *Object) NotEqual(v interface{}) *Object {
expected, ok := canonMap(&o.chain, v)
if !ok {
return o
}
if reflect.DeepEqual(expected, o.value) {
o.chain.fail("\nexpected object not equal to:\n%s",
dumpValue(expected))
}
return o
}
// ContainsKey succeeds if object contains given key.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.ContainsKey("foo")
func (o *Object) ContainsKey(key string) *Object {
if !o.containsKey(key) {
o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
key, dumpValue(o.value))
}
return o
}
// NotContainsKey succeeds if object doesn't contain given key.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.NotContainsKey("bar")
func (o *Object) NotContainsKey(key string) *Object {
if o.containsKey(key) {
o.chain.fail(
"\nexpected object not containing key '%s', but got:\n%s", key,
dumpValue(o.value))
}
return o
}
// ContainsMap succeeds if object contains given sub-object.
// Before comparison, both objects are converted to canonical form.
//
// value should map[string]interface{} or struct.
//
// Example:
// object := NewObject(t, map[string]interface{}{
// "foo": 123,
// "bar": []interface{}{"x", "y"},
// "bar": map[string]interface{}{
// "a": true,
// "b": false,
// },
// })
//
// object.ContainsMap(map[string]interface{}{ // success
// "foo": 123,
// "bar": map[string]interface{}{
// "a": true,
// },
// })
//
// object.ContainsMap(map[string]interface{}{ // failure
// "foo": 123,
// "qux": 456,
// })
//
// object.ContainsMap(map[string]interface{}{ // failure, slices should match exactly
// "bar": []interface{}{"x"},
// })
func (o *Object) ContainsMap(value interface{}) *Object {
if !o.containsMap(value) {
o.chain.fail("\nexpected object containing sub-object:\n%s\n\nbut got:\n%s",
dumpValue(value), dumpValue(o.value))
}
return o
}
// NotContainsMap succeeds if object doesn't contain given sub-object exactly.
// Before comparison, both objects are converted to canonical form.
//
// value should map[string]interface{} or struct.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123, "bar": 456})
// object.NotContainsMap(map[string]interface{}{"foo": 123, "bar": "no-no-no"})
func (o *Object) NotContainsMap(value interface{}) *Object {
if o.containsMap(value) {
o.chain.fail("\nexpected object not containing sub-object:\n%s\n\nbut got:\n%s",
dumpValue(value), dumpValue(o.value))
}
return o
}
// ValueEqual succeeds if object's value for given key is equal to given value.
// Before comparison, both values are converted to canonical form.
//
// value should map[string]interface{} or struct.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.ValueEqual("foo", 123)
func (o *Object) ValueEqual(key string, value interface{}) *Object {
if !o.containsKey(key) {
o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
key, dumpValue(o.value))
return o
}
expected, ok := canonValue(&o.chain, value)
if !ok {
return o
}
if !reflect.DeepEqual(expected, o.value[key]) {
o.chain.fail(
"\nexpected value for key '%s' equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
key,
dumpValue(expected),
dumpValue(o.value[key]),
diffValues(expected, o.value[key]))
}
return o
}
// ValueNotEqual succeeds if object's value for given key is not equal to given value.
// Before comparison, both values are converted to canonical form.
//
// value should map[string]interface{} or struct.
//
// If object doesn't contain any value for given key, failure is reported.
//
// Example:
// object := NewObject(t, map[string]interface{}{"foo": 123})
// object.ValueNotEqual("foo", "bad value") // success
// object.ValueNotEqual("bar", "bad value") // failure! (key is missing)
func (o *Object) ValueNotEqual(key string, value interface{}) *Object {
if !o.containsKey(key) {
o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
key, dumpValue(o.value))
return o
}
expected, ok := canonValue(&o.chain, value)
if !ok {
return o
}
if reflect.DeepEqual(expected, o.value[key]) {
o.chain.fail("\nexpected value for key '%s' not equal to:\n%s",
key, dumpValue(expected))
}
return o
}
func (o *Object) containsKey(key string) bool {
for k := range o.value {
if k == key {
return true
}
}
return false
}
func (o *Object) containsMap(sm interface{}) bool {
submap, ok := canonMap(&o.chain, sm)
if !ok {
return false
}
return checkContainsMap(o.value, submap)
}
func checkContainsMap(outer, inner map[string]interface{}) bool {
for k, iv := range inner {
ov, ok := outer[k]
if !ok {
return false
}
if ovm, ok := ov.(map[string]interface{}); ok {
if ivm, ok := iv.(map[string]interface{}); ok {
if !checkContainsMap(ovm, ivm) {
return false
}
continue
}
}
if !reflect.DeepEqual(ov, iv) {
return false
}
}
return true
}
+655
View File
@@ -0,0 +1,655 @@
package httpexpect
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestObjectFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Object{chain, nil}
value.chain.assertFailed(t)
value.Path("$").chain.assertFailed(t)
value.Schema("")
assert.False(t, value.Keys() == nil)
assert.False(t, value.Values() == nil)
assert.False(t, value.Value("foo") == nil)
value.Keys().chain.assertFailed(t)
value.Values().chain.assertFailed(t)
value.Value("foo").chain.assertFailed(t)
value.Empty()
value.NotEmpty()
value.Equal(nil)
value.NotEqual(nil)
value.ContainsKey("foo")
value.NotContainsKey("foo")
value.ContainsMap(nil)
value.NotContainsMap(nil)
value.ValueEqual("foo", nil)
value.ValueNotEqual("foo", nil)
}
func TestObjectGetters(t *testing.T) {
reporter := newMockReporter(t)
m := map[string]interface{}{
"foo": 123.0,
"bar": []interface{}{"456", 789.0},
"baz": map[string]interface{}{
"a": "b",
},
}
value := NewObject(reporter, m)
keys := []interface{}{"foo", "bar", "baz"}
values := []interface{}{
123.0,
[]interface{}{"456", 789.0},
map[string]interface{}{
"a": "b",
},
}
assert.Equal(t, m, value.Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, m, value.Path("$").Raw())
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "object"}`)
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "array"}`)
value.chain.assertFailed(t)
value.chain.reset()
value.Keys().ContainsOnly(keys...)
value.chain.assertOK(t)
value.chain.reset()
value.Values().ContainsOnly(values...)
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, m["foo"], value.Value("foo").Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, m["bar"], value.Value("bar").Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, m["baz"], value.Value("baz").Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, nil, value.Value("BAZ").Raw())
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectEmpty(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewObject(reporter, nil)
_ = value1
value1.chain.assertFailed(t)
value1.chain.reset()
value2 := NewObject(reporter, map[string]interface{}{})
value2.Empty()
value2.chain.assertOK(t)
value2.chain.reset()
value2.NotEmpty()
value2.chain.assertFailed(t)
value2.chain.reset()
value3 := NewObject(reporter, map[string]interface{}{"": nil})
value3.Empty()
value3.chain.assertFailed(t)
value3.chain.reset()
value3.NotEmpty()
value3.chain.assertOK(t)
value3.chain.reset()
}
func TestObjectEqualEmpty(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{})
assert.Equal(t, map[string]interface{}{}, value.Raw())
value.Equal(map[string]interface{}{})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{})
value.chain.assertFailed(t)
value.chain.reset()
value.Equal(map[string]interface{}{"": nil})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"": nil})
value.chain.assertOK(t)
value.chain.reset()
}
func TestObjectEqual(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{"foo": 123.0})
assert.Equal(t, map[string]interface{}{"foo": 123.0}, value.Raw())
value.Equal(map[string]interface{}{})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{})
value.chain.assertOK(t)
value.chain.reset()
value.Equal(map[string]interface{}{"FOO": 123.0})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"FOO": 123.0})
value.chain.assertOK(t)
value.chain.reset()
value.Equal(map[string]interface{}{"foo": 456.0})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"foo": 456.0})
value.chain.assertOK(t)
value.chain.reset()
value.Equal(map[string]interface{}{"foo": 123.0})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"foo": 123.0})
value.chain.assertFailed(t)
value.chain.reset()
value.Equal(nil)
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(nil)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectEqualStruct(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": map[string]interface{}{
"baz": []interface{}{true, false},
},
})
type (
Bar struct {
Baz []bool `json:"baz"`
}
S struct {
Foo int `json:"foo"`
Bar Bar `json:"bar"`
}
)
s := S{
Foo: 123,
Bar: Bar{
Baz: []bool{true, false},
},
}
value.Equal(s)
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(s)
value.chain.assertFailed(t)
value.chain.reset()
value.Equal(S{})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(S{})
value.chain.assertOK(t)
value.chain.reset()
}
func TestObjectContainsKey(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{"foo": 123, "bar": ""})
value.ContainsKey("foo")
value.chain.assertOK(t)
value.chain.reset()
value.NotContainsKey("foo")
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsKey("bar")
value.chain.assertOK(t)
value.chain.reset()
value.NotContainsKey("bar")
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsKey("BAR")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsKey("BAR")
value.chain.assertOK(t)
value.chain.reset()
}
func TestObjectContainsMapSuccess(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": map[string]interface{}{
"b": 333,
"c": 444,
},
},
})
submap1 := map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
}
value.ContainsMap(submap1)
value.chain.assertOK(t)
value.chain.reset()
value.NotContainsMap(submap1)
value.chain.assertFailed(t)
value.chain.reset()
submap2 := map[string]interface{}{
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": map[string]interface{}{
"c": 444,
},
},
}
value.ContainsMap(submap2)
value.chain.assertOK(t)
value.chain.reset()
value.NotContainsMap(submap2)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectContainsMapFailed(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": map[string]interface{}{
"b": 333,
"c": 444,
},
},
})
submap1 := map[string]interface{}{
"foo": 123,
"qux": 456,
}
value.ContainsMap(submap1)
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsMap(submap1)
value.chain.assertOK(t)
value.chain.reset()
submap2 := map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", "789"},
}
value.ContainsMap(submap2)
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsMap(submap2)
value.chain.assertOK(t)
value.chain.reset()
submap3 := map[string]interface{}{
"baz": map[string]interface{}{
"a": map[string]interface{}{
"b": "333",
"c": 444,
},
},
}
value.ContainsMap(submap3)
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsMap(submap3)
value.chain.assertOK(t)
value.chain.reset()
value.ContainsMap(nil)
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsMap(nil)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectContainsMapStruct(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": map[string]interface{}{
"b": 333,
"c": 444,
},
},
})
type (
A struct {
B int `json:"b"`
}
Baz struct {
A A `json:"a"`
}
S struct {
Foo int `json:"foo"`
Baz Baz `json:"baz"`
}
)
submap := S{
Foo: 123,
Baz: Baz{
A: A{
B: 333,
},
},
}
value.ContainsMap(submap)
value.chain.assertOK(t)
value.chain.reset()
value.NotContainsMap(submap)
value.chain.assertFailed(t)
value.chain.reset()
value.ContainsMap(S{})
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsMap(S{})
value.chain.assertOK(t)
value.chain.reset()
}
func TestObjectValueEqual(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": "b",
},
})
value.ValueEqual("foo", 123)
value.chain.assertOK(t)
value.chain.reset()
value.ValueNotEqual("foo", 123)
value.chain.assertFailed(t)
value.chain.reset()
value.ValueEqual("bar", []interface{}{"456", 789})
value.chain.assertOK(t)
value.chain.reset()
value.ValueNotEqual("bar", []interface{}{"456", 789})
value.chain.assertFailed(t)
value.chain.reset()
value.ValueEqual("baz", map[string]interface{}{"a": "b"})
value.chain.assertOK(t)
value.chain.reset()
value.ValueNotEqual("baz", map[string]interface{}{"a": "b"})
value.chain.assertFailed(t)
value.chain.reset()
value.ValueEqual("baz", func() {})
value.chain.assertFailed(t)
value.chain.reset()
value.ValueNotEqual("baz", func() {})
value.chain.assertFailed(t)
value.chain.reset()
value.ValueEqual("BAZ", 777)
value.chain.assertFailed(t)
value.chain.reset()
value.ValueNotEqual("BAZ", 777)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectValueEqualStruct(t *testing.T) {
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": map[string]interface{}{
"b": 333,
"c": 444,
},
},
})
type (
A struct {
B int `json:"b"`
C int `json:"c"`
}
Baz struct {
A A `json:"a"`
}
)
baz := Baz{
A: A{
B: 333,
C: 444,
},
}
value.ValueEqual("baz", baz)
value.chain.assertOK(t)
value.chain.reset()
value.ValueNotEqual("baz", baz)
value.chain.assertFailed(t)
value.chain.reset()
value.ValueEqual("baz", Baz{})
value.chain.assertFailed(t)
value.chain.reset()
value.ValueNotEqual("baz", Baz{})
value.chain.assertOK(t)
value.chain.reset()
}
func TestObjectConvertEqual(t *testing.T) {
type (
myMap map[string]interface{}
myInt int
)
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{"foo": 123})
value.Equal(map[string]interface{}{"foo": "123"})
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"foo": "123"})
value.chain.assertOK(t)
value.chain.reset()
value.Equal(map[string]interface{}{"foo": 123.0})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"foo": 123.0})
value.chain.assertFailed(t)
value.chain.reset()
value.Equal(map[string]interface{}{"foo": 123})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(map[string]interface{}{"foo": 123})
value.chain.assertFailed(t)
value.chain.reset()
value.Equal(myMap{"foo": myInt(123)})
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual(myMap{"foo": myInt(123)})
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectConvertContainsMap(t *testing.T) {
type (
myArray []interface{}
myMap map[string]interface{}
myInt int
)
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": "b",
},
})
submap := myMap{
"foo": myInt(123),
"bar": myArray{"456", myInt(789)},
}
value.ContainsMap(submap)
value.chain.assertOK(t)
value.chain.reset()
value.NotContainsMap(submap)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestObjectConvertValueEqual(t *testing.T) {
type (
myArray []interface{}
myMap map[string]interface{}
myInt int
)
reporter := newMockReporter(t)
value := NewObject(reporter, map[string]interface{}{
"foo": 123,
"bar": []interface{}{"456", 789},
"baz": map[string]interface{}{
"a": "b",
},
})
value.ValueEqual("bar", myArray{"456", myInt(789)})
value.chain.assertOK(t)
value.chain.reset()
value.ValueNotEqual("bar", myArray{"456", myInt(789)})
value.chain.assertFailed(t)
value.chain.reset()
value.ValueEqual("baz", myMap{"a": "b"})
value.chain.assertOK(t)
value.chain.reset()
value.ValueNotEqual("baz", myMap{"a": "b"})
value.chain.assertFailed(t)
value.chain.reset()
}
+100
View File
@@ -0,0 +1,100 @@
package httpexpect
import (
"net/http"
"net/http/httputil"
"strings"
"time"
"github.com/moul/http2curl"
)
// CompactPrinter implements Printer. It prints requests in compact form.
type CompactPrinter struct {
logger Logger
}
// NewCompactPrinter returns a new CompactPrinter given a logger.
func NewCompactPrinter(logger Logger) CompactPrinter {
return CompactPrinter{logger}
}
// Request implements Printer.Request.
func (p CompactPrinter) Request(req *http.Request) {
if req != nil {
p.logger.Logf("%s %s", req.Method, req.URL)
}
}
// Response implements Printer.Response.
func (CompactPrinter) Response(*http.Response, time.Duration) {
}
// DebugPrinter implements Printer. Uses net/http/httputil to dump
// both requests and responses.
type DebugPrinter struct {
logger Logger
body bool
}
// NewDebugPrinter returns a new DebugPrinter given a logger and body
// flag. If body is true, request and response body is also printed.
func NewDebugPrinter(logger Logger, body bool) DebugPrinter {
return DebugPrinter{logger, body}
}
// Request implements Printer.Request.
func (p DebugPrinter) Request(req *http.Request) {
if req == nil {
return
}
dump, err := httputil.DumpRequest(req, p.body)
if err != nil {
panic(err)
}
p.logger.Logf("%s", dump)
}
// Response implements Printer.Response.
func (p DebugPrinter) Response(resp *http.Response, duration time.Duration) {
if resp == nil {
return
}
dump, err := httputil.DumpResponse(resp, p.body)
if err != nil {
panic(err)
}
text := strings.Replace(string(dump), "\r\n", "\n", -1)
lines := strings.SplitN(text, "\n", 2)
p.logger.Logf("%s %s\n%s", lines[0], duration, lines[1])
}
// CurlPrinter implements Printer. Uses http2curl to dump requests as
// curl commands.
type CurlPrinter struct {
logger Logger
}
// NewCurlPrinter returns a new CurlPrinter given a logger.
func NewCurlPrinter(logger Logger) CurlPrinter {
return CurlPrinter{logger}
}
// Request implements Printer.Request.
func (p CurlPrinter) Request(req *http.Request) {
if req != nil {
cmd, err := http2curl.GetCurlCommand(req)
if err != nil {
panic(err)
}
p.logger.Logf("%s", cmd.String())
}
}
// Response implements Printer.Response.
func (CurlPrinter) Response(*http.Response, time.Duration) {
}
+44
View File
@@ -0,0 +1,44 @@
package httpexpect
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
)
func TestCompactPrinter(t *testing.T) {
printer := NewCompactPrinter(t)
body1 := bytes.NewBufferString("body1")
body2 := bytes.NewBufferString("body2")
req1, _ := http.NewRequest("GET", "http://example.com", body1)
req2, _ := http.NewRequest("GET", "http://example.com", nil)
printer.Request(req1)
printer.Request(req2)
printer.Request(nil)
printer.Response(&http.Response{Body: ioutil.NopCloser(body2)}, 0)
printer.Response(&http.Response{}, 0)
printer.Response(nil, 0)
}
func TestDebugPrinter(t *testing.T) {
printer := NewDebugPrinter(t, true)
body1 := bytes.NewBufferString("body1")
body2 := bytes.NewBufferString("body2")
req1, _ := http.NewRequest("GET", "http://example.com", body1)
req2, _ := http.NewRequest("GET", "http://example.com", nil)
printer.Request(req1)
printer.Request(req2)
printer.Request(nil)
printer.Response(&http.Response{Body: ioutil.NopCloser(body2)}, 0)
printer.Response(&http.Response{}, 0)
printer.Response(nil, 0)
}
+40
View File
@@ -0,0 +1,40 @@
package httpexpect
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// AssertReporter implements Reporter interface using `testify/assert'
// package. Failures are non-fatal with this reporter.
type AssertReporter struct {
backend *assert.Assertions
}
// NewAssertReporter returns a new AssertReporter object.
func NewAssertReporter(t assert.TestingT) *AssertReporter {
return &AssertReporter{assert.New(t)}
}
// Errorf implements Reporter.Errorf.
func (r *AssertReporter) Errorf(message string, args ...interface{}) {
r.backend.Fail(fmt.Sprintf(message, args...))
}
// RequireReporter implements Reporter interface using `testify/require'
// package. Failures fatal with this reporter.
type RequireReporter struct {
backend *require.Assertions
}
// NewRequireReporter returns a new RequireReporter object.
func NewRequireReporter(t require.TestingT) *RequireReporter {
return &RequireReporter{require.New(t)}
}
// Errorf implements Reporter.Errorf.
func (r *RequireReporter) Errorf(message string, args ...interface{}) {
r.backend.FailNow(fmt.Sprintf(message, args...))
}
+912
View File
@@ -0,0 +1,912 @@
package httpexpect
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"reflect"
"sort"
"strings"
"time"
"github.com/ajg/form"
"github.com/fatih/structs"
"github.com/gavv/monotime"
"github.com/google/go-querystring/query"
"github.com/imkira/go-interpol"
)
// Request provides methods to incrementally build http.Request object,
// send it, and receive response.
type Request struct {
config Config
chain chain
http *http.Request
path string
query url.Values
form url.Values
formbuf *bytes.Buffer
multipart *multipart.Writer
forcetype bool
typesetter string
bodysetter string
}
// NewRequest returns a new Request object.
//
// method defines the HTTP method (GET, POST, PUT, etc.). path defines url path.
//
// Simple interpolation is allowed for {named} parameters in path:
// - if pathargs is given, it's used to substitute first len(pathargs) parameters,
// regardless of their names
// - if WithPath() or WithPathObject() is called, it's used to substitute given
// parameters by name
//
// For example:
// req := NewRequest(config, "POST", "/repos/{user}/{repo}", "gavv", "httpexpect")
// // path will be "/repos/gavv/httpexpect"
//
// Or:
// req := NewRequest(config, "POST", "/repos/{user}/{repo}")
// req.WithPath("user", "gavv")
// req.WithPath("repo", "httpexpect")
// // path will be "/repos/gavv/httpexpect"
//
// After interpolation, path is urlencoded and appended to Config.BaseURL,
// separated by slash. If BaseURL ends with a slash and path (after interpolation)
// starts with a slash, only single slash is inserted.
func NewRequest(config Config, method, path string, pathargs ...interface{}) *Request {
if config.RequestFactory == nil {
panic("config.RequestFactory == nil")
}
if config.Client == nil {
panic("config.Client == nil")
}
chain := makeChain(config.Reporter)
n := 0
path, err := interpol.WithFunc(path, func(k string, w io.Writer) error {
if n < len(pathargs) {
if pathargs[n] == nil {
chain.fail(
"\nunexpected nil argument for url path format string:\n"+
" Request(\"%s\", %v...)", method, pathargs)
} else {
w.Write([]byte(fmt.Sprint(pathargs[n])))
}
} else {
w.Write([]byte("{"))
w.Write([]byte(k))
w.Write([]byte("}"))
}
n++
return nil
})
if err != nil {
chain.fail(err.Error())
}
hr, err := config.RequestFactory.NewRequest(method, config.BaseURL, nil)
if err != nil {
chain.fail(err.Error())
}
return &Request{
config: config,
chain: chain,
path: path,
http: hr,
}
}
// WithClient sets client.
//
// The new client overwrites Config.Client. It will be use once to send the
// request and receive a response.
//
// Example:
// req := NewRequest(config, "GET", "/path")
// req.WithClient(&http.Client{
// Transport: &http.Transport{
// DisableCompression: true,
// },
// })
func (r *Request) WithClient(client Client) *Request {
if r.chain.failed() {
return r
}
if client == nil {
r.chain.fail("\nunexpected nil client in WithClient")
return r
}
r.config.Client = client
return r
}
// WithHandler configures client to invoke the given handler directly.
//
// If Config.Client is http.Client, then only its Transport field is overwritten
// because the client may contain some state shared among requests like a cookie
// jar. Otherwise, the whole client is overwritten with a new client.
//
// Example:
// req := NewRequest(config, "GET", "/path")
// req.WithHandler(myServer.someHandler)
func (r *Request) WithHandler(handler http.Handler) *Request {
if r.chain.failed() {
return r
}
if handler == nil {
r.chain.fail("\nunexpected nil handler in WithHandler")
return r
}
if client, ok := r.config.Client.(*http.Client); ok {
client.Transport = NewBinder(handler)
} else {
r.config.Client = &http.Client{
Transport: NewBinder(handler),
Jar: NewJar(),
}
}
return r
}
// WithPath substitutes named parameters in url path.
//
// value is converted to string using fmt.Sprint(). If there is no named
// parameter '{key}' in url path, failure is reported.
//
// Named parameters are case-insensitive.
//
// Example:
// req := NewRequest(config, "POST", "/repos/{user}/{repo}")
// req.WithPath("user", "gavv")
// req.WithPath("repo", "httpexpect")
// // path will be "/repos/gavv/httpexpect"
func (r *Request) WithPath(key string, value interface{}) *Request {
if r.chain.failed() {
return r
}
ok := false
path, err := interpol.WithFunc(r.path, func(k string, w io.Writer) error {
if strings.EqualFold(k, key) {
if value == nil {
r.chain.fail(
"\nunexpected nil argument for url path format string:\n"+
" WithPath(\"%s\", %v)", key, value)
} else {
w.Write([]byte(fmt.Sprint(value)))
ok = true
}
} else {
w.Write([]byte("{"))
w.Write([]byte(k))
w.Write([]byte("}"))
}
return nil
})
if err == nil {
r.path = path
} else {
r.chain.fail(err.Error())
return r
}
if !ok {
r.chain.fail("\nunexpected key for url path format string:\n"+
" WithPath(\"%s\", %v)\n\npath:\n %q",
key, value, r.path)
return r
}
return r
}
// WithPathObject substitutes multiple named parameters in url path.
//
// object should be map or struct. If object is struct, it's converted
// to map using https://github.com/fatih/structs. Structs may contain
// "path" struct tag, similar to "json" struct tag for json.Marshal().
//
// Each map value is converted to string using fmt.Sprint(). If there
// is no named parameter for some map '{key}' in url path, failure is
// reported.
//
// Named parameters are case-insensitive.
//
// Example:
// type MyPath struct {
// Login string `path:"user"`
// Repo string
// }
//
// req := NewRequest(config, "POST", "/repos/{user}/{repo}")
// req.WithPathObject(MyPath{"gavv", "httpexpect"})
// // path will be "/repos/gavv/httpexpect"
//
// req := NewRequest(config, "POST", "/repos/{user}/{repo}")
// req.WithPathObject(map[string]string{"user": "gavv", "repo": "httpexpect"})
// // path will be "/repos/gavv/httpexpect"
func (r *Request) WithPathObject(object interface{}) *Request {
if r.chain.failed() {
return r
}
if object == nil {
return r
}
var (
m map[string]interface{}
ok bool
)
if reflect.Indirect(reflect.ValueOf(object)).Kind() == reflect.Struct {
s := structs.New(object)
s.TagName = "path"
m = s.Map()
} else {
m, ok = canonMap(&r.chain, object)
if !ok {
return r
}
}
for k, v := range m {
r.WithPath(k, v)
}
return r
}
// WithQuery adds query parameter to request URL.
//
// value is converted to string using fmt.Sprint() and urlencoded.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithQuery("a", 123)
// req.WithQuery("b", "foo")
// // URL is now http://example.com/path?a=123&b=foo
func (r *Request) WithQuery(key string, value interface{}) *Request {
if r.chain.failed() {
return r
}
if r.query == nil {
r.query = make(url.Values)
}
r.query.Add(key, fmt.Sprint(value))
return r
}
// WithQueryObject adds multiple query parameters to request URL.
//
// object is converted to query string using github.com/google/go-querystring
// if it's a struct or pointer to struct, or github.com/ajg/form otherwise.
//
// Various object types are supported. Structs may contain "url" struct tag,
// similar to "json" struct tag for json.Marshal().
//
// Example:
// type MyURL struct {
// A int `url:"a"`
// B string `url:"b"`
// }
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithQueryObject(MyURL{A: 123, B: "foo"})
// // URL is now http://example.com/path?a=123&b=foo
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithQueryObject(map[string]interface{}{"a": 123, "b": "foo"})
// // URL is now http://example.com/path?a=123&b=foo
func (r *Request) WithQueryObject(object interface{}) *Request {
if r.chain.failed() {
return r
}
if object == nil {
return r
}
var (
q url.Values
err error
)
if reflect.Indirect(reflect.ValueOf(object)).Kind() == reflect.Struct {
q, err = query.Values(object)
if err != nil {
r.chain.fail(err.Error())
return r
}
} else {
q, err = form.EncodeToValues(object)
if err != nil {
r.chain.fail(err.Error())
return r
}
}
if r.query == nil {
r.query = make(url.Values)
}
for k, v := range q {
r.query[k] = append(r.query[k], v...)
}
return r
}
// WithQueryString parses given query string and adds it to request URL.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithQuery("a", 11)
// req.WithQueryString("b=22&c=33")
// // URL is now http://example.com/path?a=11&bb=22&c=33
func (r *Request) WithQueryString(query string) *Request {
if r.chain.failed() {
return r
}
v, err := url.ParseQuery(query)
if err != nil {
r.chain.fail(err.Error())
return r
}
if r.query == nil {
r.query = make(url.Values)
}
for k, v := range v {
r.query[k] = append(r.query[k], v...)
}
return r
}
// WithURL sets request URL.
//
// This URL overwrites Config.BaseURL. Request path passed to NewRequest()
// is appended to this URL, separated by slash if necessary.
//
// Example:
// req := NewRequest(config, "PUT", "/path")
// req.WithURL("http://example.com")
// // URL is now http://example.com/path
func (r *Request) WithURL(urlStr string) *Request {
if r.chain.failed() {
return r
}
if u, err := url.Parse(urlStr); err == nil {
r.http.URL = u
} else {
r.chain.fail(err.Error())
}
return r
}
// WithHeaders adds given headers to request.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithHeaders(map[string]string{
// "Content-Type": "application/json",
// })
func (r *Request) WithHeaders(headers map[string]string) *Request {
if r.chain.failed() {
return r
}
for k, v := range headers {
r.WithHeader(k, v)
}
return r
}
// WithHeader adds given single header to request.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithHeader("Content-Type": "application/json")
func (r *Request) WithHeader(k, v string) *Request {
if r.chain.failed() {
return r
}
switch http.CanonicalHeaderKey(k) {
case "Host":
r.http.Host = v
case "Content-Type":
if !r.forcetype {
delete(r.http.Header, "Content-Type")
}
r.forcetype = true
r.typesetter = "WithHeader"
r.http.Header.Add(k, v)
default:
r.http.Header.Add(k, v)
}
return r
}
// WithCookies adds given cookies to request.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithCookies(map[string]string{
// "foo": "aa",
// "bar": "bb",
// })
func (r *Request) WithCookies(cookies map[string]string) *Request {
if r.chain.failed() {
return r
}
for k, v := range cookies {
r.WithCookie(k, v)
}
return r
}
// WithCookie adds given single cookie to request.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithCookie("name", "value")
func (r *Request) WithCookie(k, v string) *Request {
if r.chain.failed() {
return r
}
r.http.AddCookie(&http.Cookie{
Name: k,
Value: v,
})
return r
}
// WithBasicAuth sets the request's Authorization header to use HTTP
// Basic Authentication with the provided username and password.
//
// With HTTP Basic Authentication the provided username and password
// are not encrypted.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithBasicAuth("john", "secret")
func (r *Request) WithBasicAuth(username, password string) *Request {
if r.chain.failed() {
return r
}
r.http.SetBasicAuth(username, password)
return r
}
// WithProto sets HTTP protocol version.
//
// proto should have form of "HTTP/{major}.{minor}", e.g. "HTTP/1.1".
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithProto("HTTP/2.0")
func (r *Request) WithProto(proto string) *Request {
if r.chain.failed() {
return r
}
major, minor, ok := http.ParseHTTPVersion(proto)
if !ok {
r.chain.fail(
"\nunexpected protocol version %q, expected \"HTTP/{major}.{minor}\"",
proto)
return r
}
r.http.ProtoMajor = major
r.http.ProtoMinor = minor
return r
}
// WithChunked enables chunked encoding and sets request body reader.
//
// Expect() will read all available data from given reader. Content-Length
// is not set, and "chunked" Transfer-Encoding is used.
//
// If protocol version is not at least HTTP/1.1 (required for chunked
// encoding), failure is reported.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/upload")
// fh, _ := os.Open("data")
// defer fh.Close()
// req.WithHeader("Content-Type": "application/octet-stream")
// req.WithChunked(fh)
func (r *Request) WithChunked(reader io.Reader) *Request {
if r.chain.failed() {
return r
}
if !r.http.ProtoAtLeast(1, 1) {
r.chain.fail("chunked Transfer-Encoding requires at least \"HTTP/1.1\","+
"but \"HTTP/%d.%d\" is enabled", r.http.ProtoMajor, r.http.ProtoMinor)
return r
}
r.setBody("WithChunked", reader, -1, false)
return r
}
// WithBytes sets request body to given slice of bytes.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithHeader("Content-Type": "application/json")
// req.WithBytes([]byte(`{"foo": 123}`))
func (r *Request) WithBytes(b []byte) *Request {
if r.chain.failed() {
return r
}
if b == nil {
r.setBody("WithBytes", nil, 0, false)
} else {
r.setBody("WithBytes", bytes.NewReader(b), len(b), false)
}
return r
}
// WithText sets Content-Type header to "text/plain; charset=utf-8" and
// sets body to given string.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithText("hello, world!")
func (r *Request) WithText(s string) *Request {
if r.chain.failed() {
return r
}
r.setType("WithText", "text/plain; charset=utf-8", false)
r.setBody("WithText", strings.NewReader(s), len(s), false)
return r
}
// WithJSON sets Content-Type header to "application/json; charset=utf-8"
// and sets body to object, marshaled using json.Marshal().
//
// Example:
// type MyJSON struct {
// Foo int `json:"foo"`
// }
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithJSON(MyJSON{Foo: 123})
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithJSON(map[string]interface{}{"foo": 123})
func (r *Request) WithJSON(object interface{}) *Request {
if r.chain.failed() {
return r
}
b, err := json.Marshal(object)
if err != nil {
r.chain.fail(err.Error())
return r
}
r.setType("WithJSON", "application/json; charset=utf-8", false)
r.setBody("WithJSON", bytes.NewReader(b), len(b), false)
return r
}
// WithForm sets Content-Type header to "application/x-www-form-urlencoded"
// or (if WithMultipart() was called) "multipart/form-data", converts given
// object to url.Values using github.com/ajg/form, and adds it to request body.
//
// Various object types are supported, including maps and structs. Structs may
// contain "form" struct tag, similar to "json" struct tag for json.Marshal().
// See https://github.com/ajg/form for details.
//
// Multiple WithForm(), WithFormField(), and WithFile() calls may be combined.
// If WithMultipart() is called, it should be called first.
//
// Example:
// type MyForm struct {
// Foo int `form:"foo"`
// }
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithForm(MyForm{Foo: 123})
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithForm(map[string]interface{}{"foo": 123})
func (r *Request) WithForm(object interface{}) *Request {
if r.chain.failed() {
return r
}
f, err := form.EncodeToValues(object)
if err != nil {
r.chain.fail(err.Error())
return r
}
if r.multipart != nil {
r.setType("WithForm", "multipart/form-data", false)
var keys []string
for k := range f {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if err := r.multipart.WriteField(k, f[k][0]); err != nil {
r.chain.fail(err.Error())
return r
}
}
} else {
r.setType("WithForm", "application/x-www-form-urlencoded", false)
if r.form == nil {
r.form = make(url.Values)
}
for k, v := range f {
r.form[k] = append(r.form[k], v...)
}
}
return r
}
// WithFormField sets Content-Type header to "application/x-www-form-urlencoded"
// or (if WithMultipart() was called) "multipart/form-data", converts given
// value to string using fmt.Sprint(), and adds it to request body.
//
// Multiple WithForm(), WithFormField(), and WithFile() calls may be combined.
// If WithMultipart() is called, it should be called first.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithFormField("foo", 123).
// WithFormField("bar", 456)
func (r *Request) WithFormField(key string, value interface{}) *Request {
if r.chain.failed() {
return r
}
if r.multipart != nil {
r.setType("WithFormField", "multipart/form-data", false)
err := r.multipart.WriteField(key, fmt.Sprint(value))
if err != nil {
r.chain.fail(err.Error())
return r
}
} else {
r.setType("WithFormField", "application/x-www-form-urlencoded", false)
if r.form == nil {
r.form = make(url.Values)
}
r.form[key] = append(r.form[key], fmt.Sprint(value))
}
return r
}
// WithFile sets Content-Type header to "multipart/form-data", reads given
// file and adds its contents to request body.
//
// If reader is given, it's used to read file contents. Otherwise, os.Open()
// is used to read a file with given path.
//
// Multiple WithForm(), WithFormField(), and WithFile() calls may be combined.
// WithMultipart() should be called before WithFile(), otherwise WithFile()
// fails.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithFile("avatar", "./john.png")
//
// req := NewRequest(config, "PUT", "http://example.com/path")
// fh, _ := os.Open("./john.png")
// req.WithMultipart().
// WithFile("avatar", "john.png", fh)
// fh.Close()
func (r *Request) WithFile(key, path string, reader ...io.Reader) *Request {
if r.chain.failed() {
return r
}
r.setType("WithFile", "multipart/form-data", false)
if r.multipart == nil {
r.chain.fail("WithFile requires WithMultipart to be called first")
return r
}
wr, err := r.multipart.CreateFormFile(key, path)
if err != nil {
r.chain.fail(err.Error())
return r
}
var rd io.Reader
if len(reader) != 0 && reader[0] != nil {
rd = reader[0]
} else {
f, err := os.Open(path)
if err != nil {
r.chain.fail(err.Error())
return r
}
rd = f
defer f.Close()
}
if _, err := io.Copy(wr, rd); err != nil {
r.chain.fail(err.Error())
return r
}
return r
}
// WithFileBytes is like WithFile, but uses given slice of bytes as the
// file contents.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// fh, _ := os.Open("./john.png")
// b, _ := ioutil.ReadAll(fh)
// req.WithMultipart().
// WithFileBytes("avatar", "john.png", b)
// fh.Close()
func (r *Request) WithFileBytes(key, path string, data []byte) *Request {
if r.chain.failed() {
return r
}
return r.WithFile(key, path, bytes.NewReader(data))
}
// WithMultipart sets Content-Type header to "multipart/form-data".
//
// After this call, WithForm() and WithFormField() switch to multipart
// form instead of urlencoded form.
//
// If WithMultipart() is called, it should be called before WithForm(),
// WithFormField(), and WithFile().
//
// WithFile() always requires WithMultipart() to be called first.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithMultipart().
// WithForm(map[string]interface{}{"foo": 123})
func (r *Request) WithMultipart() *Request {
if r.chain.failed() {
return r
}
r.setType("WithMultipart", "multipart/form-data", false)
if r.multipart == nil {
r.formbuf = new(bytes.Buffer)
r.multipart = multipart.NewWriter(r.formbuf)
r.setBody("WithMultipart", r.formbuf, 0, false)
}
return r
}
// Expect constructs http.Request, sends it, receives http.Response, and
// returns a new Response object to inspect received response.
//
// Request is sent using Config.Client interface.
//
// Example:
// req := NewRequest(config, "PUT", "http://example.com/path")
// req.WithJSON(map[string]interface{}{"foo": 123})
// resp := req.Expect()
// resp.Status(http.StatusOK)
func (r *Request) Expect() *Response {
r.encodeRequest()
resp, elapsed := r.sendRequest()
return makeResponse(r.chain, resp, elapsed)
}
func (r *Request) encodeRequest() {
if r.chain.failed() {
return
}
r.http.URL.Path = concatPaths(r.http.URL.Path, r.path)
if r.query != nil {
r.http.URL.RawQuery = r.query.Encode()
}
if r.multipart != nil {
if err := r.multipart.Close(); err != nil {
r.chain.fail(err.Error())
return
}
r.setType("Expect", r.multipart.FormDataContentType(), true)
r.setBody("Expect", r.formbuf, r.formbuf.Len(), true)
} else if r.form != nil {
s := r.form.Encode()
r.setBody("WithForm or WithFormField", strings.NewReader(s), len(s), false)
}
}
func (r *Request) sendRequest() (resp *http.Response, elapsed time.Duration) {
if r.chain.failed() {
return
}
for _, printer := range r.config.Printers {
printer.Request(r.http)
}
start := monotime.Now()
resp, err := r.config.Client.Do(r.http)
elapsed = monotime.Since(start)
if err != nil {
r.chain.fail(err.Error())
return
}
for _, printer := range r.config.Printers {
printer.Response(resp, elapsed)
}
return
}
func (r *Request) setType(newSetter, newType string, overwrite bool) {
if r.forcetype {
return
}
if !overwrite {
previousType := r.http.Header.Get("Content-Type")
if previousType != "" && previousType != newType {
r.chain.fail(
"\nambiguous request \"Content-Type\" header values:\n %q (set by %s)\n\n"+
"and:\n %q (wanted by %s)",
previousType, r.typesetter,
newType, newSetter)
return
}
}
r.typesetter = newSetter
r.http.Header["Content-Type"] = []string{newType}
}
func (r *Request) setBody(setter string, reader io.Reader, len int, overwrite bool) {
if !overwrite && r.bodysetter != "" {
r.chain.fail(
"\nambiguous request body contents:\n set by %s\n overwritten by %s",
r.bodysetter, setter)
return
}
if len > 0 && reader == nil {
panic("invalid length")
}
if reader == nil {
r.http.Body = nil
r.http.ContentLength = 0
} else {
r.http.Body = ioutil.NopCloser(reader)
r.http.ContentLength = int64(len)
}
r.bodysetter = setter
}
func concatPaths(a, b string) string {
if a == "" {
return b
}
if b == "" {
return a
}
a = strings.TrimSuffix(a, "/")
b = strings.TrimPrefix(b, "/")
return a + "/" + b
}
+1422
View File
File diff suppressed because it is too large Load Diff
+511
View File
@@ -0,0 +1,511 @@
package httpexpect
import (
"bytes"
"encoding/json"
"io/ioutil"
"mime"
"net/http"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/ajg/form"
)
// StatusRange is enum for response status ranges.
type StatusRange int
const (
// Status1xx defines "Informational" status codes.
Status1xx StatusRange = 100
// Status2xx defines "Success" status codes.
Status2xx StatusRange = 200
// Status3xx defines "Redirection" status codes.
Status3xx StatusRange = 300
// Status4xx defines "Client Error" status codes.
Status4xx StatusRange = 400
// Status5xx defines "Server Error" status codes.
Status5xx StatusRange = 500
)
// Response provides methods to inspect attached http.Response object.
type Response struct {
chain chain
resp *http.Response
content []byte
cookies []*http.Cookie
time time.Duration
}
// NewResponse returns a new Response given a reporter used to report
// failures and http.Response to be inspected.
//
// Both reporter and response should not be nil. If response is nil,
// failure is reported.
//
// If duration is given, it defines response time to be reported by
// response.Duration().
func NewResponse(
reporter Reporter, response *http.Response, duration ...time.Duration) *Response {
var dr time.Duration
if len(duration) > 0 {
dr = duration[0]
}
return makeResponse(makeChain(reporter), response, dr)
}
func makeResponse(
chain chain, response *http.Response, duration time.Duration) *Response {
var content []byte
var cookies []*http.Cookie
if response != nil {
content = getContent(&chain, response)
cookies = response.Cookies()
} else {
chain.fail("expected non-nil response")
}
return &Response{
chain: chain,
resp: response,
content: content,
cookies: cookies,
time: duration,
}
}
func getContent(chain *chain, resp *http.Response) []byte {
if resp.Body == nil {
return []byte{}
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
chain.fail(err.Error())
return nil
}
return content
}
// Raw returns underlying http.Response object.
// This is the value originally passed to NewResponse.
func (r *Response) Raw() *http.Response {
return r.resp
}
// Duration returns a new Number object that may be used to inspect
// response time, in nanoseconds.
//
// Response time is a time interval starting just before request is sent
// and ending right after response is received, retrieved from monotonic
// clock source.
//
// Example:
// resp := NewResponse(t, response, time.Duration(10000000))
// resp.Duration().Equal(10 * time.Millisecond)
func (r *Response) Duration() *Number {
return &Number{r.chain, float64(r.time)}
}
// Status succeeds if response contains given status code.
//
// Example:
// resp := NewResponse(t, response)
// resp.Status(http.StatusOK)
func (r *Response) Status(status int) *Response {
if r.chain.failed() {
return r
}
r.checkEqual("status", statusCodeText(status), statusCodeText(r.resp.StatusCode))
return r
}
// StatusRange succeeds if response status belongs to given range.
//
// Supported ranges:
// - Status1xx - Informational
// - Status2xx - Success
// - Status3xx - Redirection
// - Status4xx - Client Error
// - Status5xx - Server Error
//
// See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.
//
// Example:
// resp := NewResponse(t, response)
// resp.StatusRange(Status2xx)
func (r *Response) StatusRange(rn StatusRange) *Response {
if r.chain.failed() {
return r
}
status := statusCodeText(r.resp.StatusCode)
actual := statusRangeText(r.resp.StatusCode)
expected := statusRangeText(int(rn))
if actual == "" || actual != expected {
if actual == "" {
r.chain.fail("\nexpected status from range:\n %q\n\nbut got:\n %q",
expected, status)
} else {
r.chain.fail(
"\nexpected status from range:\n %q\n\nbut got:\n %q (%q)",
expected, actual, status)
}
}
return r
}
func statusCodeText(code int) string {
if s := http.StatusText(code); s != "" {
return strconv.Itoa(code) + " " + s
}
return strconv.Itoa(code)
}
func statusRangeText(code int) string {
switch {
case code >= 100 && code < 200:
return "1xx Informational"
case code >= 200 && code < 300:
return "2xx Success"
case code >= 300 && code < 400:
return "3xx Redirection"
case code >= 400 && code < 500:
return "4xx Client Error"
case code >= 500 && code < 600:
return "5xx Server Error"
default:
return ""
}
}
// Headers returns a new Object that may be used to inspect header map.
//
// Example:
// resp := NewResponse(t, response)
// resp.Headers().Value("Content-Type").String().Equal("application-json")
func (r *Response) Headers() *Object {
var value map[string]interface{}
if !r.chain.failed() {
value, _ = canonMap(&r.chain, r.resp.Header)
}
return &Object{r.chain, value}
}
// Header returns a new String object that may be used to inspect given header.
//
// Example:
// resp := NewResponse(t, response)
// resp.Header("Content-Type").Equal("application-json")
// resp.Header("Date").DateTime().Le(time.Now())
func (r *Response) Header(header string) *String {
value := ""
if !r.chain.failed() {
value = r.resp.Header.Get(header)
}
return &String{r.chain, value}
}
// Cookies returns a new Array object with all cookie names set by this response.
// Returned Array contains a String value for every cookie name.
//
// Note that this returns only cookies set by Set-Cookie headers of this response.
// It doesn't return session cookies from previous responses, which may be stored
// in a cookie jar.
//
// Example:
// resp := NewResponse(t, response)
// resp.Cookies().Contains("session")
func (r *Response) Cookies() *Array {
if r.chain.failed() {
return &Array{r.chain, nil}
}
names := []interface{}{}
for _, c := range r.cookies {
names = append(names, c.Name)
}
return &Array{r.chain, names}
}
// Cookie returns a new Cookie object that may be used to inspect given cookie
// set by this response.
//
// Note that this returns only cookies set by Set-Cookie headers of this response.
// It doesn't return session cookies from previous responses, which may be stored
// in a cookie jar.
//
// Example:
// resp := NewResponse(t, response)
// resp.Cookie("session").Domain().Equal("example.com")
func (r *Response) Cookie(name string) *Cookie {
if r.chain.failed() {
return &Cookie{r.chain, nil}
}
names := []string{}
for _, c := range r.cookies {
if c.Name == name {
return &Cookie{r.chain, c}
}
names = append(names, c.Name)
}
r.chain.fail("\nexpected response with cookie:\n %q\n\nbut got only cookies:\n%s",
name, dumpValue(names))
return &Cookie{r.chain, nil}
}
// Body returns a new String object that may be used to inspect response body.
//
// Example:
// resp := NewResponse(t, response)
// resp.Body().NotEmpty()
// resp.Body().Length().Equal(100)
func (r *Response) Body() *String {
return &String{r.chain, string(r.content)}
}
// NoContent succeeds if response contains empty Content-Type header and
// empty body.
func (r *Response) NoContent() *Response {
if r.chain.failed() {
return r
}
contentType := r.resp.Header.Get("Content-Type")
r.checkEqual("\"Content-Type\" header", "", contentType)
r.checkEqual("body", "", string(r.content))
return r
}
// ContentType succeeds if response contains Content-Type header with given
// media type and charset.
//
// If charset is omitted, and mediaType is non-empty, Content-Type header
// should contain empty or utf-8 charset.
//
// If charset is omitted, and mediaType is also empty, Content-Type header
// should contain no charset.
func (r *Response) ContentType(mediaType string, charset ...string) *Response {
r.checkContentType(mediaType, charset...)
return r
}
// ContentEncoding succeeds if response has exactly given Content-Encoding list.
// Common values are empty, "gzip", "compress", "deflate", "identity" and "br".
func (r *Response) ContentEncoding(encoding ...string) *Response {
if r.chain.failed() {
return r
}
r.checkEqual("\"Content-Encoding\" header", encoding, r.resp.Header["Content-Encoding"])
return r
}
// TransferEncoding succeeds if response contains given Transfer-Encoding list.
// Common values are empty, "chunked" and "identity".
func (r *Response) TransferEncoding(encoding ...string) *Response {
if r.chain.failed() {
return r
}
r.checkEqual("\"Transfer-Encoding\" header", encoding, r.resp.TransferEncoding)
return r
}
// Text returns a new String object that may be used to inspect response body.
//
// Text succeeds if response contains "text/plain" Content-Type header
// with empty or "utf-8" charset.
//
// Example:
// resp := NewResponse(t, response)
// resp.Text().Equal("hello, world!")
func (r *Response) Text() *String {
var content string
if !r.chain.failed() && r.checkContentType("text/plain") {
content = string(r.content)
}
return &String{r.chain, content}
}
// Form returns a new Object that may be used to inspect form contents
// of response.
//
// Form succeeds if response contains "application/x-www-form-urlencoded"
// Content-Type header and if form may be decoded from response body.
// Decoding is performed using https://github.com/ajg/form.
//
// Example:
// resp := NewResponse(t, response)
// resp.Form().Value("foo").Equal("bar")
func (r *Response) Form() *Object {
object := r.getForm()
return &Object{r.chain, object}
}
func (r *Response) getForm() map[string]interface{} {
if r.chain.failed() {
return nil
}
if !r.checkContentType("application/x-www-form-urlencoded", "") {
return nil
}
decoder := form.NewDecoder(bytes.NewReader(r.content))
var object map[string]interface{}
if err := decoder.Decode(&object); err != nil {
r.chain.fail(err.Error())
return nil
}
return object
}
// JSON returns a new Value object that may be used to inspect JSON contents
// of response.
//
// JSON succeeds if response contains "application/json" Content-Type header
// with empty or "utf-8" charset and if JSON may be decoded from response body.
//
// Example:
// resp := NewResponse(t, response)
// resp.JSON().Array().Elements("foo", "bar")
func (r *Response) JSON() *Value {
value := r.getJSON()
return &Value{r.chain, value}
}
func (r *Response) getJSON() interface{} {
if r.chain.failed() {
return nil
}
if !r.checkContentType("application/json") {
return nil
}
var value interface{}
if err := json.Unmarshal(r.content, &value); err != nil {
r.chain.fail(err.Error())
return nil
}
return value
}
// JSONP returns a new Value object that may be used to inspect JSONP contents
// of response.
//
// JSONP succeeds if response contains "application/javascript" Content-Type
// header with empty or "utf-8" charset and response body of the following form:
// callback(<valid json>);
// or:
// callback(<valid json>)
//
// Whitespaces are allowed.
//
// Example:
// resp := NewResponse(t, response)
// resp.JSONP("myCallback").Array().Elements("foo", "bar")
func (r *Response) JSONP(callback string) *Value {
value := r.getJSONP(callback)
return &Value{r.chain, value}
}
var (
jsonp = regexp.MustCompile(`^\s*([^\s(]+)\s*\((.*)\)\s*;*\s*$`)
)
func (r *Response) getJSONP(callback string) interface{} {
if r.chain.failed() {
return nil
}
if !r.checkContentType("application/javascript") {
return nil
}
m := jsonp.FindSubmatch(r.content)
if len(m) != 3 || string(m[1]) != callback {
r.chain.fail(
"\nexpected JSONP body in form of:\n \"%s(<valid json>)\"\n\nbut got:\n %q\n",
callback,
string(r.content))
return nil
}
var value interface{}
if err := json.Unmarshal(m[2], &value); err != nil {
r.chain.fail(err.Error())
return nil
}
return value
}
func (r *Response) checkContentType(expectedType string, expectedCharset ...string) bool {
if r.chain.failed() {
return false
}
contentType := r.resp.Header.Get("Content-Type")
if expectedType == "" && len(expectedCharset) == 0 {
if contentType == "" {
return true
}
}
mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil {
r.chain.fail("\ngot invalid \"Content-Type\" header %q", contentType)
return false
}
if mediaType != expectedType {
r.chain.fail(
"\nexpected \"Content-Type\" header with %q media type,"+
"\nbut got %q", expectedType, mediaType)
return false
}
charset := params["charset"]
if len(expectedCharset) == 0 {
if charset != "" && !strings.EqualFold(charset, "utf-8") {
r.chain.fail(
"\nexpected \"Content-Type\" header with \"utf-8\" or empty charset,"+
"\nbut got %q", charset)
return false
}
} else {
if !strings.EqualFold(charset, expectedCharset[0]) {
r.chain.fail(
"\nexpected \"Content-Type\" header with %q charset,"+
"\nbut got %q", expectedCharset[0], charset)
return false
}
}
return true
}
func (r *Response) checkEqual(what string, expected, actual interface{}) {
if !reflect.DeepEqual(expected, actual) {
r.chain.fail("\nexpected %s equal to:\n%s\n\nbut got:\n%s", what,
dumpValue(expected), dumpValue(actual))
}
}
+872
View File
@@ -0,0 +1,872 @@
package httpexpect
import (
"bytes"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestResponseFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
resp := &Response{chain, nil, nil, nil, 0}
resp.chain.assertFailed(t)
assert.False(t, resp.Duration() == nil)
assert.False(t, resp.Headers() == nil)
assert.False(t, resp.Header("foo") == nil)
assert.False(t, resp.Cookies() == nil)
assert.False(t, resp.Cookie("foo") == nil)
assert.False(t, resp.Body() == nil)
assert.False(t, resp.JSON() == nil)
assert.False(t, resp.JSONP("") == nil)
resp.Headers().chain.assertFailed(t)
resp.Header("foo").chain.assertFailed(t)
resp.Cookies().chain.assertFailed(t)
resp.Cookie("foo").chain.assertFailed(t)
resp.Body().chain.assertFailed(t)
resp.Text().chain.assertFailed(t)
resp.JSON().chain.assertFailed(t)
resp.JSONP("").chain.assertFailed(t)
resp.Status(123)
resp.StatusRange(Status2xx)
resp.NoContent()
resp.ContentType("", "")
resp.ContentEncoding("")
resp.TransferEncoding("")
}
func TestResponseDuration(t *testing.T) {
reporter := newMockReporter(t)
duration := time.Duration(10000000)
resp := NewResponse(reporter, &http.Response{}, duration)
resp.chain.assertOK(t)
resp.chain.reset()
rt := resp.Duration()
assert.Equal(t, float64(duration), rt.Raw())
rt.Equal(10 * time.Millisecond)
rt.chain.assertOK(t)
}
func TestResponseStatusRange(t *testing.T) {
reporter := newMockReporter(t)
ranges := []StatusRange{
Status1xx,
Status2xx,
Status3xx,
Status4xx,
Status5xx,
}
cases := []struct {
Status int
Range StatusRange
}{
{99, StatusRange(-1)},
{100, Status1xx},
{199, Status1xx},
{200, Status2xx},
{299, Status2xx},
{300, Status3xx},
{399, Status3xx},
{400, Status4xx},
{499, Status4xx},
{500, Status5xx},
{599, Status5xx},
{600, StatusRange(-1)},
}
for _, test := range cases {
for _, r := range ranges {
resp := NewResponse(reporter, &http.Response{
StatusCode: test.Status,
})
resp.StatusRange(r)
if test.Range == r {
resp.chain.assertOK(t)
} else {
resp.chain.assertFailed(t)
}
}
}
}
func TestResponseHeaders(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"First-Header": {"foo"},
"Second-Header": {"bar"},
}
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: nil,
}
resp := NewResponse(reporter, httpResp)
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t, httpResp, resp.Raw())
resp.Status(http.StatusOK)
resp.chain.assertOK(t)
resp.chain.reset()
resp.Status(http.StatusNotFound)
resp.chain.assertFailed(t)
resp.chain.reset()
resp.Headers().Equal(headers).chain.assertOK(t)
for k, v := range headers {
for _, h := range []string{k, strings.ToLower(k), strings.ToUpper(k)} {
resp.Header(h).Equal(v[0]).chain.assertOK(t)
}
}
resp.Header("Bad-Header").Empty().chain.assertOK(t)
}
func TestResponseCookies(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Set-Cookie": {
"foo=aaa",
"bar=bbb; expires=Fri, 31 Dec 2010 23:59:59 GMT; " +
"path=/xxx; domain=example.com",
},
}
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: nil,
}
resp := NewResponse(reporter, httpResp)
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t, []interface{}{"foo", "bar"}, resp.Cookies().Raw())
resp.chain.assertOK(t)
c1 := resp.Cookie("foo")
resp.chain.assertOK(t)
assert.Equal(t, "foo", c1.Raw().Name)
assert.Equal(t, "aaa", c1.Raw().Value)
assert.Equal(t, "", c1.Raw().Domain)
assert.Equal(t, "", c1.Raw().Path)
c2 := resp.Cookie("bar")
resp.chain.assertOK(t)
assert.Equal(t, "bar", c2.Raw().Name)
assert.Equal(t, "bbb", c2.Raw().Value)
assert.Equal(t, "example.com", c2.Raw().Domain)
assert.Equal(t, "/xxx", c2.Raw().Path)
assert.True(t, time.Date(2010, 12, 31, 23, 59, 59, 0, time.UTC).
Equal(c2.Raw().Expires))
c3 := resp.Cookie("baz")
resp.chain.assertFailed(t)
c3.chain.assertFailed(t)
assert.True(t, c3.Raw() == nil)
}
func TestResponseNoCookies(t *testing.T) {
reporter := newMockReporter(t)
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: nil,
Body: nil,
}
resp := NewResponse(reporter, httpResp)
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t, []interface{}{}, resp.Cookies().Raw())
resp.chain.assertOK(t)
c := resp.Cookie("foo")
resp.chain.assertFailed(t)
c.chain.assertFailed(t)
assert.True(t, c.Raw() == nil)
}
func TestResponseBody(t *testing.T) {
reporter := newMockReporter(t)
httpResp := &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewBufferString("body")),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, "body", resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
}
func TestResponseNoContentEmpty(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {""},
}
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString("")),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, "", resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.NoContent()
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("")
resp.chain.assertOK(t)
resp.chain.reset()
resp.Text()
resp.chain.assertFailed(t)
resp.chain.reset()
resp.Form()
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSON()
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSONP("")
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseNoContentNil(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {""},
}
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: nil,
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, "", resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.NoContent()
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("")
resp.chain.assertOK(t)
resp.chain.reset()
resp.Text()
resp.chain.assertFailed(t)
resp.chain.reset()
resp.Form()
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSON()
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSONP("")
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseNoContentFailed(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"text/plain; charset=utf-8"},
}
body := ``
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, body, resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.NoContent()
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseContentType(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"text/plain; charset=utf-8"},
}
resp := NewResponse(reporter, &http.Response{
Header: http.Header(headers),
})
resp.ContentType("text/plain")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain", "utf-8")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain", "UTF-8")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("bad")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.ContentType("text/plain", "bad")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.ContentType("")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.ContentType("text/plain", "")
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseContentTypeEmptyCharset(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"text/plain"},
}
resp := NewResponse(reporter, &http.Response{
Header: http.Header(headers),
})
resp.ContentType("text/plain")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain", "")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain", "utf-8")
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseContentTypeInvalid(t *testing.T) {
reporter := newMockReporter(t)
headers1 := map[string][]string{
"Content-Type": {";"},
}
headers2 := map[string][]string{
"Content-Type": {"charset=utf-8"},
}
resp1 := NewResponse(reporter, &http.Response{
Header: http.Header(headers1),
})
resp2 := NewResponse(reporter, &http.Response{
Header: http.Header(headers2),
})
resp1.ContentType("")
resp1.chain.assertFailed(t)
resp1.chain.reset()
resp1.ContentType("", "")
resp1.chain.assertFailed(t)
resp1.chain.reset()
resp2.ContentType("")
resp2.chain.assertFailed(t)
resp2.chain.reset()
resp2.ContentType("", "")
resp2.chain.assertFailed(t)
resp2.chain.reset()
}
func TestResponseContentEncoding(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Encoding": {"gzip", "deflate"},
}
resp := NewResponse(reporter, &http.Response{
Header: http.Header(headers),
})
resp.ContentEncoding("gzip", "deflate")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentEncoding("deflate", "gzip")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.ContentEncoding("gzip")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.ContentEncoding()
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseTransferEncoding(t *testing.T) {
reporter := newMockReporter(t)
resp := NewResponse(reporter, &http.Response{
TransferEncoding: []string{"foo", "bar"},
})
resp.TransferEncoding("foo", "bar")
resp.chain.assertOK(t)
resp.chain.reset()
resp.TransferEncoding("foo")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.TransferEncoding()
resp.chain.assertFailed(t)
resp.chain.reset()
}
func TestResponseText(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"text/plain; charset=utf-8"},
}
body := `hello, world!`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, body, resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain", "utf-8")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/json")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.Text()
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t, "hello, world!", resp.Text().Raw())
}
func TestResponseForm(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/x-www-form-urlencoded"},
}
body := `a=1&b=2`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, body, resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/x-www-form-urlencoded")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/x-www-form-urlencoded", "")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.Form()
resp.chain.assertOK(t)
resp.chain.reset()
expected := map[string]interface{}{
"a": "1",
"b": "2",
}
assert.Equal(t, expected, resp.Form().Raw())
}
func TestResponseFormBadBody(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/x-www-form-urlencoded"},
}
body := "%"
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.Form()
resp.chain.assertFailed(t)
resp.chain.reset()
assert.True(t, resp.Form().Raw() == nil)
}
func TestResponseFormBadType(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"bad"},
}
body := "foo=bar"
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.Form()
resp.chain.assertFailed(t)
resp.chain.reset()
assert.True(t, resp.Form().Raw() == nil)
}
func TestResponseJSON(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/json; charset=utf-8"},
}
body := `{"key": "value"}`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, body, resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/json")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/json", "utf-8")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSON()
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t,
map[string]interface{}{"key": "value"}, resp.JSON().Object().Raw())
}
func TestResponseJSONBadBody(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/json; charset=utf-8"},
}
body := "{"
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.JSON()
resp.chain.assertFailed(t)
resp.chain.reset()
assert.True(t, resp.JSON().Raw() == nil)
}
func TestResponseJSONCharsetEmpty(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/json"},
}
body := `{"key": "value"}`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.JSON()
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t,
map[string]interface{}{"key": "value"}, resp.JSON().Object().Raw())
}
func TestResponseJSONCharsetBad(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/json; charset=bad"},
}
body := `{"key": "value"}`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.JSON()
resp.chain.assertFailed(t)
resp.chain.reset()
assert.Equal(t, nil, resp.JSON().Raw())
}
func TestResponseJSONP(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/javascript; charset=utf-8"},
}
body1 := `foo({"key": "value"})`
body2 := `foo({"key": "value"});`
body3 := ` foo ( {"key": "value"} ) ; `
for _, body := range []string{body1, body2, body3} {
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
assert.Equal(t, body, resp.Body().Raw())
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/javascript")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("application/javascript", "utf-8")
resp.chain.assertOK(t)
resp.chain.reset()
resp.ContentType("text/plain")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSONP("foo")
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t,
map[string]interface{}{"key": "value"}, resp.JSONP("foo").Object().Raw())
resp.JSONP("fo")
resp.chain.assertFailed(t)
resp.chain.reset()
resp.JSONP("")
resp.chain.assertFailed(t)
resp.chain.reset()
}
}
func TestResponseJSONPBadBody(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/javascript; charset=utf-8"},
}
body1 := `foo`
body2 := `foo();`
body3 := `foo(`
body4 := `foo({);`
for _, body := range []string{body1, body2, body3, body4} {
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.JSONP("foo")
resp.chain.assertFailed(t)
resp.chain.reset()
assert.True(t, resp.JSONP("foo").Raw() == nil)
}
}
func TestResponseJSONPCharsetEmpty(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/javascript"},
}
body := `foo({"key": "value"})`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.JSONP("foo")
resp.chain.assertOK(t)
resp.chain.reset()
assert.Equal(t,
map[string]interface{}{"key": "value"}, resp.JSONP("foo").Object().Raw())
}
func TestResponseJSONPCharsetBad(t *testing.T) {
reporter := newMockReporter(t)
headers := map[string][]string{
"Content-Type": {"application/javascript; charset=bad"},
}
body := `foo({"key": "value"})`
httpResp := &http.Response{
StatusCode: http.StatusOK,
Header: http.Header(headers),
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
}
resp := NewResponse(reporter, httpResp)
resp.JSONP("foo")
resp.chain.assertFailed(t)
resp.chain.reset()
assert.Equal(t, nil, resp.JSONP("foo").Raw())
}
+320
View File
@@ -0,0 +1,320 @@
package httpexpect
import (
"net/http"
"regexp"
"strings"
"time"
)
// String provides methods to inspect attached string value
// (Go representation of JSON string).
type String struct {
chain chain
value string
}
// NewString returns a new String given a reporter used to report failures
// and value to be inspected.
//
// reporter should not be nil.
//
// Example:
// str := NewString(t, "Hello")
func NewString(reporter Reporter, value string) *String {
return &String{makeChain(reporter), value}
}
// Raw returns underlying value attached to String.
// This is the value originally passed to NewString.
//
// Example:
// str := NewString(t, "Hello")
// assert.Equal(t, "Hello", str.Raw())
func (s *String) Raw() string {
return s.value
}
// Path is similar to Value.Path.
func (s *String) Path(path string) *Value {
return getPath(&s.chain, s.value, path)
}
// Schema is similar to Value.Schema.
func (s *String) Schema(schema interface{}) *String {
checkSchema(&s.chain, s.value, schema)
return s
}
// Length returns a new Number object that may be used to inspect string length.
//
// Example:
// str := NewString(t, "Hello")
// str.Length().Equal(5)
func (s *String) Length() *Number {
return &Number{s.chain, float64(len(s.value))}
}
// DateTime parses date/time from string an returns a new DateTime object.
//
// If layout is given, DateTime() uses time.Parse() with given layout.
// Otherwise, it uses http.ParseTime(). If pasing error occurred,
// DateTime reports failure and returns empty (but non-nil) object.
//
// Example:
// str := NewString(t, "Tue, 15 Nov 1994 08:12:31 GMT")
// str.DateTime().Lt(time.Now())
//
// str := NewString(t, "15 Nov 94 08:12 GMT")
// str.DateTime(time.RFC822).Lt(time.Now())
func (s *String) DateTime(layout ...string) *DateTime {
if s.chain.failed() {
return &DateTime{s.chain, time.Unix(0, 0)}
}
var (
t time.Time
err error
)
if len(layout) != 0 {
t, err = time.Parse(layout[0], s.value)
} else {
t, err = http.ParseTime(s.value)
}
if err != nil {
s.chain.fail(err.Error())
return &DateTime{s.chain, time.Unix(0, 0)}
}
return &DateTime{s.chain, t}
}
// Empty succeeds if string is empty.
//
// Example:
// str := NewString(t, "")
// str.Empty()
func (s *String) Empty() *String {
return s.Equal("")
}
// NotEmpty succeeds if string is non-empty.
//
// Example:
// str := NewString(t, "Hello")
// str.NotEmpty()
func (s *String) NotEmpty() *String {
return s.NotEqual("")
}
// Equal succeeds if string is equal to another str.
//
// Example:
// str := NewString(t, "Hello")
// str.Equal("Hello")
func (s *String) Equal(value string) *String {
if !(s.value == value) {
s.chain.fail("\nexpected string equal to:\n %q\n\nbut got:\n %q",
value, s.value)
}
return s
}
// NotEqual succeeds if string is not equal to another str.
//
// Example:
// str := NewString(t, "Hello")
// str.NotEqual("Goodbye")
func (s *String) NotEqual(value string) *String {
if !(s.value != value) {
s.chain.fail("\nexpected string not equal to:\n %q", value)
}
return s
}
// EqualFold succeeds if string is equal to another string under Unicode case-folding
// (case-insensitive match).
//
// Example:
// str := NewString(t, "Hello")
// str.EqualFold("hELLo")
func (s *String) EqualFold(value string) *String {
if !strings.EqualFold(s.value, value) {
s.chain.fail(
"\nexpected string equal to (case-insensitive):\n %q\n\nbut got:\n %q",
value, s.value)
}
return s
}
// NotEqualFold succeeds if string is not equal to another string under Unicode
// case-folding (case-insensitive match).
//
// Example:
// str := NewString(t, "Hello")
// str.NotEqualFold("gOODBYe")
func (s *String) NotEqualFold(value string) *String {
if strings.EqualFold(s.value, value) {
s.chain.fail(
"\nexpected string not equal to (case-insensitive):\n %q\n\nbut got:\n %q",
value, s.value)
}
return s
}
// Contains succeeds if string contains given substr.
//
// Example:
// str := NewString(t, "Hello")
// str.Contains("ell")
func (s *String) Contains(value string) *String {
if !strings.Contains(s.value, value) {
s.chain.fail(
"\nexpected string containing substring:\n %q\n\nbut got:\n %q",
value, s.value)
}
return s
}
// NotContains succeeds if string doesn't contain given substr.
//
// Example:
// str := NewString(t, "Hello")
// str.NotContains("bye")
func (s *String) NotContains(value string) *String {
if strings.Contains(s.value, value) {
s.chain.fail(
"\nexpected string not containing substring:\n %q\n\nbut got:\n %q",
value, s.value)
}
return s
}
// ContainsFold succeeds if string contains given substring under Unicode case-folding
// (case-insensitive match).
//
// Example:
// str := NewString(t, "Hello")
// str.ContainsFold("ELL")
func (s *String) ContainsFold(value string) *String {
if !strings.Contains(strings.ToLower(s.value), strings.ToLower(value)) {
s.chain.fail(
"\nexpected string containing substring (case-insensitive):\n %q"+
"\n\nbut got:\n %q", value, s.value)
}
return s
}
// NotContainsFold succeeds if string doesn't contain given substring under Unicode
// case-folding (case-insensitive match).
//
// Example:
// str := NewString(t, "Hello")
// str.NotContainsFold("BYE")
func (s *String) NotContainsFold(value string) *String {
if strings.Contains(strings.ToLower(s.value), strings.ToLower(value)) {
s.chain.fail(
"\nexpected string not containing substring (case-insensitive):\n %q"+
"\n\nbut got:\n %q", value, s.value)
}
return s
}
// Match matches the string with given regexp and returns a new Match object
// with found submatches.
//
// If regexp is invalid or string doesn't match regexp, Match fails and returns
// empty (but non-nil) object. regexp.Compile is used to construct regexp, and
// Regexp.FindStringSubmatch is used to construct matches.
//
// Example:
// s := NewString(t, "http://example.com/users/john")
// m := s.Match(`http://(?P<host>.+)/users/(?P<user>.+)`)
//
// m.NotEmpty()
// m.Length().Equal(3)
//
// m.Index(0).Equal("http://example.com/users/john")
// m.Index(1).Equal("example.com")
// m.Index(2).Equal("john")
//
// m.Name("host").Equal("example.com")
// m.Name("user").Equal("john")
func (s *String) Match(re string) *Match {
r, err := regexp.Compile(re)
if err != nil {
s.chain.fail(err.Error())
return makeMatch(s.chain, nil, nil)
}
m := r.FindStringSubmatch(s.value)
if m == nil {
s.chain.fail("\nexpected string matching regexp:\n `%s`\n\nbut got:\n %q",
re, s.value)
return makeMatch(s.chain, nil, nil)
}
return makeMatch(s.chain, m, r.SubexpNames())
}
// MatchAll find all matches in string for given regexp and returns a list
// of found matches.
//
// If regexp is invalid or string doesn't match regexp, MatchAll fails and
// returns empty (but non-nil) slice. regexp.Compile is used to construct
// regexp, and Regexp.FindAllStringSubmatch is used to find matches.
//
// Example:
// s := NewString(t,
// "http://example.com/users/john http://example.com/users/bob")
//
// m := s.MatchAll(`http://(?P<host>\S+)/users/(?P<user>\S+)`)
//
// m[0].Name("user").Equal("john")
// m[1].Name("user").Equal("bob")
func (s *String) MatchAll(re string) []Match {
r, err := regexp.Compile(re)
if err != nil {
s.chain.fail(err.Error())
return []Match{}
}
matches := r.FindAllStringSubmatch(s.value, -1)
if matches == nil {
s.chain.fail("\nexpected string matching regexp:\n `%s`\n\nbut got:\n %q",
re, s.value)
return []Match{}
}
ret := []Match{}
for _, m := range matches {
ret = append(ret, *makeMatch(
s.chain,
m,
r.SubexpNames()))
}
return ret
}
// NotMatch succeeds if the string doesn't match to given regexp.
//
// regexp.Compile is used to construct regexp, and Regexp.MatchString
// is used to perform match.
//
// Example:
// s := NewString(t, "a")
// s.NotMatch(`[^a]`)
func (s *String) NotMatch(re string) *String {
r, err := regexp.Compile(re)
if err != nil {
s.chain.fail(err.Error())
return s
}
if r.MatchString(s.value) {
s.chain.fail("\nexpected string not matching regexp:\n `%s`\n\nbut got:\n %q",
re, s.value)
return s
}
return s
}
+309
View File
@@ -0,0 +1,309 @@
package httpexpect
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestStringFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &String{chain, ""}
value.Path("$").chain.assertFailed(t)
value.Schema("")
value.DateTime()
value.Empty()
value.NotEmpty()
value.Equal("")
value.NotEqual("")
value.EqualFold("")
value.NotEqualFold("")
value.Contains("")
value.NotContains("")
value.ContainsFold("")
value.NotContainsFold("")
}
func TestStringGetters(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "foo")
assert.Equal(t, "foo", value.Raw())
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, "foo", value.Path("$").Raw())
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "string"}`)
value.chain.assertOK(t)
value.chain.reset()
value.Schema(`{"type": "object"}`)
value.chain.assertFailed(t)
value.chain.reset()
}
func TestStringEmpty(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewString(reporter, "")
value1.Empty()
value1.chain.assertOK(t)
value1.chain.reset()
value1.NotEmpty()
value1.chain.assertFailed(t)
value1.chain.reset()
value2 := NewString(reporter, "a")
value2.Empty()
value2.chain.assertFailed(t)
value2.chain.reset()
value2.NotEmpty()
value2.chain.assertOK(t)
value2.chain.reset()
}
func TestStringEqual(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "foo")
assert.Equal(t, "foo", value.Raw())
value.Equal("foo")
value.chain.assertOK(t)
value.chain.reset()
value.Equal("FOO")
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqual("FOO")
value.chain.assertOK(t)
value.chain.reset()
value.NotEqual("foo")
value.chain.assertFailed(t)
value.chain.reset()
}
func TestStringEqualFold(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "foo")
value.EqualFold("foo")
value.chain.assertOK(t)
value.chain.reset()
value.EqualFold("FOO")
value.chain.assertOK(t)
value.chain.reset()
value.EqualFold("foo2")
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqualFold("foo")
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqualFold("FOO")
value.chain.assertFailed(t)
value.chain.reset()
value.NotEqualFold("foo2")
value.chain.assertOK(t)
value.chain.reset()
}
func TestStringContains(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "11-foo-22")
value.Contains("foo")
value.chain.assertOK(t)
value.chain.reset()
value.Contains("FOO")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContains("FOO")
value.chain.assertOK(t)
value.chain.reset()
value.NotContains("foo")
value.chain.assertFailed(t)
value.chain.reset()
}
func TestStringContainsFold(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "11-foo-22")
value.ContainsFold("foo")
value.chain.assertOK(t)
value.chain.reset()
value.ContainsFold("FOO")
value.chain.assertOK(t)
value.chain.reset()
value.ContainsFold("foo3")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsFold("foo")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsFold("FOO")
value.chain.assertFailed(t)
value.chain.reset()
value.NotContainsFold("foo3")
value.chain.assertOK(t)
value.chain.reset()
}
func TestStringLength(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "1234567")
num := value.Length()
value.chain.assertOK(t)
num.chain.assertOK(t)
assert.Equal(t, 7.0, num.Raw())
}
func TestStringDateTime(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewString(reporter, "Tue, 15 Nov 1994 08:12:31 GMT")
dt1 := value1.DateTime()
value1.chain.assertOK(t)
dt1.chain.assertOK(t)
assert.True(t, time.Date(1994, 11, 15, 8, 12, 31, 0, time.UTC).Equal(dt1.Raw()))
value2 := NewString(reporter, "15 Nov 94 08:12 GMT")
dt2 := value2.DateTime(time.RFC822)
value2.chain.assertOK(t)
dt2.chain.assertOK(t)
assert.True(t, time.Date(1994, 11, 15, 8, 12, 0, 0, time.UTC).Equal(dt2.Raw()))
value3 := NewString(reporter, "bad")
dt3 := value3.DateTime()
value3.chain.assertFailed(t)
dt3.chain.assertFailed(t)
assert.True(t, time.Unix(0, 0).Equal(dt3.Raw()))
}
func TestStringMatchOne(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "http://example.com/users/john")
m1 := value.Match(`http://(?P<host>.+)/users/(?P<user>.+)`)
m1.chain.assertOK(t)
assert.Equal(t,
[]string{"http://example.com/users/john", "example.com", "john"},
m1.submatches)
m2 := value.Match(`http://(.+)/users/(.+)`)
m2.chain.assertOK(t)
assert.Equal(t,
[]string{"http://example.com/users/john", "example.com", "john"},
m2.submatches)
}
func TestStringMatchAll(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter,
"http://example.com/users/john http://example.com/users/bob")
m := value.MatchAll(`http://(\S+)/users/(\S+)`)
assert.Equal(t, 2, len(m))
m[0].chain.assertOK(t)
m[1].chain.assertOK(t)
assert.Equal(t,
[]string{"http://example.com/users/john", "example.com", "john"},
m[0].submatches)
assert.Equal(t,
[]string{"http://example.com/users/bob", "example.com", "bob"},
m[1].submatches)
}
func TestStringMatchStatus(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "a")
value.Match(`a`)
value.chain.assertOK(t)
value.chain.reset()
value.MatchAll(`a`)
value.chain.assertOK(t)
value.chain.reset()
value.NotMatch(`a`)
value.chain.assertFailed(t)
value.chain.reset()
value.Match(`[^a]`)
value.chain.assertFailed(t)
value.chain.reset()
value.MatchAll(`[^a]`)
value.chain.assertFailed(t)
value.chain.reset()
value.NotMatch(`[^a]`)
value.chain.assertOK(t)
value.chain.reset()
assert.Equal(t, []string{}, value.Match(`[^a]`).submatches)
assert.Equal(t, []Match{}, value.MatchAll(`[^a]`))
}
func TestStringMatchInvalid(t *testing.T) {
reporter := newMockReporter(t)
value := NewString(reporter, "a")
value.Match(`[`)
value.chain.assertFailed(t)
value.chain.reset()
value.MatchAll(`[`)
value.chain.assertFailed(t)
value.chain.reset()
value.NotMatch(`[`)
value.chain.assertFailed(t)
value.chain.reset()
}
+286
View File
@@ -0,0 +1,286 @@
package httpexpect
import (
"reflect"
)
// Value provides methods to inspect attached interface{} object
// (Go representation of arbitrary JSON value) and cast it to
// concrete type.
type Value struct {
chain chain
value interface{}
}
// NewValue returns a new Value given a reporter used to report failures
// and value to be inspected.
//
// reporter should not be nil, but value may be nil.
//
// Example:
// value := NewValue(t, map[string]interface{}{"foo": 123})
// value.Object()
//
// value := NewValue(t, []interface{}{"foo", 123})
// value.Array()
//
// value := NewValue(t, "foo")
// value.String()
//
// value := NewValue(t, 123)
// value.Number()
//
// value := NewValue(t, true)
// value.Boolean()
//
// value := NewValue(t, nil)
// value.Null()
func NewValue(reporter Reporter, value interface{}) *Value {
chain := makeChain(reporter)
if value != nil {
value, _ = canonValue(&chain, value)
}
return &Value{chain, value}
}
// Raw returns underlying value attached to Value.
// This is the value originally passed to NewValue, converted to canonical form.
//
// Example:
// value := NewValue(t, "foo")
// assert.Equal(t, "foo", number.Raw().(string))
func (v *Value) Raw() interface{} {
return v.value
}
// Path returns a new Value object for child object(s) matching given
// JSONPath expression.
//
// JSONPath is a simple XPath-like query language.
// See http://goessner.net/articles/JsonPath/.
//
// We currently use https://github.com/yalp/jsonpath, which implements
// only a subset of JSONPath, yet useful for simple queries. It doesn't
// support filters and requires double quotes for strings.
//
// Example 1:
// json := `{"users": [{"name": "john"}, {"name": "bob"}]}`
// value := NewValue(t, json)
//
// value.Path("$.users[0].name").String().Equal("john")
// value.Path("$.users[1].name").String().Equal("bob")
//
// Example 2:
// json := `{"yfGH2a": {"user": "john"}, "f7GsDd": {"user": "john"}}`
// value := NewValue(t, json)
//
// for _, user := range value.Path("$..user").Array().Iter() {
// user.String().Equal("john")
// }
func (v *Value) Path(path string) *Value {
return getPath(&v.chain, v.value, path)
}
// Schema succeeds if value matches given JSON Schema.
//
// JSON Schema specifies a JSON-based format to define the structure of
// JSON data. See http://json-schema.org/.
// We use https://github.com/xeipuuv/gojsonschema implementation.
//
// schema should be one of the following:
// - go value that can be json.Marshal-ed to a valid schema
// - type convertible to string containing valid schema
// - type convertible to string containing valid http:// or file:// URI,
// pointing to reachable and valid schema
//
// Example 1:
// schema := `{
// "type": "object",
// "properties": {
// "foo": {
// "type": "string"
// },
// "bar": {
// "type": "integer"
// }
// },
// "require": ["foo", "bar"]
// }`
//
// value := NewValue(t, map[string]interface{}{
// "foo": "a",
// "bar": 1,
// })
//
// value.Schema(schema)
//
// Example 2:
// value := NewValue(t, data)
// value.Schema("http://example.com/schema.json")
func (v *Value) Schema(schema interface{}) *Value {
checkSchema(&v.chain, v.value, schema)
return v
}
// Object returns a new Object attached to underlying value.
//
// If underlying value is not an object (map[string]interface{}), failure is reported
// and empty (but non-nil) value is returned.
//
// Example:
// value := NewValue(t, map[string]interface{}{"foo": 123})
// value.Object().ContainsKey("foo")
func (v *Value) Object() *Object {
data, ok := v.value.(map[string]interface{})
if !ok {
v.chain.fail("\nexpected object value (map or struct), but got:\n%s",
dumpValue(v.value))
}
return &Object{v.chain, data}
}
// Array returns a new Array attached to underlying value.
//
// If underlying value is not an array ([]interface{}), failure is reported and empty
// (but non-nil) value is returned.
//
// Example:
// value := NewValue(t, []interface{}{"foo", 123})
// value.Array().Elements("foo", 123)
func (v *Value) Array() *Array {
data, ok := v.value.([]interface{})
if !ok {
v.chain.fail("\nexpected array value, but got:\n%s",
dumpValue(v.value))
}
return &Array{v.chain, data}
}
// String returns a new String attached to underlying value.
//
// If underlying value is not string, failure is reported and empty (but non-nil)
// value is returned.
//
// Example:
// value := NewValue(t, "foo")
// value.String().EqualFold("FOO")
func (v *Value) String() *String {
data, ok := v.value.(string)
if !ok {
v.chain.fail("\nexpected string value, but got:\n%s",
dumpValue(v.value))
}
return &String{v.chain, data}
}
// Number returns a new Number attached to underlying value.
//
// If underlying value is not a number (numeric type convertible to float64), failure
// is reported and empty (but non-nil) value is returned.
//
// Example:
// value := NewValue(t, 123)
// value.Number().InRange(100, 200)
func (v *Value) Number() *Number {
data, ok := v.value.(float64)
if !ok {
v.chain.fail("\nexpected numeric value, but got:\n%s",
dumpValue(v.value))
}
return &Number{v.chain, data}
}
// Boolean returns a new Boolean attached to underlying value.
//
// If underlying value is not a bool, failure is reported and empty (but non-nil)
// value is returned.
//
// Example:
// value := NewValue(t, true)
// value.Boolean().True()
func (v *Value) Boolean() *Boolean {
data, ok := v.value.(bool)
if !ok {
v.chain.fail("\nexpected boolean value, but got:\n%s",
dumpValue(v.value))
}
return &Boolean{v.chain, data}
}
// Null succeeds if value is nil.
//
// Note that non-nil interface{} that points to nil value (e.g. nil slice or map)
// is also treated as null value. Empty (non-nil) slice or map, empty string, and
// zero number are not treated as null value.
//
// Example:
// value := NewValue(t, nil)
// value.Null()
//
// value := NewValue(t, []interface{}(nil))
// value.Null()
func (v *Value) Null() *Value {
if v.value != nil {
v.chain.fail("\nexpected nil value, but got:\n%s",
dumpValue(v.value))
}
return v
}
// NotNull succeeds if value is not nil.
//
// Note that non-nil interface{} that points to nil value (e.g. nil slice or map)
// is also treated as null value. Empty (non-nil) slice or map, empty string, and
// zero number are not treated as null value.
//
// Example:
// value := NewValue(t, "")
// value.NotNull()
//
// value := NewValue(t, make([]interface{}, 0)
// value.Null()
func (v *Value) NotNull() *Value {
if v.value == nil {
v.chain.fail("\nexpected non-nil value, but got:\n%s",
dumpValue(v.value))
}
return v
}
// Equal succeeds if value is equal to another value.
// Before comparison, both values are converted to canonical form.
//
// Example:
// value := NewValue(t, "foo")
// value.Equal("foo")
func (v *Value) Equal(value interface{}) *Value {
expected, ok := canonValue(&v.chain, value)
if !ok {
return v
}
if !reflect.DeepEqual(expected, v.value) {
v.chain.fail("\nexpected value equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
dumpValue(expected),
dumpValue(v.value),
diffValues(expected, v.value))
}
return v
}
// NotEqual succeeds if value is not equal to another value.
// Before comparison, both values are converted to canonical form.
//
// Example:
// value := NewValue(t, "foo")
// value.NorEqual("bar")
func (v *Value) NotEqual(value interface{}) *Value {
expected, ok := canonValue(&v.chain, value)
if !ok {
return v
}
if reflect.DeepEqual(expected, v.value) {
v.chain.fail("\nexpected value not equal to:\n%s",
dumpValue(expected))
}
return v
}
+465
View File
@@ -0,0 +1,465 @@
package httpexpect
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValueFailed(t *testing.T) {
chain := makeChain(newMockReporter(t))
chain.fail("fail")
value := &Value{chain, nil}
value.chain.assertFailed(t)
value.Path("$").chain.assertFailed(t)
value.Schema("")
assert.False(t, value.Object() == nil)
assert.False(t, value.Array() == nil)
assert.False(t, value.String() == nil)
assert.False(t, value.Number() == nil)
assert.False(t, value.Boolean() == nil)
assert.False(t, value.Path("/") == nil)
value.Object().chain.assertFailed(t)
value.Array().chain.assertFailed(t)
value.String().chain.assertFailed(t)
value.Number().chain.assertFailed(t)
value.Boolean().chain.assertFailed(t)
value.Null()
value.NotNull()
value.Equal(nil)
value.NotEqual(nil)
}
func TestValueCastNull(t *testing.T) {
reporter := newMockReporter(t)
var data interface{}
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertFailed(t)
NewValue(reporter, data).Null().chain.assertOK(t)
}
func TestValueCastIndirectNull(t *testing.T) {
reporter := newMockReporter(t)
var data []interface{}
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertFailed(t)
NewValue(reporter, data).Null().chain.assertOK(t)
}
func TestValueCastBad(t *testing.T) {
reporter := newMockReporter(t)
data := func() {}
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertFailed(t)
NewValue(reporter, data).Null().chain.assertFailed(t)
}
func TestValueCastObject(t *testing.T) {
reporter := newMockReporter(t)
data := map[string]interface{}{}
NewValue(reporter, data).Object().chain.assertOK(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertOK(t)
NewValue(reporter, data).Null().chain.assertFailed(t)
}
func TestValueCastArray(t *testing.T) {
reporter := newMockReporter(t)
data := []interface{}{}
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertOK(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertOK(t)
NewValue(reporter, data).Null().chain.assertFailed(t)
}
func TestValueCastString(t *testing.T) {
reporter := newMockReporter(t)
data := ""
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertOK(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertOK(t)
NewValue(reporter, data).Null().chain.assertFailed(t)
}
func TestValueCastNumber(t *testing.T) {
reporter := newMockReporter(t)
data := 0.0
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertOK(t)
NewValue(reporter, data).Boolean().chain.assertFailed(t)
NewValue(reporter, data).NotNull().chain.assertOK(t)
NewValue(reporter, data).Null().chain.assertFailed(t)
}
func TestValueCastBoolean(t *testing.T) {
reporter := newMockReporter(t)
data := false
NewValue(reporter, data).Object().chain.assertFailed(t)
NewValue(reporter, data).Array().chain.assertFailed(t)
NewValue(reporter, data).String().chain.assertFailed(t)
NewValue(reporter, data).Number().chain.assertFailed(t)
NewValue(reporter, data).Boolean().chain.assertOK(t)
NewValue(reporter, data).NotNull().chain.assertOK(t)
NewValue(reporter, data).Null().chain.assertFailed(t)
}
func TestValueGetObject(t *testing.T) {
type (
myMap map[string]interface{}
)
reporter := newMockReporter(t)
data1 := map[string]interface{}{"foo": 123.0}
value1 := NewValue(reporter, data1)
inner1 := value1.Object()
inner1.chain.assertOK(t)
inner1.chain.reset()
assert.Equal(t, data1, inner1.Raw())
data2 := myMap{"foo": 123.0}
value2 := NewValue(reporter, data2)
inner2 := value2.Object()
inner2.chain.assertOK(t)
inner2.chain.reset()
assert.Equal(t, map[string]interface{}(data2), inner2.Raw())
}
func TestValueGetArray(t *testing.T) {
type (
myArray []interface{}
)
reporter := newMockReporter(t)
data1 := []interface{}{"foo", 123.0}
value1 := NewValue(reporter, data1)
inner1 := value1.Array()
inner1.chain.assertOK(t)
inner1.chain.reset()
assert.Equal(t, data1, inner1.Raw())
data2 := myArray{"foo", 123.0}
value2 := NewValue(reporter, data2)
inner2 := value2.Array()
inner2.chain.assertOK(t)
inner2.chain.reset()
assert.Equal(t, []interface{}(data2), inner2.Raw())
}
func TestValueGetString(t *testing.T) {
reporter := newMockReporter(t)
value := NewValue(reporter, "foo")
inner := value.String()
inner.chain.assertOK(t)
inner.chain.reset()
assert.Equal(t, "foo", inner.Raw())
}
func TestValueGetNumber(t *testing.T) {
type (
myInt int
)
reporter := newMockReporter(t)
data1 := 123.0
value1 := NewValue(reporter, data1)
inner1 := value1.Number()
inner1.chain.assertOK(t)
inner1.chain.reset()
assert.Equal(t, data1, inner1.Raw())
data2 := 123
value2 := NewValue(reporter, data2)
inner2 := value2.Number()
inner2.chain.assertOK(t)
inner2.chain.reset()
assert.Equal(t, float64(data2), inner2.Raw())
data3 := myInt(123)
value3 := NewValue(reporter, data3)
inner3 := value3.Number()
inner3.chain.assertOK(t)
inner3.chain.reset()
assert.Equal(t, float64(data3), inner3.Raw())
}
func TestValueGetBoolean(t *testing.T) {
reporter := newMockReporter(t)
value1 := NewValue(reporter, true)
inner1 := value1.Boolean()
inner1.chain.assertOK(t)
inner1.chain.reset()
assert.Equal(t, true, inner1.Raw())
value2 := NewValue(reporter, false)
inner2 := value2.Boolean()
inner2.chain.assertOK(t)
inner2.chain.reset()
assert.Equal(t, false, inner2.Raw())
}
func TestValueEqual(t *testing.T) {
reporter := newMockReporter(t)
data1 := map[string]interface{}{"foo": "bar"}
data2 := "baz"
NewValue(reporter, data1).Equal(data1).chain.assertOK(t)
NewValue(reporter, data2).Equal(data2).chain.assertOK(t)
NewValue(reporter, data1).NotEqual(data1).chain.assertFailed(t)
NewValue(reporter, data2).NotEqual(data2).chain.assertFailed(t)
NewValue(reporter, data1).Equal(data2).chain.assertFailed(t)
NewValue(reporter, data2).Equal(data1).chain.assertFailed(t)
NewValue(reporter, data1).NotEqual(data2).chain.assertOK(t)
NewValue(reporter, data2).NotEqual(data1).chain.assertOK(t)
NewValue(reporter, nil).Equal(nil).chain.assertOK(t)
NewValue(reporter, nil).Equal(map[string]interface{}(nil)).chain.assertOK(t)
NewValue(reporter, nil).Equal(map[string]interface{}{}).chain.assertFailed(t)
NewValue(reporter, data1).Equal(func() {}).chain.assertFailed(t)
NewValue(reporter, data1).NotEqual(func() {}).chain.assertFailed(t)
}
func TestValuePathObject(t *testing.T) {
reporter := newMockReporter(t)
user0 := map[string]interface{}{"name": "john"}
user1 := map[string]interface{}{"name": "bob"}
data := map[string]interface{}{
"users": []interface{}{
user0,
user1,
},
}
value := NewValue(reporter, data)
assert.Equal(t, data, value.Path("$").Raw())
assert.Equal(t, data["users"], value.Path("$.users").Raw())
assert.Equal(t, user0, value.Path("$.users[0]").Raw())
assert.Equal(t, "john", value.Path("$.users[0].name").Raw())
assert.Equal(t, []interface{}{"john", "bob"}, value.Path("$.users[*].name").Raw())
assert.Equal(t, []interface{}{"john", "bob"}, value.Path("$..name").Raw())
value.chain.assertOK(t)
names := value.Path("$..name").Array().Iter()
names[0].String().Equal("john").chain.assertOK(t)
names[1].String().Equal("bob").chain.assertOK(t)
value.chain.assertOK(t)
for _, key := range []string{"$.bad", "!"} {
bad := value.Path(key)
assert.True(t, bad != nil)
assert.True(t, bad.Raw() == nil)
value.chain.assertFailed(t)
value.chain.reset()
}
}
func TestValuePathArray(t *testing.T) {
reporter := newMockReporter(t)
user0 := map[string]interface{}{"name": "john"}
user1 := map[string]interface{}{"name": "bob"}
data := []interface{}{
user0,
user1,
}
value := NewValue(reporter, data)
assert.Equal(t, data, value.Path("$").Raw())
assert.Equal(t, user0, value.Path("$[0]").Raw())
assert.Equal(t, "john", value.Path("$[0].name").Raw())
assert.Equal(t, []interface{}{"john", "bob"}, value.Path("$[*].name").Raw())
assert.Equal(t, []interface{}{"john", "bob"}, value.Path("$..name").Raw())
value.chain.assertOK(t)
}
func TestValuePathString(t *testing.T) {
reporter := newMockReporter(t)
data := "foo"
value := NewValue(reporter, data)
assert.Equal(t, data, value.Path("$").Raw())
value.chain.assertOK(t)
}
func TestValuePathNumber(t *testing.T) {
reporter := newMockReporter(t)
data := 123
value := NewValue(reporter, data)
assert.Equal(t, float64(data), value.Path("$").Raw())
value.chain.assertOK(t)
}
func TestValuePathBoolean(t *testing.T) {
reporter := newMockReporter(t)
data := true
value := NewValue(reporter, data)
assert.Equal(t, data, value.Path("$").Raw())
value.chain.assertOK(t)
}
func TestValuePathNull(t *testing.T) {
reporter := newMockReporter(t)
value := NewValue(reporter, nil)
assert.Equal(t, nil, value.Path("$").Raw())
value.chain.assertOK(t)
}
func TestValuePathError(t *testing.T) {
reporter := newMockReporter(t)
data := "foo"
value := NewValue(reporter, data)
for _, key := range []string{"$.bad", "!"} {
bad := value.Path(key)
assert.True(t, bad != nil)
assert.True(t, bad.Raw() == nil)
value.chain.assertFailed(t)
}
}
func TestValueSchema(t *testing.T) {
reporter := newMockReporter(t)
schema := `{
"type": "object",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "integer"
}
},
"require": ["foo", "bar"]
}`
data1 := map[string]interface{}{
"foo": "a",
"bar": 1,
}
data2 := map[string]interface{}{
"foo": "a",
"bar": "b",
}
NewValue(reporter, data1).Schema(schema).chain.assertOK(t)
NewValue(reporter, data2).Schema(schema).chain.assertFailed(t)
NewValue(reporter, data1).Schema([]byte(schema)).chain.assertOK(t)
NewValue(reporter, data2).Schema([]byte(schema)).chain.assertFailed(t)
var b interface{}
json.Unmarshal([]byte(schema), &b)
NewValue(reporter, data1).Schema(b).chain.assertOK(t)
NewValue(reporter, data2).Schema(b).chain.assertFailed(t)
tmp, _ := ioutil.TempFile("", "httpexpect")
defer os.Remove(tmp.Name())
tmp.Write([]byte(schema))
tmp.Close()
url := "file://" + tmp.Name()
NewValue(reporter, data1).Schema(url).chain.assertOK(t)
NewValue(reporter, data2).Schema(url).chain.assertFailed(t)
NewValue(reporter, data1).Schema("file:///bad/path").chain.assertFailed(t)
NewValue(reporter, data1).Schema("{ bad json").chain.assertFailed(t)
}