├── .gitignore ├── .jshintrc ├── .travis.yml ├── API.md ├── CONTRIBUTING.md ├── MAINTAINING.md ├── README.md ├── bower.json ├── browser └── mockfirebase.js ├── gulpfile.js ├── helpers ├── globals.js └── header.txt ├── package.json ├── src ├── auth.js ├── firebase.js ├── index.js ├── login.js ├── query.js ├── queue.js ├── slice.js ├── snapshot.js ├── utils.js └── validators.js ├── test ├── .jshintrc ├── smoke │ └── globals.js └── unit │ ├── auth.js │ ├── data.json │ ├── firebase.js │ ├── login.js │ ├── query.js │ ├── queue.js │ └── snapshot.js └── tutorials ├── authentication.md ├── basic.md ├── errors.md ├── override.md └── proxyquire.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | node_modules 3 | coverage 4 | browser 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '0.10' 5 | - '0.12' 6 | - iojs 7 | before_script: 8 | - gulp lint 9 | script: 10 | - gulp test 11 | - gulp karma 12 | - gulp smoke 13 | deploy: 14 | provider: npm 15 | email: katowulf@gmail.com 16 | api_key: 17 | secure: dmXXpp8A8eZdBQpk56y6ZNLTI1paJWUderck7aCb7FIc9x4+Gpk5uaZZlDxKE12d4Qye3yZKcj5RJ2V0PiwV0Rk8w3vgJkuBOQZ0P+0Wr4deRSPIQpHfFoOutbfCSu6gCZPDlAZIH0jadykIc5l/nwIxPtoXy8xkGx6aIWC5iLM= 18 | on: 19 | tags: true 20 | repo: katowulf/mockfirebase 21 | all_branches: true 22 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | Only `MockFirebase` methods are included here. For details on normal Firebase API methods, consult the [Firebase Web API documentation](https://www.firebase.com/docs/web/api/). 4 | 5 | - [Core](#core) 6 | - [`flush([delay])`](#flushdelay---ref) 7 | - [`autoFlush([delay])`](#autoflushdelaysetting---ref) 8 | - [`failNext(method, err)`](#failnextmethod-err---undefined) 9 | - [`forceCancel(err [, event] [, callback] [, context]`)](#forcecancelerr--event--callback--context---undefined) 10 | - [`getData()`](#getdata---any) 11 | - [`getKeys()`](#getkeys---array) 12 | - [`fakeEvent(event [, key] [, data] [, previousChild] [, priority])`](#fakeeventevent--key--data--previouschild--priority---ref) 13 | - [`getFlushQueue()`](#getflushqueue---array) 14 | - [Auth](#auth) 15 | - [`changeAuthState(user)`](#changeauthstateauthdata---undefined) 16 | - [`getEmailUser(email)`](#getemailuseremail---objectnull) 17 | - [Server Timestamps](#server-timestamps) 18 | - [`setClock(fn)`](#firebasesetclockfn---undefined) 19 | - [`restoreClock()`](#firebasesetclockfn---undefined) 20 | 21 | ## Core 22 | 23 | Core methods of `MockFirebase` references for manipulating data and asynchronous behavior. 24 | 25 | ##### `flush([delay])` -> `ref` 26 | 27 | Flushes the queue of deferred data and authentication operations. If a `delay` is passed, the flush operation will be triggered after the specified number of milliseconds. 28 | 29 | In MockFirebase, data operations can be executed synchronously. When calling any Firebase API method that reads or writes data (e.g. `set(data)` or `on('value')`), MockFirebase will queue the operation. You can call multiple data methods in a row before flushing. MockFirebase will execute them in the order they were called when `flush` is called. If you trigger an operation inside of another (e.g. writing data somewhere when you detect a data change using `on`), all changes will be performed during the same `flush`. 30 | 31 | `flush` will throw an exception if the queue of deferred operations is empty. 32 | 33 | Example: 34 | 35 | ```js 36 | ref.set({ 37 | foo: 'bar' 38 | }); 39 | console.assert(ref.getData() === null, 'ref does not have data'); 40 | ref.flush(); 41 | console.assert(ref.getData().foo === 'bar', 'ref has data'); 42 | ``` 43 | 44 |
45 | 46 | ##### `autoFlush([delay|setting])` -> `ref` 47 | 48 | Configures the Firebase reference to automatically flush data and authentication operations when run. If no arguments or `true` are passed, the operations will be flushed immediately (synchronously). If a `delay` is provided, the operations will be flushed after the specified number of milliseconds. If `false` is provided, `autoFlush` will be disabled. 49 | 50 |
51 | 52 | ##### `failNext(method, err)` -> `undefined` 53 | 54 | When `method` is next invoked, trigger the `onComplete` callback with the specified `err`. This is useful for simulating validation, authorization, or any other errors. The callback will be triggered with the next `flush`. 55 | 56 | `err` must be a proper `Error` object and not a string or any other primitive. 57 | 58 | Example: 59 | 60 | ```js 61 | var error = new Error('Oh no!'); 62 | ref.failNext('set', error); 63 | var err; 64 | ref.set('data', function onComplete (_err_) { 65 | err = _err_; 66 | }); 67 | console.assert(typeof err === 'undefined', 'no err'); 68 | ref.flush(); 69 | console.assert(err === error, 'err passed to callback'); 70 | ``` 71 | 72 |
73 | 74 | ##### `forceCancel(err [, event] [, callback] [, context]` -> `undefined` 75 | 76 | Simulate a security error by cancelling listeners (callbacks registered with `on`) at the path with the specified `err`. If an optional `event`, `callback`, and `context` are provided, only listeners that match will be cancelled. `forceCancel` will also invoke `off` for the matched listeners so they will be no longer notified of any future changes. Cancellation is triggered immediately and not with a `flush` call. 77 | 78 | Example: 79 | 80 | ```js 81 | var error = new Error(); 82 | function onValue (snapshot) {} 83 | function onCancel (_err_) { 84 | err = _err_; 85 | } 86 | ref.on('value', onValue, onCancel); 87 | ref.flush(); 88 | ref.forceCancel(error, 'value', onValue); 89 | console.assert(err === error, 'err passed to onCancel'); 90 | ``` 91 | 92 |
93 | 94 | ##### `getData()` -> `Any` 95 | 96 | Returns a copy of the data as it exists at the time. Any writes must be triggered with `flush` before `getData` will reflect their results. 97 | 98 |
99 | 100 | ##### `getKeys()` -> `Array` 101 | 102 | Returns an array of the keys at the path as they are ordered in Firebase. 103 | 104 |
105 | 106 | ##### `fakeEvent(event [, key] [, data] [, previousChild] [, priority])` -> `ref` 107 | 108 | Triggers a fake event that is not connected to an actual change to Firebase data. A child `key` is required unless the event is a `'value'` event. 109 | 110 | Example: 111 | 112 | ```js 113 | var snapshot; 114 | function onValue (_snapshot_) { 115 | snapshot = _snapshot_; 116 | } 117 | ref.on('value', onValue); 118 | ref.set({ 119 | foo: 'bar'; 120 | }); 121 | ref.flush(); 122 | console.assert(ref.getData().foo === 'bar', 'data has foo'); 123 | ref.fakeEvent('value', undefined, null); 124 | ref.flush(); 125 | console.assert(ref.getData() === null, 'data is null'); 126 | ``` 127 | 128 |
129 | 130 | ##### `getFlushQueue()` -> `Array` 131 | 132 | Returns a list of all the `event` objects queued to be run the next time `ref.flush` is invoked. 133 | These items can be manipulated manually by calling `event.run` or `event.cancel`. Each contains 134 | a `sourceMethod` and `sourceArguments` attribute that can be used to identify specific 135 | calls to a MockFirebase method. 136 | 137 | This is a copy of the internal array and represents the state of the flush queue at the time `getFlushQueue` is called. 138 | 139 | Example: 140 | 141 | ```js 142 | // create some child_added events 143 | var ref = new MockFirebase('OutOfOrderFlushEvents://'); 144 | 145 | var child1 = ref.push('foo'); 146 | var child2 = ref.push('bar'); 147 | var child3 = ref.push('baz'); 148 | var events = ref.getFlushQueue(); 149 | 150 | var sourceData = events[0].sourceData; 151 | console.assert(sourceData.ref === child2, 'first event is for child1'); 152 | console.assert(sourceData.method, 'first event is a push'); 153 | console.assert(sourceData.args[0], 'push was called with "bar"'); 154 | 155 | ref.on('child_added', function (snap, prevChild) { 156 | console.log('added ' + snap.val() + ' after ' + prevChild); 157 | }); 158 | 159 | // cancel the second push so it never triggers a event 160 | events[1].cancel(); 161 | // trigger the third push before the first 162 | events[2].run(); // added baz after bar 163 | // now flush the remainder of the queue normally 164 | ref.flush(); // added foo after null 165 | ``` 166 | 167 | ## Auth 168 | 169 | Authentication methods for simulating changes to the auth state of a Firebase reference. 170 | 171 | ##### `changeAuthState(authData)` -> `undefined` 172 | 173 | Changes the active authentication credentials to the `authData` object. Before changing the authentication state, `changeAuthState` checks whether the `authData` object is deeply equal to the current authentication data. `onAuth` listeners will only be triggered if the data is not deeply equal. To simulate no user being authenticated, pass `null` for `authData`. This operation is queued until the next `flush`. 174 | 175 | `authData` should adhere to the [documented schema](https://www.firebase.com/docs/web/api/firebase/onauth.html). 176 | 177 | Example: 178 | 179 | ```js 180 | ref.changeAuthState({ 181 | uid: 'theUid', 182 | provider: 'github', 183 | token: 'theToken', 184 | expires: Math.floor(new Date() / 1000) + 24 * 60 * 60, // expire in 24 hours 185 | auth: { 186 | myAuthProperty: true 187 | } 188 | }); 189 | ref.flush(); 190 | console.assert(ref.getAuth().auth.myAuthProperty, 'authData has custom property'); 191 | ``` 192 | 193 |
194 | 195 | ##### `getEmailUser(email)` -> `Object|null` 196 | 197 | Finds a user previously created with [`createUser`](https://www.firebase.com/docs/web/api/firebase/createuser.html). If no user was created with the specified `email`, `null` is returned instead. 198 | 199 | ## Server Timestamps 200 | 201 | MockFirebase allow you to simulate the behavior of [server timestamps](https://www.firebase.com/docs/web/api/servervalue/timestamp.html) when using a real Firebase instance. Unless you use `Firebase.setClock`, `Firebase.ServerValue.TIMESTAMP` will be transformed to the current date (`Date.now()`) when your data change is flushed. 202 | 203 | ##### `Firebase.setClock(fn)` -> `undefined` 204 | 205 | Instead of using `Date.now()`, MockFirebase will call the `fn` you provide to generate a timestamp. `fn` should return a number. 206 | 207 |
208 | 209 | ##### `Firebase.restoreClock()` -> `undefined` 210 | 211 | After calling `Firebase.setClock`, calling `Firebase.restoreClock` will restore the default timestamp behavior. 212 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issues 2 | 3 | If you've encountered a bug or want to request new functionality, go ahead and file an issue. Please provide as much detail as possible, including a live example, reproduction steps, and debugging steps you've already taken where possible. 4 | 5 | # Contributing 6 | 7 | * Fork the repo 8 | * `npm install` 9 | * make your additions in `./src` 10 | * Do not edit the files in `./browser`. They are built automatically for releases. [@bendrucker](https://github.com/bendrucker) will have to spend time rebasing your PR and that makes him :cry:. 11 | * add test units in `./test` 12 | * `npm test` 13 | * submit a pull request when all tests pass 14 | -------------------------------------------------------------------------------- /MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Call `gulp release` to release a new patch version. For *minor* or *major* releases, use the `--type` flag: 4 | 5 | ```bash 6 | $ gulp release --type minor 7 | ``` 8 | 9 | To push the release commit and tag: 10 | 11 | ```bash 12 | $ git push --follow-tags 13 | ``` 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MockFirebase [![Build Status](https://travis-ci.org/katowulf/mockfirebase.svg?branch=master)](https://travis-ci.org/katowulf/mockfirebase) 2 | ============ 3 | 4 | This worked back in the days of Firebase 1.X. These days, you should use the [Firebase Emulator suite](https://firebase.google.com/docs/emulator-suite). 5 | 6 | *This was an experimental library and is not supported by Firebase* 7 | 8 | ## Setup 9 | 10 | ### Node/Browserify 11 | 12 | ```bash 13 | $ npm install mockfirebase 14 | ``` 15 | 16 | ```js 17 | var MockFirebase = require('mockfirebase').MockFirebase; 18 | ``` 19 | 20 | ### AMD/Browser 21 | 22 | ```bash 23 | $ bower install mockfirebase 24 | ``` 25 | 26 | ```html 27 | 28 | ``` 29 | 30 | ## API 31 | 32 | MockFirebase supports the normal [Firebase API](https://www.firebase.com/docs/web/api/) plus a small set of utility methods documented fully in the [API Reference](API.md). Rather than make a server call that is actually asynchronous, MockFirebase allow you to either trigger callbacks synchronously or asynchronously with a specified delay ([`ref.flush`](API.md#flushdelay---ref)). 33 | 34 | ## Tutorials 35 | 36 | * [Basic](tutorials/basic.md) 37 | * [Authentication](tutorials/authentication.md) 38 | * [Simulating Errors](tutorials/errors.md) 39 | * [Overriding `window.Firebase`](tutorials/override.md) 40 | * [Overriding `require('firebase')`](tutorials/proxyquire.md) 41 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockfirebase", 3 | "version": "0.12.0", 4 | "homepage": "https://github.com/katowulf/mockfirebase", 5 | "authors": [ 6 | "Kato" 7 | ], 8 | "description": "Firebase mock library for writing unit tests", 9 | "main": "./browser/mockfirebase.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "firebase", 17 | "mock" 18 | ], 19 | "license": "MIT", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "test", 24 | "helpers", 25 | "gulpfile.js", 26 | "package.json", 27 | "src" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var plugins = require('gulp-load-plugins')(); 5 | var _ = require('lodash'); 6 | var browserify = require('browserify'); 7 | var source = require('vinyl-source-stream'); 8 | var buffer = require('vinyl-buffer'); 9 | var fs = require('fs'); 10 | var argv = require('yargs').argv; 11 | var streamToPromise = require('stream-to-promise'); 12 | var path = require('path'); 13 | var os = require('os'); 14 | 15 | var v; 16 | function version () { 17 | var previous = require('./package.json').version; 18 | if (!v) v = require('semver').inc(previous, argv.type || 'patch'); 19 | return v; 20 | } 21 | 22 | function bundle () { 23 | var pkg = require('./package.json'); 24 | return browserify({ 25 | standalone: 'mockfirebase' 26 | }) 27 | .add(pkg.main) 28 | .bundle() 29 | .pipe(source('mockfirebase.js')) 30 | .pipe(buffer()) 31 | .pipe(plugins.header(fs.readFileSync('./helpers/header.txt'), { 32 | pkg: _.extend(require('./package.json'), { 33 | version: version() 34 | }) 35 | })) 36 | .pipe(plugins.footer(fs.readFileSync('./helpers/globals.js'))); 37 | } 38 | 39 | gulp.task('bundle', function () { 40 | return bundle().pipe(gulp.dest('./browser')); 41 | }); 42 | 43 | gulp.task('cover', function () { 44 | return gulp.src(['./src/**/*.js', '!./src/login.js']) 45 | .pipe(plugins.istanbul()) 46 | .pipe(plugins.istanbul.hookRequire()); 47 | }); 48 | 49 | gulp.task('test', ['cover'], function () { 50 | return gulp.src('test/unit/*.js') 51 | .pipe(plugins.mocha({ 52 | grep: argv.grep 53 | })) 54 | .pipe(plugins.istanbul.writeReports()); 55 | }); 56 | 57 | gulp.task('karma', function () { 58 | return require('karma-as-promised').server.start({ 59 | frameworks: ['browserify', 'mocha', 'sinon'], 60 | browsers: ['PhantomJS'], 61 | client: { 62 | args: ['--grep', argv.grep] 63 | }, 64 | files: [ 65 | 'node_modules/es5-shim/es5-shim.js', 66 | 'test/unit/*.js' 67 | ], 68 | preprocessors: { 69 | 'test/unit/*.js': ['browserify'] 70 | }, 71 | browserify: { 72 | debug: true 73 | }, 74 | autoWatch: false, 75 | singleRun: true 76 | }); 77 | }); 78 | 79 | gulp.task('smoke', function () { 80 | var name = Date.now() + '-mockfirebase.js'; 81 | var dir = os.tmpdir(); 82 | var bundlePath = path.join(dir, name); 83 | return streamToPromise(bundle() 84 | .pipe(plugins.rename(name)) 85 | .pipe(gulp.dest(dir))) 86 | .then(function () { 87 | return require('karma-as-promised').server.start({ 88 | frameworks: ['mocha', 'chai'], 89 | browsers: ['PhantomJS'], 90 | client: { 91 | args: ['--grep', argv.grep] 92 | }, 93 | files: [ 94 | bundlePath, 95 | 'test/smoke/globals.js' 96 | ], 97 | autoWatch: false, 98 | singleRun: true 99 | }); 100 | }); 101 | }); 102 | 103 | gulp.task('lint', function () { 104 | return gulp.src(['./gulpfile.js', './src/**/*.js', './test/**/*.js']) 105 | .pipe(plugins.jshint()) 106 | .pipe(plugins.jshint.reporter('jshint-stylish')) 107 | .pipe(plugins.jshint.reporter('fail')); 108 | }); 109 | 110 | var pkgs = ['./package.json', './bower.json']; 111 | gulp.task('bump', function () { 112 | return gulp.src(pkgs) 113 | .pipe(plugins.bump({ 114 | version: version() 115 | })) 116 | .pipe(gulp.dest('./')); 117 | }); 118 | 119 | gulp.task('release', ['bundle', 'bump'], function () { 120 | var versionString = 'v' + version(); 121 | var message = 'Release ' + versionString; 122 | return plugins.shell.task([ 123 | 'git add -f ./browser/mockfirebase.js', 124 | 'git add ' + pkgs.join(' '), 125 | 'git commit -m "' + message + '"', 126 | 'git tag ' + versionString 127 | ])(); 128 | }); 129 | -------------------------------------------------------------------------------- /helpers/globals.js: -------------------------------------------------------------------------------- 1 | ;(function (window) { 2 | 'use strict'; 3 | if (typeof window !== 'undefined' && window.mockfirebase) { 4 | window.MockFirebase = window.mockfirebase.MockFirebase; 5 | window.MockFirebaseSimpleLogin = window.mockfirebase.MockFirebaseSimpleLogin; 6 | 7 | var originals = false; 8 | window.MockFirebase.override = function () { 9 | originals = { 10 | firebase: window.Firebase, 11 | login: window.FirebaseSimpleLogin 12 | }; 13 | window.Firebase = window.mockfirebase.MockFirebase; 14 | window.FirebaseSimpleLogin = window.mockfirebase.MockFirebaseSimpleLogin; 15 | }; 16 | window.MockFirebase.restore = function () { 17 | if (!originals) return; 18 | window.Firebase = originals.firebase; 19 | window.FirebaseSimpleLogin = originals.login; 20 | }; 21 | } 22 | })(window); 23 | -------------------------------------------------------------------------------- /helpers/header.txt: -------------------------------------------------------------------------------- 1 | /** <%= pkg.name %> - v<%= pkg.version %> 2 | <%= pkg.homepage %> 3 | * Copyright (c) 2014 <%= pkg.author %> 4 | * License: <%= pkg.license %> */ 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mockfirebase", 3 | "version": "0.12.0", 4 | "description": "Firebase mock library for writing unit tests", 5 | "main": "./src", 6 | "scripts": { 7 | "test": "gulp lint && gulp test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/katowulf/mockfirebase.git" 12 | }, 13 | "keywords": [ 14 | "firebase", 15 | "mock" 16 | ], 17 | "author": "Kato", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/katowulf/mockfirebase/issues" 21 | }, 22 | "files": [ 23 | "src/", 24 | "browser/" 25 | ], 26 | "homepage": "https://github.com/katowulf/mockfirebase", 27 | "dependencies": { 28 | "MD5": "~1.2.1", 29 | "browserify": "^6.3.2", 30 | "browserify-shim": "^3.8.0", 31 | "firebase-auto-ids": "~1.1.0", 32 | "lodash": "~2.4.1" 33 | }, 34 | "devDependencies": { 35 | "chai": "~1.9.1", 36 | "es5-shim": "~4.0.1", 37 | "gulp": "^3.8.10", 38 | "gulp-bump": "~0.1.10", 39 | "gulp-footer": "1", 40 | "gulp-header": "1", 41 | "gulp-istanbul": "^0.5.0", 42 | "gulp-jshint": "~1.8.3", 43 | "gulp-load-plugins": "~0.5.3", 44 | "gulp-mocha": "^2.0.0", 45 | "gulp-rename": "~1.2.0", 46 | "gulp-shell": "~0.2.10", 47 | "jshint-stylish": "~0.4.0", 48 | "karma": "~0.12.19", 49 | "karma-as-promised": "~0.2.0", 50 | "karma-browserify": "^1.0.0", 51 | "karma-chai": "~0.1.0", 52 | "karma-mocha": "~0.1.6", 53 | "karma-phantomjs-launcher": "~0.1.4", 54 | "karma-sinon": "~1.0.3", 55 | "semver": "~3.0.1", 56 | "sinon": "~1.10.3", 57 | "sinon-chai": "~2.5.0", 58 | "stream-to-promise": "~1.0.4", 59 | "vinyl-buffer": "1.0.0", 60 | "vinyl-source-stream": "^1.0.0", 61 | "yargs": "~1.3.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var format = require('util').format; 5 | 6 | function FirebaseAuth () { 7 | this._auth = { 8 | userData: null, 9 | listeners: [], 10 | completionListeners: [], 11 | users: [], 12 | uidCounter: 1 13 | }; 14 | } 15 | 16 | FirebaseAuth.prototype.changeAuthState = function (userData) { 17 | this._defer('changeAuthState', _.toArray(arguments), function() { 18 | if (!_.isEqual(this._auth.userData, userData)) { 19 | this._auth.userData = _.isObject(userData) ? userData : null; 20 | this._triggerAuthEvent(); 21 | } 22 | }); 23 | }; 24 | 25 | FirebaseAuth.prototype.getEmailUser = function (email) { 26 | var users = this._auth.users; 27 | return users.hasOwnProperty(email) ? _.clone(users[email]) : null; 28 | }; 29 | 30 | // number of arguments 31 | var authMethods = { 32 | authWithCustomToken: 2, 33 | authAnonymously: 1, 34 | authWithPassword: 2, 35 | authWithOAuthPopup: 2, 36 | authWithOAuthRedirect: 2, 37 | authWithOAuthToken: 3 38 | }; 39 | 40 | Object.keys(authMethods) 41 | .forEach(function (method) { 42 | var length = authMethods[method]; 43 | var callbackIndex = length - 1; 44 | FirebaseAuth.prototype[method] = function () { 45 | this._authEvent(method, arguments[callbackIndex]); 46 | }; 47 | }); 48 | 49 | FirebaseAuth.prototype.auth = function (token, callback) { 50 | console.warn('FIREBASE WARNING: FirebaseRef.auth() being deprecated. Please use FirebaseRef.authWithCustomToken() instead.'); 51 | this._authEvent('auth', callback); 52 | }; 53 | 54 | FirebaseAuth.prototype._authEvent = function (method, callback) { 55 | var err = this._nextErr(method); 56 | if (!callback) return; 57 | if (err) { 58 | // if an error occurs, we defer the error report until the next flush() 59 | // event is triggered 60 | this._defer('_authEvent', _.toArray(arguments), function() { 61 | callback(err, null); 62 | }); 63 | } 64 | else { 65 | // if there is no error, then we just add our callback to the listener 66 | // stack and wait for the next changeAuthState() call. 67 | this._auth.completionListeners.push({fn: callback}); 68 | } 69 | }; 70 | 71 | FirebaseAuth.prototype._triggerAuthEvent = function () { 72 | var completionListeners = this._auth.completionListeners; 73 | this._auth.completionListeners = []; 74 | var user = this._auth.userData; 75 | completionListeners.forEach(function (parts) { 76 | parts.fn.call(parts.context, null, _.cloneDeep(user)); 77 | }); 78 | this._auth.listeners.forEach(function (parts) { 79 | parts.fn.call(parts.context, _.cloneDeep(user)); 80 | }); 81 | }; 82 | 83 | FirebaseAuth.prototype.getAuth = function () { 84 | return this._auth.userData; 85 | }; 86 | 87 | FirebaseAuth.prototype.onAuth = function (onComplete, context) { 88 | this._auth.listeners.push({ 89 | fn: onComplete, 90 | context: context 91 | }); 92 | onComplete.call(context, this.getAuth()); 93 | }; 94 | 95 | FirebaseAuth.prototype.offAuth = function (onComplete, context) { 96 | var index = _.findIndex(this._auth.listeners, function (listener) { 97 | return listener.fn === onComplete && listener.context === context; 98 | }); 99 | if (index > -1) { 100 | this._auth.listeners.splice(index, 1); 101 | } 102 | }; 103 | 104 | FirebaseAuth.prototype.unauth = function () { 105 | if (this._auth.userData !== null) { 106 | this._auth.userData = null; 107 | this._triggerAuthEvent(); 108 | } 109 | }; 110 | 111 | FirebaseAuth.prototype.createUser = function (credentials, onComplete) { 112 | validateCredentials('createUser', credentials, [ 113 | 'email', 114 | 'password' 115 | ]); 116 | var err = this._nextErr('createUser'); 117 | var users = this._auth.users; 118 | this._defer('createUser', _.toArray(arguments), function () { 119 | var user = null; 120 | err = err || this._validateNewEmail(credentials); 121 | if (!err) { 122 | var key = credentials.email; 123 | users[key] = { 124 | uid: this._nextUid(), 125 | email: key, 126 | password: credentials.password 127 | }; 128 | user = { 129 | uid: users[key].uid 130 | }; 131 | } 132 | onComplete(err, user); 133 | }); 134 | }; 135 | 136 | FirebaseAuth.prototype.changeEmail = function (credentials, onComplete) { 137 | validateCredentials('changeEmail', credentials, [ 138 | 'oldEmail', 139 | 'newEmail', 140 | 'password' 141 | ]); 142 | var err = this._nextErr('changeEmail'); 143 | this._defer('changeEmail', _.toArray(arguments), function () { 144 | err = err || 145 | this._validateExistingEmail({ 146 | email: credentials.oldEmail 147 | }) || 148 | this._validPass({ 149 | password: credentials.password, 150 | email: credentials.oldEmail 151 | }, 'password'); 152 | if (!err) { 153 | var users = this._auth.users; 154 | var user = users[credentials.oldEmail]; 155 | delete users[credentials.oldEmail]; 156 | user.email = credentials.newEmail; 157 | users[user.email] = user; 158 | } 159 | onComplete(err); 160 | }); 161 | }; 162 | 163 | FirebaseAuth.prototype.changePassword = function (credentials, onComplete) { 164 | validateCredentials('changePassword', credentials, [ 165 | 'email', 166 | 'oldPassword', 167 | 'newPassword' 168 | ]); 169 | var err = this._nextErr('changePassword'); 170 | this._defer('changePassword', _.toArray(arguments), function () { 171 | err = err || 172 | this._validateExistingEmail(credentials) || 173 | this._validPass(credentials, 'oldPassword'); 174 | if (!err) { 175 | var key = credentials.email; 176 | var user = this._auth.users[key]; 177 | user.password = credentials.newPassword; 178 | } 179 | onComplete(err); 180 | }); 181 | }; 182 | 183 | FirebaseAuth.prototype.removeUser = function (credentials, onComplete) { 184 | validateCredentials('removeUser', credentials, [ 185 | 'email', 186 | 'password' 187 | ]); 188 | var err = this._nextErr('removeUser'); 189 | this._defer('removeUser', _.toArray(arguments), function () { 190 | err = err || 191 | this._validateExistingEmail(credentials) || 192 | this._validPass(credentials, 'password'); 193 | if (!err) { 194 | delete this._auth.users[credentials.email]; 195 | } 196 | onComplete(err); 197 | }); 198 | }; 199 | 200 | FirebaseAuth.prototype.resetPassword = function (credentials, onComplete) { 201 | validateCredentials('resetPassword', credentials, [ 202 | 'email' 203 | ]); 204 | var err = this._nextErr('resetPassword'); 205 | this._defer('resetPassword', _.toArray(arguments), function() { 206 | err = err || 207 | this._validateExistingEmail(credentials); 208 | onComplete(err); 209 | }); 210 | }; 211 | 212 | FirebaseAuth.prototype._nextUid = function () { 213 | return 'simplelogin:' + (this._auth.uidCounter++); 214 | }; 215 | 216 | FirebaseAuth.prototype._validateNewEmail = function (credentials) { 217 | if (this._auth.users.hasOwnProperty(credentials.email)) { 218 | var err = new Error('The specified email address is already in use.'); 219 | err.code = 'EMAIL_TAKEN'; 220 | return err; 221 | } 222 | return null; 223 | }; 224 | 225 | FirebaseAuth.prototype._validateExistingEmail = function (credentials) { 226 | if (!this._auth.users.hasOwnProperty(credentials.email)) { 227 | var err = new Error('The specified user does not exist.'); 228 | err.code = 'INVALID_USER'; 229 | return err; 230 | } 231 | return null; 232 | }; 233 | 234 | FirebaseAuth.prototype._validPass = function (object, name) { 235 | var err = null; 236 | var key = object.email; 237 | if (object[name] !== this._auth.users[key].password) { 238 | err = new Error('The specified password is incorrect.'); 239 | err.code = 'INVALID_PASSWORD'; 240 | } 241 | return err; 242 | }; 243 | 244 | function validateCredentials (method, credentials, fields) { 245 | validateObject(credentials, method, 'First'); 246 | fields.forEach(function (field) { 247 | validateArgument(method, credentials, 'First', field, 'string'); 248 | }); 249 | } 250 | 251 | function validateObject (object, method, position) { 252 | if (!_.isObject(object)) { 253 | throw new Error(format( 254 | 'Firebase.%s failed: %s argument must be a valid object.', 255 | method, 256 | position 257 | )); 258 | } 259 | } 260 | 261 | function validateArgument (method, object, position, name, type) { 262 | if (!object.hasOwnProperty(name) || typeof object[name] !== type) { 263 | throw new Error(format( 264 | 'Firebase.%s failed: %s argument must contain the key "%s" with type "%s"', 265 | method, 266 | position, 267 | name, 268 | type 269 | )); 270 | } 271 | } 272 | 273 | module.exports = FirebaseAuth; 274 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var assert = require('assert'); 5 | var autoId = require('firebase-auto-ids'); 6 | var Query = require('./query'); 7 | var Snapshot = require('./snapshot'); 8 | var Queue = require('./queue').Queue; 9 | var utils = require('./utils'); 10 | var Auth = require('./auth'); 11 | var validate = require('./validators'); 12 | 13 | function MockFirebase (path, data, parent, name) { 14 | this.path = path || 'Mock://'; 15 | this.errs = {}; 16 | this.priority = null; 17 | this.myName = parent ? name : extractName(path); 18 | this.flushDelay = parent ? parent.flushDelay : false; 19 | this.queue = parent ? parent.queue : new Queue(); 20 | this._events = { 21 | value: [], 22 | child_added: [], 23 | child_removed: [], 24 | child_changed: [], 25 | child_moved: [] 26 | }; 27 | this.parentRef = parent || null; 28 | this.children = {}; 29 | if (parent) parent.children[this.key()] = this; 30 | this.sortedDataKeys = []; 31 | this.data = null; 32 | this._dataChanged(_.cloneDeep(data) || null); 33 | this._lastAutoId = null; 34 | _.extend(this, Auth.prototype, new Auth()); 35 | } 36 | 37 | MockFirebase.ServerValue = { 38 | TIMESTAMP: { 39 | '.sv': 'timestamp' 40 | } 41 | }; 42 | 43 | var getServerTime, defaultClock; 44 | getServerTime = defaultClock = function () { 45 | return new Date().getTime(); 46 | }; 47 | 48 | MockFirebase.setClock = function (fn) { 49 | getServerTime = fn; 50 | }; 51 | 52 | MockFirebase.restoreClock = function () { 53 | getServerTime = defaultClock; 54 | }; 55 | 56 | MockFirebase.prototype.flush = function (delay) { 57 | this.queue.flush(delay); 58 | return this; 59 | }; 60 | 61 | MockFirebase.prototype.autoFlush = function (delay) { 62 | if( _.isUndefined(delay)) { 63 | delay = true; 64 | } 65 | if (this.flushDelay !== delay) { 66 | this.flushDelay = delay; 67 | _.each(this.children, function (child) { 68 | child.autoFlush(delay); 69 | }); 70 | if (this.parentRef) { 71 | this.parentRef.autoFlush(delay); 72 | } 73 | } 74 | return this; 75 | }; 76 | 77 | MockFirebase.prototype.getFlushQueue = function() { 78 | return this.queue.getEvents(); 79 | }; 80 | 81 | MockFirebase.prototype.failNext = function (methodName, err) { 82 | assert(err instanceof Error, 'err must be an "Error" object'); 83 | this.errs[methodName] = err; 84 | }; 85 | 86 | MockFirebase.prototype.forceCancel = function (error, event, callback, context) { 87 | var events = this._events; 88 | (event ? [event] : _.keys(events)) 89 | .forEach(function (eventName) { 90 | events[eventName] 91 | .filter(function (parts) { 92 | return !event || !callback || (callback === parts[0] && context === parts[1]); 93 | }) 94 | .forEach(function (parts) { 95 | parts[2].call(parts[1], error); 96 | this.off(event, callback, context); 97 | }, this); 98 | }, this); 99 | }; 100 | 101 | MockFirebase.prototype.getData = function () { 102 | return _.cloneDeep(this.data); 103 | }; 104 | 105 | MockFirebase.prototype.getKeys = function () { 106 | return this.sortedDataKeys.slice(); 107 | }; 108 | 109 | MockFirebase.prototype.fakeEvent = function (event, key, data, prevChild, priority) { 110 | validate.event(event); 111 | if (arguments.length < 5) priority = null; 112 | if (arguments.length < 4) prevChild = null; 113 | if (arguments.length < 3) data = null; 114 | var ref = event === 'value' ? this : this.child(key); 115 | var snapshot = new Snapshot(ref, data, priority); 116 | this._defer('fakeEvent', _.toArray(arguments), function () { 117 | this._events[event] 118 | .map(function (parts) { 119 | return { 120 | fn: parts[0], 121 | args: [snapshot], 122 | context: parts[1] 123 | }; 124 | }) 125 | .forEach(function (data) { 126 | if ('child_added' === event || 'child_moved' === event) { 127 | data.args.push(prevChild); 128 | } 129 | data.fn.apply(data.context, data.args); 130 | }); 131 | }); 132 | return this; 133 | }; 134 | 135 | MockFirebase.prototype.toString = function () { 136 | return this.path; 137 | }; 138 | 139 | MockFirebase.prototype.child = function (childPath) { 140 | assert(childPath, 'A child path is required'); 141 | var parts = _.compact(childPath.split('/')); 142 | var childKey = parts.shift(); 143 | var child = this.children[childKey]; 144 | if (!child) { 145 | child = new MockFirebase(utils.mergePaths(this.path, childKey), this._childData(childKey), this, childKey); 146 | this.children[child.key()] = child; 147 | } 148 | if (parts.length) { 149 | child = child.child(parts.join('/')); 150 | } 151 | return child; 152 | }; 153 | 154 | MockFirebase.prototype.set = function (data, callback) { 155 | var err = this._nextErr('set'); 156 | data = _.cloneDeep(data); 157 | this._defer('set', _.toArray(arguments), function() { 158 | if (err === null) { 159 | this._dataChanged(data); 160 | } 161 | if (callback) callback(err); 162 | }); 163 | }; 164 | 165 | MockFirebase.prototype.update = function (changes, callback) { 166 | assert.equal(typeof changes, 'object', 'First argument must be an object when calling "update"'); 167 | var err = this._nextErr('update'); 168 | this._defer('update', _.toArray(arguments), function () { 169 | if (!err) { 170 | var base = this.getData(); 171 | var data = _.assign(_.isObject(base) ? base : {}, changes); 172 | this._dataChanged(data); 173 | } 174 | if (callback) callback(err); 175 | }); 176 | }; 177 | 178 | MockFirebase.prototype.setPriority = function (newPriority, callback) { 179 | var err = this._nextErr('setPriority'); 180 | this._defer('setPriority', _.toArray(arguments), function () { 181 | this._priChanged(newPriority); 182 | if (callback) callback(err); 183 | }); 184 | }; 185 | 186 | MockFirebase.prototype.setWithPriority = function (data, pri, callback) { 187 | this.setPriority(pri); 188 | this.set(data, callback); 189 | }; 190 | 191 | MockFirebase.prototype.key = function () { 192 | return this.myName; 193 | }; 194 | 195 | /* istanbul ignore next */ 196 | MockFirebase.prototype.name = function () { 197 | console.warn('ref.name() is deprecated. Use ref.key()'); 198 | return this.key.apply(this, arguments); 199 | }; 200 | 201 | MockFirebase.prototype.ref = function () { 202 | return this; 203 | }; 204 | 205 | MockFirebase.prototype.parent = function () { 206 | return this.parentRef; 207 | }; 208 | 209 | MockFirebase.prototype.root = function () { 210 | var next = this; 211 | while (next.parentRef) { 212 | next = next.parentRef; 213 | } 214 | return next; 215 | }; 216 | 217 | MockFirebase.prototype.push = function (data, callback) { 218 | var child = this.child(this._newAutoId()); 219 | var err = this._nextErr('push'); 220 | if (err) child.failNext('set', err); 221 | if (arguments.length && data !== null) { 222 | // currently, callback only invoked if child exists 223 | child.set(data, callback); 224 | } 225 | return child; 226 | }; 227 | 228 | MockFirebase.prototype.once = function (event, callback, cancel, context) { 229 | validate.event(event); 230 | if (arguments.length === 3 && !_.isFunction(cancel)) { 231 | context = cancel; 232 | cancel = _.noop; 233 | } 234 | cancel = cancel || _.noop; 235 | var err = this._nextErr('once'); 236 | if (err) { 237 | this._defer('once', _.toArray(arguments), function () { 238 | cancel.call(context, err); 239 | }); 240 | } 241 | else { 242 | var fn = _.bind(function (snapshot) { 243 | this.off(event, fn, context); 244 | callback.call(context, snapshot); 245 | }, this); 246 | this._on('once', event, fn, cancel, context); 247 | } 248 | }; 249 | 250 | MockFirebase.prototype.remove = function (callback) { 251 | var err = this._nextErr('remove'); 252 | this._defer('remove', _.toArray(arguments), function () { 253 | if (err === null) { 254 | this._dataChanged(null); 255 | } 256 | if (callback) callback(err); 257 | }); 258 | return this; 259 | }; 260 | 261 | MockFirebase.prototype.on = function (event, callback, cancel, context) { 262 | validate.event(event); 263 | if (arguments.length === 3 && typeof cancel !== 'function') { 264 | context = cancel; 265 | cancel = _.noop; 266 | } 267 | cancel = cancel || _.noop; 268 | 269 | var err = this._nextErr('on'); 270 | if (err) { 271 | this._defer('on', _.toArray(arguments), function() { 272 | cancel.call(context, err); 273 | }); 274 | } 275 | else { 276 | this._on('on', event, callback, cancel, context); 277 | } 278 | return callback; 279 | }; 280 | 281 | MockFirebase.prototype.off = function (event, callback, context) { 282 | if (!event) { 283 | for (var key in this._events) { 284 | /* istanbul ignore else */ 285 | if (this._events.hasOwnProperty(key)) { 286 | this.off(key); 287 | } 288 | } 289 | } 290 | else { 291 | validate.event(event); 292 | if (callback) { 293 | var events = this._events[event]; 294 | var newEvents = this._events[event] = []; 295 | _.each(events, function (parts) { 296 | if (parts[0] !== callback || parts[1] !== context) { 297 | newEvents.push(parts); 298 | } 299 | }); 300 | } 301 | else { 302 | this._events[event] = []; 303 | } 304 | } 305 | }; 306 | 307 | MockFirebase.prototype.transaction = function (valueFn, finishedFn, applyLocally) { 308 | this._defer('transaction', _.toArray(arguments), function () { 309 | var err = this._nextErr('transaction'); 310 | var res = valueFn(this.getData()); 311 | var newData = _.isUndefined(res) || err? this.getData() : res; 312 | this._dataChanged(newData); 313 | if (typeof finishedFn === 'function') { 314 | finishedFn(err, err === null && !_.isUndefined(res), new Snapshot(this, newData, this.priority)); 315 | } 316 | }); 317 | return [valueFn, finishedFn, applyLocally]; 318 | }; 319 | 320 | MockFirebase.prototype./** 321 | * Just a stub at this point. 322 | * @param {int} limit 323 | */ 324 | limit = function (limit) { 325 | return new Query(this).limit(limit); 326 | }; 327 | 328 | MockFirebase.prototype.startAt = function (priority, key) { 329 | return new Query(this).startAt(priority, key); 330 | }; 331 | 332 | MockFirebase.prototype.endAt = function (priority, key) { 333 | return new Query(this).endAt(priority, key); 334 | }; 335 | 336 | MockFirebase.prototype._childChanged = function (ref) { 337 | var events = []; 338 | var childKey = ref.key(); 339 | var data = ref.getData(); 340 | if( data === null ) { 341 | this._removeChild(childKey, events); 342 | } 343 | else { 344 | this._updateOrAdd(childKey, data, events); 345 | } 346 | this._triggerAll(events); 347 | }; 348 | 349 | MockFirebase.prototype._dataChanged = function (unparsedData) { 350 | var pri = utils.getMeta(unparsedData, 'priority', this.priority); 351 | var data = utils.cleanData(unparsedData); 352 | 353 | if (utils.isServerTimestamp(data)) { 354 | data = getServerTime(); 355 | } 356 | 357 | if( pri !== this.priority ) { 358 | this._priChanged(pri); 359 | } 360 | if( !_.isEqual(data, this.data) ) { 361 | var oldKeys = _.keys(this.data).sort(); 362 | var newKeys = _.keys(data).sort(); 363 | var keysToRemove = _.difference(oldKeys, newKeys); 364 | var keysToChange = _.difference(newKeys, keysToRemove); 365 | var events = []; 366 | 367 | keysToRemove.forEach(function(key) { 368 | this._removeChild(key, events); 369 | }, this); 370 | 371 | if(!_.isObject(data)) { 372 | events.push(false); 373 | this.data = data; 374 | } 375 | else { 376 | keysToChange.forEach(function(key) { 377 | var childData = unparsedData[key]; 378 | if (utils.isServerTimestamp(childData)) { 379 | childData = getServerTime(); 380 | } 381 | this._updateOrAdd(key, childData, events); 382 | }, this); 383 | } 384 | 385 | // update order of my child keys 386 | this._resort(); 387 | 388 | // trigger parent notifications after all children have 389 | // been processed 390 | this._triggerAll(events); 391 | } 392 | }; 393 | 394 | MockFirebase.prototype._priChanged = function (newPriority) { 395 | if (utils.isServerTimestamp(newPriority)) { 396 | newPriority = getServerTime(); 397 | } 398 | this.priority = newPriority; 399 | if( this.parentRef ) { 400 | this.parentRef._resort(this.key()); 401 | } 402 | }; 403 | 404 | MockFirebase.prototype._getPri = function (key) { 405 | return _.has(this.children, key)? this.children[key].priority : null; 406 | }; 407 | 408 | MockFirebase.prototype._resort = function (childKeyMoved) { 409 | this.sortedDataKeys.sort(_.bind(this.childComparator, this)); 410 | // resort the data object to match our keys so value events return ordered content 411 | var oldData = _.assign({}, this.data); 412 | _.each(oldData, function(v,k) { delete this.data[k]; }, this); 413 | _.each(this.sortedDataKeys, function(k) { 414 | this.data[k] = oldData[k]; 415 | }, this); 416 | if( !_.isUndefined(childKeyMoved) && _.has(this.data, childKeyMoved) ) { 417 | this._trigger('child_moved', this.data[childKeyMoved], this._getPri(childKeyMoved), childKeyMoved); 418 | } 419 | }; 420 | 421 | MockFirebase.prototype._addKey = function (newKey) { 422 | if(_.indexOf(this.sortedDataKeys, newKey) === -1) { 423 | this.sortedDataKeys.push(newKey); 424 | this._resort(); 425 | } 426 | }; 427 | 428 | MockFirebase.prototype._dropKey = function (key) { 429 | var i = _.indexOf(this.sortedDataKeys, key); 430 | if( i > -1 ) { 431 | this.sortedDataKeys.splice(i, 1); 432 | } 433 | }; 434 | 435 | MockFirebase.prototype._defer = function (sourceMethod, sourceArgs, callback) { 436 | this.queue.push({ 437 | fn: callback, 438 | context: this, 439 | sourceData: { 440 | ref: this, 441 | method: sourceMethod, 442 | args: sourceArgs 443 | } 444 | }); 445 | if (this.flushDelay !== false) { 446 | this.flush(this.flushDelay); 447 | } 448 | }; 449 | 450 | MockFirebase.prototype._trigger = function (event, data, pri, key) { 451 | var ref = event==='value'? this : this.child(key); 452 | var snap = new Snapshot(ref, data, pri); 453 | _.each(this._events[event], function(parts) { 454 | var fn = parts[0], context = parts[1]; 455 | if(_.contains(['child_added', 'child_moved'], event)) { 456 | fn.call(context, snap, this._getPrevChild(key)); 457 | } 458 | else { 459 | fn.call(context, snap); 460 | } 461 | }, this); 462 | }; 463 | 464 | MockFirebase.prototype._triggerAll = function (events) { 465 | if (!events.length) return; 466 | events.forEach(function(event) { 467 | if (event !== false) this._trigger.apply(this, event); 468 | }, this); 469 | this._trigger('value', this.data, this.priority); 470 | if (this.parentRef) { 471 | this.parentRef._childChanged(this); 472 | } 473 | }; 474 | 475 | MockFirebase.prototype._updateOrAdd = function (key, data, events) { 476 | var exists = _.isObject(this.data) && this.data.hasOwnProperty(key); 477 | if( !exists ) { 478 | return this._addChild(key, data, events); 479 | } 480 | else { 481 | return this._updateChild(key, data, events); 482 | } 483 | }; 484 | 485 | MockFirebase.prototype._addChild = function (key, data, events) { 486 | if (!_.isObject(this.data)) { 487 | this.data = {}; 488 | } 489 | this._addKey(key); 490 | this.data[key] = utils.cleanData(data); 491 | var child = this.child(key); 492 | child._dataChanged(data); 493 | if (events) events.push(['child_added', child.getData(), child.priority, key]); 494 | }; 495 | 496 | MockFirebase.prototype._removeChild = function (key, events) { 497 | if(this._hasChild(key)) { 498 | this._dropKey(key); 499 | var data = this.data[key]; 500 | delete this.data[key]; 501 | if(_.isEmpty(this.data)) { 502 | this.data = null; 503 | } 504 | if(_.has(this.children, key)) { 505 | this.children[key]._dataChanged(null); 506 | } 507 | if (events) events.push(['child_removed', data, null, key]); 508 | } 509 | }; 510 | 511 | MockFirebase.prototype._updateChild = function (key, data, events) { 512 | var cdata = utils.cleanData(data); 513 | if(_.isObject(this.data) && _.has(this.data,key) && !_.isEqual(this.data[key], cdata)) { 514 | this.data[key] = cdata; 515 | var c = this.child(key); 516 | c._dataChanged(data); 517 | if (events) events.push(['child_changed', c.getData(), c.priority, key]); 518 | } 519 | }; 520 | 521 | MockFirebase.prototype._newAutoId = function () { 522 | return (this._lastAutoId = autoId(new Date().getTime())); 523 | }; 524 | 525 | MockFirebase.prototype._nextErr = function (type) { 526 | var err = this.errs[type]; 527 | delete this.errs[type]; 528 | return err||null; 529 | }; 530 | 531 | MockFirebase.prototype._hasChild = function (key) { 532 | return _.isObject(this.data) && _.has(this.data, key); 533 | }; 534 | 535 | MockFirebase.prototype._childData = function (key) { 536 | return this._hasChild(key)? this.data[key] : null; 537 | }; 538 | 539 | MockFirebase.prototype._getPrevChild = function (key) { 540 | // this._resort(); 541 | var keys = this.sortedDataKeys; 542 | var i = _.indexOf(keys, key); 543 | if( i === -1 ) { 544 | keys = keys.slice(); 545 | keys.push(key); 546 | keys.sort(_.bind(this.childComparator, this)); 547 | i = _.indexOf(keys, key); 548 | } 549 | return i === 0? null : keys[i-1]; 550 | }; 551 | 552 | MockFirebase.prototype._on = function (deferName, event, callback, cancel, context) { 553 | var handlers = [callback, context, cancel]; 554 | this._events[event].push(handlers); 555 | // value and child_added both trigger initial events when called so 556 | // defer those here 557 | if ('value' === event || 'child_added' === event) { 558 | this._defer(deferName, _.toArray(arguments).slice(1), function () { 559 | // make sure off() wasn't called before we triggered this 560 | if (this._events[event].indexOf(handlers) > -1) { 561 | switch (event) { 562 | case 'value': 563 | callback.call(context, new Snapshot(this, this.getData(), this.priority)); 564 | break; 565 | case 'child_added': 566 | var previousChild = null; 567 | this.sortedDataKeys 568 | .forEach(function (key) { 569 | var child = this.child(key); 570 | callback.call(context, new Snapshot(child, child.getData(), child.priority), previousChild); 571 | previousChild = key; 572 | }, this); 573 | break; 574 | } 575 | } 576 | }); 577 | } 578 | }; 579 | 580 | MockFirebase.prototype.childComparator = function (a, b) { 581 | var aPri = this._getPri(a); 582 | var bPri = this._getPri(b); 583 | var x = utils.priorityComparator(aPri, bPri); 584 | if( x === 0 ) { 585 | if( a !== b ) { 586 | x = a < b? -1 : 1; 587 | } 588 | } 589 | return x; 590 | }; 591 | 592 | function extractName(path) { 593 | return ((path || '').match(/\/([^.$\[\]#\/]+)$/)||[null, null])[1]; 594 | } 595 | 596 | module.exports = MockFirebase; 597 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.MockFirebase = require('./firebase'); 4 | /** @deprecated */ 5 | exports.MockFirebaseSimpleLogin = require('./login'); 6 | -------------------------------------------------------------------------------- /src/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var md5 = require('MD5'); 5 | 6 | /******************************************************************************* 7 | * SIMPLE LOGIN 8 | * @deprecated 9 | ******************************************************************************/ 10 | function MockFirebaseSimpleLogin (ref, callback, userData) { 11 | // allows test units to monitor the callback function to make sure 12 | // it is invoked (even if one is not declared) 13 | this.callback = function () { callback.apply(null, Array.prototype.slice.call(arguments, 0)); }; 14 | this.attempts = []; 15 | this.failMethod = MockFirebaseSimpleLogin.DEFAULT_FAIL_WHEN; 16 | this.ref = ref; // we don't use ref for anything 17 | this.autoFlushTime = MockFirebaseSimpleLogin.DEFAULT_AUTO_FLUSH; 18 | this.userData = _.cloneDeep(MockFirebaseSimpleLogin.DEFAULT_USER_DATA); 19 | if (userData) _.assign(this.userData, userData); 20 | } 21 | 22 | /*** PUBLIC METHODS AND FIXTURES ***/ 23 | 24 | MockFirebaseSimpleLogin.DEFAULT_FAIL_WHEN = function(provider, options, user) { 25 | var res = null; 26 | if( ['password', 'anonymous', 'twitter', 'facebook', 'google', 'github'].indexOf(provider) === -1 ) { 27 | console.error('MockFirebaseSimpleLogin:login() failed: unrecognized authentication provider '+provider); 28 | // res = createError(); 29 | } 30 | else if( !user ) { 31 | res = createError('INVALID_USER', 'The specified user does not exist'); 32 | } 33 | else if( provider === 'password' && user.password !== options.password ) { 34 | res = createError('INVALID_PASSWORD', 'The specified password is incorrect'); 35 | } 36 | return res; 37 | }; 38 | 39 | var USER_COUNT = 100; 40 | MockFirebaseSimpleLogin.DEFAULT_USER_DATA = {}; 41 | _.each(['password', 'anonymous', 'facebook', 'twitter', 'google', 'github'], function(provider) { 42 | var user = createDefaultUser(provider); 43 | if( provider !== 'password' ) { 44 | MockFirebaseSimpleLogin.DEFAULT_USER_DATA[provider] = user; 45 | } 46 | else { 47 | var set = MockFirebaseSimpleLogin.DEFAULT_USER_DATA[provider] = {}; 48 | set[user.email] = user; 49 | } 50 | }); 51 | 52 | MockFirebaseSimpleLogin.DEFAULT_AUTO_FLUSH = false; 53 | 54 | MockFirebaseSimpleLogin.prototype = { 55 | 56 | /***************************************************** 57 | * Test Unit Methods 58 | *****************************************************/ 59 | 60 | /** 61 | * When this method is called, any outstanding login() 62 | * attempts will be immediately resolved. If this method 63 | * is called with an integer value, then the login attempt 64 | * will resolve asynchronously after that many milliseconds. 65 | * 66 | * @param {int|boolean} [milliseconds] 67 | * @returns {MockFirebaseSimpleLogin} 68 | */ 69 | flush: function(milliseconds) { 70 | var self = this; 71 | if(_.isNumber(milliseconds) ) { 72 | setTimeout(_.bind(self.flush, self), milliseconds); 73 | } 74 | else { 75 | var attempts = self.attempts; 76 | self.attempts = []; 77 | _.each(attempts, function(x) { 78 | x[0].apply(self, x.slice(1)); 79 | }); 80 | } 81 | return self; 82 | }, 83 | 84 | /** 85 | * Automatically queue the flush() event 86 | * each time login() is called. If this method 87 | * is called with `true`, then the callback 88 | * is invoked synchronously. 89 | * 90 | * If this method is called with an integer, 91 | * the callback is triggered asynchronously 92 | * after that many milliseconds. 93 | * 94 | * If this method is called with false, then 95 | * autoFlush() is disabled. 96 | * 97 | * @param {int|boolean} [milliseconds] 98 | * @returns {MockFirebaseSimpleLogin} 99 | */ 100 | autoFlush: function(milliseconds) { 101 | this.autoFlushTime = milliseconds; 102 | if( this.autoFlushTime !== false ) { 103 | this.flush(this.autoFlushTime); 104 | } 105 | return this; 106 | }, 107 | 108 | /** 109 | * `testMethod` is passed the {string}provider, {object}options, {object}user 110 | * for each call to login(). If it returns anything other than 111 | * null, then that is passed as the error message to the 112 | * callback and the login call fails. 113 | * 114 | * 115 | * // this is a simplified example of the default implementation (MockFirebaseSimpleLogin.DEFAULT_FAIL_WHEN) 116 | * auth.failWhen(function(provider, options, user) { 117 | * if( user.email !== options.email ) { 118 | * return MockFirebaseSimpleLogin.createError('INVALID_USER'); 119 | * } 120 | * else if( user.password !== options.password ) { 121 | * return MockFirebaseSimpleLogin.createError('INVALID_PASSWORD'); 122 | * } 123 | * else { 124 | * return null; 125 | * } 126 | * }); 127 | * 128 | * 129 | * Multiple calls to this method replace the old failWhen criteria. 130 | * 131 | * @param testMethod 132 | * @returns {MockFirebaseSimpleLogin} 133 | */ 134 | failWhen: function(testMethod) { 135 | this.failMethod = testMethod; 136 | return this; 137 | }, 138 | 139 | /** 140 | * Retrieves a user account from the mock user data on this object 141 | * 142 | * @param provider 143 | * @param options 144 | */ 145 | getUser: function(provider, options) { 146 | var data = this.userData[provider]; 147 | if( provider === 'password' ) { 148 | data = (data||{})[options.email]; 149 | } 150 | return data||null; 151 | }, 152 | 153 | /***************************************************** 154 | * Public API 155 | *****************************************************/ 156 | login: function(provider, options) { 157 | var err = this.failMethod(provider, options||{}, this.getUser(provider, options)); 158 | this._notify(err, err===null? this.userData[provider]: null); 159 | }, 160 | 161 | logout: function() { 162 | this._notify(null, null); 163 | }, 164 | 165 | createUser: function(email, password, callback) { 166 | if (!callback) callback = _.noop; 167 | this._defer(function() { 168 | var user = null, err = null; 169 | if( this.userData.password.hasOwnProperty(email) ) { 170 | err = createError('EMAIL_TAKEN', 'The specified email address is already in use.'); 171 | } 172 | else { 173 | user = createEmailUser(email, password); 174 | this.userData.password[email] = user; 175 | } 176 | callback(err, user); 177 | }); 178 | }, 179 | 180 | changePassword: function(email, oldPassword, newPassword, callback) { 181 | if (!callback) callback = _.noop; 182 | this._defer(function() { 183 | var user = this.getUser('password', {email: email}); 184 | var err = this.failMethod('password', {email: email, password: oldPassword}, user); 185 | if( err ) { 186 | callback(err, false); 187 | } 188 | else { 189 | user.password = newPassword; 190 | callback(null, true); 191 | } 192 | }); 193 | }, 194 | 195 | sendPasswordResetEmail: function(email, callback) { 196 | if (!callback) callback = _.noop; 197 | this._defer(function() { 198 | var user = this.getUser('password', {email: email}); 199 | if( !user ) { 200 | callback(createError('INVALID_USER'), false); 201 | } 202 | else { 203 | callback(null, true); 204 | } 205 | }); 206 | }, 207 | 208 | removeUser: function(email, password, callback) { 209 | if (!callback) callback = _.noop; 210 | this._defer(function() { 211 | var user = this.getUser('password', {email: email}); 212 | if( !user ) { 213 | callback(createError('INVALID_USER'), false); 214 | } 215 | else if( user.password !== password ) { 216 | callback(createError('INVALID_PASSWORD'), false); 217 | } 218 | else { 219 | delete this.userData.password[email]; 220 | callback(null, true); 221 | } 222 | }); 223 | }, 224 | 225 | /***************************************************** 226 | * Private/internal methods 227 | *****************************************************/ 228 | _notify: function(error, user) { 229 | this._defer(this.callback, error, user); 230 | }, 231 | 232 | _defer: function() { 233 | var args = _.toArray(arguments); 234 | this.attempts.push(args); 235 | if( this.autoFlushTime !== false ) { 236 | this.flush(this.autoFlushTime); 237 | } 238 | } 239 | }; 240 | 241 | function createError(code, message) { 242 | return { code: code||'UNKNOWN_ERROR', message: 'FirebaseSimpleLogin: '+(message||code||'unspecific error') }; 243 | } 244 | 245 | function createEmailUser (email, password) { 246 | var id = USER_COUNT++; 247 | return { 248 | uid: 'password:'+id, 249 | id: id, 250 | email: email, 251 | password: password, 252 | provider: 'password', 253 | md5_hash: md5(email), 254 | firebaseAuthToken: 'FIREBASE_AUTH_TOKEN' //todo 255 | }; 256 | } 257 | 258 | function createDefaultUser (provider) { 259 | var id = USER_COUNT++; 260 | 261 | var out = { 262 | uid: provider+':'+id, 263 | id: id, 264 | password: id, 265 | provider: provider, 266 | firebaseAuthToken: 'FIREBASE_AUTH_TOKEN' //todo 267 | }; 268 | switch(provider) { 269 | case 'password': 270 | out.email = 'email@firebase.com'; 271 | out.md5_hash = md5(out.email); 272 | break; 273 | case 'twitter': 274 | out.accessToken = 'ACCESS_TOKEN'; //todo 275 | out.accessTokenSecret = 'ACCESS_TOKEN_SECRET'; //todo 276 | out.displayName = 'DISPLAY_NAME'; 277 | out.thirdPartyUserData = {}; //todo 278 | out.username = 'USERNAME'; 279 | break; 280 | case 'google': 281 | out.accessToken = 'ACCESS_TOKEN'; //todo 282 | out.displayName = 'DISPLAY_NAME'; 283 | out.email = 'email@firebase.com'; 284 | out.thirdPartyUserData = {}; //todo 285 | break; 286 | case 'github': 287 | out.accessToken = 'ACCESS_TOKEN'; //todo 288 | out.displayName = 'DISPLAY_NAME'; 289 | out.thirdPartyUserData = {}; //todo 290 | out.username = 'USERNAME'; 291 | break; 292 | case 'facebook': 293 | out.accessToken = 'ACCESS_TOKEN'; //todo 294 | out.displayName = 'DISPLAY_NAME'; 295 | out.thirdPartyUserData = {}; //todo 296 | break; 297 | case 'anonymous': 298 | break; 299 | default: 300 | throw new Error('Invalid auth provider', provider); 301 | } 302 | 303 | return out; 304 | } 305 | 306 | module.exports = MockFirebaseSimpleLogin; 307 | -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Slice = require('./slice'); 5 | var utils = require('./utils'); 6 | var validate = require('./validators'); 7 | 8 | function MockQuery (ref) { 9 | this.ref = function () { 10 | return ref; 11 | }; 12 | this._events = []; 13 | // startPri, endPri, startKey, endKey, and limit 14 | this._q = {}; 15 | } 16 | 17 | MockQuery.prototype.flush = function () { 18 | var ref = this.ref(); 19 | ref.flush.apply(ref, arguments); 20 | return this; 21 | }; 22 | 23 | MockQuery.prototype.autoFlush = function () { 24 | var ref = this.ref(); 25 | ref.autoFlush.apply(ref, arguments); 26 | return this; 27 | }; 28 | 29 | MockQuery.prototype.slice = function () { 30 | return new Slice(this); 31 | }; 32 | 33 | MockQuery.prototype.getData = function () { 34 | return this.slice().data; 35 | }; 36 | 37 | MockQuery.prototype.fakeEvent = function (event, snapshot) { 38 | validate.event(event); 39 | _(this._events) 40 | .filter(function (parts) { 41 | return parts[0] === event; 42 | }) 43 | .each(function (parts) { 44 | parts[1].call(parts[2], snapshot); 45 | }); 46 | }; 47 | 48 | MockQuery.prototype.on = function (event, callback, cancelCallback, context) { 49 | validate.event(event); 50 | if (arguments.length === 3 && typeof cancelCallback !== 'function') { 51 | context = cancelCallback; 52 | cancelCallback = _.noop; 53 | } 54 | cancelCallback = cancelCallback || _.noop; 55 | var self = this; 56 | var isFirst = true; 57 | var lastSlice = this.slice(); 58 | var map; 59 | function handleRefEvent (snap, prevChild) { 60 | var slice = new Slice(self, event === 'value' ? snap : utils.makeRefSnap(snap.ref().parent())); 61 | switch (event) { 62 | case 'value': 63 | if (isFirst || !lastSlice.equals(slice)) { 64 | callback.call(context, slice.snap()); 65 | } 66 | break; 67 | case 'child_moved': 68 | var x = slice.pos(snap.key()); 69 | var y = slice.insertPos(snap.key()); 70 | if (x > -1 && y > -1) { 71 | callback.call(context, snap, prevChild); 72 | } 73 | else if (x > -1 || y > -1) { 74 | map = lastSlice.changeMap(slice); 75 | } 76 | break; 77 | case 'child_added': 78 | if (slice.has(snap.key()) && lastSlice.has(snap.key())) { 79 | // is a child_added for existing event so allow it 80 | callback.call(context, snap, prevChild); 81 | } 82 | map = lastSlice.changeMap(slice); 83 | break; 84 | case 'child_removed': 85 | map = lastSlice.changeMap(slice); 86 | break; 87 | case 'child_changed': 88 | callback.call(context, snap); 89 | break; 90 | } 91 | 92 | if (map) { 93 | var newSnap = slice.snap(); 94 | var oldSnap = lastSlice.snap(); 95 | _.each(map.added, function (addKey) { 96 | self.fakeEvent('child_added', newSnap.child(addKey)); 97 | }); 98 | _.each(map.removed, function (remKey) { 99 | self.fakeEvent('child_removed', oldSnap.child(remKey)); 100 | }); 101 | } 102 | 103 | isFirst = false; 104 | lastSlice = slice; 105 | } 106 | this._events.push([event, callback, context, handleRefEvent]); 107 | this.ref().on(event, handleRefEvent, _.bind(cancelCallback, context)); 108 | }; 109 | 110 | MockQuery.prototype.off = function (event, callback, context) { 111 | var ref = this.ref(); 112 | _.each(this._events, function (parts) { 113 | if( parts[0] === event && parts[1] === callback && parts[2] === context ) { 114 | ref.off(event, parts[3]); 115 | } 116 | }); 117 | }; 118 | 119 | MockQuery.prototype.once = function (event, callback, context) { 120 | validate.event(event); 121 | var self = this; 122 | // once is tricky because we want the first match within our range 123 | // so we use the on() method above which already does the needed legwork 124 | function fn() { 125 | self.off(event, fn); 126 | // the snap is already sliced in on() so we can just pass it on here 127 | callback.apply(context, arguments); 128 | } 129 | self.on(event, fn); 130 | }; 131 | 132 | MockQuery.prototype.limit = function (intVal) { 133 | if( typeof intVal !== 'number' ) { 134 | throw new Error('Query.limit: First argument must be a positive integer.'); 135 | } 136 | var q = new MockQuery(this.ref()); 137 | _.extend(q._q, this._q, {limit: intVal}); 138 | return q; 139 | }; 140 | 141 | MockQuery.prototype.startAt = function (priority, key) { 142 | assertQuery('Query.startAt', priority, key); 143 | var q = new MockQuery(this.ref()); 144 | _.extend(q._q, this._q, {startKey: key, startPri: priority}); 145 | return q; 146 | }; 147 | 148 | MockQuery.prototype.endAt = function (priority, key) { 149 | assertQuery('Query.endAt', priority, key); 150 | var q = new MockQuery(this.ref()); 151 | _.extend(q._q, this._q, {endKey: key, endPri: priority}); 152 | return q; 153 | }; 154 | 155 | function assertQuery (method, pri, key) { 156 | if (pri !== null && typeof(pri) !== 'string' && typeof(pri) !== 'number') { 157 | throw new Error(method + ' failed: first argument must be a valid firebase priority (a string, number, or null).'); 158 | } 159 | if (!_.isUndefined(key)) { 160 | utils.assertKey(method, key, 'second'); 161 | } 162 | } 163 | 164 | module.exports = MockQuery; 165 | -------------------------------------------------------------------------------- /src/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var util = require('util'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | function FlushQueue () { 8 | this.events = []; 9 | } 10 | 11 | FlushQueue.prototype.push = function () { 12 | var self = this; 13 | this.events.push.apply(this.events, _.toArray(arguments).map(function (event) { 14 | if (typeof event === 'function') { 15 | event = { 16 | fn: event 17 | }; 18 | } 19 | return new FlushEvent(event.fn, event.context, event.sourceData) 20 | .once('done', function (event) { 21 | self.events.splice(self.events.indexOf(event), 1); 22 | }); 23 | })); 24 | }; 25 | 26 | FlushQueue.prototype.flushing = false; 27 | 28 | FlushQueue.prototype.flush = function (delay) { 29 | if (this.flushing) return; 30 | var self = this; 31 | if (!this.events.length) { 32 | throw new Error('No deferred tasks to be flushed'); 33 | } 34 | function process () { 35 | self.flushing = true; 36 | while (self.events.length) { 37 | self.events[0].run(); 38 | } 39 | self.flushing = false; 40 | } 41 | if (_.isNumber(delay)) { 42 | setTimeout(process, delay); 43 | } 44 | else { 45 | process(); 46 | } 47 | }; 48 | 49 | FlushQueue.prototype.getEvents = function () { 50 | return this.events.slice(); 51 | }; 52 | 53 | function FlushEvent (fn, context, sourceData) { 54 | this.fn = fn; 55 | this.context = context; 56 | // stores data about the event so that we can filter items in the queue 57 | this.sourceData = sourceData; 58 | 59 | EventEmitter.call(this); 60 | } 61 | 62 | util.inherits(FlushEvent, EventEmitter); 63 | 64 | FlushEvent.prototype.run = function () { 65 | this.fn.call(this.context); 66 | this.emit('done', this); 67 | }; 68 | 69 | FlushEvent.prototype.cancel = function () { 70 | this.emit('done', this); 71 | }; 72 | 73 | exports.Queue = FlushQueue; 74 | exports.Event = FlushEvent; 75 | -------------------------------------------------------------------------------- /src/slice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Snapshot = require('./snapshot'); 5 | var utils = require('./utils'); 6 | 7 | function Slice (queue, snap) { 8 | var data = snap? snap.val() : queue.ref().getData(); 9 | this.ref = snap? snap.ref() : queue.ref(); 10 | this.priority = snap? snap.getPriority() : this.ref.priority; 11 | this.pris = {}; 12 | this.data = {}; 13 | this.map = {}; 14 | this.outerMap = {}; 15 | this.keys = []; 16 | this.props = this._makeProps(queue._q, this.ref, this.ref.getKeys().length); 17 | this._build(this.ref, data); 18 | } 19 | 20 | Slice.prototype.prev = function (key) { 21 | var pos = this.pos(key); 22 | if( pos === 0 ) { return null; } 23 | else { 24 | if( pos < 0 ) { pos = this.keys.length; } 25 | return this.keys[pos-1]; 26 | } 27 | }; 28 | 29 | Slice.prototype.equals = function (slice) { 30 | return _.isEqual(this.keys, slice.keys) && _.isEqual(this.data, slice.data); 31 | }; 32 | 33 | Slice.prototype.pos = function (key) { 34 | return this.has(key) ? this.map[key] : -1; 35 | }; 36 | 37 | Slice.prototype.insertPos = function (prevChild) { 38 | var outerPos = this.outerMap[prevChild]; 39 | if( outerPos >= this.min && outerPos < this.max ) { 40 | return outerPos+1; 41 | } 42 | return -1; 43 | }; 44 | 45 | Slice.prototype.has = function (key) { 46 | return this.map.hasOwnProperty(key); 47 | }; 48 | 49 | Slice.prototype.snap = function (key) { 50 | var ref = this.ref; 51 | var data = this.data; 52 | var pri = this.priority; 53 | if( key ) { 54 | data = this.get(key); 55 | ref = ref.child(key); 56 | pri = this.pri(key); 57 | } 58 | return new Snapshot(ref, data, pri); 59 | }; 60 | 61 | Slice.prototype.get = function (key) { 62 | return this.has(key)? this.data[key] : null; 63 | }; 64 | 65 | Slice.prototype.pri = function (key) { 66 | return this.has(key)? this.pris[key] : null; 67 | }; 68 | 69 | Slice.prototype.changeMap = function (slice) { 70 | var changes = { added: [], removed: [] }; 71 | _.each(this.data, function(v,k) { 72 | if( !slice.has(k) ) { 73 | changes.removed.push(k); 74 | } 75 | }); 76 | _.each(slice.data, function(v,k) { 77 | if( !this.has(k) ) { 78 | changes.added.push(k); 79 | } 80 | }, this); 81 | return changes; 82 | }; 83 | 84 | Slice.prototype._inRange = function (props, key, pri, pos) { 85 | if( pos === -1 ) { return false; } 86 | if( !_.isUndefined(props.startPri) && utils.priorityComparator(pri, props.startPri) < 0 ) { 87 | return false; 88 | } 89 | if( !_.isUndefined(props.startKey) && utils.priorityComparator(key, props.startKey) < 0 ) { 90 | return false; 91 | } 92 | if( !_.isUndefined(props.endPri) && utils.priorityComparator(pri, props.endPri) > 0 ) { 93 | return false; 94 | } 95 | if( !_.isUndefined(props.endKey) && utils.priorityComparator(key, props.endKey) > 0 ) { 96 | return false; 97 | } 98 | if( props.max > -1 && pos > props.max ) { 99 | return false; 100 | } 101 | return pos >= props.min; 102 | }; 103 | 104 | Slice.prototype._findPos = function (pri, key, ref, isStartBoundary) { 105 | var keys = ref.getKeys(), firstMatch = -1, lastMatch = -1; 106 | var len = keys.length, i, x, k; 107 | if(_.isUndefined(pri) && _.isUndefined(key)) { 108 | return -1; 109 | } 110 | for(i = 0; i < len; i++) { 111 | k = keys[i]; 112 | x = utils.priAndKeyComparator(pri, key, ref.child(k).priority, k); 113 | if( x === 0 ) { 114 | // if the key is undefined, we may have several matching comparisons 115 | // so we will record both the first and last successful match 116 | if (firstMatch === -1) { 117 | firstMatch = i; 118 | } 119 | lastMatch = i; 120 | } 121 | else if( x < 0 ) { 122 | // we found the breakpoint where our keys exceed the match params 123 | if( i === 0 ) { 124 | // if this is 0 then our match point is before the data starts, we 125 | // will use len here because -1 already has a special meaning (no limit) 126 | // and len ensures we won't get any data (no matches) 127 | i = len; 128 | } 129 | break; 130 | } 131 | } 132 | 133 | if( firstMatch !== -1 ) { 134 | // we found a match, life is simple 135 | return isStartBoundary? firstMatch : lastMatch; 136 | } 137 | else if( i < len ) { 138 | // if we're looking for the start boundary then it's the first record after 139 | // the breakpoint. If we're looking for the end boundary, it's the last record before it 140 | return isStartBoundary? i : i -1; 141 | } 142 | else { 143 | // we didn't find one, so use len (i.e. after the data, no results) 144 | return len; 145 | } 146 | }; 147 | 148 | Slice.prototype._makeProps = function (queueProps, ref, numRecords) { 149 | var out = {}; 150 | _.each(queueProps, function(v,k) { 151 | if(!_.isUndefined(v)) { 152 | out[k] = v; 153 | } 154 | }); 155 | out.min = this._findPos(out.startPri, out.startKey, ref, true); 156 | out.max = this._findPos(out.endPri, out.endKey, ref); 157 | if( !_.isUndefined(queueProps.limit) ) { 158 | if( out.min > -1 ) { 159 | out.max = out.min + queueProps.limit; 160 | } 161 | else if( out.max > -1 ) { 162 | out.min = out.max - queueProps.limit; 163 | } 164 | else if( queueProps.limit < numRecords ) { 165 | out.max = numRecords-1; 166 | out.min = Math.max(0, numRecords - queueProps.limit); 167 | } 168 | } 169 | return out; 170 | }; 171 | 172 | Slice.prototype._build = function(ref, rawData) { 173 | var i = 0, map = this.map, keys = this.keys, outer = this.outerMap; 174 | var props = this.props, slicedData = this.data; 175 | _.each(rawData, function(v,k) { 176 | outer[k] = i < props.min? props.min - i : i - Math.max(props.min,0); 177 | if( this._inRange(props, k, ref.child(k).priority, i++) ) { 178 | map[k] = keys.length; 179 | keys.push(k); 180 | slicedData[k] = v; 181 | } 182 | }, this); 183 | }; 184 | 185 | module.exports = Slice; 186 | -------------------------------------------------------------------------------- /src/snapshot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | function MockDataSnapshot (ref, data, priority) { 6 | this.ref = function () { 7 | return ref; 8 | }; 9 | data = _.cloneDeep(data); 10 | if (_.isObject(data) && _.isEmpty(data)) { 11 | data = null; 12 | } 13 | this.val = function () { 14 | return data; 15 | }; 16 | this.getPriority = function () { 17 | return priority; 18 | }; 19 | } 20 | 21 | MockDataSnapshot.prototype.child = function (key) { 22 | var ref = this.ref().child(key); 23 | var data = this.hasChild(key) ? this.val()[key] : null; 24 | var priority = this.ref().child(key).priority; 25 | return new MockDataSnapshot(ref, data, priority); 26 | }; 27 | 28 | MockDataSnapshot.prototype.exists = function () { 29 | return this.val() !== null; 30 | }; 31 | 32 | MockDataSnapshot.prototype.forEach = function (callback, context) { 33 | _.each(this.val(), function (value, key) { 34 | callback.call(context, this.child(key)); 35 | }, this); 36 | }; 37 | 38 | MockDataSnapshot.prototype.hasChild = function (path) { 39 | return !!(this.val() && this.val()[path]); 40 | }; 41 | 42 | MockDataSnapshot.prototype.hasChildren = function () { 43 | return !!this.numChildren(); 44 | }; 45 | 46 | MockDataSnapshot.prototype.key = function () { 47 | return this.ref().key(); 48 | }; 49 | 50 | MockDataSnapshot.prototype.name = function () { 51 | console.warn('DataSnapshot.name() is deprecated. Use DataSnapshot.key()'); 52 | return this.key.apply(this, arguments); 53 | }; 54 | 55 | MockDataSnapshot.prototype.numChildren = function () { 56 | return _.size(this.val()); 57 | }; 58 | 59 | 60 | MockDataSnapshot.prototype.exportVal = function () { 61 | var exportData = {}; 62 | var priority = this.getPriority(); 63 | var hasPriority = _.isString(priority) || _.isNumber(priority); 64 | if (hasPriority) { 65 | exportData['.priority'] = priority; 66 | } 67 | if (isValue(this.val())) { 68 | if (hasPriority) { 69 | exportData['.value'] = this.val(); 70 | } 71 | else { 72 | exportData = this.val(); 73 | } 74 | } 75 | else { 76 | _.reduce(this.val(), function (acc, value, key) { 77 | acc[key] = this.child(key).exportVal(); 78 | return acc; 79 | }, exportData, this); 80 | } 81 | return exportData; 82 | }; 83 | 84 | function isValue (value) { 85 | return !_.isObject(value); 86 | } 87 | 88 | module.exports = MockDataSnapshot; 89 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Snapshot = require('./snapshot'); 4 | var _ = require('lodash'); 5 | 6 | exports.makeRefSnap = function makeRefSnap(ref) { 7 | return new Snapshot(ref, ref.getData(), ref.priority); 8 | }; 9 | 10 | exports.mergePaths = function mergePaths (base, add) { 11 | return base.replace(/\/$/, '')+'/'+add.replace(/^\//, ''); 12 | }; 13 | 14 | exports.cleanData = function cleanData(data) { 15 | var newData = _.clone(data); 16 | if(_.isObject(newData)) { 17 | if(_.has(newData, '.value')) { 18 | newData = _.clone(newData['.value']); 19 | } 20 | if(_.has(newData, '.priority')) { 21 | delete newData['.priority']; 22 | } 23 | // _.each(newData, function(v,k) { 24 | // newData[k] = cleanData(v); 25 | // }); 26 | if(_.isEmpty(newData)) { newData = null; } 27 | } 28 | return newData; 29 | }; 30 | 31 | exports.getMeta = function getMeta (data, key, defaultVal) { 32 | var val = defaultVal; 33 | var metaKey = '.' + key; 34 | if (_.isObject(data) && _.has(data, metaKey)) { 35 | val = data[metaKey]; 36 | delete data[metaKey]; 37 | } 38 | return val; 39 | }; 40 | 41 | exports.assertKey = function assertKey (method, key, argNum) { 42 | if (!argNum) argNum = 'first'; 43 | if (typeof(key) !== 'string' || key.match(/[.#$\/\[\]]/)) { 44 | throw new Error(method + ' failed: '+ argNum+' was an invalid key "'+(key+'')+'. Firebase keys must be non-empty strings and can\'t contain ".", "#", "$", "/", "[", or "]"'); 45 | } 46 | }; 47 | 48 | exports.priAndKeyComparator = function priAndKeyComparator (testPri, testKey, valPri, valKey) { 49 | var x = 0; 50 | if (!_.isUndefined(testPri)) { 51 | x = exports.priorityComparator(testPri, valPri); 52 | } 53 | if (x === 0 && !_.isUndefined(testKey) && testKey !== valKey) { 54 | x = testKey < valKey? -1 : 1; 55 | } 56 | return x; 57 | }; 58 | 59 | exports.priorityComparator = function priorityComparator (a, b) { 60 | if (a !== b) { 61 | if (a === null || b === null) { 62 | return a === null? -1 : 1; 63 | } 64 | if (typeof a !== typeof b) { 65 | return typeof a === 'number' ? -1 : 1; 66 | } else { 67 | return a > b ? 1 : -1; 68 | } 69 | } 70 | return 0; 71 | }; 72 | 73 | exports.isServerTimestamp = function isServerTimestamp (data) { 74 | return _.isObject(data) && data['.sv'] === 'timestamp'; 75 | }; 76 | -------------------------------------------------------------------------------- /src/validators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var format = require('util').format; 5 | 6 | var events = ['value', 'child_added', 'child_removed', 'child_changed', 'child_moved']; 7 | exports.event = function (name) { 8 | assert(events.indexOf(name) > -1, format('"%s" is not a valid event, must be: %s', name, events.map(function (event) { 9 | return format('"%s"', event); 10 | }).join(', '))); 11 | }; 12 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "globals": { 4 | "describe": false, 5 | "beforeEach": false, 6 | "afterEach": false, 7 | "it": false, 8 | "xit": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/smoke/globals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint browser:true */ 4 | /* globals expect:false */ 5 | 6 | describe('Custom UMD Build', function () { 7 | 8 | var OriginalFirebase, OriginalFirebaseSimpleLogin; 9 | beforeEach(function () { 10 | window.Firebase = OriginalFirebase = {}; 11 | window.FirebaseSimpleLogin = OriginalFirebaseSimpleLogin = {}; 12 | }); 13 | 14 | it('exposes the full module as "mockfirebase"', function () { 15 | expect(window).to.have.property('mockfirebase').that.is.an('object'); 16 | }); 17 | 18 | it('exposes "MockFirebase" on the window', function () { 19 | expect(window) 20 | .to.have.property('MockFirebase') 21 | .that.equals(window.mockfirebase.MockFirebase); 22 | }); 23 | 24 | it('exposes "MockFirebaseSimpleLogin" on the window', function () { 25 | expect(window) 26 | .to.have.property('MockFirebaseSimpleLogin') 27 | .that.equals(window.mockfirebase.MockFirebaseSimpleLogin); 28 | }); 29 | 30 | describe('#restore', function () { 31 | 32 | it('is a noop before #override is called', function () { 33 | window.MockFirebase.restore(); 34 | expect(window) 35 | .to.have.property('Firebase') 36 | .that.equals(OriginalFirebase); 37 | expect(window) 38 | .to.have.property('FirebaseSimpleLogin') 39 | .that.equals(OriginalFirebaseSimpleLogin); 40 | }); 41 | 42 | it('can restore Firebase', function () { 43 | window.MockFirebase.override(); 44 | window.MockFirebase.restore(); 45 | expect(window) 46 | .to.have.property('Firebase') 47 | .that.equals(OriginalFirebase); 48 | expect(window) 49 | .to.have.property('FirebaseSimpleLogin') 50 | .that.equals(OriginalFirebaseSimpleLogin); 51 | }); 52 | 53 | }); 54 | 55 | describe('#override', function () { 56 | 57 | it('can override Firebase', function () { 58 | window.MockFirebase.override(); 59 | expect(window) 60 | .to.have.property('Firebase') 61 | .that.equals(window.mockfirebase.MockFirebase); 62 | expect(window) 63 | .to.have.property('FirebaseSimpleLogin') 64 | .that.equals(window.mockfirebase.MockFirebaseSimpleLogin); 65 | }); 66 | 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/unit/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var expect = require('chai').use(require('sinon-chai')).expect; 5 | var _ = require('lodash'); 6 | var Firebase = require('../../').MockFirebase; 7 | 8 | describe('Auth', function () { 9 | 10 | var ref, spy; 11 | beforeEach(function () { 12 | ref = new Firebase().child('data'); 13 | spy = sinon.spy(); 14 | }); 15 | 16 | describe('#changeAuthState', function () { 17 | 18 | it('sets the auth data', function () { 19 | var user = {}; 20 | ref.changeAuthState(user); 21 | ref.flush(); 22 | expect(ref.getAuth()).to.equal(user); 23 | }); 24 | 25 | it('is a noop if deeply equal', function () { 26 | var user = {}; 27 | ref.changeAuthState(user); 28 | ref.flush(); 29 | ref.changeAuthState({}); 30 | expect(ref.getAuth()).to.equal(user); 31 | }); 32 | 33 | it('is a noop if deeply equal', function () { 34 | var user = {}; 35 | ref.changeAuthState(user); 36 | ref.flush(); 37 | ref.changeAuthState({}); 38 | expect(ref.getAuth()).to.equal(user); 39 | }); 40 | 41 | it('sets null for a non object', function () { 42 | ref.changeAuthState({}); 43 | ref.flush(); 44 | ref.changeAuthState('auth'); 45 | ref.flush(); 46 | expect(ref.getAuth()).to.equal(null); 47 | }); 48 | 49 | it('triggers an auth event', function () { 50 | var user = { 51 | uid: 'ben' 52 | }; 53 | ref.changeAuthState(user); 54 | ref.onAuth(spy); 55 | spy.reset(); 56 | ref.flush(); 57 | expect(spy.firstCall.args[0]).to.not.equal(user); 58 | expect(spy.firstCall.args[0]).to.deep.equal(user); 59 | }); 60 | 61 | }); 62 | 63 | describe('#getEmailUser', function () { 64 | 65 | it('gets a copy of the user by email', function () { 66 | var user = { 67 | uid: 'bd' 68 | }; 69 | ref._auth.users['ben@example.com'] = user; 70 | expect(ref.getEmailUser('ben@example.com')).to.deep.equal(user); 71 | }); 72 | 73 | it('only searches own properties', function () { 74 | expect(ref.getEmailUser('toString')).to.equal(null); 75 | }); 76 | 77 | }); 78 | 79 | describe('#auth', function () { 80 | 81 | it('calls callback on auth state change', function () { 82 | var userData = { 83 | uid: 'kato' 84 | }; 85 | spy = sinon.spy(function (error, authData) { 86 | expect(error).to.equal(null); 87 | expect(authData).to.deep.equal(userData); 88 | }); 89 | ref.auth('goodToken', spy); 90 | ref.changeAuthState(userData); 91 | ref.flush(); 92 | expect(spy.called).to.equal(true); 93 | }); 94 | 95 | }); 96 | 97 | describe('#authWithCustomToken', function () { 98 | 99 | it('calls the callback with a nextErr', function () { 100 | spy = sinon.spy(function (error, result) { 101 | expect(error.message).to.equal('INVALID_TOKEN'); 102 | expect(result).to.equal(null); 103 | }); 104 | ref.failNext('authWithCustomToken', new Error('INVALID_TOKEN')); 105 | ref.authWithCustomToken('invalidToken', spy); 106 | ref.flush(); 107 | expect(spy.called).to.equal(true); 108 | }); 109 | 110 | it('invokes the callback when auth state is set', function () { 111 | var user = { 112 | uid: 'kato' 113 | }; 114 | spy = sinon.spy(function (error, authData) { 115 | expect(error).to.equal(null); 116 | expect(authData).to.deep.equal(user); 117 | }); 118 | ref.authWithCustomToken('goodToken', spy); 119 | ref.changeAuthState(user); 120 | ref.flush(); 121 | expect(spy.called).to.equal(true); 122 | }); 123 | 124 | it('handles no callback', function () { 125 | ref.authWithCustomToken('goodToken'); 126 | }); 127 | 128 | }); 129 | 130 | describe('#getAuth', function () { 131 | 132 | it('is null by default', function () { 133 | expect(ref.getAuth()).to.equal(null); 134 | }); 135 | 136 | it('returns the value from changeAuthState', function () { 137 | ref.changeAuthState({ 138 | foo: 'bar' 139 | }); 140 | ref.flush(); 141 | expect(ref.getAuth()).to.deep.equal({ 142 | foo: 'bar' 143 | }); 144 | }); 145 | 146 | }); 147 | 148 | describe('#onAuth', function () { 149 | 150 | it('is triggered when changeAuthState modifies data', function () { 151 | ref.onAuth(spy); 152 | ref.changeAuthState({ 153 | uid: 'kato' 154 | }); 155 | ref.flush(); 156 | expect(spy).to.have.been.calledWithMatch({ 157 | uid: 'kato' 158 | }); 159 | }); 160 | 161 | it('is not be triggered if auth state does not change', function () { 162 | ref.onAuth(spy); 163 | ref.changeAuthState({ 164 | uid: 'kato' 165 | }); 166 | ref.flush(); 167 | spy.reset(); 168 | ref.changeAuthState({ 169 | uid: 'kato' 170 | }); 171 | ref.flush(); 172 | expect(spy.called).to.equal(false); 173 | }); 174 | 175 | it('can set a context', function () { 176 | var context = {}; 177 | ref.onAuth(spy, context); 178 | ref.changeAuthState(); 179 | ref.flush(); 180 | expect(spy).to.have.been.calledOn(context); 181 | }); 182 | 183 | it('synchronously triggers the callback with the current auth data', function () { 184 | ref.onAuth(spy); 185 | expect(spy).to.have.been.calledWith(null); 186 | }); 187 | 188 | }); 189 | 190 | describe('#offAuth', function () { 191 | 192 | it('removes a callback', function () { 193 | ref.onAuth(spy); 194 | ref.changeAuthState({ 195 | uid: 'kato1' 196 | }); 197 | ref.flush(); 198 | spy.reset(); 199 | ref.offAuth(spy); 200 | ref.changeAuthState({ 201 | uid: 'kato1' 202 | }); 203 | ref.flush(); 204 | expect(spy.called).to.equal(false); 205 | }); 206 | 207 | it('only removes callback that matches the context', function () { 208 | var context = {}; 209 | ref.onAuth(spy); 210 | ref.onAuth(spy, context); 211 | ref.changeAuthState({ 212 | uid: 'kato1' 213 | }); 214 | ref.flush(); 215 | expect(spy.callCount).to.equal(4); 216 | spy.reset(); 217 | // will not match any context 218 | ref.offAuth(spy, {}); 219 | ref.offAuth(spy, context); 220 | ref.changeAuthState({ 221 | uid: 'kato2' 222 | }); 223 | ref.flush(); 224 | expect(spy.callCount).to.equal(1); 225 | }); 226 | 227 | }); 228 | 229 | describe('#unauth', function () { 230 | 231 | it('sets auth data to null', function () { 232 | ref.changeAuthState({ 233 | uid: 'kato' 234 | }); 235 | ref.flush(); 236 | expect(ref.getAuth()).not.not.equal(null); 237 | ref.unauth(); 238 | expect(ref.getAuth()).to.equal(null); 239 | }); 240 | 241 | it('triggers onAuth callback if not null', function () { 242 | ref.changeAuthState({ 243 | uid: 'kato' 244 | }); 245 | ref.flush(); 246 | ref.onAuth(spy); 247 | ref.unauth(); 248 | expect(spy).to.have.been.calledWith(null); 249 | }); 250 | 251 | it('does not trigger auth events if not authenticated', function () { 252 | ref.onAuth(spy); 253 | spy.reset(); 254 | ref.unauth(); 255 | expect(spy.callCount).to.equal(0); 256 | }); 257 | 258 | }); 259 | 260 | describe('#createUser', function () { 261 | 262 | it('creates a new user', function () { 263 | ref.createUser({ 264 | email: 'new1@new1.com', 265 | password: 'new1' 266 | }, spy); 267 | ref.flush(); 268 | expect(spy).to.have.been.calledWithMatch(null, { 269 | uid: 'simplelogin:1' 270 | }); 271 | }); 272 | 273 | it('increments the id for each user', function () { 274 | ref.createUser({ 275 | email: 'new1@new1.com', 276 | password: 'new1' 277 | }, spy); 278 | ref.createUser({ 279 | email: 'new2@new2.com', 280 | password: 'new2' 281 | }, spy); 282 | ref.flush(); 283 | expect(spy.firstCall.args[1].uid).to.equal('simplelogin:1'); 284 | expect(spy.secondCall.args[1].uid).to.equal('simplelogin:2'); 285 | }); 286 | 287 | it('fails if credentials is not an object', function () { 288 | expect(ref.createUser.bind(ref, 29)).to.throw('must be a valid object'); 289 | }); 290 | 291 | it('fails if email is not a string', function () { 292 | expect(ref.createUser.bind(ref, { 293 | email: true, 294 | password: 'foo' 295 | })) 296 | .to.throw('must contain the key "email"'); 297 | }); 298 | 299 | it('fails if password is not a string', function () { 300 | expect(ref.createUser.bind(ref, { 301 | email: 'email@domain.com', 302 | password: true 303 | })) 304 | .to.throw('must contain the key "password"'); 305 | }); 306 | 307 | it('fails if user already exists', function () { 308 | ref.createUser({ 309 | email: 'duplicate@dup.com', 310 | password: 'foo' 311 | }, _.noop); 312 | ref.flush(); 313 | ref.createUser({ 314 | email: 'duplicate@dup.com', 315 | password: 'bar' 316 | }, spy); 317 | ref.flush(); 318 | var err = spy.firstCall.args[0]; 319 | expect(err.message).to.contain('email address is already in use'); 320 | expect(err.code).to.equal('EMAIL_TAKEN'); 321 | }); 322 | 323 | it('fails if failNext is set', function () { 324 | var err = new Error(); 325 | ref.failNext('createUser', err); 326 | ref.createUser({ 327 | email: 'hello', 328 | password: 'world' 329 | }, spy); 330 | ref.flush(); 331 | expect(spy.firstCall.args[0]).to.equal(err); 332 | }); 333 | 334 | }); 335 | 336 | describe('#changeEmail', function () { 337 | 338 | beforeEach(function () { 339 | ref.createUser({ 340 | email: 'kato@kato.com', 341 | password: 'kato' 342 | }, _.noop); 343 | }); 344 | 345 | it('changes the email', function () { 346 | ref.changeEmail({ 347 | oldEmail: 'kato@kato.com', 348 | newEmail: 'kato@google.com', 349 | password: 'kato' 350 | }, _.noop); 351 | ref.flush(); 352 | expect(ref.getEmailUser('kato@google.com')) 353 | .to.have.property('password', 'kato'); 354 | expect(ref.getEmailUser('kato@kato.com')) 355 | .to.equal(null); 356 | }); 357 | 358 | it('fails if credentials is not an object', function () { 359 | expect(ref.changeEmail.bind(ref, 29)).to.throw('must be a valid object'); 360 | }); 361 | 362 | it('fails if oldEmail is not a string', function () { 363 | expect(ref.changeEmail.bind(ref, { 364 | oldEmail: true, 365 | newEmail: 'foo@foo.com', 366 | password: 'bar' 367 | })) 368 | .to.throw('must contain the key "oldEmail"'); 369 | }); 370 | 371 | it('should fail if newEmail is not a string', function () { 372 | expect(ref.changeEmail.bind(ref, { 373 | oldEmail: 'old@old.com', 374 | newEmail: null, 375 | password: 'bar' 376 | })) 377 | .to.throw('must contain the key "newEmail"'); 378 | }); 379 | 380 | it('fails if password is not a string', function () { 381 | expect(ref.changeEmail.bind(ref, { 382 | oldEmail: 'old@old.com', 383 | newEmail: 'new@new.com', 384 | password: null 385 | })) 386 | .to.throw('must contain the key "password"'); 387 | }); 388 | 389 | it('fails if user does not exist', function () { 390 | ref.changeEmail({ 391 | oldEmail: 'hello@foo.com', 392 | newEmail: 'kato@google.com', 393 | password: 'bar' 394 | }, spy); 395 | ref.flush(); 396 | var err = spy.firstCall.args[0]; 397 | expect(err.code).to.equal('INVALID_USER'); 398 | expect(err.message).to.contain('user does not exist'); 399 | }); 400 | 401 | it('fails if password is incorrect', function () { 402 | ref.changeEmail({ 403 | oldEmail: 'kato@kato.com', 404 | newEmail: 'kato@google.com', 405 | password: 'wrong' 406 | }, spy); 407 | ref.flush(); 408 | var err = spy.firstCall.args[0]; 409 | expect(err.code).to.equal('INVALID_PASSWORD'); 410 | expect(err.message).to.contain('password is incorrect'); 411 | }); 412 | 413 | it('fails if failNext is set', function () { 414 | var err = new Error(); 415 | ref.failNext('changeEmail', err); 416 | ref.changeEmail({ 417 | oldEmail: 'kato@kato.com', 418 | newEmail: 'kato@google.com', 419 | password: 'right' 420 | }, spy); 421 | ref.flush(); 422 | expect(spy).to.have.been.calledWith(err); 423 | }); 424 | 425 | }); 426 | 427 | describe('#changePassword', function () { 428 | 429 | it('changes the password', function () { 430 | ref.createUser({ 431 | email: 'kato@kato.com', 432 | password: 'kato' 433 | }, _.noop); 434 | ref.changePassword({ 435 | email: 'kato@kato.com', 436 | oldPassword: 'kato', 437 | newPassword: 'kato!' 438 | }, _.noop); 439 | ref.flush(); 440 | expect(ref.getEmailUser('kato@kato.com')) 441 | .to.have.property('password', 'kato!'); 442 | }); 443 | 444 | it('fails if credentials is not an object', function () { 445 | expect(ref.changePassword.bind(ref, 29)).to.throw('must be a valid object'); 446 | }); 447 | 448 | it('fails if email is not a string', function () { 449 | expect(ref.changePassword.bind(ref, { 450 | email: true, 451 | oldPassword: 'foo', 452 | newPassword: 'bar' 453 | })) 454 | .to.throw('must contain the key "email"'); 455 | }); 456 | 457 | it('should fail if oldPassword is not a string', function () { 458 | expect(ref.changePassword.bind(ref, { 459 | email: 'new1@new1.com', 460 | oldPassword: null, 461 | newPassword: 'bar' 462 | })) 463 | .to.throw('must contain the key "oldPassword"'); 464 | }); 465 | 466 | it('fails if newPassword is not a string', function () { 467 | expect(ref.changePassword.bind(ref, { 468 | email: 'new1@new1.com', 469 | oldPassword: 'foo' 470 | })) 471 | .to.throw('must contain the key "newPassword"'); 472 | }); 473 | 474 | it('fails if user does not exist', function () { 475 | ref.changePassword({ 476 | email: 'hello', 477 | oldPassword: 'foo', 478 | newPassword: 'bar' 479 | }, spy); 480 | ref.flush(); 481 | var err = spy.firstCall.args[0]; 482 | expect(err.code).to.equal('INVALID_USER'); 483 | expect(err.message).to.contain('user does not exist'); 484 | }); 485 | 486 | it('fails if oldPassword is incorrect', function () { 487 | ref.createUser({ 488 | email: 'kato@kato.com', 489 | password: 'kato' 490 | }, _.noop); 491 | ref.changePassword({ 492 | email: 'kato@kato.com', 493 | oldPassword: 'foo', 494 | newPassword: 'bar' 495 | }, spy); 496 | ref.flush(); 497 | var err = spy.firstCall.args[0]; 498 | expect(err.code).to.equal('INVALID_PASSWORD'); 499 | expect(err.message).to.contain('password is incorrect'); 500 | }); 501 | 502 | it('fails if failNext is set', function () { 503 | ref.createUser({ 504 | email: 'kato@kato.com', 505 | password: 'kato' 506 | }, _.noop); 507 | var err = new Error(); 508 | ref.failNext('changePassword', err); 509 | ref.changePassword({ 510 | email: 'kato@kato.com', 511 | oldPassword: 'kato', 512 | newPassword: 'new' 513 | }, spy); 514 | ref.flush(); 515 | expect(spy).to.have.been.calledWith(err); 516 | }); 517 | 518 | }); 519 | 520 | describe('#removeUser', function () { 521 | 522 | it('removes the account', function () { 523 | ref.createUser({ 524 | email: 'kato@kato.com', 525 | password: 'kato' 526 | }, _.noop); 527 | ref.flush(); 528 | expect(ref.getEmailUser('kato@kato.com')).to.deep.equal({ 529 | uid: 'simplelogin:1', 530 | email: 'kato@kato.com', 531 | password: 'kato' 532 | }); 533 | ref.removeUser({ 534 | email: 'kato@kato.com', 535 | password: 'kato' 536 | }, _.noop); 537 | ref.flush(); 538 | expect(ref.getEmailUser('kato@kato.com')).to.equal(null); 539 | }); 540 | 541 | it('fails if credentials is not an object', function () { 542 | expect(ref.removeUser.bind(ref, 29)).to.throw('must be a valid object'); 543 | }); 544 | 545 | it('fails if email is not a string', function () { 546 | expect(ref.removeUser.bind(ref, { 547 | email: true, 548 | password: 'foo' 549 | })) 550 | .to.throw('must contain the key "email" with type "string"'); 551 | }); 552 | 553 | it('fails if password is not a string', function () { 554 | expect(ref.removeUser.bind(ref, { 555 | email: 'new1@new1.com', 556 | password: null 557 | })) 558 | .to.throw('must contain the key "password" with type "string"'); 559 | }); 560 | 561 | it('fails if user does not exist', function () { 562 | ref.removeUser({ 563 | email: 'hello', 564 | password: 'foo' 565 | }, spy); 566 | ref.flush(); 567 | var err = spy.firstCall.args[0]; 568 | expect(err.code).to.equal('INVALID_USER'); 569 | expect(err.message).to.contain('user does not exist'); 570 | }); 571 | 572 | it('fails if password is incorrect', function () { 573 | ref.createUser({ 574 | email: 'kato@kato.com', 575 | password: 'kato' 576 | }, _.noop); 577 | ref.removeUser({ 578 | email: 'kato@kato.com', 579 | password: 'foo' 580 | }, spy); 581 | ref.flush(); 582 | var err = spy.firstCall.args[0]; 583 | expect(err.code).to.equal('INVALID_PASSWORD'); 584 | expect(err.message).to.contain('password is incorrect'); 585 | }); 586 | 587 | it('fails if failNext is set', function () { 588 | ref.createUser({ 589 | email: 'kato@kato.com', 590 | password: 'kato' 591 | }, _.noop); 592 | var err = new Error(); 593 | ref.failNext('removeUser', err); 594 | ref.removeUser({ 595 | email: 'hello', 596 | password: 'foo' 597 | }, spy); 598 | ref.flush(); 599 | expect(spy).to.have.been.calledWith(err); 600 | }); 601 | 602 | }); 603 | 604 | describe('#resetPassword', function () { 605 | 606 | it('simulates reset if credentials are valid', function () { 607 | ref.createUser({ 608 | email: 'kato@kato.com', 609 | password: 'kato' 610 | }, _.noop); 611 | ref.resetPassword({ 612 | email: 'kato@kato.com' 613 | }, spy); 614 | ref.flush(); 615 | expect(spy).to.have.been.calledWith(null); 616 | }); 617 | 618 | it('fails if credentials is not an object', function () { 619 | expect(ref.resetPassword.bind(ref, 29)).to.throw('must be a valid object'); 620 | }); 621 | 622 | it('fails if email is not a string', function () { 623 | expect(ref.resetPassword.bind(ref, { 624 | email: true 625 | })) 626 | .to.throw('must contain the key "email" with type "string"'); 627 | }); 628 | 629 | it('fails if user does not exist', function () { 630 | ref.resetPassword({ 631 | email: 'hello' 632 | }, spy); 633 | ref.flush(); 634 | var err = spy.firstCall.args[0]; 635 | expect(err.code).to.equal('INVALID_USER'); 636 | expect(err.message).to.contain('user does not exist'); 637 | }); 638 | 639 | it('fails if failNext is set', function () { 640 | ref.createUser({ 641 | email: 'kato@kato.com', 642 | password: 'kato' 643 | }, _.noop); 644 | var err = new Error(); 645 | ref.failNext('resetPassword', err); 646 | ref.resetPassword({ 647 | email: 'kato@kato.com', 648 | password: 'foo' 649 | }, spy); 650 | ref.flush(); 651 | expect(spy).to.have.been.calledWith(err); 652 | }); 653 | 654 | }); 655 | 656 | }); 657 | -------------------------------------------------------------------------------- /test/unit/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "a": { 4 | "aString": "alpha", 5 | "aNumber": 1, 6 | "aBoolean": false 7 | }, 8 | "b": { 9 | "aString": "bravo", 10 | "aNumber": 2, 11 | "aBoolean": true 12 | }, 13 | "c": { 14 | "aString": "charlie", 15 | "aNumber": 3, 16 | "aBoolean": true 17 | }, 18 | "d": { 19 | "aString": "delta", 20 | "aNumber": 4, 21 | "aBoolean": true 22 | }, 23 | "e": { 24 | "aString": "echo", 25 | "aNumber": 5 26 | } 27 | }, 28 | "index": { 29 | "b": true, 30 | "c": 1, 31 | "e": false, 32 | "z": true 33 | }, 34 | "ordered": { 35 | "null_a": { 36 | "aNumber": 0, 37 | "aLetter": "a" 38 | }, 39 | "null_b": { 40 | "aNumber": 0, 41 | "aLetter": "b" 42 | }, 43 | "null_c": { 44 | "aNumber": 0, 45 | "aLetter": "c" 46 | }, 47 | "num_1_a": { 48 | ".priority": 1, 49 | "aNumber": 1 50 | }, 51 | "num_1_b": { 52 | ".priority": 1, 53 | "aNumber": 1 54 | }, 55 | "num_2": { 56 | ".priority": 2, 57 | "aNumber": 2 58 | }, 59 | "num_3": { 60 | ".priority": 3, 61 | "aNumber": 3 62 | }, 63 | "char_a_1": { 64 | ".priority": "a", 65 | "aNumber": 1, 66 | "aLetter": "a" 67 | }, 68 | "char_a_2": { 69 | ".priority": "a", 70 | "aNumber": 2, 71 | "aLetter": "a" 72 | }, 73 | "char_b": { 74 | ".priority": "b", 75 | "aLetter": "b" 76 | }, 77 | "char_c": { 78 | ".priority": "c", 79 | "aLetter": "c" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/unit/firebase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var expect = require('chai').use(require('sinon-chai')).expect; 5 | var _ = require('lodash'); 6 | var Firebase = require('../../').MockFirebase; 7 | 8 | describe('MockFirebase', function () { 9 | 10 | var ref, spy; 11 | beforeEach(function () { 12 | ref = new Firebase().child('data'); 13 | ref.set(require('./data.json').data); 14 | ref.flush(); 15 | spy = sinon.spy(); 16 | }); 17 | 18 | describe('Server Timestamps', function () { 19 | 20 | var clock; 21 | beforeEach(function () { 22 | clock = sinon.useFakeTimers(); 23 | }); 24 | 25 | afterEach(function () { 26 | clock.restore(); 27 | }); 28 | 29 | it('parses server timestamps', function () { 30 | ref.set(Firebase.ServerValue.TIMESTAMP); 31 | ref.flush(); 32 | expect(ref.getData()).to.equal(new Date().getTime()); 33 | }); 34 | 35 | it('parses server timestamps in child data', function () { 36 | var child = ref.child('foo'); 37 | ref.set({ 38 | foo: Firebase.ServerValue.TIMESTAMP 39 | }); 40 | ref.flush(); 41 | expect(child.getData()).to.equal(new Date().getTime()); 42 | }); 43 | 44 | it('parses server timestamps in priorities', function(){ 45 | ref.setPriority(Firebase.ServerValue.TIMESTAMP); 46 | ref.flush(); 47 | expect(ref).to.have.property('priority', new Date().getTime()); 48 | }); 49 | 50 | describe('Firebase#setClock', function () { 51 | 52 | afterEach(Firebase.restoreClock); 53 | 54 | it('sets a timestamp factory function', function () { 55 | var customClock = sinon.stub().returns(10); 56 | Firebase.setClock(customClock); 57 | ref.set(Firebase.ServerValue.TIMESTAMP); 58 | ref.flush(); 59 | expect(customClock.callCount).to.equal(1); 60 | expect(ref.getData()).to.equal(10); 61 | }); 62 | 63 | }); 64 | 65 | describe('#restoreClock', function () { 66 | 67 | it('restores the normal clock', function () { 68 | Firebase.setClock(spy); 69 | Firebase.restoreClock(); 70 | ref.set(Firebase.ServerValue.TIMESTAMP); 71 | ref.flush(); 72 | expect(spy.called).to.equal(false); 73 | expect(ref.getData()).to.equal(new Date().getTime()); 74 | }); 75 | 76 | }); 77 | 78 | }); 79 | 80 | describe('#flush', function () { 81 | 82 | it('flushes the queue and returns itself', function () { 83 | sinon.stub(ref.queue, 'flush'); 84 | expect(ref.flush(10)).to.equal(ref); 85 | expect(ref.queue.flush).to.have.been.calledWith(10); 86 | }); 87 | 88 | }); 89 | 90 | describe('#autoFlush', function () { 91 | 92 | it('enables autoflush with no args', function () { 93 | ref.autoFlush(); 94 | expect(ref.flushDelay).to.equal(true); 95 | }); 96 | 97 | it('can specify a flush delay', function () { 98 | ref.autoFlush(10); 99 | expect(ref.flushDelay).to.equal(10); 100 | }); 101 | 102 | it('sets the delay on all children', function () { 103 | ref.child('key'); 104 | ref.autoFlush(10); 105 | expect(ref.child('key').flushDelay).to.equal(10); 106 | }); 107 | 108 | it('sets the delay on a parent', function () { 109 | ref.child('key').autoFlush(10); 110 | expect(ref.flushDelay).to.equal(10); 111 | }); 112 | 113 | it('returns itself', function () { 114 | expect(ref.autoFlush()).to.equal(ref); 115 | }); 116 | 117 | }); 118 | 119 | describe('#failNext', function () { 120 | 121 | it('must be called with an Error', function () { 122 | expect(ref.failNext.bind(ref)).to.throw('"Error"'); 123 | }); 124 | 125 | }); 126 | 127 | describe('#forceCancel', function () { 128 | 129 | it('calls the cancel callback', function () { 130 | ref.on('value', _.noop, spy); 131 | var err = new Error(); 132 | ref.forceCancel(err); 133 | expect(spy).to.have.been.calledWith(err); 134 | }); 135 | 136 | it('calls the cancel callback on the context', function () { 137 | var context = {}; 138 | ref.on('value', _.noop, spy, context); 139 | ref.forceCancel(new Error(), 'value', _.noop, context); 140 | expect(spy).to.have.been.calledOn(context); 141 | }); 142 | 143 | it('turns off the listener', function () { 144 | ref.on('value', spy); 145 | ref.forceCancel(new Error()); 146 | ref.set({}); 147 | ref.flush(); 148 | expect(spy.called).to.equal(false); 149 | }); 150 | 151 | it('can match an event type', function () { 152 | var spy2 = sinon.spy(); 153 | ref.on('value', _.noop, spy); 154 | ref.on('child_added', _.noop, spy2); 155 | ref.forceCancel(new Error(), 'value'); 156 | expect(spy.called).to.equal(true); 157 | expect(spy2.called).to.equal(false); 158 | }); 159 | 160 | it('can match a callback', function () { 161 | var spy2 = sinon.spy(); 162 | ref.on('value', spy); 163 | ref.on('value', spy2); 164 | ref.forceCancel(new Error(), 'value', spy); 165 | ref.set({}); 166 | ref.flush(); 167 | expect(spy2.called).to.equal(true); 168 | }); 169 | 170 | it('can match a context', function () { 171 | var context = {}; 172 | ref.on('value', spy, spy, context); 173 | ref.on('value', spy); 174 | var err = new Error(); 175 | ref.forceCancel(err, 'value', spy, context); 176 | expect(spy) 177 | .to.have.been.calledOnce 178 | .and.calledWith(err); 179 | }); 180 | 181 | it('can take null as the cancel callback', function(){ 182 | ref.on('value', spy, null, {}); 183 | ref.forceCancel(new Error()); 184 | }); 185 | 186 | }); 187 | 188 | describe('#fakeEvent', function () { 189 | 190 | it('can trigger a fake value event', function () { 191 | ref.on('value', spy); 192 | ref.flush(); 193 | spy.reset(); 194 | var data = { 195 | foo: 'bar' 196 | }; 197 | ref.fakeEvent('value', undefined, data); 198 | expect(spy.called).to.equal(false); 199 | ref.flush(); 200 | expect(spy.callCount).to.equal(1); 201 | var snapshot = spy.firstCall.args[0]; 202 | expect(snapshot.ref()).to.equal(ref); 203 | expect(snapshot.val()).to.deep.equal(data); 204 | expect(snapshot.getPriority()).to.equal(null); 205 | }); 206 | 207 | it('can trigger a fake child_added event', function () { 208 | ref.on('child_added', spy); 209 | ref.flush(); 210 | spy.reset(); 211 | var data = { 212 | foo: 'bar' 213 | }; 214 | ref.fakeEvent('child_added', 'theKey', data, 'prevChild', 1); 215 | ref.flush(); 216 | expect(spy.callCount).to.equal(1); 217 | var snapshot = spy.firstCall.args[0]; 218 | expect(snapshot.ref()).to.equal(ref.child('theKey')); 219 | expect(spy.firstCall.args[1]).to.equal('prevChild'); 220 | expect(snapshot.getPriority()).to.equal(1); 221 | }); 222 | 223 | it('uses null as the default data', function () { 224 | ref.on('value', spy); 225 | ref.flush(); 226 | spy.reset(); 227 | ref.fakeEvent('value'); 228 | ref.flush(); 229 | var snapshot = spy.firstCall.args[0]; 230 | expect(snapshot.val()).to.equal(null); 231 | }); 232 | 233 | }); 234 | 235 | describe('#child', function () { 236 | 237 | it('requires a path', function () { 238 | expect(ref.child.bind(ref)).to.throw(); 239 | }); 240 | 241 | it('caches children', function () { 242 | expect(ref.child('foo')).to.equal(ref.child('foo')); 243 | }); 244 | 245 | it('calls child recursively for multi-segment paths', function () { 246 | var child = ref.child('foo'); 247 | sinon.spy(child, 'child'); 248 | ref.child('foo/bar'); 249 | expect(child.child).to.have.been.calledWith('bar'); 250 | }); 251 | 252 | it('can use leading slashes (#23)', function () { 253 | expect(ref.child('/children').path).to.equal('Mock://data/children'); 254 | }); 255 | 256 | it('can use trailing slashes (#23)', function () { 257 | expect(ref.child('children/').path).to.equal('Mock://data/children'); 258 | }); 259 | 260 | }); 261 | 262 | describe('#parent', function () { 263 | 264 | it('gets a parent ref', function () { 265 | expect(ref.child('a').parent().getData()).not.not.equal(null); 266 | }); 267 | 268 | }); 269 | 270 | describe('#ref', function () { 271 | 272 | it('returns itself', function () { 273 | expect(ref.ref()).to.equal(ref); 274 | }); 275 | 276 | }); 277 | 278 | describe('#set', function () { 279 | 280 | beforeEach(function () { 281 | ref.autoFlush(); 282 | }); 283 | 284 | it('should remove old keys from data', function () { 285 | ref.set({ 286 | alpha: true, 287 | bravo: false 288 | }); 289 | expect(ref.getData().a).to.equal(undefined); 290 | }); 291 | 292 | it('should set priorities on children if included in data', function () { 293 | ref.set({ 294 | a: { 295 | '.priority': 100, 296 | '.value': 'a' 297 | }, 298 | b: { 299 | '.priority': 200, 300 | '.value': 'b' 301 | } 302 | }); 303 | expect(ref.getData()).to.contain({ 304 | a: 'a', 305 | b: 'b' 306 | }); 307 | expect(ref.child('a')).to.have.property('priority', 100); 308 | expect(ref.child('b')).to.have.property('priority', 200); 309 | }); 310 | 311 | it('should have correct priority in snapshot if added with set', function () { 312 | ref.on('child_added', spy); 313 | var previousCallCount = spy.callCount; 314 | ref.set({ 315 | alphanew: { 316 | '.priority': 100, 317 | '.value': 'a' 318 | } 319 | }); 320 | expect(spy.callCount).to.equal(previousCallCount + 1); 321 | var snapshot = spy.lastCall.args[0]; 322 | expect(snapshot.getPriority()).to.equal(100); 323 | }); 324 | 325 | it('should fire child_added events with correct prevChildName', function () { 326 | ref = new Firebase('Empty://', null).autoFlush(); 327 | ref.set({ 328 | alpha: { 329 | '.priority': 200, 330 | foo: 'alpha' 331 | }, 332 | bravo: { 333 | '.priority': 300, 334 | foo: 'bravo' 335 | }, 336 | charlie: { 337 | '.priority': 100, 338 | foo: 'charlie' 339 | } 340 | }); 341 | ref.on('child_added', spy); 342 | expect(spy.callCount).to.equal(3); 343 | [null, 'charlie', 'alpha'].forEach(function (previous, index) { 344 | expect(spy.getCall(index).args[1]).to.equal(previous); 345 | }); 346 | }); 347 | 348 | it('should fire child_added events with correct priority', function () { 349 | var data = { 350 | alpha: { 351 | '.priority': 200, 352 | foo: 'alpha' 353 | }, 354 | bravo: { 355 | '.priority': 300, 356 | foo: 'bravo' 357 | }, 358 | charlie: { 359 | '.priority': 100, 360 | foo: 'charlie' 361 | } 362 | }; 363 | ref = new Firebase('Empty://', null).autoFlush(); 364 | ref.set(data); 365 | ref.on('child_added', spy); 366 | expect(spy.callCount).to.equal(3); 367 | for (var i = 0; i < 3; i++) { 368 | var snapshot = spy.getCall(i).args[0]; 369 | expect(snapshot.getPriority()) 370 | .to.equal(data[snapshot.key()]['.priority']); 371 | } 372 | }); 373 | 374 | it('should trigger child_removed if child keys are missing', function () { 375 | ref.on('child_removed', spy); 376 | var data = ref.getData(); 377 | var keys = Object.keys(data); 378 | // data must have more than one record to do this test 379 | expect(keys).to.have.length.above(1); 380 | // remove one key from data and call set() 381 | delete data[keys[0]]; 382 | ref.set(data); 383 | expect(spy.callCount).to.equal(1); 384 | }); 385 | 386 | it('should change parent from null to object when child is set', function () { 387 | ref.set(null); 388 | ref.child('newkey').set({ 389 | foo: 'bar' 390 | }); 391 | expect(ref.getData()).to.deep.equal({ 392 | newkey: { 393 | foo: 'bar' 394 | } 395 | }); 396 | }); 397 | 398 | }); 399 | 400 | describe('#setPriority', function () { 401 | 402 | it('should trigger child_moved with correct prevChildName', function () { 403 | var keys = ref.getKeys(); 404 | ref.on('child_moved', spy); 405 | ref.child(keys[0]).setPriority(250); 406 | ref.flush(); 407 | expect(spy.callCount).to.equal(1); 408 | expect(spy.firstCall.args[1]).to.equal(keys[keys.length - 1]); 409 | }); 410 | 411 | it('should trigger a callback', function () { 412 | ref.setPriority(100, spy); 413 | ref.flush(); 414 | expect(spy.called).to.equal(true); 415 | }); 416 | 417 | it('can be called on the root', function () { 418 | ref.root().setPriority(1); 419 | ref.flush(); 420 | }); 421 | 422 | }); 423 | 424 | describe('#setWithPriority', function () { 425 | 426 | it('should pass the priority to #setPriority', function () { 427 | ref.autoFlush(); 428 | sinon.spy(ref, 'setPriority'); 429 | ref.setWithPriority({}, 250); 430 | expect(ref.setPriority).to.have.been.calledWith(250); 431 | }); 432 | 433 | it('should pass the data and callback to #set', function () { 434 | var data = {}; 435 | var callback = sinon.spy(); 436 | ref.autoFlush(); 437 | sinon.spy(ref, 'set'); 438 | ref.setWithPriority(data, 250, callback); 439 | expect(ref.set).to.have.been.calledWith(data, callback); 440 | }); 441 | 442 | }); 443 | 444 | describe('#update', function () { 445 | 446 | it('must be called with an object', function () { 447 | expect(ref.update).to.throw(); 448 | }); 449 | 450 | it('extends the data', function () { 451 | ref.update({ 452 | foo: 'bar' 453 | }); 454 | ref.flush(); 455 | expect(ref.getData()).to.have.property('foo', 'bar'); 456 | }); 457 | 458 | it('handles multiple calls in the same flush', function () { 459 | ref.update({ 460 | a: 1 461 | }); 462 | ref.update({ 463 | b: 2 464 | }); 465 | ref.flush(); 466 | expect(ref.getData()).to.contain({ 467 | a: 1, 468 | b: 2 469 | }); 470 | }); 471 | 472 | it('can be called on an empty reference', function () { 473 | ref.set(null); 474 | ref.flush(); 475 | ref.update({ 476 | foo: 'bar' 477 | }); 478 | ref.flush(); 479 | expect(ref.getData()).to.deep.equal({ 480 | foo: 'bar' 481 | }); 482 | }); 483 | 484 | it('can simulate an error', function () { 485 | var err = new Error(); 486 | ref.failNext('update', err); 487 | ref.update({ 488 | foo: 'bar' 489 | }, spy); 490 | ref.flush(); 491 | expect(spy).to.have.been.calledWith(err); 492 | }); 493 | 494 | }); 495 | 496 | describe('#remove', function () { 497 | 498 | it('fires child_removed for children', function () { 499 | ref.on('child_removed', spy); 500 | ref.child('a').remove(); 501 | ref.flush(); 502 | expect(spy.called).to.equal(true); 503 | expect(spy.firstCall.args[0].key()).to.equal('a'); 504 | }); 505 | 506 | it('changes to null if all children are removed', function () { 507 | ref.getKeys().forEach(function (key) { 508 | ref.child(key).remove(); 509 | }); 510 | ref.flush(); 511 | expect(ref.getData()).to.equal(null); 512 | }); 513 | 514 | it('can simulate an error', function () { 515 | var err = new Error(); 516 | ref.failNext('remove', err); 517 | ref.remove(spy); 518 | ref.flush(); 519 | expect(spy).to.have.been.calledWith(err); 520 | }); 521 | 522 | }); 523 | 524 | describe('#on', function () { 525 | 526 | it('validates the event name', function () { 527 | expect(ref.on.bind(ref, 'bad')).to.throw(); 528 | }); 529 | 530 | it('should work when initial value is null', function () { 531 | ref.on('value', spy); 532 | ref.flush(); 533 | expect(spy.callCount).to.equal(1); 534 | ref.set('foo'); 535 | ref.flush(); 536 | expect(spy.callCount).to.equal(2); 537 | }); 538 | 539 | it('can take the context as the 3rd argument', function () { 540 | var context = {}; 541 | ref.on('value', spy, context); 542 | ref.flush(); 543 | expect(spy).to.have.been.calledOn(context); 544 | }); 545 | 546 | it('can simulate an error', function () { 547 | var context = {}; 548 | var err = new Error(); 549 | var success = spy; 550 | var fail = sinon.spy(); 551 | ref.failNext('on', err); 552 | ref.on('value', success, fail, context); 553 | ref.flush(); 554 | expect(fail) 555 | .to.have.been.calledWith(err) 556 | .and.calledOn(context); 557 | expect(success.called).to.equal(false); 558 | }); 559 | 560 | it('can simulate an error', function () { 561 | var context = {}; 562 | var err = new Error(); 563 | var success = spy; 564 | var fail = sinon.spy(); 565 | ref.failNext('on', err); 566 | ref.on('value', success, fail, context); 567 | ref.flush(); 568 | expect(fail) 569 | .to.have.been.calledWith(err) 570 | .and.calledOn(context); 571 | expect(success.called).to.equal(false); 572 | }); 573 | 574 | it('is cancelled by an off call before flush', function () { 575 | ref.on('value', spy); 576 | ref.on('child_added', spy); 577 | ref._events.value = []; 578 | ref._events.child_added = []; 579 | ref.flush(); 580 | expect(spy.called).to.equal(false); 581 | }); 582 | 583 | it('returns the callback',function(){ 584 | expect(ref.on('value', spy)).to.equal(spy); 585 | }); 586 | 587 | }); 588 | 589 | describe('#once', function () { 590 | 591 | it('validates the event name', function () { 592 | expect(ref.once.bind(ref, 'bad')).to.throw(); 593 | }); 594 | 595 | it('only fires the listener once', function () { 596 | ref.once('value', spy); 597 | ref.flush(); 598 | expect(spy.callCount).to.equal(1); 599 | ref.set({}); 600 | ref.flush(); 601 | expect(spy.callCount).to.equal(1); 602 | }); 603 | 604 | it('can catch a simulated error', function () { 605 | var cancel = sinon.spy(); 606 | var err = new Error(); 607 | ref.failNext('once', err); 608 | ref.once('value', spy, cancel); 609 | ref.flush(); 610 | ref.set({}); 611 | ref.flush(); 612 | expect(cancel).to.have.been.calledWith(err); 613 | expect(spy.called).to.equal(false); 614 | }); 615 | 616 | it('can provide a context in place of cancel', function () { 617 | var context = {}; 618 | ref.once('value', spy, context); 619 | ref.flush(); 620 | expect(spy).to.have.been.calledOn(context); 621 | }); 622 | 623 | }); 624 | 625 | describe('#off', function () { 626 | 627 | it('validates the event name', function () { 628 | expect(ref.off.bind(ref, 'bad')).to.throw(); 629 | }); 630 | 631 | it('can disable all events', function () { 632 | sinon.spy(ref, 'off'); 633 | ref.off(); 634 | expect(ref.off).to.have.been.calledWith('value'); 635 | }); 636 | 637 | it('can disable a specific event', function () { 638 | ref.on('value', spy); 639 | ref.on('child_added', spy); 640 | ref.flush(); 641 | spy.reset(); 642 | ref.off('value'); 643 | ref.push({ 644 | foo: 'bar' 645 | }); 646 | ref.flush(); 647 | expect(spy.callCount).to.equal(1); 648 | }); 649 | 650 | }); 651 | 652 | describe('#transaction', function () { 653 | 654 | it('should call the transaction function', function () { 655 | ref.transaction(spy); 656 | ref.flush(); 657 | expect(spy.called).to.equal(true); 658 | }); 659 | 660 | it('should fire the callback with a "committed" boolean and error message', function () { 661 | ref.transaction(function (currentValue) { 662 | currentValue.transacted = 'yes'; 663 | return currentValue; 664 | }, function (error, committed, snapshot) { 665 | expect(error).to.equal(null); 666 | expect(committed).to.equal(true); 667 | expect(snapshot.val().transacted).to.equal('yes'); 668 | }); 669 | ref.flush(); 670 | }); 671 | 672 | }); 673 | 674 | describe('#push', function () { 675 | 676 | it('can add data by auto id', function () { 677 | var id = ref._newAutoId(); 678 | sinon.stub(ref, '_newAutoId').returns(id); 679 | ref.push({ 680 | foo: 'bar' 681 | }); 682 | ref.flush(); 683 | expect(ref.child(id).getData()).to.deep.equal({ 684 | foo: 'bar' 685 | }); 686 | }); 687 | 688 | it('can simulate an error', function () { 689 | var err = new Error(); 690 | ref.failNext('push', err); 691 | ref.push({}, spy); 692 | ref.flush(); 693 | expect(spy).to.have.been.calledWith(err); 694 | }); 695 | 696 | it('avoids calling set when unnecessary', function () { 697 | var id = ref._newAutoId(); 698 | sinon.stub(ref, '_newAutoId').returns(id); 699 | var set = sinon.stub(ref.child(id), 'set'); 700 | ref.push(); 701 | ref.push(null); 702 | expect(set.called).to.equal(false); 703 | }); 704 | 705 | }); 706 | 707 | describe('#root', function () { 708 | 709 | it('traverses to the top of the reference', function () { 710 | expect(ref.child('foo/bar').root().path) 711 | .to.equal('Mock://'); 712 | }); 713 | 714 | }); 715 | 716 | describe('#getFlushQueue', function() { 717 | it('returns an array equal to number of flush events queued', function() { 718 | ref.set(true); 719 | ref.set(false); 720 | var list = ref.getFlushQueue(); 721 | expect(list).to.be.an('array'); 722 | expect(list.length).to.equal(2); 723 | }); 724 | 725 | it('does not change length if more items are added to the queue', function() { 726 | ref.set(true); 727 | ref.set(false); 728 | var list = ref.getFlushQueue(); 729 | expect(list.length).to.equal(2); 730 | ref.set('foo'); 731 | ref.set('bar'); 732 | expect(list.length).to.equal(2); 733 | }); 734 | 735 | it('sets the ref attribute correctly', function() { 736 | ref.set(true); 737 | var data = ref.getFlushQueue()[0].sourceData; 738 | expect(data.ref).to.equal(ref); 739 | }); 740 | 741 | it('sets the `method` attribute correctly', function() { 742 | ref.set(true); 743 | var data = ref.getFlushQueue()[0].sourceData; 744 | expect(data.method).to.equal('set'); 745 | }); 746 | 747 | it('sets the `args` attribute correctly', function() { 748 | ref.set(true); 749 | var data = ref.getFlushQueue()[0].sourceData; 750 | expect(data.args).to.be.an('array'); 751 | }); 752 | }); 753 | 754 | 755 | }); 756 | -------------------------------------------------------------------------------- /test/unit/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var expect = require('chai').use(require('sinon-chai')).expect; 5 | var Mock = require('../../'); 6 | var Firebase = Mock.MockFirebase; 7 | var FirebaseSimpleLogin = Mock.MockFirebaseSimpleLogin; 8 | 9 | describe('MockFirebaseSimpleLogin', function() { 10 | var fb, callback, auth; 11 | 12 | beforeEach(function() { 13 | // we need our own callback to test the MockFirebaseSimpleLogin API 14 | // it's not usually necessary to do this since MockFirebaseSimpleLogin 15 | // already provides a spy method auth.callback (whether or not a callback is provided) 16 | callback = sinon.spy(); 17 | fb = new Firebase().child('data'); 18 | auth = new FirebaseSimpleLogin(fb, callback); 19 | }); 20 | 21 | describe('#login', function() { 22 | it('should invoke the callback if autoFlush is set', function() { 23 | auth.autoFlush(true).login('twitter'); 24 | expect(callback.callCount).equals(1); 25 | }); 26 | 27 | it('should wait for flush', function() { 28 | auth.login('twitter'); 29 | expect(callback.callCount).equals(0); 30 | auth.flush(); 31 | expect(callback.callCount).equals(1); 32 | }); 33 | 34 | it('should return INVALID_USER on bad email address', function() { 35 | auth.autoFlush(true).login('password', {email: 'bademail', password: 'notagoodpassword'}); 36 | var call = callback.getCall(0); 37 | expect(call.args[0]).is.an('object'); 38 | expect(call.args[0].code).equals('INVALID_USER'); 39 | }); 40 | 41 | it('should return INVALID_PASSWORD on an invalid password', function() { 42 | auth.autoFlush(true).login('password', {email: 'email@firebase.com', password: 'notagoodpassword'}); 43 | var call = callback.getCall(0); 44 | expect(call.args[0]).is.an('object'); 45 | expect(call.args[0].code).equals('INVALID_PASSWORD'); 46 | }); 47 | 48 | it('should return a valid user on a good login', function() { 49 | auth.autoFlush(true).login('facebook'); 50 | var call = callback.getCall(0); 51 | expect(call.args[1]).eqls(auth.getUser('facebook')); 52 | }); 53 | }); 54 | 55 | describe('#createUser', function() { 56 | it('should return a user on success', function() { 57 | var spy = sinon.spy(); 58 | auth.createUser('newuser@firebase.com', 'password', spy); 59 | auth.flush(); 60 | expect(spy.callCount).equals(1); 61 | var call = spy.getCall(0); 62 | expect(call.args[0]).equals(null); 63 | expect(call.args[1]).eqls(auth.getUser('password', {email: 'newuser@firebase.com'})); 64 | 65 | }); 66 | 67 | it('should fail with EMAIL_TAKEN if user already exists', function() { 68 | var spy = sinon.spy(); 69 | var existingUser = auth.getUser('password', {email: 'email@firebase.com'}); 70 | expect(existingUser).is.an('object'); 71 | auth.createUser(existingUser.email, existingUser.password, spy); 72 | auth.flush(); 73 | var call = spy.getCall(0); 74 | expect(spy.called).to.equal(true); 75 | expect(call.args[0]).is.an('object'); 76 | expect(call.args[0]).to.include.keys('code'); 77 | }); 78 | }); 79 | 80 | describe('#changePassword', function() { 81 | it('should invoke callback on success', function() { 82 | var spy = sinon.spy(); 83 | var user = auth.getUser('password', {email: 'email@firebase.com'}); 84 | auth.changePassword('email@firebase.com', user.password, 'spiffy', spy); 85 | auth.flush(); 86 | expect(spy.callCount).equals(1); 87 | var call = spy.getCall(0); 88 | expect(call.args[0]).equals(null); 89 | expect(call.args[1]).equals(true); 90 | }); 91 | 92 | it('should physically modify the password', function() { 93 | var user = auth.getUser('password', {email: 'email@firebase.com'}); 94 | auth.changePassword('email@firebase.com', user.password, 'spiffynewpass'); 95 | auth.flush(); 96 | expect(user.password).equals('spiffynewpass'); 97 | }); 98 | 99 | it('should fail with INVALID_USER if bad user given', function() { 100 | var spy = sinon.spy(); 101 | auth.changePassword('notvalidemail@firebase.com', 'all your base', 'are belong to us', spy); 102 | auth.flush(); 103 | expect(spy.callCount).equals(1); 104 | var call = spy.getCall(0); 105 | expect(call.args[0]).is.an('object'); 106 | expect(call.args[0].code).equals('INVALID_USER'); 107 | expect(call.args[1]).equals(false); 108 | }); 109 | 110 | it('should fail with INVALID_PASSWORD on a mismatch', function() { 111 | var spy = sinon.spy(); 112 | auth.changePassword('email@firebase.com', 'notvalidpassword', 'newpassword', spy); 113 | auth.flush(); 114 | expect(spy.callCount).equals(1); 115 | var call = spy.getCall(0); 116 | expect(call.args[0]).is.an('object'); 117 | expect(call.args[0].code).equals('INVALID_PASSWORD'); 118 | expect(call.args[1]).equals(false); 119 | }); 120 | }); 121 | 122 | describe('#sendPasswordResetEmail', function() { 123 | it('should return null, true on success', function() { 124 | var spy = sinon.spy(); 125 | auth.sendPasswordResetEmail('email@firebase.com', spy); 126 | auth.flush(); 127 | expect(spy.callCount).equals(1); 128 | var call = spy.getCall(0); 129 | expect(call.args[0]).equals(null); 130 | expect(call.args[1]).equals(true); 131 | }); 132 | 133 | it('should fail with INVALID_USER if not a valid email', function() { 134 | var spy = sinon.spy(); 135 | auth.sendPasswordResetEmail('notavaliduser@firebase.com', spy); 136 | auth.flush(); 137 | expect(spy.callCount).equals(1); 138 | var call = spy.getCall(0); 139 | expect(call.args[0]).is.an('object'); 140 | expect(call.args[0].code).equals('INVALID_USER'); 141 | expect(call.args[1]).equals(false); 142 | }); 143 | }); 144 | 145 | describe('#removeUser', function() { 146 | it('should invoke callback', function() { 147 | var spy = sinon.spy(); 148 | var user = auth.getUser('password', {email: 'email@firebase.com'}); 149 | auth.removeUser('email@firebase.com', user.password, spy); 150 | auth.flush(); 151 | expect(spy.callCount).equals(1); 152 | var call = spy.getCall(0); 153 | expect(call.args[0]).equals(null); 154 | expect(call.args[1]).equals(true); 155 | }); 156 | 157 | it('should physically remove the user', function() { 158 | var user = auth.getUser('password', {email: 'email@firebase.com'}); 159 | expect(user).is.an('object'); 160 | auth.removeUser('email@firebase.com', user.password); 161 | auth.flush(); 162 | expect(auth.getUser('password', {email: 'email@firebase.com'})).equals(null); 163 | }); 164 | 165 | it('should fail with INVALID_USER if bad email', function() { 166 | var spy = sinon.spy(); 167 | auth.removeUser('notvaliduser@firebase.com', 'xxxxx', spy); 168 | auth.flush(); 169 | expect(spy.callCount).equals(1); 170 | var call = spy.getCall(0); 171 | expect(call.args[0]).is.an('object'); 172 | expect(call.args[0].code).equals('INVALID_USER'); 173 | }); 174 | 175 | it('should fail with INVALID_PASSWORD if bad password', function() { 176 | var spy = sinon.spy(); 177 | auth.removeUser('email@firebase.com', 'notavalidpassword', spy); 178 | auth.flush(); 179 | expect(spy.callCount).equals(1); 180 | var call = spy.getCall(0); 181 | expect(call.args[0]).is.an('object'); 182 | expect(call.args[0].code).equals('INVALID_PASSWORD'); 183 | expect(call.args[1]).equals(false); 184 | }); 185 | }); 186 | 187 | describe('#autoFlush', function() { 188 | 189 | beforeEach(function () { 190 | sinon.spy(auth, 'flush'); 191 | }); 192 | 193 | it('should flush immediately if true is used', function() { 194 | auth.autoFlush(true); 195 | expect(auth.flush).calledWith(true); 196 | }); 197 | 198 | it('should not invoke if false is used', function() { 199 | auth.autoFlush(false); 200 | expect(auth.flush.called).to.equal(false); 201 | }); 202 | 203 | it('should invoke flush with appropriate time if int is used', function() { 204 | auth.autoFlush(10); 205 | expect(auth.flush).calledWith(10); 206 | }); 207 | 208 | it('should obey MockFirebaseSimpleLogin.DEFAULT_AUTO_FLUSH', function() { 209 | FirebaseSimpleLogin.DEFAULT_AUTO_FLUSH = true; 210 | auth = new FirebaseSimpleLogin(fb, callback); 211 | sinon.spy(auth, 'flush'); 212 | expect(auth.flush.called).to.equal(false); 213 | auth.login('facebook'); 214 | expect(auth.flush).calledWith(true); 215 | }); 216 | }); 217 | 218 | }); 219 | -------------------------------------------------------------------------------- /test/unit/query.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sinon = require('sinon'); 4 | var _ = require('lodash'); 5 | var expect = require('chai').use(require('sinon-chai')).expect; 6 | var Query = require('../../src/query'); 7 | var Firebase = require('../../').MockFirebase; 8 | 9 | describe('MockQuery', function () { 10 | 11 | var ref, query; 12 | beforeEach(function () { 13 | ref = new Firebase().child('ordered'); 14 | ref.set(require('./data.json').ordered); 15 | ref.flush(); 16 | query = new Query(ref); 17 | }); 18 | 19 | describe('#ref', function() { 20 | 21 | it('returns the ref used to create the query', function() { 22 | expect(ref.limit(2).startAt('a').ref()).to.equal(ref); 23 | }); 24 | 25 | }); 26 | 27 | describe('#flush', function () { 28 | 29 | it('flushes the ref', function () { 30 | sinon.stub(ref, 'flush'); 31 | expect(query.flush(1, 2)).to.equal(query); 32 | expect(ref.flush) 33 | .to.have.been.calledOn(ref) 34 | .and.calledWith(1, 2); 35 | }); 36 | 37 | }); 38 | 39 | describe('#autoFlush', function () { 40 | 41 | it('autoFlushes the ref', function () { 42 | sinon.stub(ref, 'autoFlush'); 43 | expect(query.autoFlush(1, 2)).to.equal(query); 44 | expect(ref.autoFlush) 45 | .to.have.been.calledOn(ref) 46 | .and.calledWith(1, 2); 47 | }); 48 | 49 | }); 50 | 51 | describe('#getData', function () { 52 | 53 | it('gets data from the slice', function () { 54 | expect(query.getData()).to.deep.equal(query.slice().data); 55 | }); 56 | 57 | }); 58 | 59 | describe('#fakeEvent', function () { 60 | 61 | it('validates the event name', function () { 62 | expect(query.fakeEvent.bind(query, 'bad')).to.throw(); 63 | }); 64 | 65 | it('fires the matched event with a snapshot', function () { 66 | var added = sinon.spy(); 67 | var snapshot = {}; 68 | var context = {}; 69 | var removed = sinon.spy(); 70 | query.on('child_added', added, void 0, context); 71 | query.on('child_removed', removed); 72 | query.fakeEvent('child_added', snapshot); 73 | expect(added) 74 | .to.have.been.calledWith(snapshot) 75 | .and.calledOn(context); 76 | expect(removed.called).to.equal(false); 77 | }); 78 | }); 79 | 80 | describe('on', function() { 81 | 82 | it('validates the event name', function () { 83 | expect(query.on.bind(query, 'bad')).to.throw(); 84 | }); 85 | 86 | describe('value', function() { 87 | it('should provide value immediately', function() { 88 | var spy = sinon.spy(); 89 | ref.limit(2).on('value', spy); 90 | ref.flush(); 91 | expect(spy.called).to.equal(true); 92 | }); 93 | 94 | it('should return null if nothing in range exists', function() { 95 | var spy = sinon.spy(function(snap) { 96 | expect(snap.val()).equals(null); 97 | }); 98 | ref.limit(2).startAt('foo').endAt('foo').on('value', spy); 99 | ref.flush(); 100 | expect(spy.called).to.equal(true); 101 | }); 102 | 103 | it('should return correct keys', function() { 104 | var spy = sinon.spy(function(snap) { 105 | expect(_.keys(snap.val())).eql(['num_3', 'char_a_1', 'char_a_2']); 106 | }); 107 | ref.startAt(3).endAt('a').on('value', spy); 108 | ref.flush(); 109 | expect(spy.called).to.equal(true); 110 | }); 111 | 112 | it('should update on change', function() { 113 | var spy = sinon.spy(); 114 | ref.startAt(3, 'num_3').limit(2).on('value', spy); 115 | ref.flush(); 116 | expect(spy).callCount(1); 117 | ref.child('num_3').set({foo: 'bar'}); 118 | ref.flush(); 119 | expect(spy).callCount(2); 120 | }); 121 | 122 | it('should not update on change outside range', function() { 123 | var spy = sinon.spy(); 124 | ref.limit(1).on('value', spy); 125 | ref.flush(); 126 | expect(spy).callCount(1); 127 | ref.child('num_3').set('apple'); 128 | ref.flush(); 129 | expect(spy).callCount(1); 130 | }); 131 | 132 | it('can take the context as the 3rd argument', function () { 133 | var spy = sinon.spy(); 134 | var context = {}; 135 | ref.limit(1).on('value', spy, context); 136 | ref.flush(); 137 | expect(spy).to.have.been.calledOn(context); 138 | }); 139 | }); 140 | 141 | describe('once', function() { 142 | 143 | it('validates the event name', function () { 144 | expect(query.once.bind(query, 'bad')).to.throw(); 145 | }); 146 | 147 | it('should be triggered if value is null', function() { 148 | var spy = sinon.spy(); 149 | ref.child('notavalidkey').limit(3).once('value', spy); 150 | ref.flush(); 151 | expect(spy).callCount(1); 152 | }); 153 | 154 | it('should be triggered if value is not null', function() { 155 | var spy = sinon.spy(); 156 | ref.limit(3).once('value', spy); 157 | ref.flush(); 158 | expect(spy).callCount(1); 159 | }); 160 | 161 | it('should not get triggered twice', function() { 162 | var spy = sinon.spy(); 163 | ref.limit(3).once('value', spy); 164 | ref.flush(); 165 | ref.child('addfortest').set({hello: 'world'}); 166 | ref.flush(); 167 | expect(spy).callCount(1); 168 | }); 169 | }); 170 | 171 | describe('child_added', function() { 172 | it('should include prevChild'); 173 | 174 | it('should trigger all keys in initial range', function() { 175 | var spy = sinon.spy(); 176 | var query = ref.limit(4); 177 | var data = query.slice().data; 178 | query.on('child_added', spy); 179 | query.flush(); 180 | expect(spy).callCount(4); 181 | _.each(_.keys(data), function(k, i) { 182 | expect(spy.getCall(i).args[0].key()).equals(k); 183 | }); 184 | }); 185 | 186 | it('should notify on a new added event after init'); 187 | 188 | it('should not notify for add outside range'); 189 | 190 | it('should trigger a child_removed if using limit'); 191 | 192 | it('should work if connected from instead a once "value"', function() { 193 | var ref = new Firebase('testing://'); 194 | ref.autoFlush(); 195 | ref.child('fruit').push('apples'); 196 | ref.child('fruit').push('oranges'); 197 | 198 | var third_value = 'pear'; 199 | var model = {}; 200 | var last_key = null; 201 | ref.child('fruit').once('value', function(list_snapshot) { 202 | list_snapshot.forEach(function(snapshot){ 203 | model[snapshot.key()] = snapshot.val(); 204 | snapshot.ref().on('value', function(snapshot) { 205 | model[snapshot.key()] = snapshot.val(); 206 | }); 207 | last_key = snapshot.key(); 208 | }); 209 | 210 | ref.child('fruit').startAt(null, last_key).on('child_added', function(snapshot) { 211 | if(model[snapshot.key()] === undefined) 212 | { 213 | model[snapshot.key()] = snapshot.val(); 214 | snapshot.ref().on('value', function(snapshot) { 215 | model[snapshot.key()] = snapshot.val(); 216 | }); 217 | } 218 | }, undefined, this); 219 | }, undefined, this); 220 | 221 | var third_ref = ref.child('fruit').push(third_value); 222 | 223 | expect(model[third_ref.key()]).to.equal(third_value); 224 | 225 | }); 226 | }); 227 | 228 | describe('child_changed', function() { 229 | it('should trigger for a key in range'); 230 | 231 | it('should not trigger for a key outside of range'); 232 | }); 233 | 234 | describe('child_removed', function() { 235 | it('should trigger for a child in range'); 236 | 237 | it('should not trigger for a child out of range'); 238 | 239 | it('should trigger a child_added for replacement if using limit'); 240 | }); 241 | 242 | describe('child_moved', function() { 243 | it('should trigger if item in range moves in range'); 244 | 245 | it('should trigger child_removed if goes out of range'); 246 | 247 | it('should trigger child_added if moved in range'); 248 | }); 249 | }); 250 | 251 | describe('off', function() { 252 | it('should not notify on callbacks'); 253 | }); 254 | 255 | describe('limit', function() { 256 | it('should throw Error if non-integer argument'); 257 | 258 | it('should return correct number of results'); 259 | 260 | it('should work if does not match any results'); 261 | 262 | it('should be relevant to endAt()'); //todo not implemented 263 | 264 | it('should be relevant to startAt()'); //todo not implemented 265 | }); 266 | 267 | describe('endAt', function() { 268 | it('should make limit relative to the end of data'); 269 | 270 | it('should stop at the priority given'); 271 | 272 | it('should stop at the key given'); 273 | 274 | it('should stop at the key+priority given'); 275 | }); 276 | 277 | describe('startAt', function() { 278 | it('should make limit relative to start of data'); 279 | 280 | it('should start at the priority given'); 281 | 282 | it('should start at the key given'); 283 | 284 | it('should start at the key+priority given'); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test/unit/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').use(require('sinon-chai')).expect; 4 | var sinon = require('sinon'); 5 | var _ = require('lodash'); 6 | var Queue = require('../../src/queue').Queue; 7 | var FlushEvent = require('../../src/queue').Event; 8 | var EventEmitter = require('events').EventEmitter; 9 | 10 | describe('FlushQueue', function () { 11 | 12 | var queue, spy; 13 | beforeEach(function () { 14 | queue = new Queue(); 15 | spy = sinon.spy(); 16 | }); 17 | 18 | it('constructs an empty event queue', function () { 19 | expect(queue) 20 | .to.have.property('events') 21 | .that.is.an('array') 22 | .with.length(0); 23 | }); 24 | 25 | it('removes events when they are cancelled', function () { 26 | queue.push(_.noop); 27 | queue.getEvents()[0].cancel(); 28 | expect(queue.getEvents()).to.have.length(0); 29 | }); 30 | 31 | describe('#push', function () { 32 | 33 | it('pushes simple events onto the queue like [].push', function () { 34 | queue.push(_.noop, _.noop); 35 | expect(queue.getEvents()).to.have.length(2); 36 | }); 37 | 38 | it('pushes complex events', function () { 39 | var sourceData = { 40 | foo: 'bar' 41 | }; 42 | queue.push({ 43 | fn: _.noop, 44 | context: null, 45 | sourceData: sourceData 46 | }); 47 | var event = queue.getEvents()[0]; 48 | expect(event.sourceData).to.equal(sourceData); 49 | expect(event).to.be.an.instanceOf(FlushEvent); 50 | }); 51 | 52 | }); 53 | 54 | describe('#flush', function () { 55 | 56 | it('is throws when there are no deferreds', function () { 57 | expect(queue.flush.bind(queue)).to.throw('No deferred'); 58 | }); 59 | 60 | it('fires the events synchronously by default', function () { 61 | queue.push(spy); 62 | queue.flush(); 63 | expect(spy.called).to.equal(true); 64 | }); 65 | 66 | it('fires events added during queue processing', function () { 67 | queue.push(function () { 68 | queue.push(spy); 69 | }); 70 | queue.flush(); 71 | expect(spy.called).to.equal(true); 72 | }); 73 | 74 | it('prevents recursive flush calls', function () { 75 | queue.push(function () { 76 | queue.flush(); 77 | }); 78 | queue.flush(); 79 | }); 80 | 81 | it('can invoke events after a delay', function () { 82 | var clock = sinon.useFakeTimers(); 83 | queue.push(spy); 84 | queue.flush(100); 85 | expect(spy.called).to.equal(false); 86 | clock.tick(100); 87 | expect(spy.called).to.equal(true); 88 | }); 89 | 90 | }); 91 | 92 | describe('#getEvents', function() { 93 | 94 | it('returns a copy of the events', function () { 95 | queue.push(_.noop); 96 | expect(queue.getEvents()).to.deep.equal(queue.events); 97 | expect(queue.getEvents()).to.not.equal(queue.events); 98 | }); 99 | 100 | }); 101 | 102 | }); 103 | 104 | describe('FlushEvent', function () { 105 | 106 | var spy, context, event; 107 | beforeEach(function () { 108 | spy = sinon.spy(); 109 | context = {}; 110 | event = new FlushEvent(spy, context); 111 | }); 112 | 113 | describe('#run', function () { 114 | 115 | it('runs the event handler on the context', function () { 116 | event.run(); 117 | expect(spy).to.have.been.calledOn(context); 118 | }); 119 | 120 | it('emits a done event', function () { 121 | spy = sinon.spy(); 122 | event.on('done', spy); 123 | event.run(); 124 | expect(spy.called).to.equal(true); 125 | }); 126 | 127 | }); 128 | 129 | describe('#cancel', function () { 130 | 131 | it('emits a done event', function () { 132 | spy = sinon.spy(); 133 | event.on('done', spy); 134 | event.cancel(); 135 | expect(spy.called).to.equal(true); 136 | }); 137 | 138 | }); 139 | 140 | }); 141 | -------------------------------------------------------------------------------- /test/unit/snapshot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').use(require('sinon-chai')).expect; 4 | var sinon = require('sinon'); 5 | var Snapshot = require('../../src/snapshot'); 6 | var Firebase = require('../..').MockFirebase; 7 | 8 | describe('DataSnapshot', function () { 9 | 10 | var ref; 11 | beforeEach(function () { 12 | ref = new Firebase(); 13 | }); 14 | 15 | describe('#ref', function () { 16 | 17 | it('returns the reference', function () { 18 | expect(new Snapshot(ref).ref()).to.equal(ref); 19 | }); 20 | 21 | }); 22 | 23 | describe('#val', function () { 24 | 25 | it('returns a deep clone of the data', function () { 26 | var data = { 27 | foo: { 28 | bar: 'baz' 29 | } 30 | }; 31 | var snapshot = new Snapshot(ref, data); 32 | expect(snapshot.val()).to.deep.equal(data); 33 | expect(snapshot.val()).to.not.equal(data); 34 | expect(snapshot.val().foo).to.not.equal(data.foo); 35 | }); 36 | 37 | it('returns null for an empty object', function () { 38 | expect(new Snapshot(ref, {}).val()).to.equal(null); 39 | }); 40 | 41 | }); 42 | 43 | describe('#getPriority', function () { 44 | 45 | it('returns the priority', function () { 46 | expect(new Snapshot(ref, {}, 1).getPriority()).to.equal(1); 47 | }); 48 | 49 | }); 50 | 51 | describe('#child', function () { 52 | 53 | it('generates a snapshot for a child ref', function () { 54 | var parent = new Snapshot(ref); 55 | var child = parent.child('key'); 56 | expect(parent.ref().child('key')).to.equal(child.ref()); 57 | }); 58 | 59 | it('uses child data', function () { 60 | var parent = new Snapshot(ref, {key: 'val'}); 61 | var child = parent.child('key'); 62 | expect(child.val()).to.equal('val'); 63 | }); 64 | 65 | it('uses null when there is no child data', function () { 66 | var parent = new Snapshot(ref); 67 | var child = parent.child('key'); 68 | expect(child.val()).to.equal(null); 69 | }); 70 | 71 | it('passes the priority', function () { 72 | var parent = new Snapshot(ref); 73 | ref.child('key').setPriority(10); 74 | ref.flush(); 75 | var child = parent.child('key'); 76 | expect(child.getPriority()).to.equal(10); 77 | }); 78 | 79 | }); 80 | 81 | describe('#exists', function () { 82 | 83 | it('checks for a null value', function () { 84 | expect(new Snapshot(ref, null).exists()).to.equal(false); 85 | expect(new Snapshot(ref, {foo: 'bar'}).exists()).to.equal(true); 86 | }); 87 | 88 | }); 89 | 90 | describe('#forEach', function () { 91 | 92 | it('calls the callback with each child', function () { 93 | var snapshot = new Snapshot(ref, { 94 | foo: 'bar', 95 | bar: 'baz' 96 | }); 97 | var callback = sinon.spy(); 98 | snapshot.forEach(callback); 99 | expect(callback.firstCall.args[0].val()).to.equal('bar'); 100 | expect(callback.secondCall.args[0].val()).to.equal('baz'); 101 | }); 102 | 103 | it('can set a this value', function () { 104 | var snapshot = new Snapshot(ref, { 105 | foo: 'bar' 106 | }); 107 | var callback = sinon.spy(); 108 | var context = {}; 109 | snapshot.forEach(callback, context); 110 | expect(callback).to.always.have.been.calledOn(context); 111 | }); 112 | 113 | }); 114 | 115 | describe('#hasChild', function () { 116 | 117 | it('can handle null snapshots', function () { 118 | expect(new Snapshot(ref, null).hasChild('foo')).to.equal(false); 119 | }); 120 | 121 | it('tests for the key', function () { 122 | var snapshot = new Snapshot(ref, {foo: 'bar'}); 123 | expect(snapshot.hasChild('foo')).to.equal(true); 124 | expect(snapshot.hasChild('bar')).to.equal(false); 125 | }); 126 | 127 | }); 128 | 129 | describe('#hasChildren', function () { 130 | 131 | it('tests for children', function () { 132 | expect(new Snapshot(ref).hasChildren()).to.equal(false); 133 | expect(new Snapshot(ref, {foo: 'bar'}).hasChildren()).to.equal(true); 134 | }); 135 | 136 | }); 137 | 138 | describe('#key', function () { 139 | 140 | it('returns the ref key', function () { 141 | expect(new Snapshot(ref).key()).to.equal(ref.key()); 142 | }); 143 | 144 | }); 145 | 146 | describe('#name', function () { 147 | 148 | it('passes through to #key', function () { 149 | var snapshot = new Snapshot(ref); 150 | expect(snapshot.key()).to.equal(snapshot.name()); 151 | }); 152 | 153 | }); 154 | 155 | describe('#numChildren', function () { 156 | 157 | it('returns the object size', function () { 158 | expect(new Snapshot(ref, {foo: 'bar'}).numChildren()).to.equal(1); 159 | }); 160 | 161 | it('returns 0 for a null snapshot', function () { 162 | expect(new Snapshot(ref, null).numChildren()).to.equal(0); 163 | }); 164 | 165 | }); 166 | 167 | describe('#exportVal', function () { 168 | 169 | it('handles primitives with no priority', function () { 170 | expect(new Snapshot(ref, 'Hello world!').exportVal()).to.equal('Hello world!'); 171 | }); 172 | 173 | it('handles primitives with priorities', function () { 174 | expect(new Snapshot(ref, 'hw', 1).exportVal()).to.deep.equal({ 175 | '.value': 'hw', 176 | '.priority': 1 177 | }); 178 | }); 179 | 180 | it('recursively builds an export object', function () { 181 | ref.set({ 182 | foo: 'bar', 183 | bar: 'baz' 184 | }); 185 | ref.child('bar').setPriority(1); 186 | ref.flush(); 187 | expect(new Snapshot(ref, { 188 | foo: 'bar', 189 | bar: 'baz' 190 | }, 10).exportVal()) 191 | .to.deep.equal({ 192 | '.priority': 10, 193 | foo: 'bar', 194 | bar: { 195 | '.value': 'baz', 196 | '.priority': 1 197 | } 198 | }); 199 | }); 200 | 201 | }); 202 | 203 | }); 204 | -------------------------------------------------------------------------------- /tutorials/authentication.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Authentication 2 | 3 | MockFirebase replaces most of Firebase's authentication method with simple mocks. Authentication methods will always succeed unless an error is specifically specified using [`failNext`](../API.md#failnextmethod-err---undefined). You can still use methods like `createUser`. Instead of storing new users remotely, MockFirebase will maintain a local list of users and simulate normal Firebase behavior (e.g. prohibiting duplicate email addresses). 4 | 5 | ## Creating Users 6 | 7 | In this example, we'll create a new user via our source code and test that he is written to Firebase. 8 | 9 | ##### Source 10 | 11 | ```js 12 | var users = { 13 | ref: function () { 14 | return new Firebase('https://example.firebaseio.com'); 15 | } 16 | create: function (credentials, callback) { 17 | users.ref().createUser(credentials, callback); 18 | } 19 | }; 20 | ``` 21 | 22 | ##### Test 23 | 24 | ```js 25 | MockFirebase.override(); 26 | var ref = users.ref(); 27 | users.create({ 28 | email: 'ben@example.com', 29 | password: 'examplePass' 30 | }); 31 | users.flush(); 32 | console.assert(users.getEmailUser('ben@example.com'), 'ben was created'); 33 | ``` 34 | 35 | ## Manually Changing Authentication State 36 | 37 | MockFirebase provides a special `changeAuthState` method on references to aid in unit testing code that reacts to new user data. `changeAuthState` allows us to simulate a variety of authentication scenarios such as a new user logging in or a user logging out. 38 | 39 | In this example, we want to redirect to an admin dashboard when a user is an administrator. To accomplish this, we'll use custom authentication data. 40 | 41 | ##### Source 42 | 43 | ```js 44 | users.ref().onAuth(function (authData) { 45 | if (authData.auth.isAdmin) { 46 | document.location.href = '#/admin'; 47 | } 48 | }); 49 | ``` 50 | 51 | ##### Test 52 | 53 | ```js 54 | ref.changeAuthState({ 55 | uid: 'testUid', 56 | provider: 'custom', 57 | token: 'authToken', 58 | expires: Math.floor(new Date() / 1000) + 24 * 60 * 60, 59 | auth: { 60 | isAdmin: true 61 | } 62 | }); 63 | ref.flush(); 64 | console.assert(document.location.href === '#/admin', 'redirected to admin'); 65 | ``` -------------------------------------------------------------------------------- /tutorials/basic.md: -------------------------------------------------------------------------------- 1 | # Tutorial: MockFirebase Basics 2 | 3 | When writing unit tests with MockFirebase, you'll typically want to focus on covering one of two scenarios: 4 | 5 | 1. Your client receives data from Firebase by attaching a listener with `on` 6 | 2. Your client writes data to Firebase using a method like `set` or `push` 7 | 8 | While your application almost certainly does both reading and writing to Firebase, each test should try to cover as small a unit of functionality as possible. 9 | 10 | ## Testing Reads 11 | 12 | In this example, our source code will listen for new people on a reference we provide and call a function each time a new one is added. 13 | 14 | ##### Source 15 | 16 | ```js 17 | var ref; 18 | var people = { 19 | ref: function () { 20 | if (!ref) ref = new Firebase('htttps://example.firebaseio.com/people'); 21 | return ref; 22 | }, 23 | greet: function (person) { 24 | console.log('hi ' + person.first); 25 | }, 26 | listen: function () { 27 | people.ref().on('child_added', function (snapshot) { 28 | people.greet(snapshot.val()); 29 | }); 30 | } 31 | }; 32 | ``` 33 | 34 | In our tests, we'll override the `greet` method to verify that it's being called properly. 35 | 36 | ##### Test 37 | 38 | ```js 39 | MockFirebase.override(); 40 | people.listen(); 41 | var greeted = []; 42 | people.greet = function (person) { 43 | greeted.push(person); 44 | }; 45 | ref.push({ 46 | first: 'Michael' 47 | }); 48 | ref.push({ 49 | first: 'Ben' 50 | }); 51 | ref.flush(); 52 | console.assert(greeted.length === 2, '2 people greeted'); 53 | console.assert(greeted[0].first === 'Michael', 'Michael greeted'); 54 | console.assert(greeted[1].first === 'Ben', 'Ben greeted'); 55 | ``` 56 | 57 | We're calling [`MockFirebase.override`](override.md) to replace the real `Firebase` instance with MockFirebase. If you're loading Firebase using Node or Browserify, you need to use [proxyquire](proxyquire.md) instead. 58 | 59 | Notice that we queued up multiple changes before actually calling `ref.flush`. MockFirebase stores these changes in the order they were created and then performs local updates accordingly. You'll only need to `flush` your changes when you need listeners, callbacks, and other asynchronous responses to be triggered. 60 | 61 | ## Testing Writes 62 | 63 | Testing writes is especially easy with MockFirebase because it allows you to inspect the state of your data at any time. In this example, we'll add a new method to `people` that creates a new person with the given name: 64 | 65 | ##### Source 66 | 67 | ```js 68 | people.create = function (first) { 69 | return people.ref().push({ 70 | first: first 71 | }); 72 | }; 73 | ``` 74 | 75 | ##### Test 76 | 77 | ```js 78 | var newPersonRef = people.create('James'); 79 | ref.flush(); 80 | var autoId = newPersonRef.key(); 81 | var data = ref.getData(); 82 | console.assert(data[autoId].first === 'James', 'James was created'); 83 | ``` 84 | -------------------------------------------------------------------------------- /tutorials/errors.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Simulating Errors 2 | 3 | Except for user management methods like `createUser` that validate their arguments, MockFirebase calls will never results in asynchronous errors since all data is maintained locally. Instead, MockFirebase gives you two options for testing error handling behavior for both data and authentication methods: 4 | 5 | 1. [`failNext(method, err)`](../API.md#failnextmethod-err---undefined): specifies that the next invocation of `method` should call its completion callback with `err` 6 | 2. [`forceCancel(err [, event] [, callback] [, context]`)](../API.md#forcecancelerr--event--callback--context---undefined): cancels all data event listeners registered with `on` that match the provided arguments 7 | 8 | While `failNext` is limited to specifying a single error per method, `forceCancel` can simulate the cancellation of any number of event listeners. 9 | 10 | ## `failNext` 11 | 12 | Using `failNext` is a simple way to test behavior that handles write errors or read errors that occur immediately (e.g. an attempt to read a path a user is not authorized to view). 13 | 14 | 15 | ##### Source 16 | 17 | ```js 18 | var log = { 19 | error: function (err) { 20 | console.error(err); 21 | } 22 | }; 23 | var people = { 24 | ref: function () { 25 | return new Firebase('htttps://example.firebaseio.com/people') 26 | }, 27 | create: function (person) { 28 | people.ref().push(person, function (err) { 29 | if (err) log.error(err); 30 | }); 31 | } 32 | }; 33 | ``` 34 | 35 | In our tests, we'll override `log.error` to ensure that it's properly called. 36 | 37 | ##### Test 38 | 39 | ```js 40 | MockFirebase.override(); 41 | var ref = people.ref(); 42 | var errors = []; 43 | log.error = function (err) { 44 | errors.push(err); 45 | }; 46 | people.failNext('push'); 47 | people.create({ 48 | first: 'Ben' 49 | }); 50 | people.flush(); 51 | console.assert(errors.length === 1, 'people.create error logged'); 52 | ``` 53 | 54 | ## `forceCancel` 55 | 56 | `forceCancel` simulates more complex errors that involve a set of event listeners on a path. `forceCancel` allows you to simulate Firebase API behavior that would normally occur in rare cases when a user lost access to a particular reference. For a simple read error, you could use `failNext('on', err)` instead. 57 | 58 | In this example, we'll also record an error when we lose authentication on a path. 59 | 60 | ##### Source 61 | ```js 62 | people.ref().on('child_added', function onChildAdded (snapshot) { 63 | console.log(snapshot.val().first); 64 | }, function onCancel () { 65 | log.error(err); 66 | }); 67 | ``` 68 | 69 | ##### Test 70 | 71 | ```js 72 | var errors = []; 73 | log.error = function (err) { 74 | errors.push(err); 75 | }; 76 | var err = new Error(); 77 | people.forceCancel(err, 'child_added'); 78 | console.assert(errors.length === 1, 'child_added was cancelled'); 79 | ``` 80 | -------------------------------------------------------------------------------- /tutorials/override.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Override 2 | 3 | When writing unit tests, you'll probably want to replace calls to `Firebase` in your source code with `MockFirebase`. 4 | 5 | When `Firebase` is attached to the `window`, you can replace it using the `override` method: 6 | 7 | ```js 8 | MockFirebase.override(); 9 | ``` 10 | 11 | Now all future calls to `Firebase` will actually call `MockFirebase`. Make sure you call `override` before calling `Firebase` in your source, otherwise the reference will be created before the override is performed. -------------------------------------------------------------------------------- /tutorials/proxyquire.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Overriding `require('firebase')` 2 | 3 | In Node/Browserify, you need to patch `require` itself to override `Firebase` calls. The trio of [proxyquire](https://github.com/thlorenz/proxyquire) (Node), [proxyquireify](https://github.com/thlorenz/proxyquireify) (Browserify), and [proxyquire-universal](https://github.com/bendrucker/proxyquire-universal) (both) make this easy. 4 | 5 | ##### Source 6 | 7 | ```js 8 | // ./mySrc.js 9 | var Firebase = require('firebase'); 10 | var ref = new Firebase('myRefUrl'); 11 | ref.on('value', function (snapshot) { 12 | console.log(snapshot.val()); 13 | }); 14 | ``` 15 | 16 | ##### Test 17 | 18 | ```js 19 | // ./test.js 20 | var proxyquire = require('proxyquire'); 21 | var MockFirebase = require('mockfirebase').MockFirebase; 22 | var mock; 23 | var mySrc = proxyquire('./mySrc', { 24 | firebase: function (url) { 25 | return (mock = new MockFirebase(url)); 26 | } 27 | }); 28 | mock.flush(); 29 | // data is logged 30 | ``` 31 | --------------------------------------------------------------------------------