├── .gitignore ├── .travis.yml ├── integration_test.go ├── license ├── nointegration_test.go ├── parse.go ├── parse_test.go ├── patents └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /cover.out 2 | /go.parse.test 3 | /out.html 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | 6 | before_install: 7 | - go get -v golang.org/x/tools/cmd/vet 8 | - go get -v golang.org/x/tools/cmd/cover 9 | - go get -v github.com/golang/lint/golint 10 | 11 | install: 12 | - go install -race -v std 13 | - go get -race -t -v ./... 14 | - go install -race -v ./... 15 | 16 | script: 17 | - go vet ./... 18 | - $HOME/gopath/bin/golint . 19 | - go test -cpu=2 -race -v ./... 20 | - go test -cpu=2 -covermode=atomic ./... 21 | - go test -cpu=2 -tags=integration -race -v ./... 22 | - go test -cpu=2 -tags=integration -covermode=atomic ./... 23 | -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package parse_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "regexp" 10 | "testing" 11 | 12 | "github.com/facebookgo/ensure" 13 | "github.com/facebookgo/parse" 14 | ) 15 | 16 | var ( 17 | realTransport = true 18 | 19 | defaultParseClient = &parse.Client{ 20 | Credentials: defaultRestAPIKey, 21 | } 22 | ) 23 | 24 | func TestPostDeleteObject(t *testing.T) { 25 | t.Parallel() 26 | type obj struct { 27 | Answer int `json:"answer"` 28 | } 29 | 30 | oPostURL, err := url.Parse("classes/Foo") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | type Object struct { 36 | ID string `json:"objectId,omitempty"` 37 | } 38 | 39 | oPost := &obj{Answer: 42} 40 | oPostResponse := &Object{} 41 | oPostReq := http.Request{Method: "POST", URL: oPostURL} 42 | _, err = defaultParseClient.Do(&oPostReq, oPost, oPostResponse) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | if oPostResponse.ID == "" { 47 | t.Fatal("did not get an ID in the response") 48 | } 49 | 50 | p := fmt.Sprintf("classes/Foo/%s", oPostResponse.ID) 51 | oGetURL, err := url.Parse(p) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | oGet := &obj{} 57 | oGetReq := http.Request{Method: "GET", URL: oGetURL} 58 | _, err = defaultParseClient.Do(&oGetReq, nil, oGet) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | if oGet.Answer != oPost.Answer { 63 | t.Fatalf( 64 | "did not get expected answer %d instead got %d", 65 | oPost.Answer, 66 | oGet.Answer, 67 | ) 68 | } 69 | 70 | oDelReq := http.Request{Method: "DELETE", URL: oGetURL} 71 | _, err = defaultParseClient.Do(&oDelReq, nil, nil) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | } 76 | 77 | func TestMissingCredentials(t *testing.T) { 78 | t.Parallel() 79 | var c parse.Client 80 | req := http.Request{Method: "GET", URL: &url.URL{Path: "classes/Foo/Bar"}} 81 | _, err := c.Do(&req, nil, nil) 82 | ensure.NotNil(t, err) 83 | ensure.Err(t, err, regexp.MustCompile(`parse: api error with message="unauthorized"`)) 84 | } 85 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For parse software 4 | 5 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /nointegration_test.go: -------------------------------------------------------------------------------- 1 | // +build !integration 2 | 3 | package parse_test 4 | 5 | var realTransport = false 6 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | // Package parse provides a client for the Parse API. 2 | package parse 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | const ( 15 | userAgentHeader = "User-Agent" 16 | defaultUserAgent = "go-parse-1" 17 | masterKeyHeader = "X-Parse-Master-Key" 18 | restAPIKeyHeader = "X-Parse-REST-API-Key" 19 | sessionTokenHeader = "X-Parse-Session-Token" 20 | applicationIDHeader = "X-Parse-Application-ID" 21 | ) 22 | 23 | var ( 24 | errEmptyApplicationID = errors.New("parse: cannot use empty ApplicationID") 25 | errEmptyMasterKey = errors.New("parse: cannot use empty MasterKey") 26 | errEmptyRestAPIKey = errors.New("parse: cannot use empty RestAPIKey") 27 | errEmptySessionToken = errors.New("parse: cannot use empty SessionToken") 28 | 29 | // The default base URL for the API. 30 | defaultBaseURL = url.URL{ 31 | Scheme: "https", 32 | Host: "api.parse.com", 33 | Path: "/1/", 34 | } 35 | ) 36 | 37 | // Credentials allows for adding authentication information to a request. 38 | type Credentials interface { 39 | Modify(r *http.Request) error 40 | } 41 | 42 | // MasterKey adds the Master Key to the request. 43 | type MasterKey struct { 44 | ApplicationID string 45 | MasterKey string 46 | } 47 | 48 | // Modify adds the Master Key header. 49 | func (m MasterKey) Modify(r *http.Request) error { 50 | if m.ApplicationID == "" { 51 | return errEmptyApplicationID 52 | } 53 | if m.MasterKey == "" { 54 | return errEmptyMasterKey 55 | } 56 | if r.Header == nil { 57 | r.Header = make(http.Header) 58 | } 59 | r.Header.Set(applicationIDHeader, string(m.ApplicationID)) 60 | r.Header.Set(masterKeyHeader, string(m.MasterKey)) 61 | return nil 62 | } 63 | 64 | // RestAPIKey adds the Rest API Key to the request. 65 | type RestAPIKey struct { 66 | ApplicationID string 67 | RestAPIKey string 68 | } 69 | 70 | // Modify adds the Rest API Key header. 71 | func (k RestAPIKey) Modify(r *http.Request) error { 72 | if k.ApplicationID == "" { 73 | return errEmptyApplicationID 74 | } 75 | if k.RestAPIKey == "" { 76 | return errEmptyRestAPIKey 77 | } 78 | if r.Header == nil { 79 | r.Header = make(http.Header) 80 | } 81 | r.Header.Set(applicationIDHeader, string(k.ApplicationID)) 82 | r.Header.Set(restAPIKeyHeader, string(k.RestAPIKey)) 83 | return nil 84 | } 85 | 86 | // SessionToken adds the Rest API Key and the Session Token to the request. 87 | type SessionToken struct { 88 | ApplicationID string 89 | RestAPIKey string 90 | SessionToken string 91 | } 92 | 93 | // Modify adds the Session Token header. 94 | func (t SessionToken) Modify(r *http.Request) error { 95 | if t.ApplicationID == "" { 96 | return errEmptyApplicationID 97 | } 98 | if t.RestAPIKey == "" { 99 | return errEmptyRestAPIKey 100 | } 101 | if t.SessionToken == "" { 102 | return errEmptySessionToken 103 | } 104 | if r.Header == nil { 105 | r.Header = make(http.Header) 106 | } 107 | r.Header.Set(applicationIDHeader, string(t.ApplicationID)) 108 | r.Header.Set(restAPIKeyHeader, string(t.RestAPIKey)) 109 | r.Header.Set(sessionTokenHeader, string(t.SessionToken)) 110 | return nil 111 | } 112 | 113 | // An Error from the Parse API. When a valid Parse JSON error is found, the 114 | // returned error will be of this type. 115 | type Error struct { 116 | Message string `json:"error"` 117 | Code int `json:"code"` 118 | } 119 | 120 | func (e *Error) Error() string { 121 | var buf bytes.Buffer 122 | fmt.Fprint(&buf, "parse: api error with ") 123 | if e.Code != 0 { 124 | fmt.Fprintf(&buf, "code=%d", e.Code) 125 | } 126 | if e.Code != 0 && e.Message != "" { 127 | fmt.Fprint(&buf, " and ") 128 | } 129 | if e.Message != "" { 130 | fmt.Fprintf(&buf, "message=%q", e.Message) 131 | } 132 | return buf.String() 133 | } 134 | 135 | // A RawError with the HTTP StatusCode and Body. When a valid Parse JSON error 136 | // is not found, the returned error will be of this type. 137 | type RawError struct { 138 | StatusCode int 139 | Body []byte 140 | } 141 | 142 | func (e *RawError) Error() string { 143 | return fmt.Sprintf("parse: error with status=%d and body=%q", e.StatusCode, e.Body) 144 | } 145 | 146 | // Client provides access to the Parse API. 147 | type Client struct { 148 | // The underlying http.RoundTripper to perform the individual requests. When 149 | // nil http.DefaultTransport will be used. 150 | Transport http.RoundTripper 151 | 152 | // The base URL to parse relative URLs off. If you pass absolute URLs to 153 | // Client functions they are used as-is. When nil, the production Parse URL 154 | // will be used. 155 | BaseURL *url.URL 156 | 157 | // Credentials if set, will be included on every request. 158 | Credentials Credentials 159 | 160 | // UserAgent to use in the User-Agent header. When nil defaultUserAgent 161 | // will be used. 162 | UserAgent string 163 | } 164 | 165 | func (c *Client) transport() http.RoundTripper { 166 | if c.Transport == nil { 167 | return http.DefaultTransport 168 | } 169 | return c.Transport 170 | } 171 | 172 | // Get performs a GET method call on the given url and unmarshal response into 173 | // result. 174 | func (c *Client) Get(u *url.URL, result interface{}) (*http.Response, error) { 175 | return c.Do(&http.Request{Method: "GET", URL: u}, nil, result) 176 | } 177 | 178 | // Post performs a POST method call on the given url with the given body and 179 | // unmarshal response into result. 180 | func (c *Client) Post(u *url.URL, body, result interface{}) (*http.Response, error) { 181 | return c.Do(&http.Request{Method: "POST", URL: u}, body, result) 182 | } 183 | 184 | // Put performs a PUT method call on the given url with the given body and 185 | // unmarshal response into result. 186 | func (c *Client) Put(u *url.URL, body, result interface{}) (*http.Response, error) { 187 | return c.Do(&http.Request{Method: "PUT", URL: u}, body, result) 188 | } 189 | 190 | // Delete performs a DELETE method call on the given url and unmarshal response 191 | // into result. 192 | func (c *Client) Delete(u *url.URL, result interface{}) (*http.Response, error) { 193 | return c.Do(&http.Request{Method: "DELETE", URL: u}, nil, result) 194 | } 195 | 196 | // RoundTrip performs a RoundTrip ignoring the request and response bodies. It 197 | // is up to the caller to close them. This method modifies the request. 198 | func (c *Client) RoundTrip(req *http.Request) (*http.Response, error) { 199 | req.Proto = "HTTP/1.1" 200 | req.ProtoMajor = 1 201 | req.ProtoMinor = 1 202 | 203 | if req.URL == nil { 204 | if c.BaseURL == nil { 205 | req.URL = &defaultBaseURL 206 | } else { 207 | req.URL = c.BaseURL 208 | } 209 | } else { 210 | if !req.URL.IsAbs() { 211 | if c.BaseURL == nil { 212 | req.URL = defaultBaseURL.ResolveReference(req.URL) 213 | } else { 214 | req.URL = c.BaseURL.ResolveReference(req.URL) 215 | } 216 | } 217 | } 218 | 219 | if req.Host == "" { 220 | req.Host = req.URL.Host 221 | } 222 | 223 | if req.Header == nil { 224 | req.Header = make(http.Header) 225 | } 226 | 227 | var userAgent string 228 | if c.UserAgent == "" { 229 | userAgent = defaultUserAgent 230 | } else { 231 | userAgent = c.UserAgent 232 | } 233 | 234 | req.Header.Add(userAgentHeader, userAgent) 235 | if c.Credentials != nil { 236 | if err := c.Credentials.Modify(req); err != nil { 237 | return nil, err 238 | } 239 | } 240 | 241 | res, err := c.transport().RoundTrip(req) 242 | if err != nil { 243 | return res, err 244 | } 245 | 246 | if res.StatusCode > 399 || res.StatusCode < 200 { 247 | body, err := ioutil.ReadAll(res.Body) 248 | res.Body.Close() 249 | if err != nil { 250 | return res, err 251 | } 252 | 253 | if len(body) > 0 { 254 | var apiErr Error 255 | if json.Unmarshal(body, &apiErr) == nil { 256 | return res, &apiErr 257 | } 258 | } 259 | return res, &RawError{ 260 | StatusCode: res.StatusCode, 261 | Body: body, 262 | } 263 | } 264 | 265 | return res, nil 266 | } 267 | 268 | // Do performs a Parse API call. This method modifies the request and adds the 269 | // Authentication headers. The body is JSON encoded and for responses in the 270 | // 2xx or 3xx range the response will be JSON decoded into result, for others 271 | // an error of type Error will be returned. 272 | func (c *Client) Do(req *http.Request, body, result interface{}) (*http.Response, error) { 273 | // we need to buffer as Parse requires a Content-Length 274 | if body != nil { 275 | bd, err := json.Marshal(body) 276 | if err != nil { 277 | return nil, err 278 | } 279 | if req.Header == nil { 280 | req.Header = make(http.Header) 281 | } 282 | req.Header.Set("Content-Type", "application/json") 283 | req.Body = ioutil.NopCloser(bytes.NewReader(bd)) 284 | req.ContentLength = int64(len(bd)) 285 | } 286 | 287 | res, err := c.RoundTrip(req) 288 | if err != nil { 289 | return res, err 290 | } 291 | defer res.Body.Close() 292 | 293 | if result != nil { 294 | if err := json.NewDecoder(res.Body).Decode(result); err != nil { 295 | return res, err 296 | } 297 | } 298 | return res, nil 299 | } 300 | 301 | // WithCredentials returns a new instance of the Client using the given 302 | // Credentials. It discards the previous Credentials. 303 | func (c *Client) WithCredentials(cr Credentials) *Client { 304 | var c2 Client 305 | c2 = *c 306 | c2.Credentials = cr 307 | return &c2 308 | } 309 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package parse_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/url" 11 | "regexp" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/facebookgo/ensure" 16 | "github.com/facebookgo/parse" 17 | ) 18 | 19 | var ( 20 | defaultApplicationID = "spAVcBmdREXEk9IiDwXzlwe0p4pO7t18KFsHyk7j" 21 | defaultRestAPIKey = parse.RestAPIKey{ 22 | ApplicationID: defaultApplicationID, 23 | RestAPIKey: "t6ON64DfTrTL4QJC322HpWbhN6fzGYo8cnjVttap", 24 | } 25 | ) 26 | 27 | type transportFunc func(*http.Request) (*http.Response, error) 28 | 29 | func (t transportFunc) RoundTrip(r *http.Request) (*http.Response, error) { 30 | return t(r) 31 | } 32 | 33 | func jsonB(t testing.TB, v interface{}) []byte { 34 | b, err := json.Marshal(v) 35 | ensure.Nil(t, err) 36 | return b 37 | } 38 | 39 | func TestErrorCases(t *testing.T) { 40 | cases := []struct { 41 | Request *http.Request 42 | Body interface{} 43 | Error string 44 | StatusCode int 45 | Transport http.RoundTripper 46 | }{ 47 | { 48 | Request: &http.Request{ 49 | Method: "GET", 50 | URL: &url.URL{ 51 | Scheme: "https", 52 | Host: "api.parse.com", 53 | Path: "/1/classes/Foo/Bar", 54 | }, 55 | }, 56 | Error: `parse: api error with code=101 and message="object not found for get"`, 57 | StatusCode: http.StatusNotFound, 58 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 59 | j := jsonB(t, parse.Error{ 60 | Code: 101, 61 | Message: "object not found for get", 62 | }) 63 | return &http.Response{ 64 | StatusCode: http.StatusNotFound, 65 | Status: "404 Not Found", 66 | Body: ioutil.NopCloser(bytes.NewReader(j)), 67 | }, nil 68 | }), 69 | }, 70 | { 71 | Request: &http.Request{ 72 | Method: "GET", 73 | URL: &url.URL{ 74 | Scheme: "https", 75 | Host: "api.parse.com", 76 | Path: "/1/classes/Foo/Bar", 77 | }, 78 | }, 79 | Body: map[int]int{}, 80 | Error: "unsupported type: map[int]int", 81 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 82 | panic("not reached") 83 | }), 84 | }, 85 | { 86 | Request: &http.Request{ 87 | Method: "GET", 88 | URL: &url.URL{Path: "/"}, 89 | }, 90 | Error: `error with status=404 and body="`, 91 | StatusCode: 404, 92 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 93 | return &http.Response{ 94 | StatusCode: http.StatusNotFound, 95 | Status: "404 Not Found", 96 | Body: ioutil.NopCloser(strings.NewReader("")), 97 | }, nil 98 | }), 99 | }, 100 | } 101 | 102 | t.Parallel() 103 | for _, ec := range cases { 104 | c := &parse.Client{ 105 | Credentials: defaultRestAPIKey, 106 | } 107 | if !realTransport { 108 | c.Transport = ec.Transport 109 | } 110 | res, err := c.Do(ec.Request, ec.Body, nil) 111 | ensure.NotNil(t, err) 112 | ensure.StringContains(t, err.Error(), ec.Error, ec) 113 | if ec.StatusCode != 0 { 114 | ensure.False(t, res == nil, ec) 115 | ensure.DeepEqual(t, res.StatusCode, ec.StatusCode, ec) 116 | } 117 | } 118 | } 119 | 120 | func TestMethodHelpers(t *testing.T) { 121 | t.Parallel() 122 | expected := []string{"GET", "POST", "PUT", "DELETE"} 123 | count := 0 124 | c := &parse.Client{ 125 | Credentials: defaultRestAPIKey, 126 | BaseURL: &url.URL{ 127 | Scheme: "https", 128 | Host: "api.parse.com", 129 | Path: "/1/classes/Foo/", 130 | }, 131 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 132 | ensure.DeepEqual(t, r.Method, expected[count]) 133 | count++ 134 | return nil, errors.New("") 135 | }), 136 | } 137 | c.Get(nil, nil) 138 | c.Post(nil, nil, nil) 139 | c.Put(nil, nil, nil) 140 | c.Delete(nil, nil) 141 | ensure.DeepEqual(t, count, len(expected)) 142 | } 143 | 144 | func TestNilGetWithDefaultBaseURL(t *testing.T) { 145 | t.Parallel() 146 | done := make(chan struct{}) 147 | c := &parse.Client{ 148 | Credentials: defaultRestAPIKey, 149 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 150 | defer close(done) 151 | ensure.DeepEqual(t, r.URL.String(), "https://api.parse.com/1/") 152 | return nil, errors.New("") 153 | }), 154 | } 155 | c.Get(nil, nil) 156 | <-done 157 | } 158 | 159 | func TestRelativeGetWithDefaultBaseURL(t *testing.T) { 160 | t.Parallel() 161 | done := make(chan struct{}) 162 | c := &parse.Client{ 163 | Credentials: defaultRestAPIKey, 164 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 165 | defer close(done) 166 | ensure.DeepEqual(t, r.URL.String(), "https://api.parse.com/1/Foo") 167 | return nil, errors.New("") 168 | }), 169 | } 170 | c.Get(&url.URL{Path: "Foo"}, nil) 171 | <-done 172 | } 173 | 174 | func TestResolveReferenceWithBase(t *testing.T) { 175 | t.Parallel() 176 | done := make(chan struct{}) 177 | c := &parse.Client{ 178 | Credentials: defaultRestAPIKey, 179 | BaseURL: &url.URL{ 180 | Path: "/1/", 181 | }, 182 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 183 | defer close(done) 184 | ensure.DeepEqual(t, r.URL.String(), "/1/Foo") 185 | return nil, errors.New("") 186 | }), 187 | } 188 | c.Get(&url.URL{Path: "Foo"}, nil) 189 | <-done 190 | } 191 | 192 | func TestServerAbort(t *testing.T) { 193 | t.Parallel() 194 | for _, code := range []int{200, 500} { 195 | server := httptest.NewServer( 196 | http.HandlerFunc( 197 | func(w http.ResponseWriter, r *http.Request) { 198 | w.Header().Add("Content-Length", "4000") 199 | w.WriteHeader(code) 200 | w.Write(bytes.Repeat([]byte("a"), 3000)) 201 | }, 202 | ), 203 | ) 204 | 205 | u, err := url.Parse(server.URL) 206 | ensure.Nil(t, err) 207 | 208 | c := &parse.Client{ 209 | Credentials: defaultRestAPIKey, 210 | BaseURL: u, 211 | } 212 | res := make(map[string]interface{}) 213 | _, err = c.Get(nil, res) 214 | ensure.NotNil(t, err) 215 | server.CloseClientConnections() 216 | server.Close() 217 | } 218 | } 219 | 220 | func TestCustomHTTPTransport(t *testing.T) { 221 | t.Parallel() 222 | const message = "hello world" 223 | c := &parse.Client{ 224 | Transport: transportFunc(func(*http.Request) (*http.Response, error) { 225 | return nil, errors.New(message) 226 | }), 227 | } 228 | _, err := c.Do(&http.Request{}, nil, nil) 229 | ensure.Err(t, err, regexp.MustCompile(message)) 230 | } 231 | 232 | func TestMasterKeyEmptyApplicationID(t *testing.T) { 233 | t.Parallel() 234 | var mk parse.MasterKey 235 | ensure.Err(t, mk.Modify(nil), regexp.MustCompile("empty ApplicationID")) 236 | } 237 | 238 | func TestEmptyMasterKey(t *testing.T) { 239 | t.Parallel() 240 | mk := parse.MasterKey{ApplicationID: defaultApplicationID} 241 | ensure.Err(t, mk.Modify(nil), regexp.MustCompile("empty MasterKey")) 242 | } 243 | 244 | func TestRestAPIKeyEmptyApplicationID(t *testing.T) { 245 | t.Parallel() 246 | var mk parse.RestAPIKey 247 | ensure.Err(t, mk.Modify(nil), regexp.MustCompile("empty ApplicationID")) 248 | } 249 | 250 | func TestEmptyRestAPIKey(t *testing.T) { 251 | t.Parallel() 252 | mk := parse.RestAPIKey{ApplicationID: defaultApplicationID} 253 | ensure.Err(t, mk.Modify(nil), regexp.MustCompile("empty RestAPIKey")) 254 | } 255 | 256 | func TestEmptySessionToken(t *testing.T) { 257 | t.Parallel() 258 | var st parse.SessionToken 259 | ensure.Err(t, st.Modify(nil), regexp.MustCompile("empty ApplicationID")) 260 | } 261 | 262 | func TestEmptySessionTokenMissingRestAPIKey(t *testing.T) { 263 | t.Parallel() 264 | st := parse.SessionToken{ApplicationID: defaultApplicationID} 265 | ensure.Err(t, st.Modify(nil), regexp.MustCompile("empty RestAPIKey")) 266 | } 267 | 268 | func TestEmptySessionTokenInSessionToken(t *testing.T) { 269 | t.Parallel() 270 | st := parse.SessionToken{ 271 | ApplicationID: defaultApplicationID, 272 | RestAPIKey: "rk", 273 | } 274 | ensure.Err(t, st.Modify(nil), regexp.MustCompile("empty SessionToken")) 275 | } 276 | 277 | func TestUserAgent(t *testing.T) { 278 | t.Parallel() 279 | done := make(chan struct{}) 280 | c := &parse.Client{ 281 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 282 | defer close(done) 283 | ensure.NotDeepEqual(t, r.Header.Get("User-Agent"), "") 284 | return nil, errors.New("") 285 | }), 286 | } 287 | c.Do(&http.Request{}, nil, nil) 288 | <-done 289 | } 290 | 291 | func TestCredentiasModifyError(t *testing.T) { 292 | t.Parallel() 293 | c := parse.Client{ 294 | Credentials: parse.RestAPIKey{}, 295 | } 296 | _, err := c.Do(&http.Request{}, nil, nil) 297 | ensure.Err(t, err, regexp.MustCompile("empty ApplicationID")) 298 | } 299 | 300 | func TestAddCredentials(t *testing.T) { 301 | t.Parallel() 302 | const rk = "rk" 303 | const st = "st" 304 | done := make(chan struct{}) 305 | c := &parse.Client{ 306 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 307 | defer close(done) 308 | ensure.DeepEqual(t, r.Header.Get("X-Parse-Application-ID"), defaultApplicationID) 309 | ensure.DeepEqual(t, r.Header.Get("X-Parse-Session-Token"), st) 310 | ensure.DeepEqual(t, r.Header.Get("X-Parse-REST-API-Key"), rk) 311 | return nil, errors.New("") 312 | }), 313 | } 314 | c = c.WithCredentials(parse.SessionToken{ 315 | ApplicationID: defaultApplicationID, 316 | RestAPIKey: rk, 317 | SessionToken: st, 318 | }) 319 | c.Do(&http.Request{}, nil, nil) 320 | <-done 321 | } 322 | 323 | func TestContentLengthHeader(t *testing.T) { 324 | t.Parallel() 325 | done := make(chan struct{}) 326 | c := &parse.Client{ 327 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 328 | defer close(done) 329 | ensure.DeepEqual(t, r.ContentLength, int64(4)) 330 | return nil, errors.New("") 331 | }), 332 | } 333 | c.Post(nil, true, nil) 334 | <-done 335 | } 336 | 337 | func TestSuccessfulRequest(t *testing.T) { 338 | t.Parallel() 339 | expected := map[string]int{"answer": 42} 340 | c := &parse.Client{ 341 | Transport: transportFunc(func(r *http.Request) (*http.Response, error) { 342 | return &http.Response{ 343 | StatusCode: http.StatusOK, 344 | Body: ioutil.NopCloser(bytes.NewReader(jsonB(t, expected))), 345 | }, nil 346 | }), 347 | } 348 | var m map[string]int 349 | _, err := c.Post(nil, true, &m) 350 | ensure.Nil(t, err) 351 | ensure.DeepEqual(t, m, expected) 352 | } 353 | 354 | func TestMasterKeyModify(t *testing.T) { 355 | t.Parallel() 356 | var req http.Request 357 | k := parse.MasterKey{ 358 | ApplicationID: defaultApplicationID, 359 | MasterKey: "42", 360 | } 361 | ensure.Nil(t, k.Modify(&req)) 362 | ensure.DeepEqual(t, req.Header.Get("X-Parse-Application-ID"), k.ApplicationID) 363 | ensure.DeepEqual(t, req.Header.Get("X-Parse-Master-Key"), k.MasterKey) 364 | } 365 | 366 | func TestRestAPIKeyModify(t *testing.T) { 367 | t.Parallel() 368 | var req http.Request 369 | k := parse.RestAPIKey{ 370 | ApplicationID: defaultApplicationID, 371 | RestAPIKey: "42", 372 | } 373 | ensure.Nil(t, k.Modify(&req)) 374 | ensure.DeepEqual(t, req.Header.Get("X-Parse-Application-ID"), k.ApplicationID) 375 | ensure.DeepEqual(t, req.Header.Get("X-Parse-REST-API-Key"), k.RestAPIKey) 376 | } 377 | 378 | func TestSessionTokenModify(t *testing.T) { 379 | t.Parallel() 380 | st := parse.SessionToken{ 381 | ApplicationID: defaultApplicationID, 382 | RestAPIKey: "42", 383 | SessionToken: "43", 384 | } 385 | var req http.Request 386 | ensure.Nil(t, st.Modify(&req)) 387 | ensure.DeepEqual(t, req.Header.Get("X-Parse-Application-ID"), st.ApplicationID) 388 | ensure.DeepEqual(t, req.Header.Get("X-Parse-REST-API-Key"), st.RestAPIKey) 389 | ensure.DeepEqual(t, req.Header.Get("X-Parse-Session-Token"), st.SessionToken) 390 | } 391 | -------------------------------------------------------------------------------- /patents: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the parse software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook’s rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | parse [![Build Status](https://secure.travis-ci.org/facebookgo/parse.svg)](https://travis-ci.org/facebookgo/parse) 2 | ===== 3 | 4 | NOTE: The APIs for this package are still in flux. 5 | 6 | Package parse provides a client for the [Parse API](https://parse.com/): 7 | https://godoc.org/github.com/facebookgo/parse 8 | --------------------------------------------------------------------------------