My ng app is working fine, but I am trying to write a ngMock test for my controller; I am basically following along the example on angular's website: https://docs.angularjs.org/api/ngMock/service/$httpBackend
The problem I am running into is that it complains about unexpected request even when request is being expected.
PhantomJS 1.9.8 (Windows 8 0.0.0) NotificationsController should fetch notification list FAILED
Error: Unexpected request: GET Not valid for testsapi/AspNetController/AspNetAction
Expected GET api/AspNetController/AspNetAction
What I do not get is that, on the error line, why is there a "tests" word appended before my service url?
I thought it should be sending to 'api/AspNetController/AspNetAction'
What am I doing wrong here. I can't find any one else running into the same problem as me through google.
Edit: I noticed that, if i remove the sendRequest portion from my controller, and have the unit test log my request object in console, i see the following json.
{
"method":"GET",
"url":"Not valid for testsapi/AspNetController/AspNetAction",
"headers":{
"Content-Type":"application/json"
}
}
here is the controller code
angular.module('MainModule')
.controller('NotificationsController', ['$scope', '$location', '$timeout', 'dataService',
function ($scope, $location, $timeout, dataService) {
//createRequest returns a request object
var fetchNotificationsRequest = dataService.createRequest('GET', 'api/AspNetController/AspNetAction', null);
//sendRequest sends the request object using $http
var fetchNotificationsPromise = dataService.sendRequest(fetchNotificationsRequest);
fetchNotificationsPromise.then(function (data) {
//do something with data.
}, function (error) {
alert("Unable to fetch notifications.");
});
}]
);
Test code
describe('NotificationsController', function () {
beforeEach(module('MainModule'));
beforeEach(module('DataModule')); //for data service
var $httpBackend, $scope, $location, $timeout, dataService;
beforeEach(inject(function ($injector) {
$httpBackend = $injector.get('$httpBackend');
$scope = $injector.get('$rootScope');
$location = $injector.get('$location');
$timeout = $injector.get('$timeout');
dataService = $injector.get('dataService');
var $controller = $injector.get('$controller');
createController = function () {
return $controller('NotificationsController', {
'$scope': $scope,
'$location': $location,
'$timeout': $timeout,
'dataService': dataService,
});
};
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch notification list', function () {
$httpBackend.expectGET('api/AspNetController/AspNetAction'); //this is where things go wrong
var controller = createController();
$httpBackend.flush();
});
});
Data service code
service.createRequest = function(method, service, data) {
var req = {
method: method, //GET or POST
url: someInjectedConstant.baseUrl + service,
headers: {
'Content-Type': 'application/json'
}
}
if (data != null) {
req.data = data;
}
return req;
}
service.sendRequest = function (req) {
return $q(function (resolve, reject) {
$http(req).then(function successCallback(response) {
console.info("Incoming response: " + req.url);
console.info("Status: " + response.status);
console.info(JSON.stringify(response));
if (response.status >= 200 && response.status < 300) {
resolve(response.data);
} else {
reject(response);
}
}, function failCallback(response) {
console.info("Incoming response: " + req.url);
console.info("Error Status: " + response.status);
console.info(JSON.stringify(response));
reject(response);
});
});
}
ANSWER:
since dataService created the finalized webapi url by someInjectedConstant.baseUrl + whatever_relative_url passed in from controller, In the test that I am writting, I will have to inject someInjectedConstant and
$httpBackend.expectGET(someInjectedConstant.baseUrl + relativeUrl)
instead of just doing a $httpBackend.expectGET(relativeUrl)
Clearly Not valid for tests is getting prepended to your url somewhere in your code. It's also not adding the hardcoded domain (see note below). Check through all your code and any other parts of the test pipeline that might be adding this to the url.
A couple of points on your code:
avoid hardcoding domain names in your code (I see you've fixed this in your updated answer)
maybe someInjectedConstant could be more explicitly named
there is no need for you to wrap $http with $q, so service.sendRequest can be:
service.sendRequest = function (req) {
$http(req).then(function (response) { // no need to name the function unless you want to call another function with all success/error code in defined elsewhere
console.info("Incoming response: " + req.url);
console.info("Status: " + response.status);
console.info(JSON.stringify(response));
return response.data; // angular treats only 2xx codes as success
}, function(error) {
console.info("Incoming response: " + req.url);
console.info("Error Status: " + response.status);
console.info(JSON.stringify(response));
});
}
Related
I am working with a OAuth2 using angularjs. Now i got stuck in authentication with OAuth in which i am unable to resend last 401 api. Any Idea.
I am using this oauth2 repo.
Controller.js
app.controller('validate', ['$scope', '$rootScope', '$location', 'fullname', '$http', '$timeout', '$cookies', 'OAuth', function ($scope, $rootScope, $location, fullname, $http, $timeout, $cookies, OAuth) {
OAuth.getAccessToken($scope.user).then( function successCallBack(response){
$scope.response = response;
if($scope.response.status == 200){
console.log($scope.response.data);
$scope.accessToken = $scope.response.data.access_token;
$scope.refreshToken = $scope.response.data.refresh_token;
localStorage.setItem("accessToken", $scope.accessToken);
localStorage.setItem("refreshToken", $scope.refreshToken);
var userId = response.headers('userid');
console.log(userId);
$cookies.put("userId", userId);
window.location.href = 'user_profile.php';
}
}, function errorCallBack(response){
console.log(response);
});
}]);
app.js
app.config(['OAuthProvider', function(OAuthProvider) {
OAuthProvider.configure({
baseUrl: 'http://testzone.xxxxxx.net/api/LoginTest/Login/web/',
clientId: '123456789',
clientSecret: 'otszh9nonaosok88gsswc8k4w8ww04s',
grantPath: 'api/oauth2/token',
revokePath: 'api/oauth2/revoke'
});
}]);
app.run(['$rootScope', '$window', 'OAuth', '$cookies', '$timeout', function($rootScope, $window, OAuth, $cookies, $timeout) {
$rootScope.$on('oauth:error', function(event, rejection) {
// Ignore `invalid_grant` error - should be catched on `LoginController`.
if ('invalid_token' === rejection.data.error || 'invalid_grant' === rejection.data.error || 'invalid_request' === rejection.data.error || 'invalid_client' === rejection.data.error || 'unauthorized_client' === rejection.data.error || 'unsupported_grant_type' === rejection.data.error) {
$cookies.remove('userId');
$timeout(function(){
window.location.href = 'index.php';
},200);
}
// Refresh token when a `invalid_token` error occurs.
if ('expired_token' === rejection.data.error) {
console.log(rejection);
OAuth.getRefreshToken();
}
console.log(rejection);
console.log(rejection.data.error);
console.log(rejection.data.error_description);
// Redirect to `/login` with the `error_reason`.
//return $window.location.href = 'index.php';
});
}]);
Thanks
You can do something like this when analysing the error response:
if (rejection.status === 401) {
var authService = $injector.get('oAuthService');
var authData = ipCookie(oAuthConstants.oAuthCookieName);
var $http = $http || $injector.get('$http');
var deferred = $q.defer();
if (authData) {
authService.refreshToken().then(function () {
//this repeats the request with the original parameters
return deferred.resolve($http(rejection.config));
});
}
return deferred.promise;
}
else if (rejection.status === 403) {
var toaster = $injector.get('toaster');
toaster.pop('error', "Access Denied", "You are not authorized to do this request.");
}
else {
return $q.reject(rejection);
}
The key to repeat the last 401 api call is:
return deferred.resolve($http(rejection.config));
I hope it helps.
A refresh token is a special kind of JWT that is used to obtain a renewed id_token at any time.
Refresh tokens carry the information necessary to get a new access token. In other words, whenever an access token is required to access a specific resource, a client may use a refresh token to get a new access token issued by the authentication server.
and how to use it in angular js see this link , click here
it will guide you how to do it.
see this related project, you can get idea from it. github code
The following code gives me an error every time it is run. The part service.getTracks(req, function(tracks), gives an error on the parameter 'req' saying it is undefined. However, the program still works, it works but still gives an error. I was wondering if anyone could shed some light upon why this happens? I've been staring blindly for 4 hours now, with 0 progress... And help at all is appreciated!
Please ask questions regarding the issue if anything is blurry or something.
Best regards,
Victor
app.factory('echonestService', [
'$http',
'$rootScope',
'$cookies',
'$location',
function($http, $rootScope, $cookies, $location) {
var service = {};
service.getTracks = function(req, callback) {
$http(req).then(
function(res) {
var tracks = [];
res.data.forEach(function(e) {
tracks.push(e.song_id);
});
callback(tracks);
console.log($rootScope.previews);
$location.path('/review');
}
);
};
service.createPlaylist = function(name, tracks) {
var url = 'http://127.0.0.1:8080/api/create-playlist';
var data = {
'user_id': $cookies.get('username'),
'name': name,
'tracks': tracks,
'access_token': $cookies.get('access_token'),
'refresh_token': $cookies.get('refresh_token')
};
$http.post(url, JSON.stringify(data)).then(
function(res) {
$rootScope.playlistLink = res.data;
console.log($rootScope.playlistLink);
$location.path('/result');
},
function(err) {
console.log("error: ", err);
}
);
service.getTracks(req, function(tracks) {
data.tracks = tracks;
$http.post(url, JSON.stringify(data)).then(
function(res) {
$rootScope.playlistLink = res.data;
$rootScope.playlistName = name;
console.log($rootScope.playlistLink);
},
function(err) {
console.log("error: ", err);
}
);
});
};
return service;
}]);
The only place you're actually using req is here:
$http(req).then(
function(res) {
(...etc...)
then() will run after $http even if req is undefined, which is why your code's still working, so you don't need req defined anywhere - but since you're not defining it before using it in $http, you're getting the error.
I am following a book tutorial, I am currently building the authentication for the app. Whenever I login correctly, I can't seem to set the token back into the request. The error I am getting is:
Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function () {
return $window.localStorage.getItem('token');
}' is not a valid HTTP header field value.
Any help would be greatly appreciated
authService.js
angular.module('authService', [])
// ===================================================
// auth factory to login and get information
// inject $http for communicating with the API
// inject $q to return promise objects
// inject AuthToken to manage tokens
// ===================================================
.factory('Auth', function($http, $q, AuthToken) {
// create auth factory obj
var authFactory = {};
// login user
authFactory.login = function(username, password) {
// return promise obj and its data
return $http.post('/api/authenticate', {
username: username,
password: password
})
.success(function(data) {
console.log(data);
AuthToken.setToken(data.token);
return data;
});
};
// logout user by clearing token
authFactory.logout = function() {
AuthToken.setToken();
};
// check if user is logged in
// checks for local token
authFactory.isLoggedIn = function() {
if (AuthToken.getToken())
return true;
else
return false;
};
// get logged in user
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me', { cache : true});
else
return $q.reject({ message : 'User has no token.'});
};
return authFactory;
})
// ===================================================
// factory for handling tokens
// inject $window to store token client-side
//
//
// ===================================================
.factory('AuthToken', function($window) {
var authTokenFactory = {};
// get token out of local storage
authTokenFactory.getToken = function() {
return $window.localStorage.getItem('token');
};
// function to set token or clear token
// if a token is passed, set the token
// if there is no token, clear it from local storage
authTokenFactory.setToken = function(token) {
if (token)
$window.localStorage.setItem('token', token);
else
$window.localStorage.removeItem('token');
};
return authTokenFactory;
})
// ===================================================
// application configuration to integrate token into requests
// ===================================================
.factory('AuthInterceptor', function($q, $location, AuthToken) {
var interceptorFactory = {};
// this will happen on all http requests
interceptorFactory.request = function(config) {
// grab token
var token = AuthToken.getToken;
// if token exists add it to the header as x-access-token
if (token)
config.headers['x-access-token'] = token;
return config;
};
// happens on response errors
interceptorFactory.responseError = function(response) {
// if 403 from server
if (response.status == 403) {
AuthToken.setToken();
$location.path('/login')
}
//return the errors from server as promise
return $q.reject(response);
};
return interceptorFactory;
});
app.js
var app = angular.module('userApp', [
'ngAnimate', 'app.routes', 'authService', 'mainCtrl', 'userCtrl', 'userService']);
// app config to integrate token into req
app.config(function($httpProvider) {
// attach our auth interceptor to the http reqs
$httpProvider.interceptors.push('AuthInterceptor');
});
app.controller('mainController', function($http) {
// Bind this to view / vm-view model
var vm = this;
// define variables and objects on this
// this lets them be available to our views
// define a basic variable
vm.message = 'Hey! Message';
$http.get('/api/users')
.then(function(data) {
// bind users to vm.users
vm.users = data.users;
});
});
In custom interceptor factory
interceptorFactory.request = function(config) {
// grab token
var token = AuthToken.getToken;
// if token exists add it to the header as x-access-token
if (token)
config.headers['x-access-token'] = token;
return config;
};
change AuthToken.getToken; to AuthToken.getToken();
and your error was quite clear that you were passing function into header instead of value
Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function () {
return $window.localStorage.getItem('token');
}' is not a valid HTTP header field value.
I have LoginController and securityService.
This is LoginCtrl
// place the message if something goes wrong
$scope.authMsg = '';
$scope.login = function () {
$scope.authMsg = '';
var loginData = {email: $scope.account.email, password: $scope.account.password};
securityService.login(loginData);
};
This is securityService
login: function (logData) {
var _vm = this;
$http
.post('/api-token-auth/', logData)
.then(function (response) {
// assumes if ok, response is an object with some data, if not, a string with error
// customize according to your api
if (!response.data.token) {
_vm.authMsg = 'Incorrect credentials.';
} else {
$cookieStore.put('djangotoken', response.data.token);
$http.defaults.headers.common.Authorization = 'JWT ' + response.data.token;
$http.get('/api/account/restricted/').then(function (response) {
authService.loginConfirmed();
_vm.currentUser = response.data;
$rootScope.currentUser = response.data;
});
}
}, function (x) {
_vm.authMsg = 'Server Request Error';
});
},
This login is working fine but my problem is i don't know how can get the authMesg from service to controller because that is async. Everytime i get blank message in case of invalid login
you need to use promise service of angular to make you controller and service syn
login: function (logData) {
var _vm = this,d= $$q.defer();
$http
.post('/api-token-auth/', logData)
.then(function (response) {
// assumes if ok, response is an object with some data, if not, a string with error
// customize according to your api
if (!response.data.token) {
_vm.authMsg = 'Incorrect credentials.';
} else {
$cookieStore.put('djangotoken', response.data.token);
$http.defaults.headers.common.Authorization = 'JWT ' + response.data.token;
$http.get('/api/account/restricted/').then(function (response) {
authService.loginConfirmed();
_vm.currentUser = response.data;
$rootScope.currentUser = response.data;
});
}
d.resolve(vm.authMsg);
}, function (x) {
_vm.authMsg = 'Server Request Error';
d.reject(vm.authMsg);
});
},
In controller you need to resolve this promise
securityService.login(loginData).then(function(data){
consol.log(data); // get success data
},function(error){
consol.log(data); // get error message data
})
and inject $q in your service.
This will give you authMsg
securityService.login(loginData).authMsg
But follow #Vigneswaran Marimuthu comments, that is best practice.
I am trying to send extra header in XHR request (init with $resource).Following is my config
var app = angular.module('app',['angularMoment']).
run(function ($rootScope,$location,$route, $timeout, $http) {
var token = localStorage.getItem("userToken");
$http.defaults.headers.common.token = token;
}
I am changing hash params (eg. after login process) to navigate in app. So when I am sending any XHR request after login process (wihout mannual reload), it's sending token (request header) as NULL. But when I reload my page manually it's working fine (i.e sending token as header). Also I tried with $route.reload() but it's not working.
Please suggest how can I get rid of this issue.
Thanks
EDIT :
After trying with follwing code :
app.factory('tokenInterceptorService', ['$q', '$location', function ($q, $location) {
var tokenInterceptor = {};
var request = function (config) {
config.headers = config.headers || {};
var token = localStorage.getItem("userToken");
config.headers.token = token;
return config;
}
// if response errors with 401 redirect to lgoin
var response = function (rejection) {
if (rejection.status === 401) {
$location.path('/');
}
return $q.reject(rejection);
}
tokenInterceptor.request = request;
tokenInterceptor.response = response;
return tokenInterceptor;
}]);
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('tokenInterceptorService');
});
app.run(function ($rootScope, $location,$route, $timeout, $http) {
$rootScope.config = {};
$rootScope.config.app_url = $location.url();
$rootScope.config.app_path = $location.path();
$rootScope.layout = {};
$rootScope.layout.loading = false;
$rootScope.$on('$routeChangeStart', function () {
//need to validate
console.log($rootScope.isValidated + "app");
//show loading
$timeout(function(){
$rootScope.layout.loading = true;
});
});
$rootScope.$on('$routeChangeSuccess', function () {
//hide loading
$timeout(function(){
$rootScope.layout.loading = false;
}, 200);
});
$rootScope.$on('$routeChangeError', function () {
alert('Something went wrong. Please refresh.');
$rootScope.layout.loading = false;
});
})
It stop rendring the views in application with ".run" and trapping in $rootScope.$on('$routeChangeError', and giving the error Error: [$rootScope:inprog] $digest already in progress.
Since if I understand correctly your user token is always taken from localstorage, you can setup a watch on that localStorage key in your run function (Demo plunker for working with Localstorage in angular: http://plnkr.co/edit/7hP13JAjPybxkRuMZLZ0?p=preview )
angular.module('app',[]).run(['$rootScope', '$http', function($root, $http) {
$root.$watch(function() {
return localStorage.getItem('userToken');
}, function(userToken) {
$http.defaults.headers.common.token = userToken;
});
});
This should solve your problems without any interceptors etc.
However I'd actually recommend using http interceptor as calls to localStorage are slow, or setting the defaults where you actually set the user token after login or logout (save it also on a scope variable, and initialize it in the run part like you do now).
You need to set up an interceptor that alters every request sent to the server. You can find out more form the docs here, but essentially you need to set up a factory service on your app to add the token header like so:
app.factory('tokenInterceptorService', ['$q', '$location', 'localStorage', function ($q, $location, localStorage) {
var tokenInterceptor = {};
var request = function (config) {
config.headers = config.headers || {};
var token = localStorage.getItem("userToken");
if (token) {
config.headers.token = token;
}
return config;
}
// if response errors with 401 redirect to lgoin
var response = function (rejection) {
if (rejection.status === 401) {
$location.path('/login');
}
return $q.reject(rejection);
}
tokenInterceptor.request = request;
tokenInterceptor.response = response;
return tokenInterceptor;
}]);
and then register it during the config stage with:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('tokenInterceptorService');
});
module.run executes well before anything else in the app (but after module.config). Would the localStorage have been set by then? I think that is happening later, which is why you see this value after reloading the page.
An interceptor would be the way to go.
How are you setting the value in localStorage?
Fiddle