1
votes

I'm trying to implement JWT into my project. I've followed the steps as outline in https://www.npmjs.com/package/@nestjs/jwt#usage

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { AuthRepository } from './auth.repository';

@Module({
    imports: [
        JwtModule.register({ secret: process.env.JWT_SECRET || 'ABCDE12345' }),
        TypeOrmModule.forFeature([AuthRepository]),
    ],
    exports: [TypeOrmModule, AuthService],
    providers: [AuthService],
    controllers: [AuthController],
})
export class AuthModule {}

auth.service.ts

import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthEntity } from './auth.entity';
import { LoginDTO } from './dto/login.dto';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
import crypto from 'crypto';
import { AuthRepository } from './auth.repository';

// export interface FindWhereData {
//     readonly email: string;
//     readonly password: string;
// }

export interface LoginResponse {
    readonly token: string;
    readonly refresh_token: string;
}

@Injectable()
export class AuthService {
    constructor(
        @InjectRepository(AuthRepository)
        private authRepository: AuthRepository,
        private jwtService: JwtService
    ) {}

    /**
     * Login user service
     *
     * @param doc
     */
    public async login(doc: LoginDTO): Promise<LoginResponse> {
        // verify user email
        const user = await this.authRepository.findOne({ email: doc.email });

        if (!user) {
            throw new NotFoundException('Could not find user');
        }
        // verify password
        const passwordsMatch = await this.passwordsAreEqual(doc.password, user.password);
        if (!passwordsMatch) {
            throw new UnauthorizedException('Incorrect login credentials');
        }

        // generate JWT
        const token = await this.jwtService.signAsync({ id: user.id });

        // create the refresh token
        const refreshToken = crypto.randomBytes(256).toString('hex');

        // store the refresh token

        return {
            token: token,
            refresh_token: refreshToken,
        };
    }

    /**
     * Generate a hashed password
     *
     * @param password
     */
    public async hashPassword(password: string): Promise<string> {
        const salt = await bcrypt.genSalt();

        return await bcrypt.hash(password, salt);
    }

    /**
     * Compare password against an encrypted string
     *
     * @param password
     * @param encryptedPassword
     */
    public async passwordsAreEqual(password: string, encryptedPassword: string): Promise<boolean> {
        return await bcrypt.compare(password, encryptedPassword);
    }

    /**
     * Find a record by column attribute and value
     *
     * @param queryObject
     *
     */
    public async findWhere(queryObject): Promise<AuthEntity> {
        const authEntity = await this.authRepository.findOne({ where: queryObject });

        if (!authEntity) {
            return null;
        }

        return authEntity;
    }

    public async findOne(id: string): Promise<AuthEntity> {
        return this.authRepository.findOne(id);
    }
}

auth.repository.ts

import { EntityRepository, Repository } from "typeorm";
import { AuthEntity } from "./auth.entity";

@EntityRepository(AuthEntity)
export class AuthRepository extends Repository<AuthEntity> {}

app.module.ts

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RouterModule } from 'nest-router';
import { routes } from './routes';
import { ConfigModule } from '@nestjs/config';
import configuration from './config/configuration';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Connection } from 'typeorm';
import { AuthModule } from './auth/auth.module';

@Module({
    imports: [
        RouterModule.forRoutes(routes),
        ConfigModule.forRoot({
            load: [configuration],
        }),
        TypeOrmModule.forRoot({
            type: 'postgres',
            host: process.env.POSTGRES_HOST || 'localhost',
            port: 5432,
            username: process.env.POSTGRES_USERNAME || 'postgres',
            password: process.env.POSTGRES_PASSWORD || 'password',
            database: process.env.POSTGRES_DATABASE || 'service-auth',
            autoLoadEntities: true,
            synchronize: true,
        }),
        AuthModule,
    ],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {
    constructor(private readonly connection: Connection) {
        console.log('connection status: ' + connection.isConnected);
    }
}

auth.service.spec.ts

import { JwtModule, JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { AuthEntity } from './auth.entity';
import { AuthRepository } from './auth.repository';
import { AuthService } from './auth.service';

describe('AuthService', () => {
    let authService: AuthService;
    let authRepository: AuthRepository;
    const mockAuthRepository = () => ({
        login: jest.fn(),
    });

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [
                AuthService,
                // JwtModule,
                {
                    provide: getRepositoryToken(AuthRepository),
                    useFactory: mockAuthRepository,
                },
                {
                    provide: JwtService,
                    useValue: {
                        signAsync: jest.fn(() => 'token'),
                    }
                }
            ]
        }).compile();

        authService = await module.get<AuthService>(AuthService);
        authRepository = await module.get<AuthRepository>(AuthRepository);
    });

    /**
     * Test that the service is defined
     */
    it('should be defined', () => {
        expect(authService).toBeDefined();
    });
});

When I run npm run test I get the following error message:

FAIL  src/auth/auth.service.spec.ts
  ● AuthService › should be defined

    Nest can't resolve dependencies of the AuthService (AuthRepository, ?). Please make sure that the argument JwtService at index [1] is available in the RootTestModule context.

    Potential solutions:
    - If JwtService is a provider, is it part of the current RootTestModule?
    - If JwtService is exported from a separate @Module, is that module imported within RootTestModule?
      @Module({
        imports: [ /* the Module containing JwtService */ ]
      })

I know the error is probably pretty clear to seasoned Node/Nest developer but I cannot figure out what the RootTestModule is and how to get JwtModule imported.

I believe I have followed the setup correctly but adding this JwtModule to the AuthService is causing the service to be undefined in my unit tests.

Repo https://github.com/marcuschristiansen/nestjs-auth

1

1 Answers

0
votes

You should be adding a custom provider for the JwtService so that you can mock it. A custom provider could look like

{
  provide: JwtService,
  useValue: {
    signAsync: jest.fn(() => 'token'),
  }
}

To tell Nest to inject an object that has a signAsync() method that when called returns the string 'token' so that it will always be the same in your tests.

This object goes in the providers array, just like the AuthRepository mock