I'm learning unit-testing in angularjs and i'm trying to test my authentication controller.
Currently the test is failing with Expected Function to equal '/dashboard'. The test does not seem to be getting into User.login from what I can tell.
Controller:
angular.module('foo').controller('LoginCtrl',function($scope, $rootScope, $http, $window, $location, User){
$scope.submit = function () {
User.login($scope.user,
function (user) { // Success
$window.sessionStorage.token = user.id;
$scope.message = 'Welcome';
// Redirect to dashboard
$location.path('/dashboard');
},
function (err) {
console.log(err);
// handle login errors
$scope.message = 'Error: Invalid user or password';
}
);
};
});
Test:
describe('LoginCtrl', function() {
beforeEach(module('foo'));
var scope, ctrl, location, window, user;
beforeEach(inject(function($rootScope, $controller, $location, $window, User) {
scope = $rootScope.$new();
location = $location;
window = $window;
user = User;
ctrl = $controller('LoginCtrl', {$scope: scope, User: user});
}));
it('should redirect upon successful login', function() {
console.log('before path = '+location.path());
scope.user = {
"username": "my_user",
"password": "my_pass"
};
scope.submit();
console.log('message = '+scope.message);
console.log('after path = '+location.path());
console.log(window.sessionStorage.getItem('token'));
expect(location.path).toEqual('/dashboard');
});
});
** EDIT **
User.login code:
module.factory(
"User",
['LoopBackResource', 'LoopBackAuth', '$injector', function(Resource, LoopBackAuth, $injector) {
var R = Resource(
urlBase + "/users/:id",
{ 'id': '#id' },
{
"login": {
url: urlBase + "/users/login",
method: "POST",
interceptor: {
response: function(response) {
var accessToken = response.data;
LoopBackAuth.currentUserId = accessToken.userId;
LoopBackAuth.accessTokenId = accessToken.id;
LoopBackAuth.rememberMe = response.config.params.rememberMe !== false;
LoopBackAuth.save();
return response.resource;
}
}
}
});
Your User.login function must be calling the callback method asynchronously, so when you call scope.submit();, your callback function is not called yet => the test fails.
To test this logic, you have to mock the User.login function:
it('should redirect upon successful login', function() {
console.log('before path = '+location.path());
scope.user = {
"username": "my_user",
"password": "my_pass"
};
//user here is user = User; in your beforeEach. I avoid pasting too much code.
//Jasmine 1.3: andCallFake
//Jasmine 2.0: and.callFake
spyOn(user, "login").andCallFake(function(userData,successCallback){
successCallback(userData); //simulate the success case.
}); //mock your User.login
scope.submit();
console.log('message = '+scope.message);
console.log('after path = '+location.path());
console.log(window.sessionStorage.getItem('token'));
expect(location.path()).toEqual('/dashboard'); //fix the problem with location.path
});
Explanation:
spyOn(user, "login").andCallFake replaces the actual function with our fake function.
In this test case, you're testing should redirect upon successful login, so the precondition is the login must be successful, by mocking the login function, we can ensure this precondition is always true in the test.
You could do this similarly to test a case like: set error message when login failed, in order to test this, you need to ensure the precondition login failed is always true in the test:
it('should redirect upon successful login', function() {
console.log('before path = '+location.path());
scope.user = {
"username": "my_user",
"password": "my_pass"
};
//user here is user = User; in your beforeEach. I avoid pasting too much code.
//Jasmine 1.3: andCallFake
//Jasmine 2.0: and.callFake
spyOn(user, "login").andCallFake(function(userData,successCallback,errorCallback){
errorCallback(userData); //simulate the error case.
}); //mock your User.login
scope.submit();
expect(scope.message).toEqual('Error: Invalid user or password'); //verify that the message was set correctly.
});
Expected Function to equal '/dashboard'
The test runner is telling you it expected a string '/dashboard', but instead got a reference to a function. That's because location.path is a reference to a function. Try this instead:
expect(location.path()).toEqual('/dashboard');
Related
I'am trying to block users with unactivated email tokens from accessing. Currently unactivated users can still log in and simply get an alert. This method uses the satellizer auth system, with a promise, then and a catch.
Here is my LoginCtrl
'use strict';
angular.module('myapp').controller('LoginCtrl', function ($scope, alert, auth, $state, $auth, $timeout) {
$scope.submit = function () {
$auth.login({
email: $scope.email,
password: $scope.password
})
.then(function(res) {
var message = 'Thanks for coming back ' + res.data.user.email + '!';
if (!res.data.user.active)
message = 'Just a reminder, please activate your account soon :)';
alert('success', 'Welcome', message);
return null;
})
.then(function() {
$timeout(function() {
$state.go('main');
});
})
.catch(handleError);
}
function handleError(err) {
alert('warning', 'oops there is a problem!', err.message);
}
});
I like to display an alert message "please activate first" at the same time block login. I appreciate your help. Basically, I want to check if the user is active, then login authorized, if not, login will not be possible.
I am trying to develop a user management feature for a website using the MEAN.io stack. What I'm trying to do has worked in the past for other models on the same site so I'm not sure what is going on. My issue is that I am trying to get all the User models from the MongoDB database and pass them to an AngularJS controller so that I can display their information on a user management page. To that end, I added this function to the User controller in the backend:
exports.readUsers = function(req, res) {
var decodedToken = req.decodeToken;
var User = mongoose.model('User');
var id = req.params.id;
existUser(decodedToken, function(err, user) {
if(err) {
return sendError(res, 'INVALID_TOKEN');
}
User.find({})
.select('username')
.exec(function(err, results) {
if(err) {
return sendError(res, err);
}
res.json({success: true, userList: results});
});
});
}
This line to the routing code:
router.get('/users/all', Authorization.token, user.readUsers);
And this AngularJS controller for use with the frontend:
(function () {
"use strict";
var app = angular.module("GAP");
app.factory("UserEditFactory", function UserEditFactory($http, API_URL, AuthTokenFactory, $q) {
"use strict";
return {
readUsers: readUsers
};
//Get all users in the DB
function readUsers() {
if(AuthTokenFactory.getToken()) {
return $http.get(API_URL + "/users/all");
}else {
return $q.reject({
data: "valid token required"
});
}
}
});
app.controller("userPageController", ["UserEditFactory", "$scope", "$http", "$window", "API_URL",
function(UserEditFactory, $scope, $http, $window, API_URL) {
UserEditFactory.readUsers().then(function(data) {
console.log(data.data);
$scope.users = data.data.userList;
}, function(response) {
console.log(response);
});
}
]);
})();
When I load the page that is supposed to display this information, no data is displayed. I have determined that the AngularJS controller is calling the second function which I understand is the one used to respond to an error.
Further investigation of the object returned by the $http.get call reveals no data, and a status of -1. I'm not sure why this is happening, because I have used this exact pattern of code to get and display data from other models in the database on the same site. I can manually make HTTP calls to those working functions from this controller, and everything works fine. I'm not sure where to go from here or how to learn more about the issue. Can anyone offer insight? Let me know if you need more information.
Edit: As requested, here is the code for the AuthTokenFactory, which is an app.factory object in a common JS file.
app.factory('AuthTokenFactory', function AuthTokenFactory($window) {
'use strict';
var store = $window.localStorage;
var tokenKey = 'auth-token';
var userKey = "username";
return {
getToken: getToken,
setToken: setToken,
setUsername: setUsername
};
function getToken() {
return store.getItem(tokenKey);
}
function setToken(token) {
if (token) {
store.setItem(tokenKey, token);
} else {
store.removeItem(tokenKey);
}
}
function setUsername(username) {
if (username) {
store.setItem(userKey, username);
} else {
store.removeItem(userKey);
}
}
});
I'm new to angular and I'm following a chatapp tutorial from online. I'm getting this the error "Firebase.createUser failed: First argument must contain the key "password" " when I try to register with an email and password. The app isn't complete yet, I just finished the auth part. Google answers suggested that I update to the latest angularfire, which I did ( 1.1.3). No idea what to do.
Register state in app.js:
.state('register', {
url: '/register',
templateUrl: 'auth/register.html',
controller:'AuthCtrl as authCtrl',
resolve:{
requireNoAuth: function($state,Auth){
return Auth.$requireAuth()
.then(function(auth){
$state.go('home');
},
function(error){
return;
});
}
}
})
authController.js
angular.module('chatApp')
.controller('AuthCtrl', function (Auth, $state) {
//Using 'Controller as syntax', instead of $scope, we use 'this' to make controller
var authCtrl = this;
//user object controller
authCtrl.user = {
email:'',
pass:''
};
//login object controller. Firebase provides functions. Using promises. ( either it's fufilled, or rejected)
authCtrl.login = function () {
Auth.authWithPassword(authCtrl.user)
// .then takes in 2 parameters( onSuccess, onFaliure)
//if successfull, go home
.then(function (auth) {
$state.go('home');
},
//if failed, set error in controller, so we can call it and display message later
function (error) {
authCtrl.error = error;
});
};
//registering user
authCtrl.register = function () {
Auth.$createUser(authCtrl.user)
// prompt user to login if successful
.then(function (user) {
authCtrl.login();
},
//else bring up error
function (error) {
authCtrl.error = error;
})
}
});
authFactory.js
angular.module('chatApp')
.factory('Auth',function($firebaseAuth,FirebaseUrl){
var declare= new Firebase(FirebaseUrl);
var auth=$firebaseAuth(declare);
return auth
});
It's password, not pass.
Secondly, you're incorrectly resolving the user in your route config. Rather than using the promise chain in resolve, you just need to return the promise.
.state('register', {
url: '/register',
templateUrl: 'auth/register.html',
controller:'AuthCtrl as authCtrl',
resolve:{
requireNoAuth: function($state,Auth){
return Auth.$requireAuth(); // return the promise
}
}
})
Then in the run() phase, you can listen for routing errors:
app.run(function($rootScope, $location) {
$rootScope.$on("$routeChangeError", function(event, next, previous, error) {
if (error === "AUTH_REQUIRED") {
$location.path("/home");
}
});
});
Check out the AngularFire docs on using Auth with Routing for more information.
I am really trying to get a hold on how all of this works. I feel like this should work. I have an Auth factory and when the jwt is expired it calls its method 'delegate' which gets a new token with the refresh token. For some reason I get the error '_this.delegate is not a function'. (I am doing delegate in both cases for testing purposes)
webapp.factory('Auth', function($http, API_URL, $window, $location, jwtHelper ) {
var _this = this;
var delegate = function(){
$http.post(API_URL+'/admin/delegate', {refresh_token: $window.sessionStorage.refreshToken } ).success(function(result) {
$window.sessionStorage.authToken = result.token;
$window.sessionStorage.refreshToken = result.refresh_token;
console.log('delegate-result: '+JSON.stringify(result));
$location.path('/about');
//LocalService.set('authToken', JSON.stringify(result));
});
};
return {
//returns true if there is an auth token
isAuthenticated: function() {
var storedJwt = $window.sessionStorage.authToken;
console.log('stored JWT: '+storedJwt);
var storedPayload = jwtHelper.decodeToken(storedJwt);
console.log('payload: '+JSON.stringify(storedPayload));
if(jwtHelper.isTokenExpired(storedJwt)){
console.log('is expired expired: '+jwtHelper.getTokenExpirationDate(storedJwt));
_this.delegate();
} else {
console.log('is not expired expires: '+jwtHelper.getTokenExpirationDate(storedJwt));
//For testing
_this.delegate();
}
return $window.sessionStorage.authToken;
//LocalService.get('authToken');
},
delegate: delegate,
//login function, should be moved to login controller
login: function(email, password) {
var login = $http.post(API_URL+'/authenticate', {email: email, password: password } );
login.success(function(result) {
console.log('login-result: '+JSON.stringify(result));
$window.sessionStorage.authToken = result.token;
$window.sessionStorage.refreshToken = result.refresh_token;
$location.path('/about');
//LocalService.set('authToken', JSON.stringify(result));
});
return login;
},
All you need to do is remove _this from _this.delegate().
The function delegate() is in scope where you are using it.
There is nothing wrong with using var in a factory. A service is different
I'm fairly new to Javascript and just learning AngularJS but I've gotten most of my test cases to work with some great examples I've found. Unfortunately I can't seem to find anything to help me test my current case.
I'm testing a Controller using a mocked Service whose method returns a promise. I would like the mocked Service to return an error in order to execute the '.catch' block in the controller method. I can tell that it's not getting called correctly in a couple of ways:
I'm using istanbul for code coverage and it's telling me I'm not covering the 'catch'
The code in the '.catch' block is not getting executed from what I can tell via debugging
The controller under test, specifically need to test the '.catch' in $scope.login:
login.js
'use strict';
angular.module('ibcwebDashApp')
.controller('LoginCtrl', function ($scope, Auth, $location) {
$scope.user = {};
$scope.errors = {};
$scope.login = function(form) {
$scope.submitted = true;
if(form.$valid) {
Auth.login({
email: $scope.user.email,
password: $scope.user.password
})
.then( function() {
// Logged in, redirect to home
$location.path('/');
})
.catch( function(err) {
err = err.data;
$scope.errors.other = err.message;
});
}
};
});
The service and method I'm attempting to mock:
Auth.login
'use strict';
angular.module('ibcwebDashApp')
.factory('Auth', function Auth($location, $rootScope, Session, User, $cookieStore) {
// Get currentUser from cookie
$rootScope.currentUser = $cookieStore.get('user') || null;
$cookieStore.remove('user');
return {
/**
* Authenticate user
*
* #param {Object} user - login info
* #param {Function} callback - optional
* #return {Promise}
*/
login: function(user, callback) {
var cb = callback || angular.noop;
return Session.save({
email: user.email,
password: user.password
}, function(user) {
$rootScope.currentUser = user;
return cb();
}, function(err) {
return cb(err);
}).$promise;
},
And finally, my test file. The funny part is that all tests are passing but the 'expect' in the last test can be changed to pretty much anything and it still passes. The first two tests seem to run as expected but the last test is where I'm trying to execute the catch block by throwing an error from the mock Auth service:
login.unit.js
'use strict';
describe('Controller: LoginCtrl', function () {
var $scope, $location, loginCtrl, mockAuthService;
beforeEach(function() {
mockAuthService = jasmine.createSpyObj('Auth', ['login']);
module('ibcwebDashApp');
module(function($provide) {
$provide.value('Auth', mockAuthService);
});
inject(function($rootScope, $controller, $q, _$location_) {
//create an empty scope
$scope = $rootScope.$new();
$location = _$location_;
//declare the controller and inject our empty scope
loginCtrl = $controller('LoginCtrl', {$scope: $scope, Auth: mockAuthService});
});
});
describe('successful login', function() {
beforeEach(function() {
inject(function($q) {
mockAuthService.login.andReturn($q.when());
});
});
it('should call auth.login with the scope email and password when form is valid', function() {
//given
$scope.form = {};
$scope.form.$valid = true;
$scope.user.email = 'user#test.com';
$scope.user.password = 'password123';
//when
$scope.login($scope.form);
//then
expect($scope.submitted).toBe(true);
expect(mockAuthService.login).toHaveBeenCalledWith({email:'user#test.com', password:'password123'});
$scope.$apply(); //force return of auth.login promise
expect($location.path()).toBe('/');
});
it('should not call auth.login if form is invalid', function() {
//given
$scope.form = {};
$scope.form.$valid = false;
//when
$scope.login($scope.form);
expect(mockAuthService.login).not.toHaveBeenCalled();
});
});
describe('unsuccessful login', function() {
beforeEach(function () {
inject(function () {
mockAuthService.login.andReturn($q.when(new Error('Bad Login!')));
});
it('should set errors.other to the returned auth error message', function() {
//given
$scope.form = {};
$scope.form.$valid = true;
//when
$scope.login($scope.form);
$scope.$apply();
//then
expect($scope.errors.other).toEqual('Bad Login!');
});
});
});
});
I apologize for posting so much code but I wanted to provide as much context as possible. I really appreciate anyone who can help me out as I learn my way around unit testing Angular and promises! Thanks!!!
**UPDATE**
I was able to solve my issue with some help from below and discovering some syntactic errors. Here's what fixed this:
My beforeEach on the last test was not closed properly and actually enclosed the last test causing it not to run correctly (or maybe at all). This is why changing the expect conditions resulted in no errors.
I changed my beforeEach inject to: mockAuthService.login.andReturn($q.reject({data: {message: 'Bad Login!'}})); using the reject suggested below.
Once I properly closed the beforeEach I got an error message that $q was not defined so I had to added it to inject(function($q)
Once I corrected these issues the promise was correctly rejected and the error was caught by the appropriate code in the controller.
Before or while running your test, mock out part of the environment like this:
var originalAuthLogin = Auth.login;
Auth.login = function() {
return Promise.reject({data: {message: 'Error message'}});
};
After the test restore the environment to sanity:
Auth.login = originalAuthLogin;
This immediately calls the .catch() block of the code you're trying to test.