api-connector.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpParams } from '#angular/common/http';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Observable } from 'rxjs/Observable';
import {environment} from '../../../environments/environment';
import { catchError } from 'rxjs/operators/catchError';
#Injectable()
export class ApiConnectorService {
constructor(private http: HttpClient) { }
private getQueryString(params): string {
const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&');
console.log('QUERY STRING', queryString);
return ('?' + queryString);
}
private formatErrors(error: any) {
return new ErrorObservable(error.error);
}
get(path: string, payload: Object = {}): Observable<any> {
return this.http.get(`${environment.base_url}${path}` + this.getQueryString(payload))
.pipe(catchError(this.formatErrors));
}
put(path: string, body: Object = {}): Observable<any> {
return this.http.put(
`${environment.base_url}${path}`,
body
).pipe(catchError(this.formatErrors));
}
post(path: string, body: Object): Observable<any> {
// console.log('API SERVICE BODY', body)
return this.http.post(
`${environment.base_url}${path}`,
body
).pipe(catchError(this.formatErrors));
}
delete(path): Observable<any> {
return this.http.delete(
`${environment.base_url}${path}`
).pipe(catchError(this.formatErrors));
}
}
login.contract.ts
export interface LoginRequest {
env?: string;
userid: string;
password: string;
newpassword: string;
}
export interface LoginResponse {
token: string;
}
I am pretty new to Angular and as well Karma/Jasmine also.
I have created a simple login component and login service. While writing test cases for that purpose, I followed some docs and angular.io site. I have written some of the test cases for login component with help of docs, but I didn't manage to write test cases for login service.
How to write test cases for login service?
Here is my login.service.ts file
import { Injectable } from '#angular/core';
import { ApiConnectorService } from '../api-handlers/api-connector.service';
import { LoginRequest, LoginResponse } from './login.contract';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
#Injectable()
export class LoginService {
constructor(private apiConnector: ApiConnectorService) { }
login(payload: LoginRequest): Observable<LoginResponse> {
console.log('Login payload ', payload);
return this.apiConnector.post('/api/login', payload)
.pipe(
map((data: LoginResponse) => data)
)
}
}
Having had a think about it this is how I would approach testing your service. I can't do the exact details for the last test as I don't have details on your ApiConnectorService or LoginResponse object but I'm sure you'll get the idea.
import { TestBed, inject } from '#angular/core/testing';
import { LoginService } from './login.service';
import { LoginResponse, LoginRequest } from './login.contract';
import { Observable, of } from 'rxjs';
import { ApiConnectorService } from './api-connector.service';
class ApiConnectorServiceStub {
constructor() { }
post(address: string, payload: LoginRequest): Observable<LoginResponse> {
return of(new LoginResponse());
}
}
describe('LoginService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LoginService,
{provide: ApiConnectorService, useClass: ApiConnectorServiceStub }]
});
});
it('should be created', inject([LoginService], (service: LoginService) => {
expect(service).toBeTruthy();
}));
it('should call post on apiConnectorService with right parameters when login is called',
inject([LoginService], (service: LoginService) => {
const apiConnectorStub = TestBed.get(ApiConnectorService);
const spy = spyOn(apiConnectorStub, 'post').and.returnValue(of(new LoginResponse()));
const loginRequest = of(new LoginRequest());
service.login(loginRequest);
expect(spy).toHaveBeenCalledWith('/api/login', loginRequest);
}));
it('should map data correctly when login is called', inject([LoginService], (service: LoginService) => {
const apiConnectorStub = TestBed.get(ApiConnectorService);
// Set you apiConnector output data here
const apiData = of('Test Data');
const spy = spyOn(apiConnectorStub, 'post').and.returnValue(apiData);
const result = service.login(of(new LoginRequest()));
// Set your expected LoginResponse here.
const expextedResult = of(new LoginResponse());
expect(result).toEqual(expextedResult);
}));
});
Related
So my next step in NestJs is being able to use asymetric jwt validation with Passport. I've made it work with asymetric validation without using Passport and with symetric validation in Passport but when I change the fields to what I think it's correct to set to asymetric validation in Passport it doesn't work and I'm not being able to find any working asymetric examples.
Here's what I got:
AuthModule:
import { Module } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '#nestjs/jwt';
import { PassportModule } from '#nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
const jwtFactory = {
useFactory: async (configService: ConfigService) => {
let privateKey = configService.get<string>('JWT_PRIVATE_KEY_BASE64', '');
let publicKey = configService.get<string>('JWT_PUBLIC_KEY_BASE64', '');
// let privateKey = configService.get<string>('JWT_SECRET', '');
return {
privateKey,
publicKey,
signOptions: {
expiresIn: configService.get('JWT_EXP_H'),
},
};
},
inject: [ConfigService],
};
#Module({
imports: [
JwtModule.registerAsync(jwtFactory),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, DbRepo, JwtStrategy],
exports: [DbRepo, JwtModule, JwtStrategy, PassportModule],
})
export class AuthModule { }
AuthController:
import { Body, Controller, Post } from '#nestjs/common';
import { CreateUserDto } from 'src/dataObjects/users-create-new.dto';
import { AuthService } from './auth.service';
import { User } from 'src/dataObjects/user.entity';
import { AuthCredentialsDto } from 'src/dataObjects/user-auth-credentials.dto';
#Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
#Post('/signin')
signin(
#Body() authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
return this.authService.signin(authCredentialsDto);
}
#Post('/signup')
signup(#Body() createUserDto: CreateUserDto): Promise<User> {
return this.authService.signup(createUserDto);
}
}
AuthService:
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { AuthCredentialsDto } from 'src/dataObjects/user-auth-credentials.dto';
import { User } from 'src/dataObjects/user.entity';
import { CreateUserDto } from 'src/dataObjects/users-create-new.dto';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { JwtService } from '#nestjs/jwt';
import { UserJwtPayload } from 'src/dataObjects/user-jwt-payload.interface';
import { ConfigService } from '#nestjs/config';
#Injectable()
export class AuthService {
constructor(private dbRepo: DbRepo, private jwtService: JwtService, configService: ConfigService) {}
async signup(createUserDto: CreateUserDto): Promise<User> {
return await this.dbRepo.createUser(createUserDto);
}
async signin(
authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
const username: string = authCredentialsDto.username;
const user = await this.dbRepo.userFindByNameAndMatchingPassword(
authCredentialsDto,
);
if (user) {
const typeid = user.typeid;
const payload: UserJwtPayload = { username, typeid };
const accessToken: string = this.jwtService.sign(payload);
return { accessToken };
} else {
throw new UnauthorizedException('Incorrect login credentials!');
}
}
}
JwtStrategy:
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserJwtPayload } from 'src/dataObjects/user-jwt-payload.interface';
import { User } from 'src/dataObjects/user.entity';
import { DbRepo } from 'src/dataObjects/dbRepo';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private dbRepo: DbRepo,
private configService: ConfigService,
) {
let publicKey = configService.get<string>('JWT_PUBLIC_KEY_BASE64', '');
// let publicKey = configService.get<string>('JWT_SECRET', '');
super({
secretOrKey: publicKey,
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
algorithms: ['ES512']
});
}
async validate(payload: UserJwtPayload): Promise<User> {
console.log('payload', payload);
const { username, typeid } = payload;
const users: User[] = await this.dbRepo.getUsers({ username });
const user: User = users[0];
if (typeid > 2 || Object.keys(user).length <= 0) {
throw new UnauthorizedException();
}
return user;
}
}
.env.dev: (this is a study project, nothing here is production, so it doesn't matter to show the keys)
APP_PORT=3000
APP_GLOBAL_PREFIX=tickets
JWT_SECRET=abcdABCD1234554321
JWT_PUBLIC_KEY_BASE64=-----BEGIN PUBLIC KEY----- MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA5w7oeLUYmCBB6kvpfU1fp5nq93SI 3nZ/Ihv8fxIgYlK1XEIp6MxjdzK1+O9ykIGuSFVAzo8xvSbmkHOyGYHn+AoBKFat Cmfn2hUw41xQcQiHV7ZCljAobmFfHNH0U5SXlqvNv4urZWcDmKOThB1sOsQhju79 5gjYoauIaR741sVlf9o= -----END PUBLIC KEY-----
JWT_PRIVATE_KEY_BASE64=-----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIA1yAjkQ36YE8fzrqorkP++eFQkTHY4RGdXXkI7EsnyW9mS3lpPvd5 y4+oZyPfr3wEvgpendFV13CJzgGG5Oy2jVWgBwYFK4EEACOhgYkDgYYABADnDuh4 tRiYIEHqS+l9TV+nmer3dIjedn8iG/x/EiBiUrVcQinozGN3MrX473KQga5IVUDO jzG9JuaQc7IZgef4CgEoVq0KZ+faFTDjXFBxCIdXtkKWMChuYV8c0fRTlJeWq82/ i6tlZwOYo5OEHWw6xCGO7v3mCNihq4hpHvjWxWV/2g== -----END EC PRIVATE KEY-----
JWT_EXP_H=3600s
JWT_EXP_D=1d
Guarded class:
<...>
#Controller('users')
#UseGuards(AuthGuard())
export class UsersController {
constructor(private userService: UsersService) {}
#Get()
async getUsers(#Headers('Authorization') authorization = '', #Query() filterDto: UserDataDto): Promise<User[]> {
return this.userService.getUsers(filterDto);
}
<...> more methods
}
What happens is that when I call signin it does emit a token, but if I put it in bearer token it returns unauthorized.
I tried to remove BEGIN/END text in the keys, but the result was the same. This very code works fine in the symetric version. I mean, if I remove public/privateKey and algorithms options from AuthModule and JwtStrategy and using only secretOrKey with JWT_SECRET environment variable.
Finally if I include algorithm: ['ES512'] in AuthModule's signOptions I get this error:
src/auth/auth.module.ts:33:27 - error TS2345: Argument of type '{ useFactory: (configService: ConfigService) => Promise<{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }>; inject: (typeof ConfigService)[]; }' is not assignable to parameter of type 'JwtModuleAsyncOptions'.
The types returned by 'useFactory(...)' are incompatible between these types.
Type 'Promise<{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }>' is not assignable to type 'JwtModuleOptions | Promise<JwtModuleOptions>'.
Type 'Promise<{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }>' is not assignable to type 'Promise<JwtModuleOptions>'.
Type '{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }' is not assignable to type 'JwtModuleOptions'.
The types of 'signOptions.algorithm' are incompatible between these types.
Type 'string' is not assignable to type 'Algorithm | undefined'.
33 JwtModule.registerAsync(jwtFactory),
I did this final test because it's advised in this SO question.
How can I make this work ?
Edit
I tried to change AuthService sign method to:
const accessToken: string = this.jwtService.sign(payload, { algorithm: 'ES512' });
But I got a very strange error:
Error: error:1E08010C:DECODER routines::unsupported
at Sign.sign (node:internal/crypto/sig:131:29)
at sign (/vagrant/node_modules/jwa/index.js:152:45)
at Object.sign (/vagrant/node_modules/jwa/index.js:200:27)
at Object.jwsSign [as sign] (/vagrant/node_modules/jws/lib/sign-stream.js:32:24)
at Object.module.exports [as sign] (/vagrant/node_modules/jsonwebtoken/sign.js:204:16)
at JwtService.sign (/vagrant/node_modules/#nestjs/jwt/dist/jwt.service.js:28:20)
at AuthService.signin (/vagrant/src/auth/auth.service.ts:31:51)
ES512 is supported by jsonwebtoken and by node. If it wasn´t my non-passport-jwt asymetric version wouldn't work.
Edit 2
I put the project in this github repo: https://github.com/nelson777/nest-asymetric-validation
Maybe it's useful if someone wants to run the project
This is a repository with symetric validation working: https://github.com/nelson777/nest-symetric-validation
But I finally figured out how to do it. I got almost everything right, except for the right way to put the keys in .env. This is the correct way:
JWT_PUBLIC_KEY_BASE64="-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA5w7oeLUYmCBB6kvpfU1fp5nq93SI\n3nZ/Ihv8fxIgYlK1XEIp6MxjdzK1+O9ykIGuSFVAzo8xvSbmkHOyGYHn+AoBKFat\nCmfn2hUw41xQcQiHV7ZCljAobmFfHNH0U5SXlqvNv4urZWcDmKOThB1sOsQhju79\n5gjYoauIaR741sVlf9o=\n-----END PUBLIC KEY-----\n"
JWT_PRIVATE_KEY_BASE64="-----BEGIN EC PRIVATE KEY-----\nMIHcAgEBBEIA1yAjkQ36YE8fzrqorkP++eFQkTHY4RGdXXkI7EsnyW9mS3lpPvd5\ny4+oZyPfr3wEvgpendFV13CJzgGG5Oy2jVWgBwYFK4EEACOhgYkDgYYABADnDuh4\ntRiYIEHqS+l9TV+nmer3dIjedn8iG/x/EiBiUrVcQinozGN3MrX473KQga5IVUDO\njzG9JuaQc7IZgef4CgEoVq0KZ+faFTDjXFBxCIdXtkKWMChuYV8c0fRTlJeWq82/\ni6tlZwOYo5OEHWw6xCGO7v3mCNihq4hpHvjWxWV/2g==\n-----END EC PRIVATE KEY-----\n"
Please note the start/end double quotes, the \n where was a new line (there are several along the line, the \n before -----END * and the \n on the very end.
Thanks to user #MarceloFonseca for this answer here:https://stackoverflow.com/a/61978298/2752520
Here goes the corrected code:
AuthModule:
import { Module } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '#nestjs/jwt';
import { PassportModule } from '#nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
const jwtFactory = {
useFactory: async (configService: ConfigService) => {
return {
privateKey: configService.get<string>('JWT_PRIVATE_KEY_BASE64', ''),
publicKey: configService.get<string>('JWT_PUBLIC_KEY_BASE64', ''),
signOptions: {
expiresIn: configService.get('JWT_EXP_H'),
},
};
},
inject: [ConfigService],
};
#Module({
imports: [
JwtModule.registerAsync(jwtFactory),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, DbRepo, JwtStrategy],
exports: [DbRepo, JwtModule, JwtStrategy, PassportModule],
})
export class AuthModule { }
AuthController:
import { Body, Controller, Post } from '#nestjs/common';
import { CreateUserDto } from 'src/dataObjects/users-create-new.dto';
import { AuthService } from './auth.service';
import { User } from 'src/dataObjects/user.entity';
import { AuthCredentialsDto } from 'src/dataObjects/user-auth-credentials.dto';
#Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
#Post('/signin')
signin(
#Body() authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
return this.authService.signin(authCredentialsDto);
}
#Post('/signup')
signup(#Body() createUserDto: CreateUserDto): Promise<User> {
return this.authService.signup(createUserDto);
}
}
AuthService:
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { AuthCredentialsDto } from 'src/dataObjects/user-auth-credentials.dto';
import { User } from 'src/dataObjects/user.entity';
import { CreateUserDto } from 'src/dataObjects/users-create-new.dto';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { JwtService } from '#nestjs/jwt';
import { UserJwtPayload } from 'src/dataObjects/user-jwt-payload.interface';
import { ConfigService } from '#nestjs/config';
#Injectable()
export class AuthService {
constructor(private dbRepo: DbRepo, private jwtService: JwtService, private configService: ConfigService) { }
async signup(createUserDto: CreateUserDto): Promise<User> {
return await this.dbRepo.createUser(createUserDto);
}
async signin(
authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
const username: string = authCredentialsDto.username;
const user = await this.dbRepo.userFindByNameAndMatchingPassword(
authCredentialsDto,
);
if (user) {
const typeid = user.typeid;
const payload: UserJwtPayload = { username, typeid };
const accessToken: string = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_PRIVATE_KEY_BASE64', ''),
algorithm: 'ES512'
});
return { accessToken };
} else {
throw new UnauthorizedException('Incorrect login credentials!');
}
}
}
JwtStrategy:
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserJwtPayload } from 'src/dataObjects/user-jwt-payload.interface';
import { User } from 'src/dataObjects/user.entity';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { readFileSync } from 'node:fs';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private dbRepo: DbRepo,
private configService: ConfigService,
) {
super({
secretOrKey: configService.get<string>('JWT_PUBLIC_KEY_BASE64', ''),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
algorithms: ['ES512']
});
}
async validate(payload: UserJwtPayload): Promise<User> {
console.log('payload', payload);
const { username, typeid } = payload;
const users: User[] = await this.dbRepo.getUsers({ username });
const user: User = users[0];
if (typeid > 2 || Object.keys(user).length <= 0) {
throw new UnauthorizedException();
}
return user;
}
}
.env.dev:
APP_PORT=3000
APP_GLOBAL_PREFIX=tickets
JWT_SECRET=abcdABCD1234554321
JWT_PUBLIC_KEY_BASE64="-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA5w7oeLUYmCBB6kvpfU1fp5nq93SI\n3nZ/Ihv8fxIgYlK1XEIp6MxjdzK1+O9ykIGuSFVAzo8xvSbmkHOyGYHn+AoBKFat\nCmfn2hUw41xQcQiHV7ZCljAobmFfHNH0U5SXlqvNv4urZWcDmKOThB1sOsQhju79\n5gjYoauIaR741sVlf9o=\n-----END PUBLIC KEY-----\n"
JWT_PRIVATE_KEY_BASE64="-----BEGIN EC PRIVATE KEY-----\nMIHcAgEBBEIA1yAjkQ36YE8fzrqorkP++eFQkTHY4RGdXXkI7EsnyW9mS3lpPvd5\ny4+oZyPfr3wEvgpendFV13CJzgGG5Oy2jVWgBwYFK4EEACOhgYkDgYYABADnDuh4\ntRiYIEHqS+l9TV+nmer3dIjedn8iG/x/EiBiUrVcQinozGN3MrX473KQga5IVUDO\njzG9JuaQc7IZgef4CgEoVq0KZ+faFTDjXFBxCIdXtkKWMChuYV8c0fRTlJeWq82/\ni6tlZwOYo5OEHWw6xCGO7v3mCNihq4hpHvjWxWV/2g==\n-----END EC PRIVATE KEY-----\n"
JWT_EXP_H=3600s
JWT_EXP_D=1d
Guarded class:
<...>
#Controller('users')
#UseGuards(AuthGuard())
export class UsersController {
constructor(private userService: UsersService) {}
#Get()
async getUsers(#Headers('Authorization') authorization = '', #Query() filterDto: UserDataDto): Promise<User[]> {
return this.userService.getUsers(filterDto);
}
<...> more methods
}
I pushed the changes to the repo I had created. So now there's a working example there with asymetric validation on NestJs and Passport.
I use canActivate in history.guard and how can I check if the user login or not!
the value which I console always return false!
Do I need to create a new function in auth.service or just edit in history.guard ? Is there any way instead of using subscribe ??
auth.service.ts
import { Injectable } from '#angular/core';
import { Router } from '#angular/router';
import { Subject } from 'rxjs/Subject';
import { ApiService, VERSION, ENDPOINT } from '../api/api.service';
import { Observable, BehaviorSubject } from 'rxjs';
#Injectable()
export class AuthService {
logger = new BehaviorSubject<Object>(false);
referralRoute: string;
constructor(
private router: Router,
private api: ApiService
) {
}
logout() {
localStorage.removeItem('access-token');
localStorage.removeItem('uid');
localStorage.removeItem('client');
this.redirectToLogin();
this.logger.next(false);
}
postLogin(body: any) {
this.api.get(['token.json'], {}).subscribe(
(res: any) => {
localStorage.setItem('access-token', res['access-token']);
localStorage.setItem('uid', res['uid']);
localStorage.setItem('client', res['client']);
this.logger.next(true);
this.redirectToPrevStep();
},
(err) => {
this.logger.next(err);
});
}
checkLogin(body: any) {
this.api.get([VERSION, ENDPOINT.checkLogin], {}).subscribe(
(res: any) => {
this.logger.next(true);
},
(err) => {
this.logger.next(err);
});
}
checkUserLogin() {
const isLogin = !!localStorage.getItem('JWT_TOKEN');
if (isLogin) {
this.logger.next(true);
} else {
this.logger.next(false);
}
}
subscribeLogger(): Observable<Object> {
return this.logger.asObservable();
}
isAuthenticated() {
const token = localStorage.getItem('access-token');
let isAuthenticated: boolean;
if (this.isTokenInvalid()) {
localStorage.removeItem('access-token');
isAuthenticated = false;
} else {
isAuthenticated = true;
}
return isAuthenticated;
}
getUserInfo() {
const token = localStorage.getItem('access-token');
// let userInfo = this.jwtHelper.decodeToken(token);
return {};
// this.jwtHelper.decodeToken(token),
// this.jwtHelper.getTokenExpirationDate(token),
// this.jwtHelper.isTokenExpired(token)
// );
}
isTokenInvalid() {
const token = localStorage.getItem('access-token');
if (!token) {
return true
} else {
// this.api.setHeaders(token);
return false;
}
}
/**
* Helper method for set up referral route, enable useful redirect after login
* #method setRoute
* #param {string} route Route as defined in app.routes, eg. /user/1
*/
setRoute(route: string): void {
this.referralRoute = route;
}
redirectToPrevStep() {
const route = this.referralRoute ? this.referralRoute : '/';
this.router.navigateByUrl(route);
}
redirectToLogin(current: string = '/') {
// Store current url as referral and use latter for login redirection
this.setRoute(current);
window.scroll(0, 0);
this.router.navigate(['/auth/login']);
}
}
history.guard.ts
import { Injectable } from '#angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { AuthService } from '../../core/service/auth/auth.service';
#Injectable({ providedIn: 'root' })
export class HistoryGuard implements CanActivate {
checkUserLogin: boolean;
constructor(
private router: Router,
private auth: AuthService
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const checkUserLogin = this.auth.subscribeLogger().subscribe(
(data: any) => {
this.checkUserLogin = data;
}
);
if (!this.checkUserLogin) {
return this.router.navigate(['mypage']);
}
else {
return this.checkUserLogin;
}
}
}
history.module.ts
import { NgModule } from '#angular/core';
import { HistoryComponent } from './history.component';
import { HistoryItemComponent } from './history-item/history-item.component';
import { RouterModule, Routes } from '#angular/router';
import { CommonModule } from '#angular/common';
import { HistoryGuard } from './history.guard';
const routes: Routes = [
{
path: '',
component: HistoryComponent,
canActivate: [HistoryGuard]
},
{
path: ':id',
component: HistoryItemComponent,
canActivate: [HistoryGuard]
}
];
#NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes)
],
declarations: [HistoryComponent, HistoryItemComponent]
})
export class HistoryModule { }
Hi this how I implemented AuthGuard, you can check just if in local storage is a JWT token or not, because on logout you should delete jwt token from localStorage and that's it
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (this.authService.isLoggedIn()) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
}
}
// Auth service
isLoggedIn() {
return Boolean(this.getToken());
}
getToken() {
return this.localStorage$.retrieve('authenticationToken');
}
logout() {
this.localStorage$.clear('authenticationtoken');
}
This is how your canActivate should look like:
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.auth.subscribeLogger().pipe(
tap(login => {
if(!login) {
this.router.navigate(['mypage']); // If user is not logged in, just navigate away
}
})
);
}
I have API for getting information about one specific restaurant in the database, but I have to get it with a POST request. I successfully get restaurantID from auth.service and another API when the restaurant is logged in, But when I tried to log restaurant in console, I get undefined. Uniformly I don't have permission to show API here. The code:
Informacije component:
import { Component, OnInit } from '#angular/core';
import { AuthService } from '../services/auth.service';
import { RestaurantService } from '../services/restaurant.service';
import { Restaurant } from '../models/Restaurant';
import { LoggedRestaurant } from '../models/LoggedRestaurant';
import { Observable } from 'rxjs';
#Component({
selector: 'app-informacije',
templateUrl: './informacije.component.html',
styleUrls: ['./informacije.component.scss']
})
export class InformacijeComponent implements OnInit {
restaurant: Restaurant;
loggedRestaurant: LoggedRestaurant;
restaurantID = this.authService.currRestaurant[0].id;
constructor(private restaurantService: RestaurantService, private authService: AuthService ) { }
getRestaurant() {
return this.restaurantService.getRestaurant(this.restaurantID).toPromise().then(data => {
this.loggedRestaurant = data;
});
}
async ngOnInit() {
await this.restaurantService.getRestaurant(this.restaurantID).subscribe(
data => {
this.loggedRestaurant = data;
console.log(this.loggedRestaurant)
})
this.restaurant = this.authService.currRestaurant[0];
console.log(this.restaurant)
console.log(this.loggedRestaurant)
console.log(this.restaurantService.restaurantID)
}
}
restaurant.service
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Restaurant } from '../models/Restaurant';
import { LoggedRestaurant } from '../models/LoggedRestaurant';
import { AuthService } from './auth.service'
#Injectable({
providedIn: 'root'
})
export class RestaurantService {
private restaurantUrl = 'xxxxxx';
public restaurant: Restaurant;
public loggedRestaurant: LoggedRestaurant
public restaurantID = this.authService.currRestaurant[0].id
constructor(private http: HttpClient, private authService: AuthService) { }
getRestaurant(ID): Observable<LoggedRestaurant> {
console.log('ID je razmak' + this.restaurantID);
return this.http.post<LoggedRestaurant>(this.restaurantUrl, ID);
}
}
auth.service
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '#angular/common/http';
import { throwError, Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Restaurant } from '../models/Restaurant';
#Injectable({
providedIn: 'root'
})
export class AuthService {
loginUrl = 'xxxxx';
errorData: {};
constructor(private http: HttpClient) { }
redirectUrl: string;
login(email: string, password: string) {
var postData = {email: email, password: password};
return this.http.post<Restaurant>(this.loginUrl, postData)
.pipe(map(restaurant => {
if (restaurant) {
localStorage.setItem('currentRestaurant', JSON.stringify(restaurant));
return restaurant;
}
}),
catchError(this.handleError)
);
}
isLoggedIn() {
if (localStorage.getItem('currentRestaurant')) {
return true;
}
return false;
}
getAuthorizationToken() {
const currentRestaurant = JSON.parse(localStorage.getItem('currentRestaurant'));
return currentRestaurant.token;
}
logout() {
localStorage.removeItem('currentRestaurant');
}
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(`Backend returned code ${error.status}, ` + `body was: ${error.error}`);
}
// return an observable with a user-facing error message
this.errorData = {
errorTitle: 'Oops! Request for document failed',
errorDesc: 'Something bad happened. Please try again later.'
};
return throwError(this.errorData);
}
currRestaurant: Restaurant = JSON.parse(localStorage.getItem('currentRestaurant'));
currID = this. currRestaurant.id;
}
Being a post request, if you want to see the data you need to return the full response.
Add {observe: 'response'} to your request like so:
getRestaurant(ID): Observable<HttpResponse<LoggedRestaurant>> {
console.log('ID je razmak' + this.restaurantID);
return this.http.post<LoggedRestaurant>(this.restaurantUrl, ID, {observe:'response'});
}
and retrieve it like so:
this.restaurantService.getRestaurant(this.restaurantID).subscribe(
data => {
this.loggedRestaurant = data.body;
console.log(this.loggedRestaurant)
})
Let me know if that worked :)
Try like this:
import { Component, OnInit } from '#angular/core';
import { AuthService } from '../services/auth.service';
import { RestaurantService } from '../services/restaurant.service';
import { Restaurant } from '../models/Restaurant';
import { LoggedRestaurant } from '../models/LoggedRestaurant';
import { Observable } from 'rxjs';
#Component({
selector: 'app-informacije',
templateUrl: './informacije.component.html',
styleUrls: ['./informacije.component.scss']
})
export class InformacijeComponent implements OnInit {
restaurant: Restaurant;
loggedRestaurant: LoggedRestaurant;
restaurantID = this.authService.currRestaurant[0].id;
constructor(private restaurantService: RestaurantService, private authService: AuthService ) { }
getRestaurant() {
return this.restaurantService.getRestaurant(this.restaurantID).toPromise().then(data => {
this.loggedRestaurant = data;
});
}
ngOnInit() {
this.restaurant = this.authService.currRestaurant[0];
this.restaurantService.getRestaurant(this.restaurantID).subscribe(
data => {
this.loggedRestaurant = data;
console.log(this.loggedRestaurant)
})
console.log(this.restaurant)
console.log(this.loggedRestaurant)
console.log(this.restaurantService.restaurantID)
}
}
restaurant.service
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Restaurant } from '../models/Restaurant';
import { LoggedRestaurant } from '../models/LoggedRestaurant';
import { AuthService } from './auth.service'
#Injectable({
providedIn: 'root'
})
export class RestaurantService {
private restaurantUrl = 'xxxxxx';
public restaurant: Restaurant;
public loggedRestaurant: LoggedRestaurant
public restaurantID = this.authService.currRestaurant[0].id
constructor(private http: HttpClient, private authService: AuthService) { }
getRestaurant(ID): Observable<LoggedRestaurant> {
console.log('ID je razmak' + this.restaurantID);
return this.http.post<LoggedRestaurant>(this.restaurantUrl, ID);
}
}
I am trying to do unit testing by using Karma/Jasmine, I am getting error
Argument of type 'Observable' is not assignable to parameter of type 'LoginRequest'. Property 'userid' is missing in type 'Observable'
I am getting the error in compile time in login.service.spec.ts where i am calling service.login(loginRequest);.
login.service.spec.ts
import { ComponentFixture, TestBed, inject } from '#angular/core/testing';
import { ApiConnectorService } from '../api-handlers/api-connector.service';
import { LoginService } from './login.service';
import { HttpClient, HttpHandler } from '#angular/common/http';
import { Observable } from 'rxjs';
import { of } from 'rxjs/observable/of';
import { LoginResponse, LoginRequest } from './login.contract';
class ApiConnectorServiceStub {
constructor() { }
post(address: string, payload: LoginRequest): Observable<LoginResponse> {
let str:LoginResponse = {token:'success'};
return of(str);
}
}
describe('LoginService', () => {
let service: LoginService;
let fixture: ComponentFixture<LoginService>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [LoginService, ApiConnectorService, HttpClient, HttpHandler,
{provide: ApiConnectorService, useClass: ApiConnectorServiceStub}]
});
});
it('should be created', inject([LoginService], (service: LoginService) => {
expect(service).toBeTruthy();
}));
it('should call post on apiConnectorService with right parameters when login is called',
inject([LoginService], (service: LoginService) => {
const apiConnectorStub = TestBed.get(ApiConnectorService);
const str:LoginResponse = {token:'success'};
const spy = spyOn(apiConnectorStub, 'post').and.returnValue(of(str));
const lRequest: LoginRequest={userid:'spraju#gmail.com',password:'hsjshsj',newpassword:'hsjshsj'};
const loginRequest = of(lRequest);
service.login(loginRequest);
expect(spy).toHaveBeenCalledWith('/api/login', loginRequest);
}));
});
login.service.ts
import { Injectable } from '#angular/core';
import { ApiConnectorService } from '../api-handlers/api-connector.service';
import { LoginRequest, LoginResponse } from './login.contract';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import * as marked from 'marked';
#Injectable()
export class LoginService {
constructor(private apiConnector: ApiConnectorService) { }
login(payload: LoginRequest): Observable<LoginResponse> {
console.log('Login payload ', payload);
return this.apiConnector.post('/api/login', payload)
.pipe(
map((data: LoginResponse) => data)
)
}
}
login.contract.ts
export interface LoginRequest {
env?: string;
userid: string;
password: string;
newpassword: string;
}
export interface LoginResponse {
token: string;
}
api-connector.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpParams } from '#angular/common/http';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { Observable } from 'rxjs/Observable';
import {environment} from '../../../environments/environment';
import { catchError } from 'rxjs/operators/catchError';
#Injectable()
export class ApiConnectorService {
constructor(private http: HttpClient) { }
private getQueryString(params): string {
const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&');
console.log('QUERY STRING', queryString);
return ('?' + queryString);
}
private formatErrors(error: any) {
return new ErrorObservable(error.error);
}
post(path: string, body: Object): Observable<any> {
// console.log('API SERVICE BODY', body)
return this.http.post(
`${environment.base_url}${path}`,
body
).pipe(catchError(this.formatErrors));
}
}
In your test, touch are doing
const loginRequest = of(lRequest);
service.login(loginRequest)
but the login method takes a LoginRequest as a parameter - you are attempting to pass an Observable
So change
const loginRequest = of(lRequest);
to
const loginRequest = lRequest;
I have a very common/simple scenario. I have an http service which makes a call to local json files and get data which will ultimately be replaced by actual api calls.
I want to mock the service and write test on ngOnInit of Component but it seems its not working.
factory-form.component.ts
import { Component, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
import { Factory } from './factory';
import { FactoryService } from './factory.service';
#Component({
moduleId: module.id,
selector: 'factory-form',
templateUrl: './factory-form.component.html',
styleUrls: ['./factory-form.component.css'],
providers: [FactoryService]
})
export class FactoryFormComponent implements OnInit {
private model: Factory = new Factory();
countries;
factoryStatuses;
productTypes;
risks;
private errorMessage: string;
private submitted: boolean = false;
private active: boolean = true;
constructor(private factoryService: FactoryService) {
}
ngOnInit(): void {
this.getCountries();
}
private getCountries() {
this.factoryService.getCountry()
.subscribe(countries => this.countries = countries,
error => this.errorMessage = error);
}
onSubmit(): void {
this.submitted = true;
this.factoryService.saveFactory(this.model);
}
}
factory.service.ts
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Factory } from './factory';
import { IDictionary } from '../shared/dictionary';
import { AppSettings } from '../shared/app.constants';
#Injectable()
export class FactoryService {
constructor(private http: Http) {
}
getCountry(): Observable<IDictionary[]> {
return this.http.get(AppSettings.countryUrl)
.map(this.extractData)
.do(data => console.log("get Countries: " + JSON.stringify(data)))
.catch(this.handleError);
}
private extractData(response: Response) {
let body = response.json();
return body || {};
}
private handleError(error: Response) {
console.log(error);
return Observable.throw(error.json().error || "500 internal server error");
}
}
factory.service.mock.ts
import {provide, Provider} from '#angular/core';
import {FactoryService} from './factory.service';
import * as Rx from 'rxjs/Rx';
export class MockFactoryService extends FactoryService{
public fakeResponse: any = [{"id": 1, "name": "uk"}];
public getCountry(): Rx.Observable<any> {
let subject = new Rx.ReplaySubject()
subject.next(this.fakeResponse);
return subject;
}
public setResponse(response: any): void {
this.fakeResponse = response;
}
public getProvider(): Provider {
return provide(FactoryService, { useValue: this });
}
}
factory-form.component.spec.ts
/* tslint:disable:no-unused-varfoiable */
import { By } from '#angular/platform-browser';
import { DebugElement, provide } from '#angular/core';
import {
beforeEach, beforeEachProviders,
describe, xdescribe,
expect, it, xit,
async, inject,
addProviders,
fakeAsync,
tick,
ComponentFixture,
TestComponentBuilder
} from '#angular/core/testing';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { FactoryService } from './factory.service';
import { MockFactoryService } from './factory.service.mock';
import { FactoryFormComponent } from './factory-form.component';
beforeEachProviders(() => [FactoryFormComponent, provide(FactoryService, { useClass: MockFactoryService })]);
describe('When loading the FactoryFormComponent', () => {
var builder;
beforeEach(inject([TestComponentBuilder], (tcb) => {
builder = tcb;
builder.overrideProviders(FactoryFormComponent,
[
{ provide: FactoryService, useClass: MockFactoryService }
])
.overrideTemplate('<h1>fake tempalte</h1>');
}));
it('should call ngOnInit', async(() => {
let component = builder.createAsync(FactoryFormComponent);
console.log("component?" + JSON.stringify(component));
component.then((fixture: ComponentFixture<FactoryFormComponent>) => {
console.log("inside block");
fixture.detectChanges();
var compiled = fixture.debugElement.nativeElement;
expect(fixture.componentInstance.ngOnInit).toHaveBeenCalled();
expect(fixture.componentInstance.MockFactoryService.getCountry).toHaveBeenCalled();
}).catch((error) => {
console.log("error occured: " + error);
});;
}));
});
this is the output
12 07 2016 16:54:56.314:INFO [watcher]: Changed file "C:/Projects/ethical_resourcing/src/Ethos.Client/dist/app/factory/factory-form.component.spec.js".
12 07 2016 16:54:56.393:INFO [watcher]: Changed file "C:/Projects/ethical_resourcing/src/Ethos.Client/dist/app/factory/factory-form.component.spec.js.map".
12 07 2016 16:54:56.988:WARN [web-server]: 404: /base/dist/vendor/systemjs/dist/system-polyfills.js.map
LOG: 'component?{"_handler":{"resolved":false}}'
Chrome 51.0.2704 (Windows 7 0.0.0): Executed 1 of 1 SUCCESS (0.108 secs / 0.1 secs)
for some reason its passing because its not going to the inner block. I deliberately broke the creatAsync.then() code to see what is coming as component value.
If someone has a better suggestion to mock the service that would be great.
Update: I did try with templateUrl and overriding it but no luck. maybe missing some fundamental thing.
update 2: I am using angular-cli and just call ng test.