├── .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 |
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 |
--------------------------------------------------------------------------------