├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── firebase.go ├── firebase_test.go └── mock_firebase └── firebase.go /.gitignore: -------------------------------------------------------------------------------- 1 | keys.go 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Cosmin Nicolaescu 4 | Copyright (c) 2014-2015 Justin Tulloss 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | $(GOPATH)/src/%: 2 | go get $* 3 | 4 | mock_firebase/firebase.go: firebase.go | $(GOPATH)/src/code.google.com/p/gomock/mockgen $(GOPATH)/src/code.google.com/p/gomock/gomock 5 | mockgen -source $^ -imports ".=github.com/JustinTulloss/firebase" -destination $@ 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go Firebase 2 | =========== 3 | 4 | Helper library for invoking the Firebase REST API. 5 | 6 | [![Circle CI](https://circleci.com/gh/JustinTulloss/firebase.svg?style=svg)](https://circleci.com/gh/JustinTulloss/firebase) 7 | 8 | ### Usage 9 | 10 | [Read the godocs](https://godoc.org/github.com/JustinTulloss/firebase) 11 | 12 | ### Test 13 | 14 | Set the `FIREBASE_TEST_URL` and `FIREBASE_TEST_AUTH` environment variables to a valid URL and secret, respectively. 15 | 16 | Then run: 17 | ```sh 18 | go test github.com/JustinTulloss/firebase... 19 | ``` 20 | -------------------------------------------------------------------------------- /firebase.go: -------------------------------------------------------------------------------- 1 | // Package firebase gives a thin wrapper around the firebase REST API. It tries 2 | // to mirror the Official firebase API somewhat closely. https://www.firebase.com/docs/web/api/ 3 | package firebase 4 | 5 | import ( 6 | "bytes" 7 | "database/sql/driver" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | "reflect" 14 | "regexp" 15 | "strconv" 16 | "time" 17 | 18 | "github.com/ancientlore/go-avltree" 19 | "github.com/facebookgo/httpcontrol" 20 | ) 21 | 22 | var keyExtractor = regexp.MustCompile(`https://.*/([^/]+)/?$`) 23 | 24 | // Timestamp is a time.Time with support for going from and to firebase 25 | // ServerValue.TIMESTAMP fields. 26 | // 27 | // Thanks to Gal Ben-Haim for the inspiration 28 | // https://medium.com/coding-and-deploying-in-the-cloud/time-stamps-in-golang-abcaf581b72f 29 | type Timestamp time.Time 30 | 31 | const milliDivider = 1000000 32 | 33 | func (t *Timestamp) MarshalJSON() ([]byte, error) { 34 | ts := time.Time(*t).UnixNano() / milliDivider // Milliseconds 35 | stamp := fmt.Sprint(ts) 36 | 37 | return []byte(stamp), nil 38 | } 39 | 40 | func (t *Timestamp) UnmarshalJSON(b []byte) error { 41 | ts, err := strconv.ParseInt(string(b), 10, 64) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | seconds := int64(ts) / 1000 47 | nanoseconds := (int64(ts) % 1000) * milliDivider 48 | *t = Timestamp(time.Unix(seconds, nanoseconds)) 49 | 50 | return nil 51 | } 52 | 53 | func (t *Timestamp) Scan(src interface{}) error { 54 | fmt.Printf("is this even happening?\n") 55 | ts, ok := src.(time.Time) 56 | if !ok { 57 | return errors.New("Did not receive a time.Time type as src") 58 | } 59 | *t = Timestamp(ts) 60 | return nil 61 | } 62 | 63 | func (t *Timestamp) Value() (driver.Value, error) { 64 | if t == nil { 65 | return driver.Value(nil), nil 66 | } 67 | return driver.Value(time.Time(*t)), nil 68 | } 69 | 70 | func (t Timestamp) String() string { 71 | return time.Time(t).String() 72 | } 73 | 74 | type ServerValue struct { 75 | Value string `json:".sv"` 76 | } 77 | 78 | type FirebaseError struct { 79 | Message string `json:"error"` 80 | } 81 | 82 | func (f *FirebaseError) Error() string { 83 | return f.Message 84 | } 85 | 86 | // Use this value to represent a Firebase server timestamp in a data structure. 87 | // This should be used when you're sending data to Firebase, as opposed to 88 | // the Timestamp type. 89 | var ServerTimestamp ServerValue = ServerValue{"timestamp"} 90 | 91 | type KeyedValue struct { 92 | avltree.Pair 93 | OrderBy string 94 | } 95 | 96 | func (p *KeyedValue) GetComparable() reflect.Value { 97 | value := reflect.Indirect(reflect.ValueOf(p.Value)) 98 | var comparable reflect.Value 99 | switch value.Kind() { 100 | case reflect.Map: 101 | comparable = value.MapIndex(reflect.ValueOf(p.OrderBy)) 102 | case reflect.Struct: 103 | comparable = value.FieldByName(p.OrderBy) 104 | default: 105 | panic("Can only get comparable for maps and structs") 106 | } 107 | if comparable.Kind() == reflect.Interface || comparable.Kind() == reflect.Ptr { 108 | return comparable.Elem() 109 | } 110 | return comparable 111 | } 112 | 113 | func (a *KeyedValue) Compare(b avltree.Interface) int { 114 | if a.OrderBy == "" || a.OrderBy == KeyProp { 115 | return a.Pair.Compare(b.(*KeyedValue).Pair) 116 | } 117 | ac := a.GetComparable() 118 | bc := b.(*KeyedValue).GetComparable() 119 | if ac.Kind() != bc.Kind() { 120 | panic(fmt.Sprintf("Cannot compare %s to %s", ac.Kind(), bc.Kind())) 121 | } 122 | switch ac.Kind() { 123 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 124 | ai := ac.Int() 125 | bi := bc.Int() 126 | if ai < bi { 127 | return -1 128 | } else if ai == bi { 129 | return 0 130 | } else if ai > bi { 131 | return 1 132 | } 133 | case reflect.Float32, reflect.Float64: 134 | af := ac.Float() 135 | bf := bc.Float() 136 | if af < bf { 137 | return -1 138 | } else if af == bf { 139 | return 0 140 | } else if af > bf { 141 | return 1 142 | } 143 | case reflect.String: 144 | as := ac.String() 145 | bs := bc.String() 146 | if as < bs { 147 | return -1 148 | } else if as == bs { 149 | return 0 150 | } else if as > bs { 151 | return 1 152 | } 153 | default: 154 | panic(fmt.Sprintf("Can only compare strings, floats, and ints. Not %s", ac.Kind())) 155 | } 156 | return 0 157 | } 158 | 159 | // A function that provides an interface to copy decoded data into in 160 | // Client#Iterator 161 | type Destination func() interface{} 162 | 163 | // Api is the internal interface for interacting with Firebase. 164 | // Consumers of this package can mock this interface for testing purposes, regular 165 | // consumers can just use the default implementation and can ignore this completely. 166 | // Arguments are as follows: 167 | // - `method`: The http method for this call 168 | // - `path`: The full firebase url to call 169 | // - `body`: Data to be marshalled to JSON (it's the responsibility of Call to do the marshalling and unmarshalling) 170 | // - `params`: Additional parameters to be passed to firebase 171 | // - `dest`: The object to save the unmarshalled response body to. 172 | // It's up to this method to unmarshal correctly, the default implemenation just uses `json.Unmarshal` 173 | type Api interface { 174 | Call(method, path, auth string, body interface{}, params map[string]string, dest interface{}) error 175 | } 176 | 177 | type Client interface { 178 | // Returns the absolute URL path for the client 179 | String() string 180 | 181 | // Returns the last part of the URL path for the client. 182 | Key() string 183 | 184 | //Gets the value referenced by the client and unmarshals it into 185 | // the passed in destination. 186 | Value(destination interface{}) error 187 | 188 | // Iterator returns a channel that will emit objects in order defined by 189 | // Client#OrderBy 190 | Iterator(d Destination) <-chan *KeyedValue 191 | 192 | // Shallow returns a list of keys at a particular location 193 | // Only supports objects, unlike the REST artument which supports 194 | // literals. If the location is a literal, use Client#Value() 195 | Shallow() ([]string, error) 196 | 197 | // Child returns a reference to the child specified by `path`. This does not 198 | // actually make a request to firebase, but you can then manipulate the reference 199 | // by calling one of the other methods (such as `Value`, `Update`, or `Set`). 200 | Child(path string) Client 201 | 202 | // Query functions. They map directly to the Firebase operations. 203 | // https://www.firebase.com/docs/rest/guide/retrieving-data.html#section-rest-queries 204 | OrderBy(prop string) Client 205 | EqualTo(value string) Client 206 | StartAt(value string) Client 207 | EndAt(value string) Client 208 | LimitToFirst(value int) Client 209 | LimitToLast(value int) Client 210 | 211 | // Creates a new value under this reference. 212 | // Returns a reference to the newly created value. 213 | // https://www.firebase.com/docs/web/api/firebase/push.html 214 | Push(value interface{}, params map[string]string) (Client, error) 215 | 216 | // Overwrites the value at the specified path and returns a reference 217 | // that points to the path specified by `path` 218 | Set(path string, value interface{}, params map[string]string) (Client, error) 219 | 220 | // Update performs a partial update with the given value at the specified path. 221 | // Returns an error if the update could not be performed. 222 | // https://www.firebase.com/docs/web/api/firebase/update.html 223 | Update(path string, value interface{}, params map[string]string) error 224 | 225 | // Remove deletes the data at the current reference. 226 | // https://www.firebase.com/docs/web/api/firebase/remove.html 227 | Remove(path string, params map[string]string) error 228 | 229 | // Rules returns the security rules for the database. 230 | // https://www.firebase.com/docs/rest/api/#section-security-rules 231 | Rules(params map[string]string) (*Rules, error) 232 | 233 | // SetRules overwrites the existing security rules with the new rules given. 234 | // https://www.firebase.com/docs/rest/api/#section-security-rules 235 | SetRules(rules *Rules, params map[string]string) error 236 | } 237 | 238 | // This is the actual default implementation 239 | type client struct { 240 | // The ordering being enforced on this client 241 | Order string 242 | // url is the client's base URL used for all calls. 243 | url string 244 | 245 | // auth is authentication token used when making calls. 246 | // The token is optional and can also be overwritten on an individual 247 | // call basis via params. 248 | auth string 249 | 250 | // api is the underlying client used to make calls. 251 | api Api 252 | 253 | params map[string]string 254 | } 255 | 256 | // Rules is the structure for security rules. 257 | type Rules map[string]interface{} 258 | 259 | // f is the internal implementation of the Firebase API client. 260 | type f struct{} 261 | 262 | var ( 263 | connectTimeout = time.Duration(30 * time.Second) // timeout for http connection 264 | readWriteTimeout = time.Duration(10 * time.Second) // timeout for http read/write 265 | ) 266 | 267 | // httpClient is the HTTP client used to make calls to Firebase with the default API 268 | var httpClient = newTimeoutClient(connectTimeout, readWriteTimeout) 269 | 270 | func NewClient(root, auth string, api Api) Client { 271 | if api == nil { 272 | api = new(f) 273 | } 274 | 275 | return &client{url: root, auth: auth, api: api} 276 | } 277 | 278 | func (c *client) String() string { 279 | return c.url 280 | } 281 | 282 | func (c *client) Key() string { 283 | matches := keyExtractor.FindAllStringSubmatch(c.url, 1) 284 | // This is kind of an error. There should always be a / somewhere, 285 | // but if you just have the raw domain you don't really need one. So 286 | // we assume this is the case and return "" 287 | if len(matches) == 0 { 288 | return "" 289 | } 290 | return matches[0][1] 291 | } 292 | 293 | func (c *client) Value(destination interface{}) error { 294 | err := c.api.Call("GET", c.url, c.auth, nil, c.params, destination) 295 | if err != nil { 296 | return err 297 | } 298 | return nil 299 | } 300 | 301 | func (c *client) Iterator(d Destination) <-chan *KeyedValue { 302 | if d == nil { 303 | d = func() interface{} { return &map[string]interface{}{} } 304 | } 305 | out := make(chan *KeyedValue) 306 | go func() { 307 | tree := avltree.NewObjectTree(0) 308 | unorderedVal := map[string]json.RawMessage{} 309 | // XXX: What do we do in case of error? 310 | c.Value(&unorderedVal) 311 | for key, _ := range unorderedVal { 312 | destination := d() 313 | json.Unmarshal(unorderedVal[key], destination) 314 | tree.Add(&KeyedValue{ 315 | Pair: avltree.Pair{ 316 | Key: key, 317 | Value: destination, 318 | }, 319 | OrderBy: c.Order, 320 | }) 321 | } 322 | for in := range tree.Iter() { 323 | out <- in.(*KeyedValue) 324 | } 325 | close(out) 326 | }() 327 | return out 328 | } 329 | 330 | func (c *client) Shallow() ([]string, error) { 331 | c.params = c.newParamMap("shallow", "true") 332 | ch := c.Iterator(nil) 333 | keySlice := []string{} 334 | for kv := range ch { 335 | keySlice = append(keySlice, kv.Key) 336 | } 337 | return keySlice, nil 338 | } 339 | 340 | func (c *client) Child(path string) Client { 341 | u := c.url + "/" + path 342 | return &client{ 343 | api: c.api, 344 | auth: c.auth, 345 | url: u, 346 | params: c.params, 347 | } 348 | } 349 | 350 | const ( 351 | KeyProp = "$key" 352 | ) 353 | 354 | // These are some shenanigans, golang. Shenanigans I say. 355 | func (c *client) newParamMap(key string, value interface{}) map[string]string { 356 | ret := make(map[string]string, len(c.params)+1) 357 | for key, value := range c.params { 358 | ret[key] = value 359 | } 360 | jsonVal, _ := json.Marshal(value) 361 | ret[key] = string(jsonVal) 362 | return ret 363 | } 364 | 365 | func (c *client) clientWithNewParam(key string, value interface{}) *client { 366 | return &client{ 367 | api: c.api, 368 | auth: c.auth, 369 | url: c.url, 370 | params: c.newParamMap(key, value), 371 | } 372 | } 373 | 374 | // Query functions. They map directly to the Firebase operations. 375 | // https://www.firebase.com/docs/rest/guide/retrieving-data.html#section-rest-queries 376 | func (c *client) OrderBy(prop string) Client { 377 | newC := c.clientWithNewParam("orderBy", prop) 378 | newC.Order = prop 379 | return newC 380 | } 381 | 382 | func (c *client) EqualTo(value string) Client { 383 | return c.clientWithNewParam("equalTo", value) 384 | } 385 | 386 | func (c *client) StartAt(value string) Client { 387 | return c.clientWithNewParam("startAt", value) 388 | } 389 | 390 | func (c *client) EndAt(value string) Client { 391 | return c.clientWithNewParam("endAt", value) 392 | } 393 | 394 | func (c *client) LimitToFirst(value int) Client { 395 | return c.clientWithNewParam("limitToFirst", value) 396 | } 397 | 398 | func (c *client) LimitToLast(value int) Client { 399 | return c.clientWithNewParam("limitToLast", value) 400 | } 401 | 402 | func (c *client) Push(value interface{}, params map[string]string) (Client, error) { 403 | res := map[string]string{} 404 | err := c.api.Call("POST", c.url, c.auth, value, params, &res) 405 | if err != nil { 406 | return nil, err 407 | } 408 | 409 | return &client{ 410 | api: c.api, 411 | auth: c.auth, 412 | url: c.url + "/" + res["name"], 413 | params: c.params, 414 | }, nil 415 | } 416 | 417 | func (c *client) Set(path string, value interface{}, params map[string]string) (Client, error) { 418 | u := c.url + "/" + path 419 | 420 | err := c.api.Call("PUT", u, c.auth, value, params, nil) 421 | if err != nil { 422 | return nil, err 423 | } 424 | 425 | return &client{ 426 | api: c.api, 427 | auth: c.auth, 428 | url: u, 429 | params: c.params, 430 | }, nil 431 | } 432 | 433 | func (c *client) Update(path string, value interface{}, params map[string]string) error { 434 | err := c.api.Call("PATCH", c.url+"/"+path, c.auth, value, params, nil) 435 | return err 436 | } 437 | 438 | func (c *client) Remove(path string, params map[string]string) error { 439 | err := c.api.Call("DELETE", c.url+"/"+path, c.auth, nil, params, nil) 440 | 441 | return err 442 | } 443 | 444 | func (c *client) Rules(params map[string]string) (*Rules, error) { 445 | res := &Rules{} 446 | err := c.api.Call("GET", c.url+"/.settings/rules", c.auth, nil, params, res) 447 | if err != nil { 448 | return nil, err 449 | } 450 | 451 | return res, nil 452 | } 453 | 454 | func (c *client) SetRules(rules *Rules, params map[string]string) error { 455 | err := c.api.Call("PUT", c.url+"/.settings/rules", c.auth, rules, params, nil) 456 | 457 | return err 458 | } 459 | 460 | // Call invokes the appropriate HTTP method on a given Firebase URL. 461 | func (f *f) Call(method, path, auth string, body interface{}, params map[string]string, dest interface{}) error { 462 | 463 | // Every path needs to end in .json for the Firebase REST API 464 | path += ".json" 465 | qs := url.Values{} 466 | 467 | // if the client has an auth, set it as a query string. 468 | // the caller can also override this on a per-call basis 469 | // which will happen via params below 470 | if len(auth) > 0 { 471 | qs.Set("auth", auth) 472 | } 473 | 474 | for k, v := range params { 475 | qs.Set(k, v) 476 | } 477 | 478 | if len(qs) > 0 { 479 | path += "?" + qs.Encode() 480 | } 481 | 482 | encodedBody, err := json.Marshal(body) 483 | if err != nil { 484 | return err 485 | } 486 | 487 | req, err := http.NewRequest(method, path, bytes.NewReader(encodedBody)) 488 | if err != nil { 489 | return err 490 | } 491 | 492 | req.Close = true 493 | 494 | res, err := httpClient.Do(req) 495 | if err != nil { 496 | return err 497 | } 498 | defer res.Body.Close() 499 | 500 | decoder := json.NewDecoder(res.Body) 501 | if res.StatusCode >= 400 { 502 | err := &FirebaseError{} 503 | decoder.Decode(err) 504 | return err 505 | } 506 | 507 | if dest != nil && res.ContentLength != 0 { 508 | err = decoder.Decode(dest) 509 | if err != nil { 510 | return err 511 | } 512 | } 513 | 514 | return nil 515 | } 516 | 517 | func newTimeoutClient(connectTimeout time.Duration, readWriteTimeout time.Duration) *http.Client { 518 | return &http.Client{ 519 | Transport: &httpcontrol.Transport{ 520 | RequestTimeout: readWriteTimeout, 521 | DialTimeout: connectTimeout, 522 | MaxTries: 3, 523 | MaxIdleConnsPerHost: 200, 524 | }, 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /firebase_test.go: -------------------------------------------------------------------------------- 1 | package firebase_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/JustinTulloss/firebase" 12 | "github.com/JustinTulloss/firebase/mock_firebase" 13 | "github.com/golang/mock/gomock" 14 | ) 15 | 16 | type Name struct { 17 | First string `json:",omitempty"` 18 | Last string `json:",omitempty"` 19 | } 20 | 21 | func nameAlloc() interface{} { 22 | return &Name{} 23 | } 24 | 25 | /* 26 | Set the two variables below and set them to your own 27 | Firebase URL and credentials (optional) if you're forking the code 28 | and want to test your changes. 29 | */ 30 | 31 | // enter your firebase credentials for testing. 32 | var ( 33 | testUrl string = os.Getenv("FIREBASE_TEST_URL") 34 | testAuth string = os.Getenv("FIREBASE_TEST_AUTH") 35 | ) 36 | 37 | func TestValue(t *testing.T) { 38 | client := firebase.NewClient(testUrl+"/tests", testAuth, nil) 39 | 40 | var r map[string]interface{} 41 | err := client.Value(&r) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | 46 | if r == nil { 47 | t.Fatalf("No values returned from the server\n") 48 | } 49 | } 50 | 51 | func TestChild(t *testing.T) { 52 | client := firebase.NewClient(testUrl+"/tests", testAuth, nil) 53 | 54 | r := client.Child("") 55 | 56 | if r == nil { 57 | t.Fatalf("No child returned from the server\n") 58 | } 59 | } 60 | 61 | func TestPush(t *testing.T) { 62 | client := firebase.NewClient(testUrl+"/tests", testAuth, nil) 63 | 64 | name := &Name{First: "FirstName", Last: "LastName"} 65 | 66 | r, err := client.Push(name, nil) 67 | 68 | if err != nil { 69 | t.Fatalf("%v\n", err) 70 | } 71 | 72 | if r == nil { 73 | t.Fatalf("No client returned from the server\n") 74 | } 75 | 76 | newName := &Name{} 77 | c2 := firebase.NewClient(r.String(), testAuth, nil) 78 | c2.Value(newName) 79 | if !reflect.DeepEqual(name, newName) { 80 | t.Errorf("Expected %v to equal %v", name, newName) 81 | } 82 | } 83 | 84 | func TestSet(t *testing.T) { 85 | c1 := firebase.NewClient(testUrl+"/tests/users", testAuth, nil) 86 | 87 | name := &Name{First: "First", Last: "last"} 88 | c2, _ := c1.Push(name, nil) 89 | 90 | newName := &Name{First: "NewFirst", Last: "NewLast"} 91 | r, err := c2.Set("", newName, map[string]string{"print": "silent"}) 92 | 93 | if err != nil { 94 | t.Fatalf("%v\n", err) 95 | } 96 | 97 | if r == nil { 98 | t.Fatalf("No client returned from the server\n") 99 | } 100 | } 101 | 102 | func TestUpdate(t *testing.T) { 103 | c1 := firebase.NewClient(testUrl+"/tests/users", testAuth, nil) 104 | 105 | name := &Name{First: "First", Last: "last"} 106 | c2, _ := c1.Push(name, nil) 107 | 108 | newName := &Name{Last: "NewLast"} 109 | err := c2.Update("", newName, nil) 110 | 111 | if err != nil { 112 | t.Fatalf("%v\n", err) 113 | } 114 | } 115 | 116 | func TestRemovet(t *testing.T) { 117 | c1 := firebase.NewClient(testUrl+"/tests/users", testAuth, nil) 118 | 119 | name := &Name{First: "First", Last: "last"} 120 | c2, _ := c1.Push(name, nil) 121 | 122 | err := c2.Remove("", nil) 123 | if err != nil { 124 | t.Fatalf("%v\n", err) 125 | } 126 | 127 | var val map[string]interface{} 128 | c3 := firebase.NewClient(c2.String(), testAuth, nil) 129 | err = c3.Value(&val) 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | 134 | if len(val) != 0 { 135 | t.Errorf("Expected %s to be removed, was %v", c2.String(), val) 136 | } 137 | } 138 | 139 | func TestRules(t *testing.T) { 140 | client := firebase.NewClient(testUrl, testAuth, nil) 141 | 142 | r, err := client.Rules(nil) 143 | 144 | if err != nil { 145 | t.Fatalf("Error retrieving rules: %v\n", err) 146 | } 147 | 148 | if r == nil { 149 | t.Fatalf("No child returned from the server\n") 150 | } 151 | } 152 | 153 | func TestSetRules(t *testing.T) { 154 | client := firebase.NewClient(testUrl, testAuth, nil) 155 | 156 | rules := &firebase.Rules{ 157 | "rules": map[string]interface{}{ 158 | ".read": "auth.username == 'admin'", 159 | ".write": "auth.username == 'admin'", 160 | "ordered": map[string]interface{}{ 161 | ".indexOn": []string{"First"}, 162 | "kids": map[string]interface{}{ 163 | ".indexOn": []string{"Age"}, 164 | }, 165 | }, 166 | }, 167 | } 168 | 169 | err := client.SetRules(rules, nil) 170 | 171 | if err != nil { 172 | t.Fatalf("Error setting rules: %v\n", err) 173 | } 174 | } 175 | 176 | func TestOrderBy(t *testing.T) { 177 | client := firebase.NewClient(testUrl, testAuth, nil).Child("ordered") 178 | defer client.Remove("", nil) 179 | 180 | names := []*Name{ 181 | &Name{First: "BBBB", Last: "YYYY"}, 182 | &Name{First: "AAAA", Last: "ZZZZZ"}, 183 | } 184 | 185 | for _, n := range names { 186 | _, err := client.Push(n, nil) 187 | if err != nil { 188 | t.Fatalf("Couldn't push new name: %s\n", err) 189 | } 190 | } 191 | 192 | i := 0 193 | for n := range client.OrderBy(firebase.KeyProp).Iterator(nameAlloc) { 194 | if n.Value.(*Name).First != names[i].First { 195 | t.Fatalf("Key order was not delivered") 196 | } 197 | i++ 198 | } 199 | if i == 0 { 200 | t.Fatalf("Did not receive names ordered by key") 201 | } 202 | 203 | expectedOrder := []*Name{names[1], names[0]} 204 | i = 0 205 | for n := range client.OrderBy("First").Iterator(nameAlloc) { 206 | if n.Value.(*Name).First != expectedOrder[i].First { 207 | t.Fatalf("Child prop order was not delivered") 208 | } 209 | i++ 210 | } 211 | if i == 0 { 212 | t.Fatalf("Did not receive names ordered by first name") 213 | } 214 | 215 | kids := map[string]map[string]interface{}{ 216 | "a": map[string]interface{}{"Name": "Bob", "Age": 14}, 217 | "b": map[string]interface{}{"Name": "Alice", "Age": 13}, 218 | } 219 | _, err := client.Set("kids", kids, nil) 220 | if err != nil { 221 | t.Fatalf("Could not set kids: %s\n", err) 222 | } 223 | 224 | expectedKidsOrder := []map[string]interface{}{kids["b"], kids["a"]} 225 | i = 0 226 | for n := range client.Child("kids").OrderBy("Age").Iterator(nil) { 227 | if (*n.Value.(*map[string]interface{}))["First"] != expectedKidsOrder[i]["First"] { 228 | t.Fatalf("Child prop order by age was not delivered") 229 | } 230 | i++ 231 | } 232 | if i == 0 { 233 | t.Fatalf("Did not receive names ordered by age") 234 | } 235 | } 236 | 237 | func TestTimestamp(t *testing.T) { 238 | ts := firebase.Timestamp(time.Now()) 239 | marshaled, err := json.Marshal(&ts) 240 | if err != nil { 241 | t.Fatalf("Could not marshal a timestamp to json: %s\n", err) 242 | } 243 | unmarshaledTs := firebase.Timestamp{} 244 | err = json.Unmarshal(marshaled, &unmarshaledTs) 245 | if err != nil { 246 | t.Fatalf("Could not unmarshal a timestamp to json: %s\n", err) 247 | } 248 | // Compare unix timestamps as we lose some fidelity in the nanoseconds 249 | if time.Time(ts).Unix() != time.Time(unmarshaledTs).Unix() { 250 | t.Fatalf("Unmarshaled time %s not equivalent to marshaled time %s", 251 | unmarshaledTs, 252 | ts, 253 | ) 254 | } 255 | } 256 | 257 | func TestServerTimestamp(t *testing.T) { 258 | b, err := json.Marshal(firebase.ServerTimestamp) 259 | if err != nil { 260 | t.Fatalf("Could not marshal server timestamp: %s\n", err) 261 | } 262 | if string(b) != `{".sv":"timestamp"}` { 263 | t.Fatalf("Unexpected timestamp json value: %s\n", b) 264 | } 265 | } 266 | 267 | func TestIterator(t *testing.T) { 268 | client := firebase.NewClient(testUrl+"/test-iterator", testAuth, nil) 269 | defer client.Remove("", nil) 270 | names := []Name{ 271 | {First: "FirstName", Last: "LastName"}, 272 | {First: "Second", Last: "Seconder"}, 273 | } 274 | for _, name := range names { 275 | _, err := client.Push(name, nil) 276 | if err != nil { 277 | t.Fatalf("%v\n", err) 278 | } 279 | } 280 | 281 | var i = 0 282 | for nameEntry := range client.Iterator(nameAlloc) { 283 | name := nameEntry.Value.(*Name) 284 | if !reflect.DeepEqual(&names[i], name) { 285 | t.Errorf("Expected %v to equal %v", &names[i], name) 286 | } 287 | i++ 288 | } 289 | if i != len(names) { 290 | t.Fatalf("Did not receive all names, received %d\n", i) 291 | } 292 | } 293 | 294 | func TestShallow(t *testing.T) { 295 | client := firebase.NewClient(testUrl+"/test-shallow", testAuth, nil) 296 | defer client.Remove("", nil) 297 | client.Set("one", 1, nil) 298 | client.Set("two", 2, nil) 299 | keys, err := client.Shallow() 300 | if err != nil { 301 | t.Errorf("Error when calling shallow: %s\n", err) 302 | } 303 | if !reflect.DeepEqual(keys, []string{"one", "two"}) { 304 | t.Errorf("keys (%v) were not correct\n", keys) 305 | } 306 | } 307 | 308 | func TestKey(t *testing.T) { 309 | client := firebase.NewClient(testUrl+"/test", testAuth, nil) 310 | if client.Key() != "test" { 311 | t.Errorf("Key should have been test, was %s\n", client.Key()) 312 | } 313 | 314 | client = firebase.NewClient(testUrl, testAuth, nil) 315 | if client.Key() != "" { 316 | t.Errorf("Key should have been empty, was %s\n", client.Key()) 317 | } 318 | 319 | client = client.Child("/a/b/c/d/e/f/g") 320 | if client.Key() != "g" { 321 | t.Errorf("Key should have been 'g', was %s\n", client.Key()) 322 | } 323 | } 324 | 325 | func TestMockable(t *testing.T) { 326 | mockCtrl := gomock.NewController(t) 327 | defer mockCtrl.Finish() 328 | 329 | mockFire := mock_firebase.NewMockClient(mockCtrl) 330 | mockFire.EXPECT().Child("test") 331 | mockFire.Child("test") 332 | } 333 | 334 | func TestMain(m *testing.M) { 335 | if testUrl == "" || testAuth == "" { 336 | fmt.Printf("You need to set FIREBASE_TEST_URL and FIREBASE_TEST_AUTH\n") 337 | os.Exit(1) 338 | } 339 | os.Exit(m.Run()) 340 | } 341 | -------------------------------------------------------------------------------- /mock_firebase/firebase.go: -------------------------------------------------------------------------------- 1 | // Automatically generated by MockGen. DO NOT EDIT! 2 | // Source: firebase.go 3 | 4 | package mock_firebase 5 | 6 | import ( 7 | . "github.com/JustinTulloss/firebase" 8 | gomock "github.com/golang/mock/gomock" 9 | ) 10 | 11 | // Mock of Api interface 12 | type MockApi struct { 13 | ctrl *gomock.Controller 14 | recorder *_MockApiRecorder 15 | } 16 | 17 | // Recorder for MockApi (not exported) 18 | type _MockApiRecorder struct { 19 | mock *MockApi 20 | } 21 | 22 | func NewMockApi(ctrl *gomock.Controller) *MockApi { 23 | mock := &MockApi{ctrl: ctrl} 24 | mock.recorder = &_MockApiRecorder{mock} 25 | return mock 26 | } 27 | 28 | func (_m *MockApi) EXPECT() *_MockApiRecorder { 29 | return _m.recorder 30 | } 31 | 32 | func (_m *MockApi) Call(method string, path string, auth string, body interface{}, params map[string]string, dest interface{}) error { 33 | ret := _m.ctrl.Call(_m, "Call", method, path, auth, body, params, dest) 34 | ret0, _ := ret[0].(error) 35 | return ret0 36 | } 37 | 38 | func (_mr *_MockApiRecorder) Call(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { 39 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Call", arg0, arg1, arg2, arg3, arg4, arg5) 40 | } 41 | 42 | // Mock of Client interface 43 | type MockClient struct { 44 | ctrl *gomock.Controller 45 | recorder *_MockClientRecorder 46 | } 47 | 48 | // Recorder for MockClient (not exported) 49 | type _MockClientRecorder struct { 50 | mock *MockClient 51 | } 52 | 53 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 54 | mock := &MockClient{ctrl: ctrl} 55 | mock.recorder = &_MockClientRecorder{mock} 56 | return mock 57 | } 58 | 59 | func (_m *MockClient) EXPECT() *_MockClientRecorder { 60 | return _m.recorder 61 | } 62 | 63 | func (_m *MockClient) String() string { 64 | ret := _m.ctrl.Call(_m, "String") 65 | ret0, _ := ret[0].(string) 66 | return ret0 67 | } 68 | 69 | func (_mr *_MockClientRecorder) String() *gomock.Call { 70 | return _mr.mock.ctrl.RecordCall(_mr.mock, "String") 71 | } 72 | 73 | func (_m *MockClient) Key() string { 74 | ret := _m.ctrl.Call(_m, "Key") 75 | ret0, _ := ret[0].(string) 76 | return ret0 77 | } 78 | 79 | func (_mr *_MockClientRecorder) Key() *gomock.Call { 80 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Key") 81 | } 82 | 83 | func (_m *MockClient) Value(destination interface{}) error { 84 | ret := _m.ctrl.Call(_m, "Value", destination) 85 | ret0, _ := ret[0].(error) 86 | return ret0 87 | } 88 | 89 | func (_mr *_MockClientRecorder) Value(arg0 interface{}) *gomock.Call { 90 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Value", arg0) 91 | } 92 | 93 | func (_m *MockClient) Iterator(d Destination) <-chan *KeyedValue { 94 | ret := _m.ctrl.Call(_m, "Iterator", d) 95 | ret0, _ := ret[0].(<-chan *KeyedValue) 96 | return ret0 97 | } 98 | 99 | func (_mr *_MockClientRecorder) Iterator(arg0 interface{}) *gomock.Call { 100 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Iterator", arg0) 101 | } 102 | 103 | func (_m *MockClient) Shallow() ([]string, error) { 104 | ret := _m.ctrl.Call(_m, "Shallow") 105 | ret0, _ := ret[0].([]string) 106 | ret1, _ := ret[1].(error) 107 | return ret0, ret1 108 | } 109 | 110 | func (_mr *_MockClientRecorder) Shallow() *gomock.Call { 111 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Shallow") 112 | } 113 | 114 | func (_m *MockClient) Child(path string) Client { 115 | ret := _m.ctrl.Call(_m, "Child", path) 116 | ret0, _ := ret[0].(Client) 117 | return ret0 118 | } 119 | 120 | func (_mr *_MockClientRecorder) Child(arg0 interface{}) *gomock.Call { 121 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Child", arg0) 122 | } 123 | 124 | func (_m *MockClient) OrderBy(prop string) Client { 125 | ret := _m.ctrl.Call(_m, "OrderBy", prop) 126 | ret0, _ := ret[0].(Client) 127 | return ret0 128 | } 129 | 130 | func (_mr *_MockClientRecorder) OrderBy(arg0 interface{}) *gomock.Call { 131 | return _mr.mock.ctrl.RecordCall(_mr.mock, "OrderBy", arg0) 132 | } 133 | 134 | func (_m *MockClient) EqualTo(value string) Client { 135 | ret := _m.ctrl.Call(_m, "EqualTo", value) 136 | ret0, _ := ret[0].(Client) 137 | return ret0 138 | } 139 | 140 | func (_mr *_MockClientRecorder) EqualTo(arg0 interface{}) *gomock.Call { 141 | return _mr.mock.ctrl.RecordCall(_mr.mock, "EqualTo", arg0) 142 | } 143 | 144 | func (_m *MockClient) StartAt(value string) Client { 145 | ret := _m.ctrl.Call(_m, "StartAt", value) 146 | ret0, _ := ret[0].(Client) 147 | return ret0 148 | } 149 | 150 | func (_mr *_MockClientRecorder) StartAt(arg0 interface{}) *gomock.Call { 151 | return _mr.mock.ctrl.RecordCall(_mr.mock, "StartAt", arg0) 152 | } 153 | 154 | func (_m *MockClient) EndAt(value string) Client { 155 | ret := _m.ctrl.Call(_m, "EndAt", value) 156 | ret0, _ := ret[0].(Client) 157 | return ret0 158 | } 159 | 160 | func (_mr *_MockClientRecorder) EndAt(arg0 interface{}) *gomock.Call { 161 | return _mr.mock.ctrl.RecordCall(_mr.mock, "EndAt", arg0) 162 | } 163 | 164 | func (_m *MockClient) LimitToFirst(value int) Client { 165 | ret := _m.ctrl.Call(_m, "LimitToFirst", value) 166 | ret0, _ := ret[0].(Client) 167 | return ret0 168 | } 169 | 170 | func (_mr *_MockClientRecorder) LimitToFirst(arg0 interface{}) *gomock.Call { 171 | return _mr.mock.ctrl.RecordCall(_mr.mock, "LimitToFirst", arg0) 172 | } 173 | 174 | func (_m *MockClient) LimitToLast(value int) Client { 175 | ret := _m.ctrl.Call(_m, "LimitToLast", value) 176 | ret0, _ := ret[0].(Client) 177 | return ret0 178 | } 179 | 180 | func (_mr *_MockClientRecorder) LimitToLast(arg0 interface{}) *gomock.Call { 181 | return _mr.mock.ctrl.RecordCall(_mr.mock, "LimitToLast", arg0) 182 | } 183 | 184 | func (_m *MockClient) Push(value interface{}, params map[string]string) (Client, error) { 185 | ret := _m.ctrl.Call(_m, "Push", value, params) 186 | ret0, _ := ret[0].(Client) 187 | ret1, _ := ret[1].(error) 188 | return ret0, ret1 189 | } 190 | 191 | func (_mr *_MockClientRecorder) Push(arg0, arg1 interface{}) *gomock.Call { 192 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Push", arg0, arg1) 193 | } 194 | 195 | func (_m *MockClient) Set(path string, value interface{}, params map[string]string) (Client, error) { 196 | ret := _m.ctrl.Call(_m, "Set", path, value, params) 197 | ret0, _ := ret[0].(Client) 198 | ret1, _ := ret[1].(error) 199 | return ret0, ret1 200 | } 201 | 202 | func (_mr *_MockClientRecorder) Set(arg0, arg1, arg2 interface{}) *gomock.Call { 203 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Set", arg0, arg1, arg2) 204 | } 205 | 206 | func (_m *MockClient) Update(path string, value interface{}, params map[string]string) error { 207 | ret := _m.ctrl.Call(_m, "Update", path, value, params) 208 | ret0, _ := ret[0].(error) 209 | return ret0 210 | } 211 | 212 | func (_mr *_MockClientRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call { 213 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Update", arg0, arg1, arg2) 214 | } 215 | 216 | func (_m *MockClient) Remove(path string, params map[string]string) error { 217 | ret := _m.ctrl.Call(_m, "Remove", path, params) 218 | ret0, _ := ret[0].(error) 219 | return ret0 220 | } 221 | 222 | func (_mr *_MockClientRecorder) Remove(arg0, arg1 interface{}) *gomock.Call { 223 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Remove", arg0, arg1) 224 | } 225 | 226 | func (_m *MockClient) Rules(params map[string]string) (*Rules, error) { 227 | ret := _m.ctrl.Call(_m, "Rules", params) 228 | ret0, _ := ret[0].(*Rules) 229 | ret1, _ := ret[1].(error) 230 | return ret0, ret1 231 | } 232 | 233 | func (_mr *_MockClientRecorder) Rules(arg0 interface{}) *gomock.Call { 234 | return _mr.mock.ctrl.RecordCall(_mr.mock, "Rules", arg0) 235 | } 236 | 237 | func (_m *MockClient) SetRules(rules *Rules, params map[string]string) error { 238 | ret := _m.ctrl.Call(_m, "SetRules", rules, params) 239 | ret0, _ := ret[0].(error) 240 | return ret0 241 | } 242 | 243 | func (_mr *_MockClientRecorder) SetRules(arg0, arg1 interface{}) *gomock.Call { 244 | return _mr.mock.ctrl.RecordCall(_mr.mock, "SetRules", arg0, arg1) 245 | } 246 | --------------------------------------------------------------------------------