From c5088d230054d1e3d9d1dc35577dab1df507602c Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 16 Oct 2014 13:25:14 -0700 Subject: [PATCH 01/17] Copied Simple Login code over to new file --- src/user.js | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 src/user.js diff --git a/src/user.js b/src/user.js new file mode 100644 index 00000000..e5222629 --- /dev/null +++ b/src/user.js @@ -0,0 +1,235 @@ +/* istanbul ignore next */ +(function() { + 'use strict'; + var AngularFireAuth; + + // Defines the `$firebaseSimpleLogin` service that provides simple + // user authentication support for AngularFire. + angular.module("firebase").factory("$firebaseSimpleLogin", [ + "$q", "$timeout", "$rootScope", function($q, $t, $rs) { + // The factory returns an object containing the authentication state + // of the current user. This service takes one argument: + // + // * `ref` : A Firebase reference. + // + // The returned object has the following properties: + // + // * `user`: Set to "null" if the user is currently logged out. This + // value will be changed to an object when the user successfully logs + // in. This object will contain details of the logged in user. The + // exact properties will vary based on the method used to login, but + // will at a minimum contain the `id` and `provider` properties. + // + // The returned object will also have the following methods available: + // $login(), $logout(), $createUser(), $changePassword(), $removeUser(), + // and $getCurrentUser(). + return function(ref) { + var auth = new AngularFireAuth($q, $t, $rs, ref); + return auth.construct(); + }; + } + ]); + + AngularFireAuth = function($q, $t, $rs, ref) { + this._q = $q; + this._timeout = $t; + this._rootScope = $rs; + this._loginDeferred = null; + this._getCurrentUserDeferred = []; + this._currentUserData = undefined; + + if (typeof ref == "string") { + throw new Error("Please provide a Firebase reference instead " + + "of a URL, eg: new Firebase(url)"); + } + this._fRef = ref; + }; + + AngularFireAuth.prototype = { + construct: function() { + var object = { + user: null, + $login: this.login.bind(this), + $logout: this.logout.bind(this), + $createUser: this.createUser.bind(this), + $changePassword: this.changePassword.bind(this), + $removeUser: this.removeUser.bind(this), + $getCurrentUser: this.getCurrentUser.bind(this), + $sendPasswordResetEmail: this.sendPasswordResetEmail.bind(this) + }; + this._object = object; + + // Initialize Simple Login. + if (!window.FirebaseSimpleLogin) { + var err = new Error("FirebaseSimpleLogin is undefined. " + + "Did you forget to include firebase-simple-login.js?"); + this._rootScope.$broadcast("$firebaseSimpleLogin:error", err); + throw err; + } + + var client = new FirebaseSimpleLogin(this._fRef, + this._onLoginEvent.bind(this)); + this._authClient = client; + return this._object; + }, + + // The login method takes a provider (for Simple Login) and authenticates + // the Firebase reference with which the service was initialized. This + // method returns a promise, which will be resolved when the login succeeds + // (and rejected when an error occurs). + login: function(provider, options) { + var deferred = this._q.defer(); + var self = this; + + // To avoid the promise from being fulfilled by our initial login state, + // make sure we have it before triggering the login and creating a new + // promise. + this.getCurrentUser().then(function() { + self._loginDeferred = deferred; + self._authClient.login(provider, options); + }); + + return deferred.promise; + }, + + // Unauthenticate the Firebase reference. + logout: function() { + // Tell the simple login client to log us out. + this._authClient.logout(); + + // Forget who we were immediately, so that any getCurrentUser() calls + // will resolve the user as logged out even before the _onLoginEvent() + // fires and resets this._currentUserData to null again. + this._currentUserData = null; + }, + + // Creates a user for Firebase Simple Login. Function 'cb' receives an + // error as the first argument and a Simple Login user object as the second + // argument. Note that this function only creates the user, if you wish to + // log in as the newly created user, call $login() after the promise for + // this method has been fulfilled. + createUser: function(email, password) { + var self = this; + var deferred = this._q.defer(); + + self._authClient.createUser(email, password, function(err, user) { + if (err) { + self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); + deferred.reject(err); + } else { + deferred.resolve(user); + } + }); + + return deferred.promise; + }, + + // Changes the password for a Firebase Simple Login user. Take an email, + // old password and new password as three mandatory arguments. Returns a + // promise. + changePassword: function(email, oldPassword, newPassword) { + var self = this; + var deferred = this._q.defer(); + + self._authClient.changePassword(email, oldPassword, newPassword, + function(err) { + if (err) { + self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); + deferred.reject(err); + } else { + deferred.resolve(); + } + } + ); + + return deferred.promise; + }, + + // Gets a promise for the current user info. + getCurrentUser: function() { + var self = this; + var deferred = this._q.defer(); + + if (self._currentUserData !== undefined) { + deferred.resolve(self._currentUserData); + } else { + self._getCurrentUserDeferred.push(deferred); + } + + return deferred.promise; + }, + + // Remove a user for the listed email address. Returns a promise. + removeUser: function(email, password) { + var self = this; + var deferred = this._q.defer(); + + self._authClient.removeUser(email, password, function(err) { + if (err) { + self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); + deferred.reject(err); + } else { + deferred.resolve(); + } + }); + + return deferred.promise; + }, + + // Send a password reset email to the user for an email + password account. + sendPasswordResetEmail: function(email) { + var self = this; + var deferred = this._q.defer(); + + self._authClient.sendPasswordResetEmail(email, function(err) { + if (err) { + self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); + deferred.reject(err); + } else { + deferred.resolve(); + } + }); + + return deferred.promise; + }, + + // Internal callback for any Simple Login event. + _onLoginEvent: function(err, user) { + // HACK -- calls to logout() trigger events even if we're not logged in, + // making us get extra events. Throw them away. This should be fixed by + // changing Simple Login so that its callbacks refer directly to the + // action that caused them. + if (this._currentUserData === user && err === null) { + return; + } + + var self = this; + if (err) { + if (self._loginDeferred) { + self._loginDeferred.reject(err); + self._loginDeferred = null; + } + self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); + } else { + this._currentUserData = user; + + self._timeout(function() { + self._object.user = user; + if (user) { + self._rootScope.$broadcast("$firebaseSimpleLogin:login", user); + } else { + self._rootScope.$broadcast("$firebaseSimpleLogin:logout"); + } + if (self._loginDeferred) { + self._loginDeferred.resolve(user); + self._loginDeferred = null; + } + while (self._getCurrentUserDeferred.length > 0) { + var def = self._getCurrentUserDeferred.pop(); + def.resolve(user); + } + }); + } + } + }; +})(); From a884b5834043ec3bdb2ec556b506156768f8911b Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Wed, 22 Oct 2014 14:37:55 -0700 Subject: [PATCH 02/17] Updated authentication methods to login v2 --- .jshintrc | 3 +- src/user.js | 287 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 174 insertions(+), 116 deletions(-) diff --git a/.jshintrc b/.jshintrc index 14e5e8db..40b7d093 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,11 +10,10 @@ "forin": true, "indent": 2, "latedef": true, - "maxlen": 115, "noempty": true, "nonbsp": true, "strict": true, "trailing": true, "undef": true, "unused": true -} \ No newline at end of file +} diff --git a/src/user.js b/src/user.js index e5222629..9e724b2a 100644 --- a/src/user.js +++ b/src/user.js @@ -1,11 +1,11 @@ /* istanbul ignore next */ (function() { 'use strict'; - var AngularFireAuth; + var AngularFireUser; - // Defines the `$firebaseSimpleLogin` service that provides simple + // Defines the `$angularFireUser` service that provides simple // user authentication support for AngularFire. - angular.module("firebase").factory("$firebaseSimpleLogin", [ + angular.module("firebase").factory("$angularFireUser", [ "$q", "$timeout", "$rootScope", function($q, $t, $rs) { // The factory returns an object containing the authentication state // of the current user. This service takes one argument: @@ -14,29 +14,36 @@ // // The returned object has the following properties: // - // * `user`: Set to "null" if the user is currently logged out. This + // * `authData`: Set to "null" if the user is currently logged out. This // value will be changed to an object when the user successfully logs - // in. This object will contain details of the logged in user. The + // in. This object will contain details about the logged in user. The // exact properties will vary based on the method used to login, but - // will at a minimum contain the `id` and `provider` properties. + // will at a minimum contain the `uid` and `provider` properties. // // The returned object will also have the following methods available: // $login(), $logout(), $createUser(), $changePassword(), $removeUser(), // and $getCurrentUser(). return function(ref) { - var auth = new AngularFireAuth($q, $t, $rs, ref); + var auth = new AngularFireUser($q, $t, $rs, ref); return auth.construct(); }; } ]); - AngularFireAuth = function($q, $t, $rs, ref) { + AngularFireUser = function($q, $t, $rs, ref) { this._q = $q; this._timeout = $t; this._rootScope = $rs; this._loginDeferred = null; this._getCurrentUserDeferred = []; - this._currentUserData = undefined; + this._currentAuthData = ref.getAuth(); + + // TODO: these events don't seem to fire upon page reload + if (this._currentAuthData) { + this._rootScope.$broadcast("$angularFireUser:login", this._currentAuthData); + } else { + this._rootScope.$broadcast("$angularFireUser:logout"); + } if (typeof ref == "string") { throw new Error("Please provide a Firebase reference instead " + @@ -45,62 +52,153 @@ this._fRef = ref; }; - AngularFireAuth.prototype = { + AngularFireUser.prototype = { construct: function() { - var object = { - user: null, + this._object = { + authData: null, + // Authentication + $auth: this.auth.bind(this), + $authWithPassword: this.authWithPassword.bind(this), + $authAnonymously: this.authAnonymously.bind(this), + $authWithOAuthPopup: this.authWithOAuthPopup.bind(this), + $authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this), + $authWithOAuthToken: this.authWithOAuthToken.bind(this), + $unauth: this.unauth.bind(this), $login: this.login.bind(this), $logout: this.logout.bind(this), + + // Authentication state + $getCurrentUser: this.getCurrentUser.bind(this), + $requireUser: this.requireUser.bind(this), + + // User management $createUser: this.createUser.bind(this), $changePassword: this.changePassword.bind(this), $removeUser: this.removeUser.bind(this), - $getCurrentUser: this.getCurrentUser.bind(this), $sendPasswordResetEmail: this.sendPasswordResetEmail.bind(this) }; - this._object = object; - - // Initialize Simple Login. - if (!window.FirebaseSimpleLogin) { - var err = new Error("FirebaseSimpleLogin is undefined. " + - "Did you forget to include firebase-simple-login.js?"); - this._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - throw err; - } - var client = new FirebaseSimpleLogin(this._fRef, - this._onLoginEvent.bind(this)); - this._authClient = client; return this._object; }, - // The login method takes a provider (for Simple Login) and authenticates - // the Firebase reference with which the service was initialized. This - // method returns a promise, which will be resolved when the login succeeds - // (and rejected when an error occurs). - login: function(provider, options) { + // TODO: remove the promise? + // Synchronously retrieves the current auth data. + getCurrentUser: function() { + var deferred = this._q.defer(); + + deferred.resolve(this._currentAuthData); + + return deferred.promise; + }, + + // Returns a promise which is resolved if a user is authenticated and rejects the promise if + // the user does not exist. This can be used in routers to require routes to have a user + // logged in. + requireUser: function() { var deferred = this._q.defer(); + + if (this._currentAuthData) { + deferred.resolve(this._currentAuthData); + } else { + deferred.reject(); + } + + return deferred.promise; + }, + + _updateAuthData: function(authData) { var self = this; + this._timeout(function() { + self._object.authData = authData; + self._currentAuthData = authData; + }); + }, + + _onCompletionHandler: function(deferred, error, authData) { + if (error !== null) { + this._rootScope.$broadcast("$angularFireUser:error", error); + deferred.reject(error); + } else { + this._rootScope.$broadcast("$angularFireUser:login", authData); + this._updateAuthData(authData); + deferred.resolve(authData); + } + }, - // To avoid the promise from being fulfilled by our initial login state, - // make sure we have it before triggering the login and creating a new - // promise. - this.getCurrentUser().then(function() { - self._loginDeferred = deferred; - self._authClient.login(provider, options); + auth: function(authToken) { + var deferred = this._q.defer(); + var self = this; + this._fRef.authWithPassword(authToken, this._onCompletionHandler.bind(this, deferred), function(error) { + self._rootScope.$broadcast("$angularFireUser:error", error); }); + return deferred.promise; + }, + + authWithPassword: function(credentials, options) { + var deferred = this._q.defer(); + this._fRef.authWithPassword(credentials, this._onCompletionHandler.bind(this, deferred), options); + return deferred.promise; + }, + + authAnonymously: function(options) { + var deferred = this._q.defer(); + this._fRef.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); + return deferred.promise; + }, + + authWithOAuthPopup: function(provider, options) { + var deferred = this._q.defer(); + this._fRef.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); + return deferred.promise; + }, + + authWithOAuthRedirect: function(provider, options) { + var deferred = this._q.defer(); + this._fRef.authWithOAuthRedirect(provider, this._onCompletionHandler.bind(this, deferred), options); + return deferred.promise; + }, + + authWithOAuthToken: function(provider, credentials, options) { + var deferred = this._q.defer(); + this._fRef.authWithOAuthToken(provider, credentials, this._onCompletionHandler.bind(this, deferred), options); + return deferred.promise; + }, + + unauth: function() { + if (this._currentAuthData) { + this._fRef.unauth(); + this._updateAuthData(null); + this._rootScope.$broadcast("$angularFireUser:logout"); + } + }, + + // The login method takes a provider and authenticates the Firebase reference + // with which the service was initialized. This method returns a promise, which + // will be resolved when the login succeeds (and rejected when an error occurs). + login: function(provider, options) { + var deferred = this._q.defer(); + + if (provider === 'anonymous') { + this._fRef.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); + } else if (provider === 'password') { + this._fRef.authWithPassword(options, this._onCompletionHandler.bind(this, deferred)); + } else { + this._fRef.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); + } return deferred.promise; }, // Unauthenticate the Firebase reference. logout: function() { - // Tell the simple login client to log us out. - this._authClient.logout(); - - // Forget who we were immediately, so that any getCurrentUser() calls - // will resolve the user as logged out even before the _onLoginEvent() - // fires and resets this._currentUserData to null again. - this._currentUserData = null; + // TODO: update comment? + // Simple Login fires _onLoginEvent() even if no user is logged in. We don't care about + // firing this logout event multiple times, so explicitly check if a user is defined. + if (this._currentAuthData) { + this._fRef.unauth(); + this._updateAuthData(null); + this._rootScope.$broadcast("$angularFireUser:logout"); + } }, // Creates a user for Firebase Simple Login. Function 'cb' receives an @@ -112,12 +210,15 @@ var self = this; var deferred = this._q.defer(); - self._authClient.createUser(email, password, function(err, user) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); + this._fRef.createUser({ + email: email, + password: password + }, function(error) { + if (error !== null) { + self._rootScope.$broadcast("$angularFireUser:error", error); + deferred.reject(error); } else { - deferred.resolve(user); + deferred.resolve(); } }); @@ -131,11 +232,15 @@ var self = this; var deferred = this._q.defer(); - self._authClient.changePassword(email, oldPassword, newPassword, - function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); + self._fRef.changePassword({ + email: email, + oldPassword: oldPassword, + newPassword: newPassword + }, function(error) { + if (error !== null) { + // TODO: do we want to send the error code as well? + self._rootScope.$broadcast("$angularFireUser:error", error); + deferred.reject(error); } else { deferred.resolve(); } @@ -145,29 +250,19 @@ return deferred.promise; }, - // Gets a promise for the current user info. - getCurrentUser: function() { - var self = this; - var deferred = this._q.defer(); - - if (self._currentUserData !== undefined) { - deferred.resolve(self._currentUserData); - } else { - self._getCurrentUserDeferred.push(deferred); - } - - return deferred.promise; - }, - // Remove a user for the listed email address. Returns a promise. removeUser: function(email, password) { var self = this; var deferred = this._q.defer(); - self._authClient.removeUser(email, password, function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); + self._fRef.removeUser({ + email: email, + password: password + }, function(error) { + if (error !== null) { + // TODO: do we want to send the error code as well? + self._rootScope.$broadcast("$angularFireUser:error", error); + deferred.reject(error); } else { deferred.resolve(); } @@ -181,55 +276,19 @@ var self = this; var deferred = this._q.defer(); - self._authClient.sendPasswordResetEmail(email, function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); + self._fRef.resetPassword({ + email: email + }, function(error) { + if (error !== null) { + // TODO: do we want to send the error code as well? + self._rootScope.$broadcast("$angularFireUser:error", error); + deferred.reject(error); } else { deferred.resolve(); } }); return deferred.promise; - }, - - // Internal callback for any Simple Login event. - _onLoginEvent: function(err, user) { - // HACK -- calls to logout() trigger events even if we're not logged in, - // making us get extra events. Throw them away. This should be fixed by - // changing Simple Login so that its callbacks refer directly to the - // action that caused them. - if (this._currentUserData === user && err === null) { - return; - } - - var self = this; - if (err) { - if (self._loginDeferred) { - self._loginDeferred.reject(err); - self._loginDeferred = null; - } - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - } else { - this._currentUserData = user; - - self._timeout(function() { - self._object.user = user; - if (user) { - self._rootScope.$broadcast("$firebaseSimpleLogin:login", user); - } else { - self._rootScope.$broadcast("$firebaseSimpleLogin:logout"); - } - if (self._loginDeferred) { - self._loginDeferred.resolve(user); - self._loginDeferred = null; - } - while (self._getCurrentUserDeferred.length > 0) { - var def = self._getCurrentUserDeferred.pop(); - def.resolve(user); - } - }); - } } }; })(); From a6f69345d54115c40d75dbe8f2924fa684fbc004 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Wed, 22 Oct 2014 14:41:23 -0700 Subject: [PATCH 03/17] Created new test spec for login methods --- tests/automatic_karma.conf.js | 2 +- tests/unit/Authentication.spec.js | 718 ++++++++++++++++++++++++++++++ tests/unit/UserManagement.spec.js | 0 3 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 tests/unit/Authentication.spec.js create mode 100644 tests/unit/UserManagement.spec.js diff --git a/tests/automatic_karma.conf.js b/tests/automatic_karma.conf.js index 6d4c2be0..fe5625fc 100644 --- a/tests/automatic_karma.conf.js +++ b/tests/automatic_karma.conf.js @@ -34,7 +34,7 @@ module.exports = function(config) { '../src/module.js', '../src/**/*.js', 'mocks/**/*.js', - 'unit/**/*.spec.js' + 'unit/Authentication.spec.js' ] }); }; diff --git a/tests/unit/Authentication.spec.js b/tests/unit/Authentication.spec.js new file mode 100644 index 00000000..fd384395 --- /dev/null +++ b/tests/unit/Authentication.spec.js @@ -0,0 +1,718 @@ +'use strict'; +describe('$angularFireUser', function () { + + var $firebase, $angularFireUser, $timeout, $rootScope, $utils; + + beforeEach(function() { + module('firebase'); + module('mock.firebase'); + module('mock.utils'); + // have to create these before the first call to inject + // or they will not be registered with the angular mock injector + angular.module('firebase').provider('TestArrayFactory', { + $get: function() { + return function() {} + } + }).provider('TestObjectFactory', { + $get: function() { + return function() {}; + } + }); + inject(function (_$firebase_, _$angularFireUser_, _$timeout_, _$rootScope_, $firebaseUtils) { + $firebase = _$firebase_; + $angularFireUser = _$angularFireUser_; + $timeout = _$timeout_; + $rootScope = _$rootScope_; + $utils = $firebaseUtils; + }); + }); + + describe('', function() { + it('should accept a Firebase ref', function() { + var ref = new Firebase('Mock://'); + var auth = new $angularFireUser(ref); + expect($fb.$ref()).toBe(ref); + }); + + xit('should throw an error if passed a string', function() { + expect(function() { + $firebase('hello world'); + }).toThrowError(/valid Firebase reference/); + }); + + xit('should accept a factory name for arrayFactory', function() { + var ref = new Firebase('Mock://'); + var app = angular.module('firebase'); + // if this does not throw an error we are fine + expect($firebase(ref, {arrayFactory: 'TestArrayFactory'})).toBeAn('object'); + }); + + xit('should accept a factory name for objectFactory', function() { + var ref = new Firebase('Mock://'); + var app = angular.module('firebase'); + app.provider('TestObjectFactory', { + $get: function() { + return function() {} + } + }); + // if this does not throw an error we are fine + expect($firebase(ref, {objectFactory: 'TestObjectFactory'})).toBeAn('object'); + }); + + xit('should throw an error if factory name for arrayFactory does not exist', function() { + var ref = new Firebase('Mock://'); + expect(function() { + $firebase(ref, {arrayFactory: 'notarealarrayfactorymethod'}) + }).toThrowError(); + }); + + xit('should throw an error if factory name for objectFactory does not exist', function() { + var ref = new Firebase('Mock://'); + expect(function() { + $firebase(ref, {objectFactory: 'notarealobjectfactorymethod'}) + }).toThrowError(); + }); + }); + + xdescribe('$ref', function() { + var $fb; + beforeEach(function() { + $fb = $firebase(new Firebase('Mock://').child('data')); + }); + + it('should return ref that created the $firebase instance', function() { + var ref = new Firebase('Mock://'); + var $fb = new $firebase(ref); + expect($fb.$ref()).toBe(ref); + }); + }); + + xdescribe('$push', function() { + var $fb, flushAll; + beforeEach(function() { + $fb = $firebase(new Firebase('Mock://').child('data')); + flushAll = flush.bind(null, $fb.$ref()); + }); + + it('should return a promise', function() { + var res = $fb.$push({foo: 'bar'}); + expect(angular.isObject(res)).toBe(true); + expect(typeof res.then).toBe('function'); + }); + + it('should resolve to the ref for new id', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$push({foo: 'bar'}).then(whiteSpy, blackSpy); + flushAll(); + var newId = $fb.$ref().getLastAutoId(); + expect(whiteSpy).toHaveBeenCalled(); + expect(blackSpy).not.toHaveBeenCalled(); + var ref = whiteSpy.calls.argsFor(0)[0]; + expect(ref.name()).toBe(newId); + }); + + it('should reject if fails', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$ref().failNext('push', 'failpush'); + $fb.$push({foo: 'bar'}).then(whiteSpy, blackSpy); + flushAll(); + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith('failpush'); + }); + + it('should save correct data into Firebase', function() { + var spy = jasmine.createSpy('push callback').and.callFake(function(ref) { + expect($fb.$ref().getData()[ref.name()]).toEqual({foo: 'pushtest'}); + }); + $fb.$push({foo: 'pushtest'}).then(spy); + flushAll(); + expect(spy).toHaveBeenCalled(); + }); + + it('should work on a query', function() { + var ref = new Firebase('Mock://').child('ordered').limit(5); + var $fb = $firebase(ref); + flushAll(); + expect(ref.ref().push).not.toHaveBeenCalled(); + $fb.$push({foo: 'querytest'}); + flushAll(); + expect(ref.ref().push).toHaveBeenCalled(); + }); + }); + + xdescribe('$set', function() { + var $fb, flushAll; + beforeEach(function() { + $fb = $firebase(new Firebase('Mock://').child('data')); + flushAll = flush.bind(null, $fb.$ref()); + }); + + it('should return a promise', function() { + var res = $fb.$set(null); + expect(angular.isObject(res)).toBe(true); + expect(typeof res.then).toBe('function'); + }); + + it('should resolve to ref for child key', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$set('reftest', {foo: 'bar'}).then(whiteSpy, blackSpy); + flushAll(); + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalledWith($fb.$ref().child('reftest')); + }); + + it('should resolve to ref if no key', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$set({foo: 'bar'}).then(whiteSpy, blackSpy); + flushAll(); + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalledWith($fb.$ref()); + }); + + it('should save a child if key used', function() { + $fb.$set('foo', 'bar'); + flushAll(); + expect($fb.$ref().getData()['foo']).toEqual('bar'); + }); + + it('should save everything if no key', function() { + $fb.$set(true); + flushAll(); + expect($fb.$ref().getData()).toBe(true); + }); + + it('should reject if fails', function() { + $fb.$ref().failNext('set', 'setfail'); + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$set({foo: 'bar'}).then(whiteSpy, blackSpy); + flushAll(); + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith('setfail'); + }); + + it('should affect query keys only if query used', function() { + var ref = new Firebase('Mock://').child('ordered').limit(1); + var $fb = $firebase(ref); + ref.flush(); + var expKeys = ref.slice().keys; + $fb.$set({hello: 'world'}); + ref.flush(); + var args = ref.ref().update.calls.mostRecent().args[0]; + expect(Object.keys(args)).toEqual(['hello'].concat(expKeys)); + }); + }); + + xdescribe('$remove', function() { + var $fb, flushAll; + beforeEach(function() { + $fb = $firebase(new Firebase('Mock://').child('data')); + flushAll = flush.bind(null, $fb.$ref()); + }); + + it('should return a promise', function() { + var res = $fb.$remove(); + expect(angular.isObject(res)).toBe(true); + expect(typeof res.then).toBe('function'); + }); + + it('should resolve to ref if no key', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$remove().then(whiteSpy, blackSpy); + flushAll(); + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalledWith($fb.$ref()); + }); + + it('should resolve to ref if query', function() { + var spy = jasmine.createSpy('resolve'); + var ref = new Firebase('Mock://').child('ordered').limit(2); + var $fb = $firebase(ref); + $fb.$remove().then(spy); + flushAll(); + expect(spy).toHaveBeenCalledWith(ref); + }); + + it('should resolve to child ref if key', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$remove('b').then(whiteSpy, blackSpy); + flushAll(); + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalledWith($fb.$ref().child('b')); + }); + + it('should remove a child if key used', function() { + $fb.$remove('c'); + flushAll(); + var dat = $fb.$ref().getData(); + expect(angular.isObject(dat)).toBe(true); + expect(dat.hasOwnProperty('c')).toBe(false); + }); + + it('should remove everything if no key', function() { + $fb.$remove(); + flushAll(); + expect($fb.$ref().getData()).toBe(null); + }); + + it('should reject if fails', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$ref().failNext('remove', 'test_fail_remove'); + $fb.$remove().then(whiteSpy, blackSpy); + flushAll(); + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith('test_fail_remove'); + }); + + it('should remove data in Firebase', function() { + $fb.$remove(); + flushAll(); + expect($fb.$ref().remove).toHaveBeenCalled(); + }); + + //todo-test https://github.com/katowulf/mockfirebase/issues/9 + it('should only remove keys in query if used on a query', function() { + var ref = new Firebase('Mock://').child('ordered').limit(2); + var keys = ref.slice().keys; + var origKeys = ref.ref().getKeys(); + var expLength = origKeys.length - keys.length; + expect(keys.length).toBeGreaterThan(0); + expect(origKeys.length).toBeGreaterThan(keys.length); + var $fb = $firebase(ref); + flushAll(ref); + $fb.$remove(); + flushAll(ref); + keys.forEach(function(key) { + expect(ref.ref().child(key).remove).toHaveBeenCalled(); + }); + origKeys.forEach(function(key) { + if( keys.indexOf(key) === -1 ) { + expect(ref.ref().child(key).remove).not.toHaveBeenCalled(); + } + }); + }); + }); + + xdescribe('$update', function() { + var $fb, flushAll; + beforeEach(function() { + $fb = $firebase(new Firebase('Mock://').child('data')); + flushAll = flush.bind(null, $fb.$ref()); + }); + + it('should return a promise', function() { + expect($fb.$update({foo: 'bar'})).toBeAPromise(); + }); + + it('should resolve to ref when done', function() { + var spy = jasmine.createSpy('resolve'); + $fb.$update('index', {foo: 'bar'}).then(spy); + flushAll(); + var arg = spy.calls.argsFor(0)[0]; + expect(arg).toBeAFirebaseRef(); + expect(arg.name()).toBe('index'); + }); + + it('should reject if failed', function() { + var whiteSpy = jasmine.createSpy('resolve'); + var blackSpy = jasmine.createSpy('reject'); + $fb.$ref().failNext('update', 'oops'); + $fb.$update({index: {foo: 'bar'}}).then(whiteSpy, blackSpy); + flushAll(); + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalled(); + }); + + it('should not destroy untouched keys', function() { + flushAll(); + var data = $fb.$ref().getData(); + data.a = 'foo'; + delete data.b; + expect(Object.keys(data).length).toBeGreaterThan(1); + $fb.$update({a: 'foo', b: null}); + flushAll(); + expect($fb.$ref().getData()).toEqual(data); + }); + + it('should replace keys specified', function() { + $fb.$update({a: 'foo', b: null}); + flushAll(); + var data = $fb.$ref().getData(); + expect(data.a).toBe('foo'); + expect(data.b).toBeUndefined(); + }); + + it('should work on a query object', function() { + var $fb2 = $firebase($fb.$ref().limit(1)); + flushAll(); + $fb2.$update({foo: 'bar'}); + flushAll(); + expect($fb2.$ref().ref().getData().foo).toBe('bar'); + }); + }); + + xdescribe('$transaction', function() { + var $fb, flushAll; + beforeEach(function() { + $fb = $firebase(new Firebase('Mock://').child('data')); + flushAll = flush.bind(null, $fb.$ref()); + }); + + it('should return a promise', function() { + expect($fb.$transaction('a', function() {})).toBeAPromise(); + }); + + it('should resolve to snapshot on success', function() { + var whiteSpy = jasmine.createSpy('success'); + var blackSpy = jasmine.createSpy('failed'); + $fb.$transaction('a', function() { return true; }).then(whiteSpy, blackSpy); + flushAll(); + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalled(); + expect(whiteSpy.calls.argsFor(0)[0]).toBeASnapshot(); + }); + + it('should resolve to null on abort', function() { + var spy = jasmine.createSpy('success'); + $fb.$transaction('a', function() {}).then(spy); + flushAll(); + expect(spy).toHaveBeenCalledWith(null); + }); + + it('should reject if failed', function() { + var whiteSpy = jasmine.createSpy('success'); + var blackSpy = jasmine.createSpy('failed'); + $fb.$ref().child('a').failNext('transaction', 'test_fail'); + $fb.$transaction('a', function() { return true; }).then(whiteSpy, blackSpy); + flushAll(); + expect(whiteSpy).not.toHaveBeenCalled(); + expect(blackSpy).toHaveBeenCalledWith('test_fail'); + }); + + it('should modify data in firebase', function() { + var newData = {hello: 'world'}; + $fb.$transaction('c', function() { return newData; }); + flushAll(); + expect($fb.$ref().child('c').getData()).toEqual(jasmine.objectContaining(newData)); + }); + + it('should work okay on a query', function() { + var whiteSpy = jasmine.createSpy('success'); + var blackSpy = jasmine.createSpy('failed'); + $fb.$transaction(function() { return 'happy'; }).then(whiteSpy, blackSpy); + flushAll(); + expect(blackSpy).not.toHaveBeenCalled(); + expect(whiteSpy).toHaveBeenCalled(); + expect(whiteSpy.calls.argsFor(0)[0]).toBeASnapshot(); + }); + }); + + xdescribe('$asArray', function() { + var $ArrayFactory, $fb; + + function flushAll() { + flush($fb.$ref()); + } + + beforeEach(function() { + $ArrayFactory = stubArrayFactory(); + $fb = $firebase(new Firebase('Mock://').child('data'), {arrayFactory: $ArrayFactory}); + }); + + it('should call $FirebaseArray constructor with correct args', function() { + var arr = $fb.$asArray(); + expect($ArrayFactory).toHaveBeenCalledWith($fb, jasmine.any(Function), jasmine.objectContaining({})); + expect(arr.$$$readyPromise).toBeAPromise(); + }); + + it('should return the factory value (an array)', function() { + var factory = stubArrayFactory(); + var res = $firebase($fb.$ref(), {arrayFactory: factory}).$asArray(); + expect(res).toBe(factory.$myArray); + }); + + it('should explode if ArrayFactory does not return an array', function() { + expect(function() { + function fn() { return {}; } + $firebase(new Firebase('Mock://').child('data'), {arrayFactory: fn}).$asArray(); + }).toThrowError(Error); + }); + + it('should contain data in ref() after load', function() { + var count = Object.keys($fb.$ref().getData()).length; + expect(count).toBeGreaterThan(1); + var arr = $fb.$asArray(); + flushAll(); + expect(arr.$$added.calls.count()).toBe(count); + }); + + it('should return same instance if called multiple times', function() { + expect($fb.$asArray()).toBe($fb.$asArray()); + }); + + it('should use arrayFactory', function() { + var spy = stubArrayFactory(); + $firebase($fb.$ref(), {arrayFactory: spy}).$asArray(); + expect(spy).toHaveBeenCalled(); + }); + + it('should match query keys if query used', function() { + // needs to contain more than 2 items in data for this limit to work + expect(Object.keys($fb.$ref().getData()).length).toBeGreaterThan(2); + var ref = $fb.$ref().limit(2); + var arr = $firebase(ref, {arrayFactory: $ArrayFactory}).$asArray(); + flushAll(); + expect(arr.$$added.calls.count()).toBe(2); + }); + + it('should return new instance if old one is destroyed', function() { + var arr = $fb.$asArray(); + // invoke the destroy function + arr.$$$destroyFn(); + expect($fb.$asObject()).not.toBe(arr); + }); + + it('should call $$added if child_added event is received', function() { + var arr = $fb.$asArray(); + // flush all the existing data through + flushAll(); + arr.$$added.calls.reset(); + // now add a new record and see if it sticks + $fb.$ref().push({hello: 'world'}); + flushAll(); + expect(arr.$$added.calls.count()).toBe(1); + }); + + it('should call $$updated if child_changed event is received', function() { + var arr = $fb.$asArray(); + // flush all the existing data through + flushAll(); + // now change a new record and see if it sticks + $fb.$ref().child('c').set({hello: 'world'}); + flushAll(); + expect(arr.$$updated.calls.count()).toBe(1); + }); + + it('should call $$moved if child_moved event is received', function() { + var arr = $fb.$asArray(); + // flush all the existing data through + flushAll(); + // now change a new record and see if it sticks + $fb.$ref().child('c').setPriority(299); + flushAll(); + expect(arr.$$moved.calls.count()).toBe(1); + }); + + it('should call $$removed if child_removed event is received', function() { + var arr = $fb.$asArray(); + // flush all the existing data through + flushAll(); + // now change a new record and see if it sticks + $fb.$ref().child('a').remove(); + flushAll(); + expect(arr.$$removed.calls.count()).toBe(1); + }); + + it('should call $$error if an error event occurs', function() { + var arr = $fb.$asArray(); + // flush all the existing data through + flushAll(); + $fb.$ref().forceCancel('test_failure'); + flushAll(); + expect(arr.$$error).toHaveBeenCalledWith('test_failure'); + }); + + it('should resolve readyPromise after initial data loaded', function() { + var arr = $fb.$asArray(); + var spy = jasmine.createSpy('resolved').and.callFake(function(arrRes) { + var count = arrRes.$$added.calls.count(); + expect(count).toBe($fb.$ref().getKeys().length); + }); + arr.$$$readyPromise.then(spy); + expect(spy).not.toHaveBeenCalled(); + flushAll($fb.$ref()); + expect(spy).toHaveBeenCalled(); + }); + + it('should cancel listeners if destroyFn is invoked', function() { + var arr = $fb.$asArray(); + var ref = $fb.$ref(); + flushAll(); + expect(ref.on).toHaveBeenCalled(); + arr.$$$destroyFn(); + expect(ref.off.calls.count()).toBe(ref.on.calls.count()); + }); + + it('should trigger an angular compile', function() { + $fb.$asObject(); // creates the listeners + var ref = $fb.$ref(); + flushAll(); + $utils.wait.completed.calls.reset(); + ref.push({newa: 'newa'}); + flushAll(); + expect($utils.wait.completed).toHaveBeenCalled(); + }); + + it('should batch requests', function() { + $fb.$asArray(); // creates listeners + flushAll(); + $utils.wait.completed.calls.reset(); + var ref = $fb.$ref(); + ref.push({newa: 'newa'}); + ref.push({newb: 'newb'}); + ref.push({newc: 'newc'}); + ref.push({newd: 'newd'}); + flushAll(); + expect($utils.wait.completed.calls.count()).toBe(1); + }); + }); + + xdescribe('$asObject', function() { + var $fb; + + function flushAll() { + flush($fb.$ref()); + } + + beforeEach(function() { + var Factory = stubObjectFactory(); + $fb = $firebase(new Firebase('Mock://').child('data'), {objectFactory: Factory}); + $fb.$Factory = Factory; + }); + + it('should contain data in ref() after load', function() { + var data = $fb.$ref().getData(); + var obj = $fb.$asObject(); + flushAll(); + expect(obj.$$updated.calls.argsFor(0)[0].val()).toEqual(jasmine.objectContaining(data)); + }); + + it('should return same instance if called multiple times', function() { + expect($fb.$asObject()).toBe($fb.$asObject()); + }); + + it('should use recordFactory', function() { + var res = $fb.$asObject(); + expect(res).toBeInstanceOf($fb.$Factory); + }); + + it('should only contain query keys if query used', function() { + var ref = $fb.$ref().limit(2); + // needs to have more data than our query slice + expect(ref.ref().getKeys().length).toBeGreaterThan(2); + var obj = $fb.$asObject(); + flushAll(); + var snap = obj.$$updated.calls.argsFor(0)[0]; + expect(snap.val()).toEqual(jasmine.objectContaining(ref.getData())); + }); + + it('should call $$updated if value event is received', function() { + var obj = $fb.$asObject(); + var ref = $fb.$ref(); + flushAll(); + obj.$$updated.calls.reset(); + expect(obj.$$updated).not.toHaveBeenCalled(); + ref.set({foo: 'bar'}); + flushAll(); + expect(obj.$$updated).toHaveBeenCalled(); + }); + + it('should call $$error if an error event occurs', function() { + var ref = $fb.$ref(); + var obj = $fb.$asObject(); + flushAll(); + expect(obj.$$error).not.toHaveBeenCalled(); + ref.forceCancel('test_cancel'); + flushAll(); + expect(obj.$$error).toHaveBeenCalledWith('test_cancel'); + }); + + it('should resolve readyPromise after initial data loaded', function() { + var obj = $fb.$asObject(); + var spy = jasmine.createSpy('resolved').and.callFake(function(obj) { + var snap = obj.$$updated.calls.argsFor(0)[0]; + expect(snap.val()).toEqual(jasmine.objectContaining($fb.$ref().getData())); + }); + obj.$$$readyPromise.then(spy); + expect(spy).not.toHaveBeenCalled(); + flushAll(); + expect(spy).toHaveBeenCalled(); + }); + + it('should cancel listeners if destroyFn is invoked', function() { + var obj = $fb.$asObject(); + var ref = $fb.$ref(); + flushAll(); + expect(ref.on).toHaveBeenCalled(); + obj.$$$destroyFn(); + expect(ref.off.calls.count()).toBe(ref.on.calls.count()); + }); + + it('should trigger an angular compile', function() { + $fb.$asObject(); // creates the listeners + var ref = $fb.$ref(); + flushAll(); + $utils.wait.completed.calls.reset(); + ref.push({newa: 'newa'}); + flushAll(); + expect($utils.wait.completed).toHaveBeenCalled(); + }); + + it('should batch requests', function() { + var obj = $fb.$asObject(); // creates listeners + flushAll(); + $utils.wait.completed.calls.reset(); + var ref = $fb.$ref(); + ref.push({newa: 'newa'}); + ref.push({newb: 'newb'}); + ref.push({newc: 'newc'}); + ref.push({newd: 'newd'}); + flushAll(); + expect($utils.wait.completed.calls.count()).toBe(1); + }); + }); + + function stubArrayFactory() { + var arraySpy = []; + angular.forEach(['$$added', '$$updated', '$$moved', '$$removed', '$$error'], function(m) { + arraySpy[m] = jasmine.createSpy(m); + }); + var factory = jasmine.createSpy('ArrayFactory') + .and.callFake(function(inst, destroyFn, readyPromise) { + arraySpy.$$$inst = inst; + arraySpy.$$$destroyFn = destroyFn; + arraySpy.$$$readyPromise = readyPromise; + return arraySpy; + }); + factory.$myArray = arraySpy; + return factory; + } + + function stubObjectFactory() { + function Factory(inst, destFn, readyPromise) { + this.$$$inst = inst; + this.$$$destroyFn = destFn; + this.$$$readyPromise = readyPromise; + } + angular.forEach(['$$updated', '$$error'], function(m) { + Factory.prototype[m] = jasmine.createSpy(m); + }); + return Factory; + } + + function flush() { + // the order of these flush events is significant + Array.prototype.slice.call(arguments, 0).forEach(function(o) { + o.flush(); + }); + try { $timeout.flush(); } + catch(e) {} + } +}); diff --git a/tests/unit/UserManagement.spec.js b/tests/unit/UserManagement.spec.js new file mode 100644 index 00000000..e69de29b From b2e7e92f68b257db26b7f5f703f8b52ad06ba58d Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Fri, 24 Oct 2014 15:46:04 -0700 Subject: [PATCH 04/17] Renamed AngularFireUser to FirebaseUser and updated auth state methods --- src/user.js | 122 ++++++++++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/src/user.js b/src/user.js index 9e724b2a..feaa6266 100644 --- a/src/user.js +++ b/src/user.js @@ -1,12 +1,12 @@ /* istanbul ignore next */ (function() { - 'use strict'; - var AngularFireUser; + "use strict"; + var FirebaseUser; - // Defines the `$angularFireUser` service that provides simple + // Defines the `$firebaseUser` service that provides simple // user authentication support for AngularFire. - angular.module("firebase").factory("$angularFireUser", [ - "$q", "$timeout", "$rootScope", function($q, $t, $rs) { + angular.module("firebase").factory("$firebaseUser", [ + "$q", "$timeout", function($q, $t) { // The factory returns an object containing the authentication state // of the current user. This service takes one argument: // @@ -24,35 +24,25 @@ // $login(), $logout(), $createUser(), $changePassword(), $removeUser(), // and $getCurrentUser(). return function(ref) { - var auth = new AngularFireUser($q, $t, $rs, ref); + var auth = new FirebaseUser($q, $t, ref); return auth.construct(); }; } ]); - AngularFireUser = function($q, $t, $rs, ref) { + FirebaseUser = function($q, $t, ref) { this._q = $q; this._timeout = $t; - this._rootScope = $rs; - this._loginDeferred = null; - this._getCurrentUserDeferred = []; + // TODO: do I even need this._currentAuthData? Can I always just use ref.getAuth() since it's synchronous? this._currentAuthData = ref.getAuth(); - // TODO: these events don't seem to fire upon page reload - if (this._currentAuthData) { - this._rootScope.$broadcast("$angularFireUser:login", this._currentAuthData); - } else { - this._rootScope.$broadcast("$angularFireUser:logout"); - } - - if (typeof ref == "string") { - throw new Error("Please provide a Firebase reference instead " + - "of a URL, eg: new Firebase(url)"); + if (typeof ref === "string") { + throw new Error("Please provide a Firebase reference instead of a URL when calling `new Firebase()`."); } this._fRef = ref; }; - AngularFireUser.prototype = { + FirebaseUser.prototype = { construct: function() { this._object = { authData: null, @@ -68,7 +58,9 @@ $logout: this.logout.bind(this), // Authentication state - $getCurrentUser: this.getCurrentUser.bind(this), + $onAuth: this.onAuth.bind(this), + $offAuth: this.offAuth.bind(this), + $getAuth: this.getCurrentUser.bind(this), $requireUser: this.requireUser.bind(this), // User management @@ -81,33 +73,13 @@ return this._object; }, - // TODO: remove the promise? - // Synchronously retrieves the current auth data. - getCurrentUser: function() { - var deferred = this._q.defer(); - - deferred.resolve(this._currentAuthData); - - return deferred.promise; - }, - - // Returns a promise which is resolved if a user is authenticated and rejects the promise if - // the user does not exist. This can be used in routers to require routes to have a user - // logged in. - requireUser: function() { - var deferred = this._q.defer(); - - if (this._currentAuthData) { - deferred.resolve(this._currentAuthData); - } else { - deferred.reject(); - } - - return deferred.promise; - }, + /********************/ + /* Authentication */ + /********************/ _updateAuthData: function(authData) { var self = this; + // TODO: Is the _timeout here needed? this._timeout(function() { self._object.authData = authData; self._currentAuthData = authData; @@ -116,20 +88,18 @@ _onCompletionHandler: function(deferred, error, authData) { if (error !== null) { - this._rootScope.$broadcast("$angularFireUser:error", error); deferred.reject(error); } else { - this._rootScope.$broadcast("$angularFireUser:login", authData); this._updateAuthData(authData); deferred.resolve(authData); } }, + // TODO: do we even want this method here? auth: function(authToken) { var deferred = this._q.defer(); - var self = this; this._fRef.authWithPassword(authToken, this._onCompletionHandler.bind(this, deferred), function(error) { - self._rootScope.$broadcast("$angularFireUser:error", error); + // TODO: what do we do here? }); return deferred.promise; }, @@ -168,7 +138,6 @@ if (this._currentAuthData) { this._fRef.unauth(); this._updateAuthData(null); - this._rootScope.$broadcast("$angularFireUser:logout"); } }, @@ -197,17 +166,57 @@ if (this._currentAuthData) { this._fRef.unauth(); this._updateAuthData(null); - this._rootScope.$broadcast("$angularFireUser:logout"); } }, + + /**************************/ + /* Authentication State */ + /**************************/ + // Asynchronously fires the provided callback with the current authentication data every time + // the authentication data changes. It also fires as soon as the authentication data is + // retrieved from the server. + onAuth: function(callback) { + this._onAuthCallback = callback; + this._fRef.onAuth(callback); + }, + + // Detaches the callback previously attached with onAuth(). + offAuth: function() { + this._fRef.offAuth(this._onAuthCallback); + }, + + // Synchronously retrieves the current authentication data. + getAuth: function() { + return this._currentAuthData; + }, + + // Returns a promise which is resolved if a user is authenticated and rejects otherwise. This + // can be used to require routes to have a logged in user. + requireUser: function() { + // TODO: this should not fire until after onAuth has been called at least once... + + var deferred = this._q.defer(); + + if (this._currentAuthData) { + deferred.resolve(this._currentAuthData); + } else { + deferred.reject(); + } + + return deferred.promise; + }, + + + /*********************/ + /* User Management */ + /*********************/ // Creates a user for Firebase Simple Login. Function 'cb' receives an // error as the first argument and a Simple Login user object as the second // argument. Note that this function only creates the user, if you wish to // log in as the newly created user, call $login() after the promise for // this method has been fulfilled. createUser: function(email, password) { - var self = this; var deferred = this._q.defer(); this._fRef.createUser({ @@ -215,7 +224,6 @@ password: password }, function(error) { if (error !== null) { - self._rootScope.$broadcast("$angularFireUser:error", error); deferred.reject(error); } else { deferred.resolve(); @@ -238,8 +246,6 @@ newPassword: newPassword }, function(error) { if (error !== null) { - // TODO: do we want to send the error code as well? - self._rootScope.$broadcast("$angularFireUser:error", error); deferred.reject(error); } else { deferred.resolve(); @@ -260,8 +266,6 @@ password: password }, function(error) { if (error !== null) { - // TODO: do we want to send the error code as well? - self._rootScope.$broadcast("$angularFireUser:error", error); deferred.reject(error); } else { deferred.resolve(); @@ -280,8 +284,6 @@ email: email }, function(error) { if (error !== null) { - // TODO: do we want to send the error code as well? - self._rootScope.$broadcast("$angularFireUser:error", error); deferred.reject(error); } else { deferred.resolve(); From f90d0d2469623b3c4224c525f035179461a89bef Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Fri, 24 Oct 2014 15:47:44 -0700 Subject: [PATCH 05/17] Renamed _fRef to _ref --- src/user.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/user.js b/src/user.js index feaa6266..69fc8ab7 100644 --- a/src/user.js +++ b/src/user.js @@ -39,7 +39,7 @@ if (typeof ref === "string") { throw new Error("Please provide a Firebase reference instead of a URL when calling `new Firebase()`."); } - this._fRef = ref; + this._ref = ref; }; FirebaseUser.prototype = { @@ -98,7 +98,7 @@ // TODO: do we even want this method here? auth: function(authToken) { var deferred = this._q.defer(); - this._fRef.authWithPassword(authToken, this._onCompletionHandler.bind(this, deferred), function(error) { + this._ref.authWithPassword(authToken, this._onCompletionHandler.bind(this, deferred), function(error) { // TODO: what do we do here? }); return deferred.promise; @@ -106,37 +106,37 @@ authWithPassword: function(credentials, options) { var deferred = this._q.defer(); - this._fRef.authWithPassword(credentials, this._onCompletionHandler.bind(this, deferred), options); + this._ref.authWithPassword(credentials, this._onCompletionHandler.bind(this, deferred), options); return deferred.promise; }, authAnonymously: function(options) { var deferred = this._q.defer(); - this._fRef.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); + this._ref.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); return deferred.promise; }, authWithOAuthPopup: function(provider, options) { var deferred = this._q.defer(); - this._fRef.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); + this._ref.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); return deferred.promise; }, authWithOAuthRedirect: function(provider, options) { var deferred = this._q.defer(); - this._fRef.authWithOAuthRedirect(provider, this._onCompletionHandler.bind(this, deferred), options); + this._ref.authWithOAuthRedirect(provider, this._onCompletionHandler.bind(this, deferred), options); return deferred.promise; }, authWithOAuthToken: function(provider, credentials, options) { var deferred = this._q.defer(); - this._fRef.authWithOAuthToken(provider, credentials, this._onCompletionHandler.bind(this, deferred), options); + this._ref.authWithOAuthToken(provider, credentials, this._onCompletionHandler.bind(this, deferred), options); return deferred.promise; }, unauth: function() { if (this._currentAuthData) { - this._fRef.unauth(); + this._ref.unauth(); this._updateAuthData(null); } }, @@ -148,11 +148,11 @@ var deferred = this._q.defer(); if (provider === 'anonymous') { - this._fRef.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); + this._ref.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); } else if (provider === 'password') { - this._fRef.authWithPassword(options, this._onCompletionHandler.bind(this, deferred)); + this._ref.authWithPassword(options, this._onCompletionHandler.bind(this, deferred)); } else { - this._fRef.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); + this._ref.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); } return deferred.promise; @@ -164,7 +164,7 @@ // Simple Login fires _onLoginEvent() even if no user is logged in. We don't care about // firing this logout event multiple times, so explicitly check if a user is defined. if (this._currentAuthData) { - this._fRef.unauth(); + this._ref.unauth(); this._updateAuthData(null); } }, @@ -178,12 +178,12 @@ // retrieved from the server. onAuth: function(callback) { this._onAuthCallback = callback; - this._fRef.onAuth(callback); + this._ref.onAuth(callback); }, // Detaches the callback previously attached with onAuth(). offAuth: function() { - this._fRef.offAuth(this._onAuthCallback); + this._ref.offAuth(this._onAuthCallback); }, // Synchronously retrieves the current authentication data. @@ -219,7 +219,7 @@ createUser: function(email, password) { var deferred = this._q.defer(); - this._fRef.createUser({ + this._ref.createUser({ email: email, password: password }, function(error) { @@ -240,7 +240,7 @@ var self = this; var deferred = this._q.defer(); - self._fRef.changePassword({ + self._ref.changePassword({ email: email, oldPassword: oldPassword, newPassword: newPassword @@ -261,7 +261,7 @@ var self = this; var deferred = this._q.defer(); - self._fRef.removeUser({ + self._ref.removeUser({ email: email, password: password }, function(error) { @@ -280,7 +280,7 @@ var self = this; var deferred = this._q.defer(); - self._fRef.resetPassword({ + self._ref.resetPassword({ email: email }, function(error) { if (error !== null) { From 5fa9454b4dfc40275bcdd4f3691b6e5196810185 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 30 Oct 2014 13:38:57 -0700 Subject: [PATCH 06/17] Updated comments and cleaned up code --- src/user.js | 150 +++++++++++++++++++++++++--------------------------- 1 file changed, 71 insertions(+), 79 deletions(-) diff --git a/src/user.js b/src/user.js index 69fc8ab7..14d38111 100644 --- a/src/user.js +++ b/src/user.js @@ -3,26 +3,20 @@ "use strict"; var FirebaseUser; - // Defines the `$firebaseUser` service that provides simple - // user authentication support for AngularFire. + // Define a service which provides user authentication and management. angular.module("firebase").factory("$firebaseUser", [ "$q", "$timeout", function($q, $t) { - // The factory returns an object containing the authentication state - // of the current user. This service takes one argument: + // This factory returns an object containing the current authentication state of the client. + // This service takes one argument: // - // * `ref` : A Firebase reference. + // * `ref`: A Firebase reference. // // The returned object has the following properties: // - // * `authData`: Set to "null" if the user is currently logged out. This - // value will be changed to an object when the user successfully logs - // in. This object will contain details about the logged in user. The - // exact properties will vary based on the method used to login, but - // will at a minimum contain the `uid` and `provider` properties. - // - // The returned object will also have the following methods available: - // $login(), $logout(), $createUser(), $changePassword(), $removeUser(), - // and $getCurrentUser(). + // * `authData`: Set to "null" if the client is not currently authenticated. This value will + // be changed to an object when the client is authenticated. This object will contain + // details about the authentication state. The exact properties will vary based on the + // method used to authenticate, but will at a minimum contain `uid` and `provider`. return function(ref) { var auth = new FirebaseUser($q, $t, ref); return auth.construct(); @@ -33,6 +27,7 @@ FirebaseUser = function($q, $t, ref) { this._q = $q; this._timeout = $t; + // TODO: do I even need this._currentAuthData? Can I always just use ref.getAuth() since it's synchronous? this._currentAuthData = ref.getAuth(); @@ -45,11 +40,13 @@ FirebaseUser.prototype = { construct: function() { this._object = { + // The client's current authentication state authData: null, - // Authentication + + // Authentication methods $auth: this.auth.bind(this), - $authWithPassword: this.authWithPassword.bind(this), $authAnonymously: this.authAnonymously.bind(this), + $authWithPassword: this.authWithPassword.bind(this), $authWithOAuthPopup: this.authWithOAuthPopup.bind(this), $authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this), $authWithOAuthToken: this.authWithOAuthToken.bind(this), @@ -57,13 +54,13 @@ $login: this.login.bind(this), $logout: this.logout.bind(this), - // Authentication state + // Authentication state methods $onAuth: this.onAuth.bind(this), $offAuth: this.offAuth.bind(this), $getAuth: this.getCurrentUser.bind(this), $requireUser: this.requireUser.bind(this), - // User management + // User management methods $createUser: this.createUser.bind(this), $changePassword: this.changePassword.bind(this), $removeUser: this.removeUser.bind(this), @@ -77,6 +74,7 @@ /********************/ /* Authentication */ /********************/ + // Updates the authentication state of the client. _updateAuthData: function(authData) { var self = this; // TODO: Is the _timeout here needed? @@ -86,7 +84,8 @@ }); }, - _onCompletionHandler: function(deferred, error, authData) { + // Common login completion handler for all authentication methods. + _onLoginHandler: function(deferred, error, authData) { if (error !== null) { deferred.reject(error); } else { @@ -96,73 +95,64 @@ }, // TODO: do we even want this method here? - auth: function(authToken) { + // Authenticates the Firebase reference with a custom authentication token. + authWithCustomToken: function(authToken) { var deferred = this._q.defer(); - this._ref.authWithPassword(authToken, this._onCompletionHandler.bind(this, deferred), function(error) { + + this._ref.authWithCustomToken(authToken, this._onLoginHandler.bind(this, deferred), function(error) { // TODO: what do we do here? }); + return deferred.promise; }, - authWithPassword: function(credentials, options) { + // Authenticates the Firebase reference anonymously. + authAnonymously: function(options) { var deferred = this._q.defer(); - this._ref.authWithPassword(credentials, this._onCompletionHandler.bind(this, deferred), options); + + this._ref.authAnonymously(this._onLoginHandler.bind(this, deferred), options); + return deferred.promise; }, - authAnonymously: function(options) { + // Authenticates the Firebase reference with an email/password user. + authWithPassword: function(credentials, options) { var deferred = this._q.defer(); - this._ref.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); + + this._ref.authWithPassword(credentials, this._onLoginHandler.bind(this, deferred), options); + return deferred.promise; }, + // Authenticates the Firebase reference with the OAuth popup flow. authWithOAuthPopup: function(provider, options) { var deferred = this._q.defer(); - this._ref.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); + + this._ref.authWithOAuthPopup(provider, this._onLoginHandler.bind(this, deferred), options); + return deferred.promise; }, + // Authenticates the Firebase reference with the OAuth redirect flow. authWithOAuthRedirect: function(provider, options) { var deferred = this._q.defer(); - this._ref.authWithOAuthRedirect(provider, this._onCompletionHandler.bind(this, deferred), options); - return deferred.promise; - }, - authWithOAuthToken: function(provider, credentials, options) { - var deferred = this._q.defer(); - this._ref.authWithOAuthToken(provider, credentials, this._onCompletionHandler.bind(this, deferred), options); - return deferred.promise; - }, + this._ref.authWithOAuthRedirect(provider, this._onLoginHandler.bind(this, deferred), options); - unauth: function() { - if (this._currentAuthData) { - this._ref.unauth(); - this._updateAuthData(null); - } + return deferred.promise; }, - // The login method takes a provider and authenticates the Firebase reference - // with which the service was initialized. This method returns a promise, which - // will be resolved when the login succeeds (and rejected when an error occurs). - login: function(provider, options) { + // Authenticates the Firebase reference with an OAuth token. + authWithOAuthToken: function(provider, credentials, options) { var deferred = this._q.defer(); - if (provider === 'anonymous') { - this._ref.authAnonymously(this._onCompletionHandler.bind(this, deferred), options); - } else if (provider === 'password') { - this._ref.authWithPassword(options, this._onCompletionHandler.bind(this, deferred)); - } else { - this._ref.authWithOAuthPopup(provider, this._onCompletionHandler.bind(this, deferred), options); - } + this._ref.authWithOAuthToken(provider, credentials, this._onLoginHandler.bind(this, deferred), options); return deferred.promise; }, - // Unauthenticate the Firebase reference. - logout: function() { - // TODO: update comment? - // Simple Login fires _onLoginEvent() even if no user is logged in. We don't care about - // firing this logout event multiple times, so explicitly check if a user is defined. + // Unauthenticates the Firebase reference. + unauth: function() { if (this._currentAuthData) { this._ref.unauth(); this._updateAuthData(null); @@ -191,18 +181,27 @@ return this._currentAuthData; }, + // Helper callback method which returns a promise which is resolved if a user is authenticated + // and rejects otherwise. This can be used to require that routes have a logged in user. + _requireUserOnAuthCallback: function(deferred, authData) { + console.log("_requireUserOnAuthCallback() fired with ", authData); + if (authData) { + deferred.resolve(authData); + } else { + deferred.reject(); + } + + // Turn off this onAuth() callback since we just needed to get the authentication state once. + this._ref.offAuth(this._requireUserOnAuthCallback); + }, + // Returns a promise which is resolved if a user is authenticated and rejects otherwise. This - // can be used to require routes to have a logged in user. + // can be used to require that routes have a logged in user. requireUser: function() { - // TODO: this should not fire until after onAuth has been called at least once... - var deferred = this._q.defer(); - if (this._currentAuthData) { - deferred.resolve(this._currentAuthData); - } else { - deferred.reject(); - } + // TODO: this should not fire until after onAuth has been called at least once... + this._ref.onAuth(this._requireUserOnAuthCallback.bind(this, deferred)); return deferred.promise; }, @@ -211,11 +210,9 @@ /*********************/ /* User Management */ /*********************/ - // Creates a user for Firebase Simple Login. Function 'cb' receives an - // error as the first argument and a Simple Login user object as the second - // argument. Note that this function only creates the user, if you wish to - // log in as the newly created user, call $login() after the promise for - // this method has been fulfilled. + // Creates a new email/password user. Note that this function only creates the user, if you + // wish to log in as the newly created user, call $authWithPassword() after the promise for + // this method has been resolved. createUser: function(email, password) { var deferred = this._q.defer(); @@ -233,14 +230,11 @@ return deferred.promise; }, - // Changes the password for a Firebase Simple Login user. Take an email, - // old password and new password as three mandatory arguments. Returns a - // promise. + // Changes the password for an email/password user. changePassword: function(email, oldPassword, newPassword) { - var self = this; var deferred = this._q.defer(); - self._ref.changePassword({ + this._ref.changePassword({ email: email, oldPassword: oldPassword, newPassword: newPassword @@ -256,12 +250,11 @@ return deferred.promise; }, - // Remove a user for the listed email address. Returns a promise. + // Removes an email/password user. removeUser: function(email, password) { - var self = this; var deferred = this._q.defer(); - self._ref.removeUser({ + this._ref.removeUser({ email: email, password: password }, function(error) { @@ -275,12 +268,11 @@ return deferred.promise; }, - // Send a password reset email to the user for an email + password account. + // Sends a password reset email to an email/password user. sendPasswordResetEmail: function(email) { - var self = this; var deferred = this._q.defer(); - self._ref.resetPassword({ + this._ref.resetPassword({ email: email }, function(error) { if (error !== null) { From af9359d8fc3b2d9d085e869d15eb55090fd88664 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 30 Oct 2014 14:25:13 -0700 Subject: [PATCH 07/17] Removed _currentAuthData and renamed some methods --- src/user.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/user.js b/src/user.js index 14d38111..29b69a73 100644 --- a/src/user.js +++ b/src/user.js @@ -28,9 +28,6 @@ this._q = $q; this._timeout = $t; - // TODO: do I even need this._currentAuthData? Can I always just use ref.getAuth() since it's synchronous? - this._currentAuthData = ref.getAuth(); - if (typeof ref === "string") { throw new Error("Please provide a Firebase reference instead of a URL when calling `new Firebase()`."); } @@ -44,20 +41,18 @@ authData: null, // Authentication methods - $auth: this.auth.bind(this), + $authWithCustomToken: this.authWithCustomToken.bind(this), $authAnonymously: this.authAnonymously.bind(this), $authWithPassword: this.authWithPassword.bind(this), $authWithOAuthPopup: this.authWithOAuthPopup.bind(this), $authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this), $authWithOAuthToken: this.authWithOAuthToken.bind(this), $unauth: this.unauth.bind(this), - $login: this.login.bind(this), - $logout: this.logout.bind(this), // Authentication state methods $onAuth: this.onAuth.bind(this), $offAuth: this.offAuth.bind(this), - $getAuth: this.getCurrentUser.bind(this), + $getAuth: this.getAuth.bind(this), $requireUser: this.requireUser.bind(this), // User management methods @@ -80,7 +75,6 @@ // TODO: Is the _timeout here needed? this._timeout(function() { self._object.authData = authData; - self._currentAuthData = authData; }); }, @@ -153,7 +147,7 @@ // Unauthenticates the Firebase reference. unauth: function() { - if (this._currentAuthData) { + if (this.getAuth() !== null) { this._ref.unauth(); this._updateAuthData(null); } @@ -178,7 +172,7 @@ // Synchronously retrieves the current authentication data. getAuth: function() { - return this._currentAuthData; + return this._ref.getAuth(); }, // Helper callback method which returns a promise which is resolved if a user is authenticated From e0f64c3639aed235cb13beb65015d75de2949a94 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 30 Oct 2014 14:39:46 -0700 Subject: [PATCH 08/17] Converted double quotes to single quotes --- src/user.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/user.js b/src/user.js index 29b69a73..98dc71fc 100644 --- a/src/user.js +++ b/src/user.js @@ -1,11 +1,11 @@ /* istanbul ignore next */ (function() { - "use strict"; + 'use strict'; var FirebaseUser; // Define a service which provides user authentication and management. - angular.module("firebase").factory("$firebaseUser", [ - "$q", "$timeout", function($q, $t) { + angular.module('firebase').factory('$firebaseUser', [ + '$q', '$timeout', function($q, $t) { // This factory returns an object containing the current authentication state of the client. // This service takes one argument: // @@ -13,7 +13,7 @@ // // The returned object has the following properties: // - // * `authData`: Set to "null" if the client is not currently authenticated. This value will + // * `authData`: Set to 'null' if the client is not currently authenticated. This value will // be changed to an object when the client is authenticated. This object will contain // details about the authentication state. The exact properties will vary based on the // method used to authenticate, but will at a minimum contain `uid` and `provider`. @@ -28,8 +28,8 @@ this._q = $q; this._timeout = $t; - if (typeof ref === "string") { - throw new Error("Please provide a Firebase reference instead of a URL when calling `new Firebase()`."); + if (typeof ref === 'string') { + throw new Error('Please provide a Firebase reference instead of a URL when calling `new Firebase()`.'); } this._ref = ref; }; @@ -178,7 +178,7 @@ // Helper callback method which returns a promise which is resolved if a user is authenticated // and rejects otherwise. This can be used to require that routes have a logged in user. _requireUserOnAuthCallback: function(deferred, authData) { - console.log("_requireUserOnAuthCallback() fired with ", authData); + console.log('_requireUserOnAuthCallback() fired with:', authData); if (authData) { deferred.resolve(authData); } else { From d3c69bba3f884d85c989faa393f0db9cf034eb8a Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 30 Oct 2014 16:44:48 -0700 Subject: [PATCH 09/17] Added waitForAuth() method and renamed requireAuth() --- src/user.js | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/user.js b/src/user.js index 98dc71fc..7e7c65e8 100644 --- a/src/user.js +++ b/src/user.js @@ -53,7 +53,8 @@ $onAuth: this.onAuth.bind(this), $offAuth: this.offAuth.bind(this), $getAuth: this.getAuth.bind(this), - $requireUser: this.requireUser.bind(this), + $requireAuth: this.requireAuth.bind(this), + $waitForAuth: this.waitForAuth.bind(this), // User management methods $createUser: this.createUser.bind(this), @@ -93,9 +94,7 @@ authWithCustomToken: function(authToken) { var deferred = this._q.defer(); - this._ref.authWithCustomToken(authToken, this._onLoginHandler.bind(this, deferred), function(error) { - // TODO: what do we do here? - }); + this._ref.authWithCustomToken(authToken, this._onLoginHandler.bind(this, deferred)); return deferred.promise; }, @@ -175,27 +174,36 @@ return this._ref.getAuth(); }, - // Helper callback method which returns a promise which is resolved if a user is authenticated - // and rejects otherwise. This can be used to require that routes have a logged in user. - _requireUserOnAuthCallback: function(deferred, authData) { - console.log('_requireUserOnAuthCallback() fired with:', authData); - if (authData) { + // Helper onAuth() callback method for the two router-related methods. + _routerMethodOnAuthCallback: function(deferred, rejectIfAuthDataIsNull, authData) { + if (authData !== null) { deferred.resolve(authData); - } else { + } else if (rejectIfAuthDataIsNull) { deferred.reject(); + } else { + deferred.resolve(null); } - // Turn off this onAuth() callback since we just needed to get the authentication state once. - this._ref.offAuth(this._requireUserOnAuthCallback); + // Turn off this onAuth() callback since we just needed to get the authentication data once. + this._ref.offAuth(this._routerMethodOnAuthCallback); + }, + + // Returns a promise which is resolved if the client is authenticated and rejects otherwise. + // This can be used to require that a route has a logged in user. + requireAuth: function() { + var deferred = this._q.defer(); + + this._ref.onAuth(this._routerMethodOnAuthCallback.bind(this, deferred, /* rejectIfAuthDataIsNull */ true)); + + return deferred.promise; }, - // Returns a promise which is resolved if a user is authenticated and rejects otherwise. This - // can be used to require that routes have a logged in user. - requireUser: function() { + // Returns a promise which is resolved with the client's current authenticated data. This can + // be used in a route's resolve() method to grab the current authentication data. + waitForAuth: function() { var deferred = this._q.defer(); - // TODO: this should not fire until after onAuth has been called at least once... - this._ref.onAuth(this._requireUserOnAuthCallback.bind(this, deferred)); + this._ref.onAuth(this._routerMethodOnAuthCallback.bind(this, deferred, /* rejectIfAuthDataIsNull */ false)); return deferred.promise; }, From a329c4b73478e22d00a845e59707d4cfb09754bf Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 30 Oct 2014 16:47:36 -0700 Subject: [PATCH 10/17] Removed unneeded authData property --- src/user.js | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/user.js b/src/user.js index 7e7c65e8..fb618436 100644 --- a/src/user.js +++ b/src/user.js @@ -11,12 +11,8 @@ // // * `ref`: A Firebase reference. // - // The returned object has the following properties: - // - // * `authData`: Set to 'null' if the client is not currently authenticated. This value will - // be changed to an object when the client is authenticated. This object will contain - // details about the authentication state. The exact properties will vary based on the - // method used to authenticate, but will at a minimum contain `uid` and `provider`. + // The returned object contains methods for authenticating clients, retrieving authentication + // state, and managing users. return function(ref) { var auth = new FirebaseUser($q, $t, ref); return auth.construct(); @@ -37,9 +33,6 @@ FirebaseUser.prototype = { construct: function() { this._object = { - // The client's current authentication state - authData: null, - // Authentication methods $authWithCustomToken: this.authWithCustomToken.bind(this), $authAnonymously: this.authAnonymously.bind(this), @@ -70,21 +63,11 @@ /********************/ /* Authentication */ /********************/ - // Updates the authentication state of the client. - _updateAuthData: function(authData) { - var self = this; - // TODO: Is the _timeout here needed? - this._timeout(function() { - self._object.authData = authData; - }); - }, - // Common login completion handler for all authentication methods. _onLoginHandler: function(deferred, error, authData) { if (error !== null) { deferred.reject(error); } else { - this._updateAuthData(authData); deferred.resolve(authData); } }, @@ -148,7 +131,6 @@ unauth: function() { if (this.getAuth() !== null) { this._ref.unauth(); - this._updateAuthData(null); } }, From 67925c000beaa1e13ad35f7437865cda10920328 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 30 Oct 2014 16:56:31 -0700 Subject: [PATCH 11/17] Removed SimpleLogin code and bumped Firebase to 1.1.x We will bump Firebase to 2.0.x later We will update the manual test suite later --- .jshintrc | 3 +- README.md | 4 +- bower.json | 3 +- package.json | 2 +- src/firebaseSimpleLogin.js | 235 ------------------------------------- src/user.js | 1 - tests/manual_karma.conf.js | 1 - 7 files changed, 5 insertions(+), 244 deletions(-) delete mode 100644 src/firebaseSimpleLogin.js diff --git a/.jshintrc b/.jshintrc index 40b7d093..bbf11574 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,8 +1,7 @@ { "predef": [ "angular", - "Firebase", - "FirebaseSimpleLogin" + "Firebase" ], "bitwise": true, "browser": true, diff --git a/README.md b/README.md index a40ee064..152226d5 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,6 @@ tests, run `grunt test:unit`. To only run the end-to-end [Protractor](https://gi tests, run `grunt test:e2e`. In addition to the automated test suite, there is an additional manual test suite that ensures that -the `$firebaseSimpleLogin` service is working properly with the authentication providers. These tests -can be run with `grunt test:manual`. Note that you must click "Close this window", login to Twitter, +the `$firebaseUser` service is working properly with the authentication providers. These tests can +be run with `grunt test:manual`. Note that you must click "Close this window", login to Twitter, etc. when prompted in order for these tests to complete successfully. diff --git a/bower.json b/bower.json index ed23197e..d06c0365 100644 --- a/bower.json +++ b/bower.json @@ -31,8 +31,7 @@ ], "dependencies": { "angular": "1.2.x || 1.3.x", - "firebase": "1.0.x", - "firebase-simple-login": "1.6.x" + "firebase": "1.1.x" }, "devDependencies": { "lodash": "~2.4.1", diff --git a/package.json b/package.json index 57264d6c..296788de 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "package.json" ], "dependencies": { - "firebase": "1.0.x" + "firebase": "1.1.x" }, "devDependencies": { "coveralls": "^2.11.1", diff --git a/src/firebaseSimpleLogin.js b/src/firebaseSimpleLogin.js deleted file mode 100644 index e5222629..00000000 --- a/src/firebaseSimpleLogin.js +++ /dev/null @@ -1,235 +0,0 @@ -/* istanbul ignore next */ -(function() { - 'use strict'; - var AngularFireAuth; - - // Defines the `$firebaseSimpleLogin` service that provides simple - // user authentication support for AngularFire. - angular.module("firebase").factory("$firebaseSimpleLogin", [ - "$q", "$timeout", "$rootScope", function($q, $t, $rs) { - // The factory returns an object containing the authentication state - // of the current user. This service takes one argument: - // - // * `ref` : A Firebase reference. - // - // The returned object has the following properties: - // - // * `user`: Set to "null" if the user is currently logged out. This - // value will be changed to an object when the user successfully logs - // in. This object will contain details of the logged in user. The - // exact properties will vary based on the method used to login, but - // will at a minimum contain the `id` and `provider` properties. - // - // The returned object will also have the following methods available: - // $login(), $logout(), $createUser(), $changePassword(), $removeUser(), - // and $getCurrentUser(). - return function(ref) { - var auth = new AngularFireAuth($q, $t, $rs, ref); - return auth.construct(); - }; - } - ]); - - AngularFireAuth = function($q, $t, $rs, ref) { - this._q = $q; - this._timeout = $t; - this._rootScope = $rs; - this._loginDeferred = null; - this._getCurrentUserDeferred = []; - this._currentUserData = undefined; - - if (typeof ref == "string") { - throw new Error("Please provide a Firebase reference instead " + - "of a URL, eg: new Firebase(url)"); - } - this._fRef = ref; - }; - - AngularFireAuth.prototype = { - construct: function() { - var object = { - user: null, - $login: this.login.bind(this), - $logout: this.logout.bind(this), - $createUser: this.createUser.bind(this), - $changePassword: this.changePassword.bind(this), - $removeUser: this.removeUser.bind(this), - $getCurrentUser: this.getCurrentUser.bind(this), - $sendPasswordResetEmail: this.sendPasswordResetEmail.bind(this) - }; - this._object = object; - - // Initialize Simple Login. - if (!window.FirebaseSimpleLogin) { - var err = new Error("FirebaseSimpleLogin is undefined. " + - "Did you forget to include firebase-simple-login.js?"); - this._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - throw err; - } - - var client = new FirebaseSimpleLogin(this._fRef, - this._onLoginEvent.bind(this)); - this._authClient = client; - return this._object; - }, - - // The login method takes a provider (for Simple Login) and authenticates - // the Firebase reference with which the service was initialized. This - // method returns a promise, which will be resolved when the login succeeds - // (and rejected when an error occurs). - login: function(provider, options) { - var deferred = this._q.defer(); - var self = this; - - // To avoid the promise from being fulfilled by our initial login state, - // make sure we have it before triggering the login and creating a new - // promise. - this.getCurrentUser().then(function() { - self._loginDeferred = deferred; - self._authClient.login(provider, options); - }); - - return deferred.promise; - }, - - // Unauthenticate the Firebase reference. - logout: function() { - // Tell the simple login client to log us out. - this._authClient.logout(); - - // Forget who we were immediately, so that any getCurrentUser() calls - // will resolve the user as logged out even before the _onLoginEvent() - // fires and resets this._currentUserData to null again. - this._currentUserData = null; - }, - - // Creates a user for Firebase Simple Login. Function 'cb' receives an - // error as the first argument and a Simple Login user object as the second - // argument. Note that this function only creates the user, if you wish to - // log in as the newly created user, call $login() after the promise for - // this method has been fulfilled. - createUser: function(email, password) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.createUser(email, password, function(err, user) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(user); - } - }); - - return deferred.promise; - }, - - // Changes the password for a Firebase Simple Login user. Take an email, - // old password and new password as three mandatory arguments. Returns a - // promise. - changePassword: function(email, oldPassword, newPassword) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.changePassword(email, oldPassword, newPassword, - function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(); - } - } - ); - - return deferred.promise; - }, - - // Gets a promise for the current user info. - getCurrentUser: function() { - var self = this; - var deferred = this._q.defer(); - - if (self._currentUserData !== undefined) { - deferred.resolve(self._currentUserData); - } else { - self._getCurrentUserDeferred.push(deferred); - } - - return deferred.promise; - }, - - // Remove a user for the listed email address. Returns a promise. - removeUser: function(email, password) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.removeUser(email, password, function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(); - } - }); - - return deferred.promise; - }, - - // Send a password reset email to the user for an email + password account. - sendPasswordResetEmail: function(email) { - var self = this; - var deferred = this._q.defer(); - - self._authClient.sendPasswordResetEmail(email, function(err) { - if (err) { - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - deferred.reject(err); - } else { - deferred.resolve(); - } - }); - - return deferred.promise; - }, - - // Internal callback for any Simple Login event. - _onLoginEvent: function(err, user) { - // HACK -- calls to logout() trigger events even if we're not logged in, - // making us get extra events. Throw them away. This should be fixed by - // changing Simple Login so that its callbacks refer directly to the - // action that caused them. - if (this._currentUserData === user && err === null) { - return; - } - - var self = this; - if (err) { - if (self._loginDeferred) { - self._loginDeferred.reject(err); - self._loginDeferred = null; - } - self._rootScope.$broadcast("$firebaseSimpleLogin:error", err); - } else { - this._currentUserData = user; - - self._timeout(function() { - self._object.user = user; - if (user) { - self._rootScope.$broadcast("$firebaseSimpleLogin:login", user); - } else { - self._rootScope.$broadcast("$firebaseSimpleLogin:logout"); - } - if (self._loginDeferred) { - self._loginDeferred.resolve(user); - self._loginDeferred = null; - } - while (self._getCurrentUserDeferred.length > 0) { - var def = self._getCurrentUserDeferred.pop(); - def.resolve(user); - } - }); - } - } - }; -})(); diff --git a/src/user.js b/src/user.js index fb618436..447a9293 100644 --- a/src/user.js +++ b/src/user.js @@ -72,7 +72,6 @@ } }, - // TODO: do we even want this method here? // Authenticates the Firebase reference with a custom authentication token. authWithCustomToken: function(authToken) { var deferred = this._q.defer(); diff --git a/tests/manual_karma.conf.js b/tests/manual_karma.conf.js index fd8083f7..e60343dc 100644 --- a/tests/manual_karma.conf.js +++ b/tests/manual_karma.conf.js @@ -13,7 +13,6 @@ module.exports = function(config) { '../bower_components/angular/angular.js', '../bower_components/angular-mocks/angular-mocks.js', '../bower_components/firebase/firebase.js', - '../bower_components/firebase-simple-login/firebase-simple-login.js', '../src/module.js', '../src/**/*.js', 'manual/**/*.spec.js' From 5b326f8973e8a844a2c67b2e0171ea25cc267b5f Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Fri, 31 Oct 2014 09:48:42 -0700 Subject: [PATCH 12/17] Got rid of login tests --- tests/automatic_karma.conf.js | 2 +- tests/unit/Authentication.spec.js | 718 ------------------------------ tests/unit/UserManagement.spec.js | 0 3 files changed, 1 insertion(+), 719 deletions(-) delete mode 100644 tests/unit/Authentication.spec.js delete mode 100644 tests/unit/UserManagement.spec.js diff --git a/tests/automatic_karma.conf.js b/tests/automatic_karma.conf.js index fe5625fc..6d4c2be0 100644 --- a/tests/automatic_karma.conf.js +++ b/tests/automatic_karma.conf.js @@ -34,7 +34,7 @@ module.exports = function(config) { '../src/module.js', '../src/**/*.js', 'mocks/**/*.js', - 'unit/Authentication.spec.js' + 'unit/**/*.spec.js' ] }); }; diff --git a/tests/unit/Authentication.spec.js b/tests/unit/Authentication.spec.js deleted file mode 100644 index fd384395..00000000 --- a/tests/unit/Authentication.spec.js +++ /dev/null @@ -1,718 +0,0 @@ -'use strict'; -describe('$angularFireUser', function () { - - var $firebase, $angularFireUser, $timeout, $rootScope, $utils; - - beforeEach(function() { - module('firebase'); - module('mock.firebase'); - module('mock.utils'); - // have to create these before the first call to inject - // or they will not be registered with the angular mock injector - angular.module('firebase').provider('TestArrayFactory', { - $get: function() { - return function() {} - } - }).provider('TestObjectFactory', { - $get: function() { - return function() {}; - } - }); - inject(function (_$firebase_, _$angularFireUser_, _$timeout_, _$rootScope_, $firebaseUtils) { - $firebase = _$firebase_; - $angularFireUser = _$angularFireUser_; - $timeout = _$timeout_; - $rootScope = _$rootScope_; - $utils = $firebaseUtils; - }); - }); - - describe('', function() { - it('should accept a Firebase ref', function() { - var ref = new Firebase('Mock://'); - var auth = new $angularFireUser(ref); - expect($fb.$ref()).toBe(ref); - }); - - xit('should throw an error if passed a string', function() { - expect(function() { - $firebase('hello world'); - }).toThrowError(/valid Firebase reference/); - }); - - xit('should accept a factory name for arrayFactory', function() { - var ref = new Firebase('Mock://'); - var app = angular.module('firebase'); - // if this does not throw an error we are fine - expect($firebase(ref, {arrayFactory: 'TestArrayFactory'})).toBeAn('object'); - }); - - xit('should accept a factory name for objectFactory', function() { - var ref = new Firebase('Mock://'); - var app = angular.module('firebase'); - app.provider('TestObjectFactory', { - $get: function() { - return function() {} - } - }); - // if this does not throw an error we are fine - expect($firebase(ref, {objectFactory: 'TestObjectFactory'})).toBeAn('object'); - }); - - xit('should throw an error if factory name for arrayFactory does not exist', function() { - var ref = new Firebase('Mock://'); - expect(function() { - $firebase(ref, {arrayFactory: 'notarealarrayfactorymethod'}) - }).toThrowError(); - }); - - xit('should throw an error if factory name for objectFactory does not exist', function() { - var ref = new Firebase('Mock://'); - expect(function() { - $firebase(ref, {objectFactory: 'notarealobjectfactorymethod'}) - }).toThrowError(); - }); - }); - - xdescribe('$ref', function() { - var $fb; - beforeEach(function() { - $fb = $firebase(new Firebase('Mock://').child('data')); - }); - - it('should return ref that created the $firebase instance', function() { - var ref = new Firebase('Mock://'); - var $fb = new $firebase(ref); - expect($fb.$ref()).toBe(ref); - }); - }); - - xdescribe('$push', function() { - var $fb, flushAll; - beforeEach(function() { - $fb = $firebase(new Firebase('Mock://').child('data')); - flushAll = flush.bind(null, $fb.$ref()); - }); - - it('should return a promise', function() { - var res = $fb.$push({foo: 'bar'}); - expect(angular.isObject(res)).toBe(true); - expect(typeof res.then).toBe('function'); - }); - - it('should resolve to the ref for new id', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$push({foo: 'bar'}).then(whiteSpy, blackSpy); - flushAll(); - var newId = $fb.$ref().getLastAutoId(); - expect(whiteSpy).toHaveBeenCalled(); - expect(blackSpy).not.toHaveBeenCalled(); - var ref = whiteSpy.calls.argsFor(0)[0]; - expect(ref.name()).toBe(newId); - }); - - it('should reject if fails', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$ref().failNext('push', 'failpush'); - $fb.$push({foo: 'bar'}).then(whiteSpy, blackSpy); - flushAll(); - expect(whiteSpy).not.toHaveBeenCalled(); - expect(blackSpy).toHaveBeenCalledWith('failpush'); - }); - - it('should save correct data into Firebase', function() { - var spy = jasmine.createSpy('push callback').and.callFake(function(ref) { - expect($fb.$ref().getData()[ref.name()]).toEqual({foo: 'pushtest'}); - }); - $fb.$push({foo: 'pushtest'}).then(spy); - flushAll(); - expect(spy).toHaveBeenCalled(); - }); - - it('should work on a query', function() { - var ref = new Firebase('Mock://').child('ordered').limit(5); - var $fb = $firebase(ref); - flushAll(); - expect(ref.ref().push).not.toHaveBeenCalled(); - $fb.$push({foo: 'querytest'}); - flushAll(); - expect(ref.ref().push).toHaveBeenCalled(); - }); - }); - - xdescribe('$set', function() { - var $fb, flushAll; - beforeEach(function() { - $fb = $firebase(new Firebase('Mock://').child('data')); - flushAll = flush.bind(null, $fb.$ref()); - }); - - it('should return a promise', function() { - var res = $fb.$set(null); - expect(angular.isObject(res)).toBe(true); - expect(typeof res.then).toBe('function'); - }); - - it('should resolve to ref for child key', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$set('reftest', {foo: 'bar'}).then(whiteSpy, blackSpy); - flushAll(); - expect(blackSpy).not.toHaveBeenCalled(); - expect(whiteSpy).toHaveBeenCalledWith($fb.$ref().child('reftest')); - }); - - it('should resolve to ref if no key', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$set({foo: 'bar'}).then(whiteSpy, blackSpy); - flushAll(); - expect(blackSpy).not.toHaveBeenCalled(); - expect(whiteSpy).toHaveBeenCalledWith($fb.$ref()); - }); - - it('should save a child if key used', function() { - $fb.$set('foo', 'bar'); - flushAll(); - expect($fb.$ref().getData()['foo']).toEqual('bar'); - }); - - it('should save everything if no key', function() { - $fb.$set(true); - flushAll(); - expect($fb.$ref().getData()).toBe(true); - }); - - it('should reject if fails', function() { - $fb.$ref().failNext('set', 'setfail'); - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$set({foo: 'bar'}).then(whiteSpy, blackSpy); - flushAll(); - expect(whiteSpy).not.toHaveBeenCalled(); - expect(blackSpy).toHaveBeenCalledWith('setfail'); - }); - - it('should affect query keys only if query used', function() { - var ref = new Firebase('Mock://').child('ordered').limit(1); - var $fb = $firebase(ref); - ref.flush(); - var expKeys = ref.slice().keys; - $fb.$set({hello: 'world'}); - ref.flush(); - var args = ref.ref().update.calls.mostRecent().args[0]; - expect(Object.keys(args)).toEqual(['hello'].concat(expKeys)); - }); - }); - - xdescribe('$remove', function() { - var $fb, flushAll; - beforeEach(function() { - $fb = $firebase(new Firebase('Mock://').child('data')); - flushAll = flush.bind(null, $fb.$ref()); - }); - - it('should return a promise', function() { - var res = $fb.$remove(); - expect(angular.isObject(res)).toBe(true); - expect(typeof res.then).toBe('function'); - }); - - it('should resolve to ref if no key', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$remove().then(whiteSpy, blackSpy); - flushAll(); - expect(blackSpy).not.toHaveBeenCalled(); - expect(whiteSpy).toHaveBeenCalledWith($fb.$ref()); - }); - - it('should resolve to ref if query', function() { - var spy = jasmine.createSpy('resolve'); - var ref = new Firebase('Mock://').child('ordered').limit(2); - var $fb = $firebase(ref); - $fb.$remove().then(spy); - flushAll(); - expect(spy).toHaveBeenCalledWith(ref); - }); - - it('should resolve to child ref if key', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$remove('b').then(whiteSpy, blackSpy); - flushAll(); - expect(blackSpy).not.toHaveBeenCalled(); - expect(whiteSpy).toHaveBeenCalledWith($fb.$ref().child('b')); - }); - - it('should remove a child if key used', function() { - $fb.$remove('c'); - flushAll(); - var dat = $fb.$ref().getData(); - expect(angular.isObject(dat)).toBe(true); - expect(dat.hasOwnProperty('c')).toBe(false); - }); - - it('should remove everything if no key', function() { - $fb.$remove(); - flushAll(); - expect($fb.$ref().getData()).toBe(null); - }); - - it('should reject if fails', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$ref().failNext('remove', 'test_fail_remove'); - $fb.$remove().then(whiteSpy, blackSpy); - flushAll(); - expect(whiteSpy).not.toHaveBeenCalled(); - expect(blackSpy).toHaveBeenCalledWith('test_fail_remove'); - }); - - it('should remove data in Firebase', function() { - $fb.$remove(); - flushAll(); - expect($fb.$ref().remove).toHaveBeenCalled(); - }); - - //todo-test https://github.com/katowulf/mockfirebase/issues/9 - it('should only remove keys in query if used on a query', function() { - var ref = new Firebase('Mock://').child('ordered').limit(2); - var keys = ref.slice().keys; - var origKeys = ref.ref().getKeys(); - var expLength = origKeys.length - keys.length; - expect(keys.length).toBeGreaterThan(0); - expect(origKeys.length).toBeGreaterThan(keys.length); - var $fb = $firebase(ref); - flushAll(ref); - $fb.$remove(); - flushAll(ref); - keys.forEach(function(key) { - expect(ref.ref().child(key).remove).toHaveBeenCalled(); - }); - origKeys.forEach(function(key) { - if( keys.indexOf(key) === -1 ) { - expect(ref.ref().child(key).remove).not.toHaveBeenCalled(); - } - }); - }); - }); - - xdescribe('$update', function() { - var $fb, flushAll; - beforeEach(function() { - $fb = $firebase(new Firebase('Mock://').child('data')); - flushAll = flush.bind(null, $fb.$ref()); - }); - - it('should return a promise', function() { - expect($fb.$update({foo: 'bar'})).toBeAPromise(); - }); - - it('should resolve to ref when done', function() { - var spy = jasmine.createSpy('resolve'); - $fb.$update('index', {foo: 'bar'}).then(spy); - flushAll(); - var arg = spy.calls.argsFor(0)[0]; - expect(arg).toBeAFirebaseRef(); - expect(arg.name()).toBe('index'); - }); - - it('should reject if failed', function() { - var whiteSpy = jasmine.createSpy('resolve'); - var blackSpy = jasmine.createSpy('reject'); - $fb.$ref().failNext('update', 'oops'); - $fb.$update({index: {foo: 'bar'}}).then(whiteSpy, blackSpy); - flushAll(); - expect(whiteSpy).not.toHaveBeenCalled(); - expect(blackSpy).toHaveBeenCalled(); - }); - - it('should not destroy untouched keys', function() { - flushAll(); - var data = $fb.$ref().getData(); - data.a = 'foo'; - delete data.b; - expect(Object.keys(data).length).toBeGreaterThan(1); - $fb.$update({a: 'foo', b: null}); - flushAll(); - expect($fb.$ref().getData()).toEqual(data); - }); - - it('should replace keys specified', function() { - $fb.$update({a: 'foo', b: null}); - flushAll(); - var data = $fb.$ref().getData(); - expect(data.a).toBe('foo'); - expect(data.b).toBeUndefined(); - }); - - it('should work on a query object', function() { - var $fb2 = $firebase($fb.$ref().limit(1)); - flushAll(); - $fb2.$update({foo: 'bar'}); - flushAll(); - expect($fb2.$ref().ref().getData().foo).toBe('bar'); - }); - }); - - xdescribe('$transaction', function() { - var $fb, flushAll; - beforeEach(function() { - $fb = $firebase(new Firebase('Mock://').child('data')); - flushAll = flush.bind(null, $fb.$ref()); - }); - - it('should return a promise', function() { - expect($fb.$transaction('a', function() {})).toBeAPromise(); - }); - - it('should resolve to snapshot on success', function() { - var whiteSpy = jasmine.createSpy('success'); - var blackSpy = jasmine.createSpy('failed'); - $fb.$transaction('a', function() { return true; }).then(whiteSpy, blackSpy); - flushAll(); - expect(blackSpy).not.toHaveBeenCalled(); - expect(whiteSpy).toHaveBeenCalled(); - expect(whiteSpy.calls.argsFor(0)[0]).toBeASnapshot(); - }); - - it('should resolve to null on abort', function() { - var spy = jasmine.createSpy('success'); - $fb.$transaction('a', function() {}).then(spy); - flushAll(); - expect(spy).toHaveBeenCalledWith(null); - }); - - it('should reject if failed', function() { - var whiteSpy = jasmine.createSpy('success'); - var blackSpy = jasmine.createSpy('failed'); - $fb.$ref().child('a').failNext('transaction', 'test_fail'); - $fb.$transaction('a', function() { return true; }).then(whiteSpy, blackSpy); - flushAll(); - expect(whiteSpy).not.toHaveBeenCalled(); - expect(blackSpy).toHaveBeenCalledWith('test_fail'); - }); - - it('should modify data in firebase', function() { - var newData = {hello: 'world'}; - $fb.$transaction('c', function() { return newData; }); - flushAll(); - expect($fb.$ref().child('c').getData()).toEqual(jasmine.objectContaining(newData)); - }); - - it('should work okay on a query', function() { - var whiteSpy = jasmine.createSpy('success'); - var blackSpy = jasmine.createSpy('failed'); - $fb.$transaction(function() { return 'happy'; }).then(whiteSpy, blackSpy); - flushAll(); - expect(blackSpy).not.toHaveBeenCalled(); - expect(whiteSpy).toHaveBeenCalled(); - expect(whiteSpy.calls.argsFor(0)[0]).toBeASnapshot(); - }); - }); - - xdescribe('$asArray', function() { - var $ArrayFactory, $fb; - - function flushAll() { - flush($fb.$ref()); - } - - beforeEach(function() { - $ArrayFactory = stubArrayFactory(); - $fb = $firebase(new Firebase('Mock://').child('data'), {arrayFactory: $ArrayFactory}); - }); - - it('should call $FirebaseArray constructor with correct args', function() { - var arr = $fb.$asArray(); - expect($ArrayFactory).toHaveBeenCalledWith($fb, jasmine.any(Function), jasmine.objectContaining({})); - expect(arr.$$$readyPromise).toBeAPromise(); - }); - - it('should return the factory value (an array)', function() { - var factory = stubArrayFactory(); - var res = $firebase($fb.$ref(), {arrayFactory: factory}).$asArray(); - expect(res).toBe(factory.$myArray); - }); - - it('should explode if ArrayFactory does not return an array', function() { - expect(function() { - function fn() { return {}; } - $firebase(new Firebase('Mock://').child('data'), {arrayFactory: fn}).$asArray(); - }).toThrowError(Error); - }); - - it('should contain data in ref() after load', function() { - var count = Object.keys($fb.$ref().getData()).length; - expect(count).toBeGreaterThan(1); - var arr = $fb.$asArray(); - flushAll(); - expect(arr.$$added.calls.count()).toBe(count); - }); - - it('should return same instance if called multiple times', function() { - expect($fb.$asArray()).toBe($fb.$asArray()); - }); - - it('should use arrayFactory', function() { - var spy = stubArrayFactory(); - $firebase($fb.$ref(), {arrayFactory: spy}).$asArray(); - expect(spy).toHaveBeenCalled(); - }); - - it('should match query keys if query used', function() { - // needs to contain more than 2 items in data for this limit to work - expect(Object.keys($fb.$ref().getData()).length).toBeGreaterThan(2); - var ref = $fb.$ref().limit(2); - var arr = $firebase(ref, {arrayFactory: $ArrayFactory}).$asArray(); - flushAll(); - expect(arr.$$added.calls.count()).toBe(2); - }); - - it('should return new instance if old one is destroyed', function() { - var arr = $fb.$asArray(); - // invoke the destroy function - arr.$$$destroyFn(); - expect($fb.$asObject()).not.toBe(arr); - }); - - it('should call $$added if child_added event is received', function() { - var arr = $fb.$asArray(); - // flush all the existing data through - flushAll(); - arr.$$added.calls.reset(); - // now add a new record and see if it sticks - $fb.$ref().push({hello: 'world'}); - flushAll(); - expect(arr.$$added.calls.count()).toBe(1); - }); - - it('should call $$updated if child_changed event is received', function() { - var arr = $fb.$asArray(); - // flush all the existing data through - flushAll(); - // now change a new record and see if it sticks - $fb.$ref().child('c').set({hello: 'world'}); - flushAll(); - expect(arr.$$updated.calls.count()).toBe(1); - }); - - it('should call $$moved if child_moved event is received', function() { - var arr = $fb.$asArray(); - // flush all the existing data through - flushAll(); - // now change a new record and see if it sticks - $fb.$ref().child('c').setPriority(299); - flushAll(); - expect(arr.$$moved.calls.count()).toBe(1); - }); - - it('should call $$removed if child_removed event is received', function() { - var arr = $fb.$asArray(); - // flush all the existing data through - flushAll(); - // now change a new record and see if it sticks - $fb.$ref().child('a').remove(); - flushAll(); - expect(arr.$$removed.calls.count()).toBe(1); - }); - - it('should call $$error if an error event occurs', function() { - var arr = $fb.$asArray(); - // flush all the existing data through - flushAll(); - $fb.$ref().forceCancel('test_failure'); - flushAll(); - expect(arr.$$error).toHaveBeenCalledWith('test_failure'); - }); - - it('should resolve readyPromise after initial data loaded', function() { - var arr = $fb.$asArray(); - var spy = jasmine.createSpy('resolved').and.callFake(function(arrRes) { - var count = arrRes.$$added.calls.count(); - expect(count).toBe($fb.$ref().getKeys().length); - }); - arr.$$$readyPromise.then(spy); - expect(spy).not.toHaveBeenCalled(); - flushAll($fb.$ref()); - expect(spy).toHaveBeenCalled(); - }); - - it('should cancel listeners if destroyFn is invoked', function() { - var arr = $fb.$asArray(); - var ref = $fb.$ref(); - flushAll(); - expect(ref.on).toHaveBeenCalled(); - arr.$$$destroyFn(); - expect(ref.off.calls.count()).toBe(ref.on.calls.count()); - }); - - it('should trigger an angular compile', function() { - $fb.$asObject(); // creates the listeners - var ref = $fb.$ref(); - flushAll(); - $utils.wait.completed.calls.reset(); - ref.push({newa: 'newa'}); - flushAll(); - expect($utils.wait.completed).toHaveBeenCalled(); - }); - - it('should batch requests', function() { - $fb.$asArray(); // creates listeners - flushAll(); - $utils.wait.completed.calls.reset(); - var ref = $fb.$ref(); - ref.push({newa: 'newa'}); - ref.push({newb: 'newb'}); - ref.push({newc: 'newc'}); - ref.push({newd: 'newd'}); - flushAll(); - expect($utils.wait.completed.calls.count()).toBe(1); - }); - }); - - xdescribe('$asObject', function() { - var $fb; - - function flushAll() { - flush($fb.$ref()); - } - - beforeEach(function() { - var Factory = stubObjectFactory(); - $fb = $firebase(new Firebase('Mock://').child('data'), {objectFactory: Factory}); - $fb.$Factory = Factory; - }); - - it('should contain data in ref() after load', function() { - var data = $fb.$ref().getData(); - var obj = $fb.$asObject(); - flushAll(); - expect(obj.$$updated.calls.argsFor(0)[0].val()).toEqual(jasmine.objectContaining(data)); - }); - - it('should return same instance if called multiple times', function() { - expect($fb.$asObject()).toBe($fb.$asObject()); - }); - - it('should use recordFactory', function() { - var res = $fb.$asObject(); - expect(res).toBeInstanceOf($fb.$Factory); - }); - - it('should only contain query keys if query used', function() { - var ref = $fb.$ref().limit(2); - // needs to have more data than our query slice - expect(ref.ref().getKeys().length).toBeGreaterThan(2); - var obj = $fb.$asObject(); - flushAll(); - var snap = obj.$$updated.calls.argsFor(0)[0]; - expect(snap.val()).toEqual(jasmine.objectContaining(ref.getData())); - }); - - it('should call $$updated if value event is received', function() { - var obj = $fb.$asObject(); - var ref = $fb.$ref(); - flushAll(); - obj.$$updated.calls.reset(); - expect(obj.$$updated).not.toHaveBeenCalled(); - ref.set({foo: 'bar'}); - flushAll(); - expect(obj.$$updated).toHaveBeenCalled(); - }); - - it('should call $$error if an error event occurs', function() { - var ref = $fb.$ref(); - var obj = $fb.$asObject(); - flushAll(); - expect(obj.$$error).not.toHaveBeenCalled(); - ref.forceCancel('test_cancel'); - flushAll(); - expect(obj.$$error).toHaveBeenCalledWith('test_cancel'); - }); - - it('should resolve readyPromise after initial data loaded', function() { - var obj = $fb.$asObject(); - var spy = jasmine.createSpy('resolved').and.callFake(function(obj) { - var snap = obj.$$updated.calls.argsFor(0)[0]; - expect(snap.val()).toEqual(jasmine.objectContaining($fb.$ref().getData())); - }); - obj.$$$readyPromise.then(spy); - expect(spy).not.toHaveBeenCalled(); - flushAll(); - expect(spy).toHaveBeenCalled(); - }); - - it('should cancel listeners if destroyFn is invoked', function() { - var obj = $fb.$asObject(); - var ref = $fb.$ref(); - flushAll(); - expect(ref.on).toHaveBeenCalled(); - obj.$$$destroyFn(); - expect(ref.off.calls.count()).toBe(ref.on.calls.count()); - }); - - it('should trigger an angular compile', function() { - $fb.$asObject(); // creates the listeners - var ref = $fb.$ref(); - flushAll(); - $utils.wait.completed.calls.reset(); - ref.push({newa: 'newa'}); - flushAll(); - expect($utils.wait.completed).toHaveBeenCalled(); - }); - - it('should batch requests', function() { - var obj = $fb.$asObject(); // creates listeners - flushAll(); - $utils.wait.completed.calls.reset(); - var ref = $fb.$ref(); - ref.push({newa: 'newa'}); - ref.push({newb: 'newb'}); - ref.push({newc: 'newc'}); - ref.push({newd: 'newd'}); - flushAll(); - expect($utils.wait.completed.calls.count()).toBe(1); - }); - }); - - function stubArrayFactory() { - var arraySpy = []; - angular.forEach(['$$added', '$$updated', '$$moved', '$$removed', '$$error'], function(m) { - arraySpy[m] = jasmine.createSpy(m); - }); - var factory = jasmine.createSpy('ArrayFactory') - .and.callFake(function(inst, destroyFn, readyPromise) { - arraySpy.$$$inst = inst; - arraySpy.$$$destroyFn = destroyFn; - arraySpy.$$$readyPromise = readyPromise; - return arraySpy; - }); - factory.$myArray = arraySpy; - return factory; - } - - function stubObjectFactory() { - function Factory(inst, destFn, readyPromise) { - this.$$$inst = inst; - this.$$$destroyFn = destFn; - this.$$$readyPromise = readyPromise; - } - angular.forEach(['$$updated', '$$error'], function(m) { - Factory.prototype[m] = jasmine.createSpy(m); - }); - return Factory; - } - - function flush() { - // the order of these flush events is significant - Array.prototype.slice.call(arguments, 0).forEach(function(o) { - o.flush(); - }); - try { $timeout.flush(); } - catch(e) {} - } -}); diff --git a/tests/unit/UserManagement.spec.js b/tests/unit/UserManagement.spec.js deleted file mode 100644 index e69de29b..00000000 From 6f3aecfbf1bea77a46ebf912d3a481b5f77e38ea Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 6 Nov 2014 11:00:33 -0800 Subject: [PATCH 13/17] Bumped Firebase to 2.0.x and renamed user file --- bower.json | 2 +- package.json | 2 +- src/{user.js => FirebaseUser.js} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{user.js => FirebaseUser.js} (100%) diff --git a/bower.json b/bower.json index eb410fbc..f76f2835 100644 --- a/bower.json +++ b/bower.json @@ -31,7 +31,7 @@ ], "dependencies": { "angular": "1.2.x || 1.3.x", - "firebase": "1.1.x" + "firebase": "2.0.x" }, "devDependencies": { "lodash": "~2.4.1", diff --git a/package.json b/package.json index 32c15b2e..07e2ffb9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "package.json" ], "dependencies": { - "firebase": "1.1.x" + "firebase": "2.0.x" }, "devDependencies": { "coveralls": "^2.11.1", diff --git a/src/user.js b/src/FirebaseUser.js similarity index 100% rename from src/user.js rename to src/FirebaseUser.js From 0dcced946f7b88abea357197a50a2ee5df3c54f9 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Wed, 12 Nov 2014 16:47:06 -0800 Subject: [PATCH 14/17] `onAuth()` now returns a dispose method (equivalent to `offAuth()`) --- src/FirebaseUser.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/FirebaseUser.js b/src/FirebaseUser.js index 447a9293..585a1ece 100644 --- a/src/FirebaseUser.js +++ b/src/FirebaseUser.js @@ -141,13 +141,15 @@ // the authentication data changes. It also fires as soon as the authentication data is // retrieved from the server. onAuth: function(callback) { + var self = this; + this._onAuthCallback = callback; this._ref.onAuth(callback); - }, - // Detaches the callback previously attached with onAuth(). - offAuth: function() { - this._ref.offAuth(this._onAuthCallback); + // Return a method to detach the `onAuth()` callback. + return function() { + self._ref.offAuth(callback); + }; }, // Synchronously retrieves the current authentication data. From 27998d27a226e9222d27e90fbfb2e85718b962aa Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Wed, 12 Nov 2014 16:48:46 -0800 Subject: [PATCH 15/17] Removed unneeded _onAuthCallback variable --- src/FirebaseUser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FirebaseUser.js b/src/FirebaseUser.js index 585a1ece..fedf5183 100644 --- a/src/FirebaseUser.js +++ b/src/FirebaseUser.js @@ -143,7 +143,6 @@ onAuth: function(callback) { var self = this; - this._onAuthCallback = callback; this._ref.onAuth(callback); // Return a method to detach the `onAuth()` callback. From 2534e50f690ef7f53cdb8c843f05055a8e5cf3c8 Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Wed, 12 Nov 2014 18:58:28 -0800 Subject: [PATCH 16/17] Renamed `$firebaseUser` to `$firebaseAuth` --- src/{FirebaseUser.js => FirebaseAuth.js} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/{FirebaseUser.js => FirebaseAuth.js} (97%) diff --git a/src/FirebaseUser.js b/src/FirebaseAuth.js similarity index 97% rename from src/FirebaseUser.js rename to src/FirebaseAuth.js index fedf5183..c1d1455b 100644 --- a/src/FirebaseUser.js +++ b/src/FirebaseAuth.js @@ -1,10 +1,10 @@ /* istanbul ignore next */ (function() { 'use strict'; - var FirebaseUser; + var FirebaseAuth; // Define a service which provides user authentication and management. - angular.module('firebase').factory('$firebaseUser', [ + angular.module('firebase').factory('$firebaseAuth', [ '$q', '$timeout', function($q, $t) { // This factory returns an object containing the current authentication state of the client. // This service takes one argument: @@ -14,13 +14,13 @@ // The returned object contains methods for authenticating clients, retrieving authentication // state, and managing users. return function(ref) { - var auth = new FirebaseUser($q, $t, ref); + var auth = new FirebaseAuth($q, $t, ref); return auth.construct(); }; } ]); - FirebaseUser = function($q, $t, ref) { + FirebaseAuth = function($q, $t, ref) { this._q = $q; this._timeout = $t; @@ -30,7 +30,7 @@ this._ref = ref; }; - FirebaseUser.prototype = { + FirebaseAuth.prototype = { construct: function() { this._object = { // Authentication methods From f8e58bdc56a8cbb3f21b2625a5a8287b7d35683c Mon Sep 17 00:00:00 2001 From: jacobawenger Date: Thu, 13 Nov 2014 14:50:48 -0800 Subject: [PATCH 17/17] Removed unneeded $offAuth method and added error message to promise rejection --- src/FirebaseAuth.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FirebaseAuth.js b/src/FirebaseAuth.js index c1d1455b..7f6dbf2e 100644 --- a/src/FirebaseAuth.js +++ b/src/FirebaseAuth.js @@ -44,7 +44,6 @@ // Authentication state methods $onAuth: this.onAuth.bind(this), - $offAuth: this.offAuth.bind(this), $getAuth: this.getAuth.bind(this), $requireAuth: this.requireAuth.bind(this), $waitForAuth: this.waitForAuth.bind(this), @@ -161,7 +160,7 @@ if (authData !== null) { deferred.resolve(authData); } else if (rejectIfAuthDataIsNull) { - deferred.reject(); + deferred.reject("AUTH_REQUIRED"); } else { deferred.resolve(null); }