├── VERSION ├── circle.yml ├── .travis.yml ├── .gitignore ├── util.go ├── doc.go ├── api.go ├── .github └── workflows │ └── codeql-analysis.yml ├── examples ├── httpbin │ └── httpbin.go └── github_auth_token │ └── github_auth_token.go ├── README.md ├── request.go ├── session.go ├── session_test.go └── api_test.go /VERSION: -------------------------------------------------------------------------------- 1 | v3.2.0 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - go test -race -coverprofile=coverage.txt -covermode=atomic 4 | post: 5 | - bash <(curl -s https://codecov.io/bash) 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.3 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | - 1.7 8 | - 1.8 9 | - tip 10 | notificaitons: 11 | email: 12 | recipients: jason.mcvetta@gmail.com 13 | on_success: change 14 | on_failure: always 15 | before_script: 16 | - go get github.com/bmizerany/assert 17 | - go get github.com/jmcvetta/randutil 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | # IntelliJ configs 25 | .idea 26 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | package napping 7 | 8 | import ( 9 | "encoding/json" 10 | ) 11 | 12 | // pretty pretty-prints an interface using the JSON marshaler 13 | func pretty(v interface{}) string { 14 | b, _ := json.MarshalIndent(v, "", "\t") 15 | return string(b) 16 | } 17 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | /* 7 | Package napping is a client library for interacting with RESTful APIs. 8 | 9 | Example: 10 | 11 | type Foo struct { 12 | Bar string 13 | } 14 | type Spam struct { 15 | Eggs int 16 | } 17 | payload := Foo{ 18 | Bar: "baz", 19 | } 20 | result := Spam{} 21 | url := "http://foo.com/bar" 22 | resp, err := napping.Post(url, &payload, &result, nil) 23 | if err != nil { 24 | panic(err) 25 | } 26 | if resp.Status() == 200 { 27 | println(result.Eggs) 28 | } 29 | 30 | See the "examples" folder for a more complete example. 31 | */ 32 | package napping 33 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | package napping 7 | 8 | /* 9 | This module implements the Napping API. 10 | */ 11 | 12 | import ( 13 | "net/url" 14 | ) 15 | 16 | // Send composes and sends and HTTP request. 17 | func Send(r *Request) (*Response, error) { 18 | s := Session{} 19 | return s.Send(r) 20 | } 21 | 22 | // Get sends a GET request. 23 | func Get(url string, p *url.Values, result, errMsg interface{}) (*Response, error) { 24 | s := Session{} 25 | return s.Get(url, p, result, errMsg) 26 | } 27 | 28 | // Options sends an OPTIONS request. 29 | func Options(url string, result, errMsg interface{}) (*Response, error) { 30 | s := Session{} 31 | return s.Options(url, result, errMsg) 32 | } 33 | 34 | // Head sends a HEAD request. 35 | func Head(url string, result, errMsg interface{}) (*Response, error) { 36 | s := Session{} 37 | return s.Head(url, result, errMsg) 38 | } 39 | 40 | // Post sends a POST request. 41 | func Post(url string, payload, result, errMsg interface{}) (*Response, error) { 42 | s := Session{} 43 | return s.Post(url, payload, result, errMsg) 44 | } 45 | 46 | // Put sends a PUT request. 47 | func Put(url string, payload, result, errMsg interface{}) (*Response, error) { 48 | s := Session{} 49 | return s.Put(url, payload, result, errMsg) 50 | } 51 | 52 | // Patch sends a PATCH request. 53 | func Patch(url string, payload, result, errMsg interface{}) (*Response, error) { 54 | s := Session{} 55 | return s.Patch(url, payload, result, errMsg) 56 | } 57 | 58 | // Delete sends a DELETE request. 59 | func Delete(url string, p *url.Values, result, errMsg interface{}) (*Response, error) { 60 | s := Session{} 61 | return s.Delete(url, p, result, errMsg) 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [develop, master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [develop] 9 | schedule: 10 | - cron: '0 3 * * 3' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /examples/httpbin/httpbin.go: -------------------------------------------------------------------------------- 1 | // Example demonstrating use of package napping, with HTTP Basic 2 | // Get over HTTP, to exploit http://httpbin.org/. 3 | 4 | // 5 | // Implemented the following as demo: 6 | // 7 | // Get -> http://httpbin.org/user-agent 8 | // Get -> http://httpbin.org/get?var=12345 9 | 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "gopkg.in/jmcvetta/napping.v3" 15 | "log" 16 | ) 17 | 18 | func init() { 19 | log.SetFlags(log.Ltime | log.Lshortfile) 20 | } 21 | 22 | type ResponseUserAgent struct { 23 | Useragent string `json:"user-agent"` 24 | } 25 | 26 | // A Params is a map containing URL parameters. 27 | type Params map[string]string 28 | 29 | func main() { 30 | // 31 | // Struct to hold error response 32 | // 33 | e := struct { 34 | Message string 35 | }{} 36 | 37 | // Start Session 38 | s := napping.Session{} 39 | url := "http://httpbin.org/user-agent" 40 | fmt.Println("URL:>", url) 41 | 42 | res := ResponseUserAgent{} 43 | resp, err := s.Get(url, nil, &res, nil) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | // 48 | // Process response 49 | // 50 | println("") 51 | fmt.Println("response Status:", resp.Status()) 52 | 53 | if resp.Status() == 200 { 54 | // fmt.Printf("Result: %s\n\n", resp.response) 55 | // resp.Unmarshal(&e) 56 | fmt.Println("res:", res.Useragent) 57 | } else { 58 | fmt.Println("Bad response status from httpbin server") 59 | fmt.Printf("\t Status: %v\n", resp.Status()) 60 | fmt.Printf("\t Message: %v\n", e.Message) 61 | } 62 | fmt.Println("--------------------------------------------------------------------------------") 63 | println("") 64 | 65 | url = "http://httpbin.org/get" 66 | fmt.Println("URL:>", url) 67 | p := napping.Params{"foo": "bar"}.AsUrlValues() 68 | 69 | res = ResponseUserAgent{} 70 | resp, err = s.Get(url, &p, &res, nil) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | // 75 | // Process response 76 | // 77 | println("") 78 | fmt.Println("response Status:", resp.Status()) 79 | fmt.Println("--------------------------------------------------------------------------------") 80 | fmt.Println("Header") 81 | fmt.Println(resp.HttpResponse().Header) 82 | fmt.Println("--------------------------------------------------------------------------------") 83 | fmt.Println("RawText") 84 | fmt.Println(resp.RawText()) 85 | println("") 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Napping: HTTP for Gophers 2 | 3 | Package `napping` is a [Go][] client library for interacting with 4 | [RESTful APIs][]. Napping was inspired by Python's excellent [Requests][] 5 | library. 6 | 7 | 8 | ## Status 9 | 10 | | System | Status | 11 | |-----------|:---------------------------------------------------------------------------------------------------------------------:| 12 | | Travis CI | [![Travis Build Status](https://travis-ci.org/jmcvetta/napping.png)](https://travis-ci.org/jmcvetta/napping) | 13 | | CircleCI | [![Circle CI](https://circleci.com/gh/jmcvetta/napping.svg?style=svg)](https://circleci.com/gh/jmcvetta/napping) | 14 | | Coveralls | [![Coveralls](https://img.shields.io/coveralls/jmcvetta/napping/master.svg)](https://coveralls.io/r/jmcvetta/napping) | 15 | | Codecov | [![Codecov](https://img.shields.io/codecov/c/github/jmcvetta/napping.svg)](https://codecov.io/gh/jmcvetta/napping) | 16 | 17 | Used by, and developed in conjunction with, [Neoism][]. 18 | 19 | 20 | ## Installation 21 | 22 | ### Requirements 23 | 24 | Napping is [tested with Go 1.3 or later](https://github.com/jmcvetta/napping/blob/develop/.travis.yml#L2). 25 | 26 | 27 | ### Development 28 | 29 | ``` 30 | go get github.com/jmcvetta/napping 31 | ``` 32 | 33 | ### Stable 34 | 35 | Napping is versioned using [`gopkg.in`](http://gopkg.in). 36 | 37 | Current release is `v3`. 38 | 39 | ``` 40 | go get gopkg.in/jmcvetta/napping.v3 41 | ``` 42 | 43 | 44 | ## Documentation 45 | 46 | See [![GoDoc](http://godoc.org/github.com/jmcvetta/napping?status.png)](http://godoc.org/github.com/jmcvetta/napping) 47 | for automatically generated API documentation. 48 | 49 | Check out [github_auth_token][auth-token] for a working example 50 | showing how to retrieve an auth token from the Github API. 51 | 52 | 53 | ## Support 54 | 55 | Support and consulting services are available from [Silicon Beach Heavy 56 | Industries](http://siliconheavy.com). 57 | 58 | 59 | 60 | ## Contributing 61 | 62 | Contributions in the form of Pull Requests are gladly accepted. Before 63 | submitting a PR, please ensure your code passes all tests, and that your 64 | changes do not decrease test coverage. I.e. if you add new features also add 65 | corresponding new tests. 66 | 67 | 68 | ## License 69 | 70 | This is Free Software, released under the terms of the [GPL v3][]. 71 | 72 | 73 | [Go]: http://golang.org 74 | [RESTful APIs]: http://en.wikipedia.org/wiki/Representational_state_transfer#RESTful_web_APIs 75 | [Requests]: http://python-requests.org 76 | [GPL v3]: http://www.gnu.org/copyleft/gpl.html 77 | [auth-token]: https://github.com/jmcvetta/napping/blob/master/examples/github_auth_token/github_auth_token.go 78 | [Neoism]: https://github.com/jmcvetta/neoism 79 | -------------------------------------------------------------------------------- /examples/github_auth_token/github_auth_token.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | // Example demonstrating use of package napping, with HTTP Basic 7 | // authentictation over HTTPS, to retrieve a Github auth token. 8 | package main 9 | 10 | /* 11 | 12 | NOTE: This example may only work on *nix systems due to gopass requirements. 13 | 14 | */ 15 | 16 | import ( 17 | "fmt" 18 | "github.com/howeyc/gopass" 19 | "github.com/kr/pretty" 20 | "gopkg.in/jmcvetta/napping.v3" 21 | "log" 22 | "net/url" 23 | "time" 24 | ) 25 | 26 | func init() { 27 | log.SetFlags(log.Ltime | log.Lshortfile) 28 | } 29 | 30 | func main() { 31 | // 32 | // Prompt user for Github username/password 33 | // 34 | var username string 35 | fmt.Printf("Github username: ") 36 | _, err := fmt.Scanf("%s", &username) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | fmt.Printf("github.com/howeyc/gopass") 41 | passwd, err := gopass.GetPasswd() 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | // 46 | // Compose request 47 | // 48 | // http://developer.github.com/v3/oauth/#create-a-new-authorization 49 | // 50 | payload := struct { 51 | Scopes []string `json:"scopes"` 52 | Note string `json:"note"` 53 | }{ 54 | Scopes: []string{"public_repo"}, 55 | Note: "testing Go napping" + time.Now().String(), 56 | } 57 | // 58 | // Struct to hold response data 59 | // 60 | res := struct { 61 | Id int 62 | Url string 63 | Scopes []string 64 | Token string 65 | App map[string]string 66 | Note string 67 | NoteUrl string `json:"note_url"` 68 | UpdatedAt string `json:"updated_at"` 69 | CreatedAt string `json:"created_at"` 70 | }{} 71 | // 72 | // Struct to hold error response 73 | // 74 | e := struct { 75 | Message string 76 | Errors []struct { 77 | Resource string 78 | Field string 79 | Code string 80 | } 81 | }{} 82 | // 83 | // Setup HTTP Basic auth for this session (ONLY use this with SSL). Auth 84 | // can also be configured on a per-request basis when using Send(). 85 | // 86 | s := napping.Session{ 87 | Userinfo: url.UserPassword(username, string(passwd)), 88 | } 89 | url := "https://api.github.com/authorizations" 90 | // 91 | // Send request to server 92 | // 93 | resp, err := s.Post(url, &payload, &res, &e) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | // 98 | // Process response 99 | // 100 | println("") 101 | if resp.Status() == 201 { 102 | fmt.Printf("Github auth token: %s\n\n", res.Token) 103 | } else { 104 | fmt.Println("Bad response status from Github server") 105 | fmt.Printf("\t Status: %v\n", resp.Status()) 106 | fmt.Printf("\t Message: %v\n", e.Message) 107 | fmt.Printf("\t Errors: %v\n", e.Message) 108 | pretty.Println(e.Errors) 109 | } 110 | println("") 111 | } 112 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | package napping 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // A Params is a map containing URL parameters. 18 | type Params map[string]string 19 | 20 | // AsUrlValues converts Params to url.Values 21 | func (p Params) AsUrlValues() url.Values { 22 | result := url.Values{} 23 | for key, value := range p { 24 | result.Set(key, value) 25 | } 26 | return result 27 | } 28 | 29 | // A Request describes an HTTP request to be executed, data structures into 30 | // which the result will be unmarshaled, and the server's response. By using 31 | // a single object for both the request and the response we allow easy access 32 | // to Result and Error objects without needing type assertions. 33 | type Request struct { 34 | Url string // Raw URL string 35 | Method string // HTTP method to use 36 | Params *url.Values // URL query parameters 37 | Payload interface{} // Data to JSON-encode and POST 38 | 39 | // Can be set to true if Payload is of type *bytes.Buffer and client wants 40 | // to send it as-is 41 | RawPayload bool 42 | 43 | // Result is a pointer to a data structure. On success (HTTP status < 300), 44 | // response from server is unmarshaled into Result. 45 | Result interface{} 46 | 47 | // CaptureResponseBody can be set to capture the response body for external use. 48 | CaptureResponseBody bool 49 | 50 | // ResponseBody exports the raw response body if CaptureResponseBody is true. 51 | ResponseBody *bytes.Buffer 52 | 53 | // Error is a pointer to a data structure. On error (HTTP status >= 300), 54 | // response from server is unmarshaled into Error. 55 | Error interface{} 56 | 57 | // Optional 58 | Userinfo *url.Userinfo 59 | Header *http.Header 60 | 61 | // Custom Transport if needed. 62 | Transport *http.Transport 63 | 64 | // The following fields are populated by Send(). 65 | timestamp time.Time // Time when HTTP request was sent 66 | status int // HTTP status for executed request 67 | response *http.Response // Response object from http package 68 | body []byte // Body of server's response (JSON or otherwise) 69 | } 70 | 71 | // A Response is a Request object that has been executed. 72 | type Response Request 73 | 74 | // Timestamp returns the time when HTTP request was sent. 75 | func (r *Response) Timestamp() time.Time { 76 | return r.timestamp 77 | } 78 | 79 | // RawText returns the body of the server's response as raw text. 80 | func (r *Response) RawText() string { 81 | return strings.TrimSpace(string(r.body)) 82 | } 83 | 84 | // Status returns the HTTP status for the executed request, or 0 if request has 85 | // not yet been sent. 86 | func (r *Response) Status() int { 87 | return r.status 88 | } 89 | 90 | // HttpResponse returns the underlying Response object from http package. 91 | func (r *Response) HttpResponse() *http.Response { 92 | return r.response 93 | } 94 | 95 | // Unmarshal parses the JSON-encoded data in the server's response, and stores 96 | // the result in the value pointed to by v. 97 | func (r *Response) Unmarshal(v interface{}) error { 98 | return json.Unmarshal(r.body, v) 99 | } 100 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | package napping 7 | 8 | /* 9 | This module provides a Session object to manage and persist settings across 10 | requests (cookies, auth, proxies). 11 | */ 12 | 13 | import ( 14 | "bytes" 15 | "encoding/base64" 16 | "encoding/json" 17 | "errors" 18 | "io/ioutil" 19 | "log" 20 | "net/http" 21 | "net/url" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | // Session defines the napping session structure 27 | type Session struct { 28 | Client *http.Client 29 | Log bool // Log request and response 30 | 31 | // Optional 32 | Userinfo *url.Userinfo 33 | 34 | // Optional defaults - can be overridden in a Request 35 | Header *http.Header 36 | Params *url.Values 37 | } 38 | 39 | // Send constructs and sends an HTTP request. 40 | func (s *Session) Send(r *Request) (response *Response, err error) { 41 | r.Method = strings.ToUpper(r.Method) 42 | // 43 | // Create a URL object from the raw url string. This will allow us to compose 44 | // query parameters programmatically and be guaranteed of a well-formed URL. 45 | // 46 | u, err := url.Parse(r.Url) 47 | if err != nil { 48 | s.log("URL", r.Url) 49 | s.log(err) 50 | return 51 | } 52 | // 53 | // Default query parameters 54 | // 55 | p := url.Values{} 56 | if s.Params != nil { 57 | for k, v := range *s.Params { 58 | p[k] = v 59 | } 60 | } 61 | // 62 | // Parameters that were present in URL 63 | // 64 | if u.Query() != nil { 65 | for k, v := range u.Query() { 66 | p[k] = v 67 | } 68 | } 69 | // 70 | // User-supplied params override default 71 | // 72 | if r.Params != nil { 73 | for k, v := range *r.Params { 74 | p[k] = v 75 | } 76 | } 77 | // 78 | // Encode parameters 79 | // 80 | u.RawQuery = p.Encode() 81 | // 82 | // Attach params to response 83 | // 84 | r.Params = &p 85 | // 86 | // Create a Request object; if populated, Data field is JSON encoded as 87 | // request body 88 | // 89 | header := http.Header{} 90 | if s.Header != nil { 91 | for k := range *s.Header { 92 | v := s.Header.Get(k) 93 | header.Set(k, v) 94 | } 95 | } 96 | var req *http.Request 97 | var buf *bytes.Buffer 98 | if r.Payload != nil { 99 | if r.RawPayload { 100 | var ok bool 101 | // buf can be nil interface at this point 102 | // so we'll do extra nil check 103 | buf, ok = r.Payload.(*bytes.Buffer) 104 | if !ok { 105 | err = errors.New("Payload must be of type *bytes.Buffer if RawPayload is set to true") 106 | return 107 | } 108 | 109 | // do not overwrite the content type with raw payload 110 | } else { 111 | var b []byte 112 | b, err = json.Marshal(&r.Payload) 113 | if err != nil { 114 | s.log(err) 115 | return 116 | } 117 | buf = bytes.NewBuffer(b) 118 | 119 | // Overwrite the content type to json since we're pushing the payload as json 120 | header.Set("Content-Type", "application/json") 121 | } 122 | if buf != nil { 123 | req, err = http.NewRequest(r.Method, u.String(), buf) 124 | } else { 125 | req, err = http.NewRequest(r.Method, u.String(), nil) 126 | } 127 | if err != nil { 128 | s.log(err) 129 | return 130 | } 131 | } else { // no data to encode 132 | req, err = http.NewRequest(r.Method, u.String(), nil) 133 | if err != nil { 134 | s.log(err) 135 | return 136 | } 137 | } 138 | // 139 | // Merge Session and Request options 140 | // 141 | var userinfo *url.Userinfo 142 | if u.User != nil { 143 | userinfo = u.User 144 | } 145 | if s.Userinfo != nil { 146 | userinfo = s.Userinfo 147 | } 148 | // Prefer Request's user credentials 149 | if r.Userinfo != nil { 150 | userinfo = r.Userinfo 151 | } 152 | if r.Header != nil { 153 | for k, v := range *r.Header { 154 | header.Set(k, v[0]) // Is there always guarnateed to be at least one value for a header? 155 | } 156 | } 157 | if header.Get("Accept") == "" { 158 | header.Add("Accept", "application/json") // Default, can be overridden with Opts 159 | } 160 | req.Header = header 161 | // 162 | // Set HTTP Basic authentication if userinfo is supplied 163 | // 164 | if userinfo != nil { 165 | pwd, _ := userinfo.Password() 166 | req.SetBasicAuth(userinfo.Username(), pwd) 167 | if u.Scheme != "https" { 168 | s.log("WARNING: Using HTTP Basic Auth in cleartext is insecure.") 169 | } 170 | } 171 | // 172 | // Execute the HTTP request 173 | // 174 | 175 | // Debug log request 176 | if s.Log { 177 | s.log("--------------------------------------------------------------------------------") 178 | s.log("REQUEST") 179 | s.log("--------------------------------------------------------------------------------") 180 | s.log("Method:", req.Method) 181 | s.log("URL:", req.URL) 182 | s.log("Header:", req.Header) 183 | s.log("Form:", req.Form) 184 | s.log("Payload:") 185 | if r.RawPayload && s.Log && buf != nil { 186 | s.log(base64.StdEncoding.EncodeToString(buf.Bytes())) 187 | } else { 188 | s.log(pretty(r.Payload)) 189 | } 190 | } 191 | 192 | r.timestamp = time.Now() 193 | var client *http.Client 194 | if s.Client != nil { 195 | client = s.Client 196 | } else { 197 | client = &http.Client{} 198 | if r.Transport != nil { 199 | client.Transport = r.Transport 200 | } 201 | 202 | s.Client = client 203 | } 204 | resp, err := client.Do(req) 205 | if err != nil { 206 | s.log(err) 207 | return 208 | } 209 | defer resp.Body.Close() 210 | r.status = resp.StatusCode 211 | r.response = resp 212 | 213 | // 214 | // Unmarshal 215 | // 216 | r.body, err = ioutil.ReadAll(resp.Body) 217 | if err != nil { 218 | s.log(err) 219 | return 220 | } 221 | if string(r.body) != "" { 222 | if resp.StatusCode < 300 && r.Result != nil { 223 | err = json.Unmarshal(r.body, r.Result) 224 | } 225 | if resp.StatusCode >= 400 && r.Error != nil { 226 | json.Unmarshal(r.body, r.Error) // Should we ignore unmarshal error? 227 | } 228 | } 229 | if r.CaptureResponseBody { 230 | r.ResponseBody = bytes.NewBuffer(r.body) 231 | } 232 | rsp := Response(*r) 233 | response = &rsp 234 | 235 | // Debug log response 236 | if s.Log { 237 | s.log("--------------------------------------------------------------------------------") 238 | s.log("RESPONSE") 239 | s.log("--------------------------------------------------------------------------------") 240 | s.log("Status: ", response.status) 241 | s.log("Header:") 242 | s.log(pretty(response.HttpResponse().Header)) 243 | s.log("Body:") 244 | 245 | if response.body != nil { 246 | raw := json.RawMessage{} 247 | if json.Unmarshal(response.body, &raw) == nil { 248 | s.log(pretty(&raw)) 249 | } else { 250 | s.log(pretty(response.RawText())) 251 | } 252 | } else { 253 | s.log("Empty response body") 254 | } 255 | } 256 | 257 | return 258 | } 259 | 260 | // Get sends a GET request. 261 | func (s *Session) Get(url string, p *url.Values, result, errMsg interface{}) (*Response, error) { 262 | r := Request{ 263 | Method: "GET", 264 | Url: url, 265 | Params: p, 266 | Result: result, 267 | Error: errMsg, 268 | } 269 | return s.Send(&r) 270 | } 271 | 272 | // Options sends an OPTIONS request. 273 | func (s *Session) Options(url string, result, errMsg interface{}) (*Response, error) { 274 | r := Request{ 275 | Method: "OPTIONS", 276 | Url: url, 277 | Result: result, 278 | Error: errMsg, 279 | } 280 | return s.Send(&r) 281 | } 282 | 283 | // Head sends a HEAD request. 284 | func (s *Session) Head(url string, result, errMsg interface{}) (*Response, error) { 285 | r := Request{ 286 | Method: "HEAD", 287 | Url: url, 288 | Result: result, 289 | Error: errMsg, 290 | } 291 | return s.Send(&r) 292 | } 293 | 294 | // Post sends a POST request. 295 | func (s *Session) Post(url string, payload, result, errMsg interface{}) (*Response, error) { 296 | r := Request{ 297 | Method: "POST", 298 | Url: url, 299 | Payload: payload, 300 | Result: result, 301 | Error: errMsg, 302 | } 303 | return s.Send(&r) 304 | } 305 | 306 | // Put sends a PUT request. 307 | func (s *Session) Put(url string, payload, result, errMsg interface{}) (*Response, error) { 308 | r := Request{ 309 | Method: "PUT", 310 | Url: url, 311 | Payload: payload, 312 | Result: result, 313 | Error: errMsg, 314 | } 315 | return s.Send(&r) 316 | } 317 | 318 | // Patch sends a PATCH request. 319 | func (s *Session) Patch(url string, payload, result, errMsg interface{}) (*Response, error) { 320 | r := Request{ 321 | Method: "PATCH", 322 | Url: url, 323 | Payload: payload, 324 | Result: result, 325 | Error: errMsg, 326 | } 327 | return s.Send(&r) 328 | } 329 | 330 | // Delete sends a DELETE request. 331 | func (s *Session) Delete(url string, p *url.Values, result, errMsg interface{}) (*Response, error) { 332 | r := Request{ 333 | Method: "DELETE", 334 | Url: url, 335 | Params: p, 336 | Result: result, 337 | Error: errMsg, 338 | } 339 | return s.Send(&r) 340 | } 341 | 342 | // Debug method for logging 343 | // Centralizing logging in one method 344 | // avoids spreading conditionals everywhere 345 | func (s *Session) log(args ...interface{}) { 346 | if s.Log { 347 | log.Println(args...) 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | package napping 7 | 8 | import ( 9 | "bytes" 10 | "crypto/tls" 11 | "encoding/base64" 12 | "encoding/json" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | "net/http/httptest" 17 | "net/url" 18 | "regexp" 19 | "strings" 20 | "testing" 21 | 22 | "github.com/jmcvetta/randutil" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func init() { 27 | log.SetFlags(log.Ltime | log.Lshortfile) 28 | } 29 | 30 | // 31 | // Request Tests 32 | // 33 | 34 | type hfunc http.HandlerFunc 35 | 36 | type payload struct { 37 | Foo string 38 | } 39 | 40 | var reqTests = []struct { 41 | method string 42 | params bool 43 | payload bool 44 | }{ 45 | {"GET", true, false}, 46 | {"POST", false, true}, 47 | {"PUT", false, true}, 48 | {"DELETE", false, false}, 49 | } 50 | 51 | type pair struct { 52 | r Request 53 | hf hfunc 54 | } 55 | 56 | func paramHandler(t *testing.T, p url.Values, f hfunc) hfunc { 57 | return func(w http.ResponseWriter, req *http.Request) { 58 | if f != nil { 59 | f(w, req) 60 | } 61 | q := req.URL.Query() 62 | for k := range p { 63 | if !assert.Equal(t, p[k], q[k]) { 64 | msg := "Bad query params: " + q.Encode() 65 | t.Error(msg) 66 | return 67 | } 68 | } 69 | } 70 | } 71 | 72 | func payloadHandler(t *testing.T, p payload, f hfunc) hfunc { 73 | return func(w http.ResponseWriter, req *http.Request) { 74 | if f != nil { 75 | f(w, req) 76 | } 77 | if req.ContentLength <= 0 { 78 | t.Error("Content-Length must be greater than 0.") 79 | return 80 | } 81 | if req.Header.Get("Content-Type") != "application/json" { 82 | t.Error("Bad content type") 83 | return 84 | } 85 | body, err := ioutil.ReadAll(req.Body) 86 | if err != nil { 87 | t.Error("Body is nil") 88 | return 89 | } 90 | var s payload 91 | err = json.Unmarshal(body, &s) 92 | if err != nil { 93 | t.Error("JSON Unmarshal failed: ", err) 94 | return 95 | } 96 | if s != p { 97 | t.Error("Bad request body") 98 | return 99 | } 100 | } 101 | } 102 | 103 | func methodHandler(t *testing.T, method string, f hfunc) hfunc { 104 | return func(w http.ResponseWriter, req *http.Request) { 105 | if f != nil { 106 | f(w, req) 107 | } 108 | if req.Method != method { 109 | t.Error("Incorrect method, got ", req.Method, " expected ", method) 110 | } 111 | } 112 | } 113 | 114 | func headerHandler(t *testing.T, h http.Header, f hfunc) hfunc { 115 | return func(w http.ResponseWriter, req *http.Request) { 116 | if f != nil { 117 | f(w, req) 118 | } 119 | for k := range h { 120 | expected := h.Get(k) 121 | actual := req.Header.Get(k) 122 | if expected != actual { 123 | t.Error("Missing/bad header") 124 | } 125 | return 126 | } 127 | } 128 | } 129 | 130 | func TestRequest(t *testing.T) { 131 | // NOTE: Do we really need to test different combinations for different 132 | // HTTP methods? 133 | pairs := []pair{} 134 | for _, test := range reqTests { 135 | baseReq := Request{ 136 | Method: test.method, 137 | } 138 | allReq := baseReq // allRR has all supported attribues for this verb 139 | var allHF hfunc // allHF is combination of all relevant handlers 140 | // 141 | // Generate a random key/value pair 142 | // 143 | key, err := randutil.AlphaString(8) 144 | if err != nil { 145 | t.Error(err) 146 | } 147 | value, err := randutil.AlphaString(8) 148 | if err != nil { 149 | t.Error(err) 150 | } 151 | // 152 | // Method 153 | // 154 | r := baseReq 155 | f := methodHandler(t, test.method, nil) 156 | allHF = methodHandler(t, test.method, allHF) 157 | pairs = append(pairs, pair{r, f}) 158 | // 159 | // Header 160 | // 161 | h := http.Header{} 162 | h.Add(key, value) 163 | r = baseReq 164 | r.Header = &h 165 | allReq.Header = &h 166 | f = headerHandler(t, h, nil) 167 | allHF = headerHandler(t, h, allHF) 168 | pairs = append(pairs, pair{r, f}) 169 | // 170 | // Params 171 | // 172 | if test.params { 173 | p := Params{key: value}.AsUrlValues() 174 | f = paramHandler(t, p, nil) 175 | allHF = paramHandler(t, p, allHF) 176 | r = baseReq 177 | r.Params = &p 178 | allReq.Params = &p 179 | pairs = append(pairs, pair{r, f}) 180 | } 181 | // 182 | // Payload 183 | // 184 | if test.payload { 185 | p := payload{value} 186 | f = payloadHandler(t, p, nil) 187 | allHF = payloadHandler(t, p, allHF) 188 | r = baseReq 189 | r.Payload = p 190 | allReq.Payload = p 191 | pairs = append(pairs, pair{r, f}) 192 | } 193 | // 194 | // All 195 | // 196 | pairs = append(pairs, pair{allReq, allHF}) 197 | } 198 | for _, p := range pairs { 199 | srv := httptest.NewServer(http.HandlerFunc(p.hf)) 200 | defer srv.Close() 201 | // 202 | // Good request 203 | // 204 | p.r.Url = "http://" + srv.Listener.Addr().String() 205 | _, err := Send(&p.r) 206 | if err != nil { 207 | t.Error(err) 208 | } 209 | } 210 | } 211 | 212 | func TestInvalidTLS(t *testing.T) { 213 | srv := httptest.NewTLSServer(http.HandlerFunc(handleEmptyOK)) 214 | defer srv.Close() 215 | // The first request, which is supposed to fail, will print something similar to 216 | // "20:45:27 server.go:2161: http: TLS handshake error from 127.0.0.1:56293: remote error: bad certificate" to the console. 217 | // NOTE: Is this something that should be capture and silently ignored? 218 | s := Session{} 219 | r := Request{ 220 | Url: "https://" + srv.Listener.Addr().String(), 221 | Method: "GET", 222 | } 223 | _, err := s.Send(&r) 224 | if err == nil { 225 | t.Fatal("Invalid TLS without custom Transport object. The request should have errored out!") 226 | } 227 | 228 | s2 := Session{} 229 | r2 := Request{ 230 | Url: "https://" + srv.Listener.Addr().String(), 231 | Method: "GET", 232 | Transport: &http.Transport{ 233 | TLSClientConfig: &tls.Config{ 234 | InsecureSkipVerify: true, 235 | }, 236 | }, 237 | } 238 | 239 | resp2, err2 := s2.Send(&r2) 240 | if err2 != nil { 241 | t.Fatal(err2) 242 | } 243 | if resp2.Status() != http.StatusOK { 244 | t.Fatalf("Expected status %d but got %d", http.StatusOK, resp2.Status()) 245 | } 246 | } 247 | 248 | func TestBasicAuth(t *testing.T) { 249 | srv := httptest.NewServer(http.HandlerFunc(handleGetBasicAuth)) 250 | defer srv.Close() 251 | s := Session{} 252 | r := Request{ 253 | Url: "http://" + srv.Listener.Addr().String(), 254 | Method: "GET", 255 | Userinfo: url.UserPassword("jtkirk", "Beam me up, Scotty!"), 256 | } 257 | resp, err := s.Send(&r) 258 | if err != nil { 259 | t.Fatal(err) 260 | } 261 | if resp.Status() != 200 { 262 | t.Fatalf("Expected status 200 but got %d", resp.Status()) 263 | } 264 | } 265 | 266 | func TestBasicUrlAuth(t *testing.T) { 267 | srv := httptest.NewServer(http.HandlerFunc(handleGetBasicAuth)) 268 | defer srv.Close() 269 | s := Session{} 270 | testURL, _ := url.Parse("http://" + srv.Listener.Addr().String()) 271 | testURL.User = url.UserPassword("jtkirk", "Beam me up, Scotty!") 272 | r := Request{ 273 | Url: testURL.String(), 274 | Method: "GET", 275 | } 276 | resp, err := s.Send(&r) 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | if resp.Status() != 200 { 281 | t.Fatalf("Expected status 200 but got %d", resp.Status()) 282 | } 283 | } 284 | 285 | func TestRawPayload(t *testing.T) { 286 | srv := httptest.NewServer(http.HandlerFunc(handleEmptyOK)) 287 | defer srv.Close() 288 | s := Session{} 289 | testURL, _ := url.Parse("http://" + srv.Listener.Addr().String()) 290 | r := Request{ 291 | Url: testURL.String(), 292 | Method: "POST", 293 | Payload: bytes.NewBuffer([]byte("foobar")), 294 | RawPayload: true, 295 | } 296 | resp, err := s.Send(&r) 297 | if err != nil { 298 | t.Fatal(err) 299 | } 300 | if resp.Status() != 200 { 301 | t.Fatalf("Expected status 200 but got %d", resp.Status()) 302 | } 303 | } 304 | 305 | func TestRawPayloadFail(t *testing.T) { 306 | srv := httptest.NewServer(http.HandlerFunc(handleEmptyOK)) 307 | defer srv.Close() 308 | s := Session{} 309 | testURL, _ := url.Parse("http://" + srv.Listener.Addr().String()) 310 | j := struct{}{} 311 | r := Request{ 312 | Url: testURL.String(), 313 | Method: "POST", 314 | Payload: &j, 315 | RawPayload: true, 316 | } 317 | _, err := s.Send(&r) 318 | if err == nil { 319 | t.Fatal("Expect invalid raw payload type") 320 | } 321 | } 322 | 323 | // 324 | // TODO: Response Tests 325 | // 326 | 327 | func TestErrMsg(t *testing.T) {} 328 | 329 | func TestStatus(t *testing.T) {} 330 | 331 | func TestUnmarshal(t *testing.T) {} 332 | 333 | func TestUnmarshalFail(t *testing.T) {} 334 | 335 | func handleEmptyOK(w http.ResponseWriter, req *http.Request) { 336 | w.WriteHeader(http.StatusOK) 337 | } 338 | 339 | func handleGetBasicAuth(w http.ResponseWriter, req *http.Request) { 340 | authRegex := regexp.MustCompile(`[Bb]asic (?P\S+)`) 341 | str := req.Header.Get("Authorization") 342 | matches := authRegex.FindStringSubmatch(str) 343 | if len(matches) != 2 { 344 | msg := "Regex doesn't match" 345 | log.Print(msg) 346 | http.Error(w, msg, http.StatusBadRequest) 347 | return 348 | } 349 | encoded := matches[1] 350 | b, err := base64.URLEncoding.DecodeString(encoded) 351 | if err != nil { 352 | msg := "Base64 decode failed" 353 | log.Print(msg) 354 | http.Error(w, msg, http.StatusBadRequest) 355 | return 356 | } 357 | parts := strings.Split(string(b), ":") 358 | if len(parts) != 2 { 359 | msg := "String split failed" 360 | log.Print(msg) 361 | http.Error(w, msg, http.StatusBadRequest) 362 | return 363 | } 364 | username := parts[0] 365 | password := parts[1] 366 | if username != "jtkirk" || password != "Beam me up, Scotty!" { 367 | code := http.StatusUnauthorized 368 | text := http.StatusText(code) 369 | http.Error(w, text, code) 370 | return 371 | } 372 | w.WriteHeader(200) 373 | } 374 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2013 Jason McVetta. This is Free Software, released 2 | // under the terms of the GPL v3. See http://www.gnu.org/copyleft/gpl.html for 3 | // details. Resist intellectual serfdom - the ownership of ideas is akin to 4 | // slavery. 5 | 6 | package napping 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "net/http" 15 | "net/http/httptest" 16 | "net/url" 17 | "strings" 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func init() { 24 | log.SetFlags(log.Ltime | log.Lshortfile) 25 | } 26 | 27 | func TestInvalidUrl(t *testing.T) { 28 | // 29 | // Missing protocol scheme - url.Parse should fail 30 | // 31 | 32 | url := "://foobar.com" 33 | _, err := Get(url, nil, nil, nil) 34 | assert.NotEqual(t, nil, err) 35 | // 36 | // Unsupported protocol scheme - HttpClient.Do should fail 37 | // 38 | url = "foo://bar.com" 39 | _, err = Get(url, nil, nil, nil) 40 | assert.NotEqual(t, nil, err) 41 | } 42 | 43 | type structType struct { 44 | Foo int 45 | Bar string 46 | } 47 | 48 | type errorStruct struct { 49 | Status int 50 | Message string 51 | } 52 | 53 | var ( 54 | fooParams = Params{"foo": "bar"} 55 | barParams = Params{"bar": "baz"} 56 | fooStruct = structType{ 57 | Foo: 111, 58 | Bar: "foo", 59 | } 60 | barStruct = structType{ 61 | Foo: 222, 62 | Bar: "bar", 63 | } 64 | ) 65 | 66 | func TestGet(t *testing.T) { 67 | srv := httptest.NewServer(http.HandlerFunc(HandleGet)) 68 | defer srv.Close() 69 | // 70 | // Good request 71 | // 72 | url := "http://" + srv.Listener.Addr().String() 73 | p := fooParams.AsUrlValues() 74 | res := structType{} 75 | resp, err := Get(url, &p, &res, nil) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | assert.Equal(t, 200, resp.Status()) 80 | assert.Equal(t, res, barStruct) 81 | // 82 | // Bad request 83 | // 84 | url = "http://" + srv.Listener.Addr().String() 85 | p = Params{"bad": "value"}.AsUrlValues() 86 | e := errorStruct{} 87 | resp, err = Get(url, &p, nil, nil) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if resp.Status() == 200 { 92 | t.Error("Server returned 200 success when it should have failed") 93 | } 94 | assert.Equal(t, 500, resp.Status()) 95 | expected := errorStruct{ 96 | Message: "Bad query params: bad=value", 97 | Status: 500, 98 | } 99 | resp.Unmarshal(&e) 100 | assert.Equal(t, e, expected) 101 | } 102 | 103 | // TestDefaultParams tests using per-session default query parameters. 104 | func TestDefaultParams(t *testing.T) { 105 | srv := httptest.NewServer(http.HandlerFunc(HandleGet)) 106 | defer srv.Close() 107 | // 108 | // Good request 109 | // 110 | url := "http://" + srv.Listener.Addr().String() 111 | p := fooParams.AsUrlValues() 112 | res := structType{} 113 | s := Session{ 114 | Params: &p, 115 | } 116 | resp, err := s.Get(url, nil, &res, nil) 117 | if err != nil { 118 | t.Error(err) 119 | } 120 | assert.Equal(t, 200, resp.Status()) 121 | assert.Equal(t, res, barStruct) 122 | // 123 | // Bad request 124 | // 125 | url = "http://" + srv.Listener.Addr().String() 126 | p = Params{"bad": "value"}.AsUrlValues() 127 | e := errorStruct{} 128 | resp, err = Get(url, &p, nil, nil) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | if resp.Status() == 200 { 133 | t.Error("Server returned 200 success when it should have failed") 134 | } 135 | assert.Equal(t, 500, resp.Status()) 136 | expected := errorStruct{ 137 | Message: "Bad query params: bad=value", 138 | Status: 500, 139 | } 140 | resp.Unmarshal(&e) 141 | assert.Equal(t, e, expected) 142 | } 143 | 144 | func TestDelete(t *testing.T) { 145 | srv := httptest.NewServer(http.HandlerFunc(HandleDelete)) 146 | defer srv.Close() 147 | url := "http://" + srv.Listener.Addr().String() 148 | resp, err := Delete(url, nil, nil, nil) 149 | if err != nil { 150 | t.Error(err) 151 | } 152 | assert.Equal(t, 200, resp.Status()) 153 | } 154 | 155 | func TestHead(t *testing.T) { 156 | // TODO: test result 157 | srv := httptest.NewServer(http.HandlerFunc(HandleHead)) 158 | defer srv.Close() 159 | url := "http://" + srv.Listener.Addr().String() 160 | resp, err := Head(url, nil, nil) 161 | if err != nil { 162 | t.Error(err) 163 | } 164 | assert.Equal(t, 200, resp.Status()) 165 | } 166 | 167 | func TestOptions(t *testing.T) { 168 | // TODO: test result 169 | srv := httptest.NewServer(http.HandlerFunc(HandleOptions)) 170 | defer srv.Close() 171 | url := "http://" + srv.Listener.Addr().String() 172 | resp, err := Options(url, nil, nil) 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | assert.Equal(t, 200, resp.Status()) 177 | } 178 | 179 | func TestPost(t *testing.T) { 180 | srv := httptest.NewServer(http.HandlerFunc(HandlePost)) 181 | defer srv.Close() 182 | s := Session{} 183 | s.Log = true 184 | url := "http://" + srv.Listener.Addr().String() 185 | payload := fooStruct 186 | res := structType{} 187 | resp, err := s.Post(url, &payload, &res, nil) 188 | if err != nil { 189 | t.Error(err) 190 | } 191 | assert.Equal(t, 200, resp.Status()) 192 | assert.Equal(t, res, barStruct) 193 | } 194 | 195 | func TestPostUnmarshalable(t *testing.T) { 196 | srv := httptest.NewServer(http.HandlerFunc(HandlePost)) 197 | defer srv.Close() 198 | type ft func() 199 | var f ft 200 | url := "http://" + srv.Listener.Addr().String() 201 | res := structType{} 202 | payload := f 203 | _, err := Post(url, &payload, &res, nil) 204 | assert.NotEqual(t, nil, err) 205 | _, ok := err.(*json.UnsupportedTypeError) 206 | if !ok { 207 | t.Log(err) 208 | t.Error("Expected json.UnsupportedTypeError") 209 | } 210 | } 211 | 212 | func TestPostParamsInUrl(t *testing.T) { 213 | srv := httptest.NewServer(http.HandlerFunc(HandlePost)) 214 | defer srv.Close() 215 | s := Session{} 216 | s.Log = true 217 | u := "http://" + srv.Listener.Addr().String() 218 | u += "?spam=eggs" // Add query params to URL 219 | payload := fooStruct 220 | res := structType{} 221 | resp, err := s.Post(u, &payload, &res, nil) 222 | if err != nil { 223 | t.Error(err) 224 | } 225 | expected := &url.Values{} 226 | expected.Add("spam", "eggs") 227 | assert.Equal(t, expected, resp.Params) 228 | } 229 | 230 | func TestPut(t *testing.T) { 231 | srv := httptest.NewServer(http.HandlerFunc(HandlePut)) 232 | defer srv.Close() 233 | url := "http://" + srv.Listener.Addr().String() 234 | res := structType{} 235 | resp, err := Put(url, &fooStruct, &res, nil) 236 | if err != nil { 237 | t.Error(err) 238 | } 239 | assert.Equal(t, resp.Status(), 200) 240 | // Server should return NO data 241 | assert.Equal(t, resp.RawText(), "") 242 | } 243 | 244 | func TestPatch(t *testing.T) { 245 | srv := httptest.NewServer(http.HandlerFunc(HandlePatch)) 246 | defer srv.Close() 247 | url := "http://" + srv.Listener.Addr().String() 248 | res := structType{} 249 | resp, err := Patch(url, &fooStruct, &res, nil) 250 | if err != nil { 251 | t.Error(err) 252 | } 253 | assert.Equal(t, resp.Status(), 200) 254 | // Server should return NO data 255 | assert.Equal(t, resp.RawText(), "") 256 | } 257 | 258 | func TestRawRequestWithData(t *testing.T) { 259 | srv := httptest.NewServer(http.HandlerFunc(HandleRaw)) 260 | defer srv.Close() 261 | 262 | var payload = bytes.NewBufferString("napping") 263 | res := structType{} 264 | req := Request{ 265 | Url: "http://" + srv.Listener.Addr().String(), 266 | Method: "PUT", 267 | RawPayload: true, 268 | Payload: payload, 269 | Result: &res, 270 | } 271 | 272 | resp, err := Send(&req) 273 | if err != nil { 274 | t.Error(err) 275 | } 276 | 277 | assert.Equal(t, resp.Status(), 200) 278 | assert.Equal(t, res.Bar, "napping") 279 | } 280 | 281 | func TestRawRequestWithoutData(t *testing.T) { 282 | srv := httptest.NewServer(http.HandlerFunc(HandleRaw)) 283 | defer srv.Close() 284 | 285 | var payload *bytes.Buffer 286 | res := structType{} 287 | req := Request{ 288 | Url: "http://" + srv.Listener.Addr().String(), 289 | Method: "PUT", 290 | RawPayload: true, 291 | Payload: payload, 292 | Result: &res, 293 | } 294 | 295 | resp, err := Send(&req) 296 | if err != nil { 297 | t.Error(err) 298 | } 299 | 300 | assert.Equal(t, resp.Status(), 200) 301 | assert.Equal(t, res.Bar, "empty") 302 | } 303 | 304 | func TestRawRequestInvalidType(t *testing.T) { 305 | srv := httptest.NewServer(http.HandlerFunc(HandleRaw)) 306 | defer srv.Close() 307 | 308 | payload := structType{} 309 | res := structType{} 310 | req := Request{ 311 | Url: "http://" + srv.Listener.Addr().String(), 312 | Method: "PUT", 313 | RawPayload: true, 314 | Payload: payload, 315 | Result: &res, 316 | } 317 | 318 | _, err := Send(&req) 319 | 320 | if err == nil { 321 | t.Error("Validation error expected") 322 | } else { 323 | assert.Equal(t, err.Error(), "Payload must be of type *bytes.Buffer if RawPayload is set to true") 324 | } 325 | } 326 | 327 | // TestRawResponse tests capturing the raw response body. 328 | func TestRawResponse(t *testing.T) { 329 | srv := httptest.NewServer(http.HandlerFunc(HandleRaw)) 330 | defer srv.Close() 331 | 332 | var payload = bytes.NewBufferString("napping") 333 | req := Request{ 334 | Url: "http://" + srv.Listener.Addr().String(), 335 | Method: "PUT", 336 | RawPayload: true, 337 | CaptureResponseBody: true, 338 | Payload: payload, 339 | } 340 | 341 | resp, err := Send(&req) 342 | if err != nil { 343 | t.Error(err) 344 | } 345 | 346 | assert.Equal(t, resp.Status(), 200) 347 | rawResponseStruct := structType{ 348 | Foo: 0, 349 | Bar: "napping", 350 | } 351 | 352 | blob, _ := json.Marshal(rawResponseStruct) 353 | assert.Equal(t, bytes.Equal(resp.ResponseBody.Bytes(), blob), true) 354 | } 355 | 356 | func JSONError(w http.ResponseWriter, msg string, code int) { 357 | e := errorStruct{ 358 | Status: code, 359 | Message: msg, 360 | } 361 | blob, err := json.Marshal(e) 362 | if err != nil { 363 | http.Error(w, msg, code) 364 | return 365 | } 366 | http.Error(w, string(blob), code) 367 | } 368 | 369 | func HandleGet(w http.ResponseWriter, req *http.Request) { 370 | method := strings.ToUpper(req.Method) 371 | if method != "GET" { 372 | msg := fmt.Sprintf("Expected method GET, received %s", method) 373 | http.Error(w, msg, 500) 374 | return 375 | } 376 | u := req.URL 377 | q := u.Query() 378 | for k := range fooParams { 379 | if fooParams[k] != q.Get(k) { 380 | msg := "Bad query params: " + u.Query().Encode() 381 | JSONError(w, msg, http.StatusInternalServerError) 382 | return 383 | } 384 | } 385 | // 386 | // Generate response 387 | // 388 | blob, err := json.Marshal(barStruct) 389 | if err != nil { 390 | JSONError(w, err.Error(), http.StatusInternalServerError) 391 | return 392 | } 393 | req.Header.Add("content-type", "application/json") 394 | w.Write(blob) 395 | } 396 | 397 | func HandleDelete(w http.ResponseWriter, req *http.Request) { 398 | method := strings.ToUpper(req.Method) 399 | if method != "DELETE" { 400 | msg := fmt.Sprintf("Expected method DELETE, received %s", method) 401 | http.Error(w, msg, 500) 402 | return 403 | } 404 | } 405 | 406 | func HandleHead(w http.ResponseWriter, req *http.Request) { 407 | method := strings.ToUpper(req.Method) 408 | if method != "HEAD" { 409 | msg := fmt.Sprintf("Expected method HEAD, received %s", method) 410 | http.Error(w, msg, 500) 411 | return 412 | } 413 | } 414 | 415 | func HandleOptions(w http.ResponseWriter, req *http.Request) { 416 | method := strings.ToUpper(req.Method) 417 | if method != "OPTIONS" { 418 | msg := fmt.Sprintf("Expected method OPTIONS, received %s", method) 419 | http.Error(w, msg, 500) 420 | return 421 | } 422 | } 423 | 424 | func HandlePost(w http.ResponseWriter, req *http.Request) { 425 | method := strings.ToUpper(req.Method) 426 | if method != "POST" { 427 | msg := fmt.Sprintf("Expected method POST, received %s", method) 428 | http.Error(w, msg, 500) 429 | return 430 | } 431 | // 432 | // Parse Payload 433 | // 434 | if req.ContentLength <= 0 { 435 | msg := "Content-Length must be greater than 0." 436 | JSONError(w, msg, http.StatusLengthRequired) 437 | return 438 | } 439 | body, err := ioutil.ReadAll(req.Body) 440 | if err != nil { 441 | JSONError(w, err.Error(), http.StatusInternalServerError) 442 | return 443 | } 444 | var s structType 445 | err = json.Unmarshal(body, &s) 446 | if err != nil { 447 | JSONError(w, err.Error(), http.StatusBadRequest) 448 | return 449 | } 450 | if s != fooStruct { 451 | msg := "Bad request body" 452 | JSONError(w, msg, http.StatusBadRequest) 453 | return 454 | } 455 | // 456 | // Compose Response 457 | // 458 | blob, err := json.Marshal(barStruct) 459 | if err != nil { 460 | JSONError(w, err.Error(), http.StatusInternalServerError) 461 | return 462 | } 463 | req.Header.Add("content-type", "application/json") 464 | w.Write(blob) 465 | } 466 | 467 | func HandlePut(w http.ResponseWriter, req *http.Request) { 468 | method := strings.ToUpper(req.Method) 469 | if method != "PUT" { 470 | msg := fmt.Sprintf("Expected method PUT, received %s", method) 471 | http.Error(w, msg, 500) 472 | return 473 | } 474 | // 475 | // Parse Payload 476 | // 477 | if req.ContentLength <= 0 { 478 | msg := "Content-Length must be greater than 0." 479 | JSONError(w, msg, http.StatusLengthRequired) 480 | return 481 | } 482 | body, err := ioutil.ReadAll(req.Body) 483 | if err != nil { 484 | JSONError(w, err.Error(), http.StatusInternalServerError) 485 | return 486 | } 487 | var s structType 488 | err = json.Unmarshal(body, &s) 489 | if err != nil { 490 | JSONError(w, err.Error(), http.StatusBadRequest) 491 | return 492 | } 493 | if s != fooStruct { 494 | msg := "Bad request body" 495 | JSONError(w, msg, http.StatusBadRequest) 496 | return 497 | } 498 | return 499 | } 500 | 501 | func HandlePatch(w http.ResponseWriter, req *http.Request) { 502 | method := strings.ToUpper(req.Method) 503 | if method != "PATCH" { 504 | msg := fmt.Sprintf("Expected method PATCH, received %s", method) 505 | http.Error(w, msg, 500) 506 | return 507 | } 508 | // 509 | // Parse Payload 510 | // 511 | if req.ContentLength <= 0 { 512 | msg := "Content-Length must be greater than 0." 513 | JSONError(w, msg, http.StatusLengthRequired) 514 | return 515 | } 516 | body, err := ioutil.ReadAll(req.Body) 517 | if err != nil { 518 | JSONError(w, err.Error(), http.StatusInternalServerError) 519 | return 520 | } 521 | var s structType 522 | err = json.Unmarshal(body, &s) 523 | if err != nil { 524 | JSONError(w, err.Error(), http.StatusBadRequest) 525 | return 526 | } 527 | if s != fooStruct { 528 | msg := "Bad request body" 529 | JSONError(w, msg, http.StatusBadRequest) 530 | return 531 | } 532 | return 533 | } 534 | 535 | func HandleRaw(w http.ResponseWriter, req *http.Request) { 536 | var err error 537 | var result = structType{} 538 | if req.ContentLength <= 0 { 539 | result.Bar = "empty" 540 | } else { 541 | var body []byte 542 | body, err = ioutil.ReadAll(req.Body) 543 | if err == nil { 544 | result.Bar = string(body) 545 | } 546 | } 547 | 548 | if err != nil { 549 | JSONError(w, err.Error(), http.StatusInternalServerError) 550 | return 551 | } 552 | 553 | var blob []byte 554 | blob, err = json.Marshal(result) 555 | 556 | if err != nil { 557 | JSONError(w, err.Error(), http.StatusInternalServerError) 558 | return 559 | } 560 | 561 | w.Header().Add("content-type", "application/json") 562 | w.Write(blob) 563 | 564 | return 565 | } 566 | --------------------------------------------------------------------------------