How do I use asymmetric jwt validation in NestJs? - javascript

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.

Related

Error compiling Nest js APP - Nest can't resolve dependencies of the UsersService (?)

Im learning NestJs and using a tutorial to install User Auth with JWT
I have a user module which on its on works great but with the new auth service Im getting the error
Error: Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument UsersModel at index [0] is available in the AuthModule context.
The auth module is as follows:
import { Module } from "#nestjs/common"
import { UsersModule } from "../users/users.module";
import { AuthService } from "./auth.service"
import { PassportModule } from "#nestjs/passport"
import { JwtModule } from '#nestjs/jwt';
import { AuthController } from './auth.controller';
import { UsersService } from "../users/users.service";
import { MongooseModule } from "#nestjs/mongoose"
import { UserSchema } from "users/schemas/users.schema";
import { LocalStrategy } from './local.auth';
#Module({
imports: [UsersModule, PassportModule, JwtModule.register({
secret: 'secretKey',
signOptions: { expiresIn: '60s' },
}), MongooseModule.forFeature([{ name: "user", schema: UserSchema }])],
providers: [AuthService, UsersService, LocalStrategy],
controllers: [AuthController],
})
export class AuthModule { }
And the users service
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from 'users/interfaces/users.interface';
#Injectable()
export class UsersService {
constructor(#InjectModel('Users') private readonly userModel: Model<User>) {}
//Get all users
async getUsers(): Promise<User[]> {
const users = await this.userModel.find().exec();
return users
}
//Get single user
async getUser(query: object ): Promise<User> {
return this.userModel.findOne(query);
}
async addUser(
firstname: string,
lastname: string,
jobtitle: string,
startdate: string,
password: string,
email: string): Promise<User> {
return this.userModel.create({
firstname,
lastname,
jobtitle,
startdate,
password,
email
});
}
}
And for context the auth service and user interface
import { Injectable } from '#nestjs/common';
import { UsersService } from 'src/users/users.service';
import * as bcrypt from 'bcrypt';
import { JwtService } from '#nestjs/jwt';
import { NotAcceptableException } from '#nestjs/common/exceptions';
#Injectable()
export class AuthService {
constructor(private readonly usersService: UsersService, private jwtService: JwtService) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.getUser({email});
if(!user) return null;
const passwordValid = await bcrypt.compare(password, user.password)
if(!user) {
throw new NotAcceptableException('could not find the user');
}
if(user && passwordValid) {
return user;
}
return null;
}
async login(user: any) {
const payload = {username: user.username, sub: user._id};
return {
access_token: this.jwtService.sign(payload),
};
}
}
import { Document } from "mongoose";
export interface User extends Document {
readonly firstname: string;
readonly lastname: string;
readonly jobtitle: string;
readonly startdate: string;
readonly password: string;
readonly email: string;
}
So I have checked to make sure all imports are correct which they are and made sure all modules are aware of the imports needed - so Im a little stumped
So long as your UserModule has exports: [UserService] you can remove the UserService from the AuthModule's providers array. Every time you add a provider to a providers array you are telling Nest to create that provider in the context of the current module, and not possibly re-use one even if it exists in an imported module.
To add to jay's answer, You need this line in your users.module.ts not in auth.module.ts
MongooseModule.forFeature([{ name: "user", schema: UserSchema }])]
also, the name you provide here should be the same in #InjectModel('Users'), you have "user" here and 'Users' there.

Nest can't resolve dependencies of the UsersService

I am trying to set up Authorisation in NestJs using Mongoose via a User Service. I'm getting the following errors when trying to Unit Test:
I understand that the UserModel is injected into the UsersService and needs to be resolved when using the UsersModule but I can't work out how to do this when creating the TestingModule. Please can someone shed some light on this for me?
Code is below, thanks:
USER MODULE
## users.module.ts
import { Module } from '#nestjs/common';
import { UsersService } from './users.service';
#Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
## users.service.ts
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './interfaces/user.interface';
import { CreateUserDto } from './dto/create-user.dto';
#Injectable()
export class UsersService {
constructor(#InjectModel('User') private readonly userModel: Model<User>) {}
async store(userData: CreateUserDto): Promise<User> {
const newUser = new this.userModel(userData);
return newUser.save();
}
async find(data: Record<string, any> = {}) {
return this.userModel
.findOne(data)
.exec();
}
async findById(id: string): Promise<User> {
return this.userModel
.findById(id)
.exec();
}
async findByIdOrFail(id: string): Promise<User> {
return this.userModel
.findById(id)
.orFail()
.exec();
}
async update(id: string, data: Record<string, any> = {}): Promise<User> {
return this.userModel
.findByIdAndUpdate(id, data, {
runValidators: true,
useFindAndModify: false,
new: true,
})
.orFail()
.exec();
}
async destroy(id: string): Promise<User> {
return this.userModel
.findByIdAndRemove(id, {
useFindAndModify: false,
})
.orFail()
.exec();
}
}
AUTH MODULE
## auth.module.ts
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
#Module({
imports: [UsersModule],
providers: [AuthService]
})
export class AuthModule {}
## auth.service.ts
import { Injectable } from '#nestjs/common';
import { UsersService } from '../users/users.service';
#Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
}
AUTH SERVICE TEST
## auth.service.spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { MongooseModule } from '#nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { UserSchema } from '../users/schemas/user.schema';
const mongod = new MongoMemoryServer();
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const uri = await mongod.getUri();
const module: TestingModule = await Test.createTestingModule({
imports: [
MongooseModule.forRoot(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
}),
MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
UsersModule,
],
providers: [
AuthService
],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
In unit tests, you should mock all necessary dependencies for AuthService. It's not e2e test, you shouldn't initialize MongooseModule here.
Your beforeEach should look like that:
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: createSpyObj(UsersService) }
]
}).compile();
service = module.get<AuthService>(AuthService);
});
Combining Maciej's suggestion above and this blog: https://medium.com/#davguij/mocking-typescript-classes-with-jest-8ef992170d1 , I arrived at the following solution:
## /auth/auth.service.spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
jest.mock('../users/users.service');
describe.only('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
UsersModule,
],
providers: [
AuthService,
],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should validate user ok', async () => {
const res = await service.validateUser('username');
expect(res).toBeDefined();
});
});
I also had to create a mock class, under mocks as suggested in the above article:
## /users/__mocks__/users.service.ts
import { Injectable } from '#nestjs/common';
#Injectable()
export class UsersService {
async find(data: Record<string, any> = {}) {
return {
"firstname": "firstname",
"lastname": "lastname"
}
}
}
which mocks the below class (same as above, but reduced for the sake of this explanation):
## /users/users.service.ts
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Model } from 'mongoose';
import { User } from './interfaces/user.interface';
#Injectable()
export class UsersService {
constructor(#InjectModel('User') private readonly userModel: Model<User>) {}
async find(data: Record<string, any> = {}) {
return this.userModel
.findOne(data)
.exec();
}
}

guard in angular how can I get value from auth.service.ts (user login or not)

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
}
})
);
}

Argument of type 'Observable<LoginRequest>' is not assignable to parameter of type 'LoginRequest'

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;

Karma/Jasmine test cases for Angular2 service

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);
}));
});

Categories