Skip to content

Commit

Permalink
added unit tests for identity service to increase coverage upto 85%
Browse files Browse the repository at this point in the history
  • Loading branch information
holashchand committed Jun 13, 2024
1 parent c00b5df commit 09e6c5c
Show file tree
Hide file tree
Showing 19 changed files with 564 additions and 52 deletions.
9 changes: 7 additions & 2 deletions services/identity-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,14 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
"**/*.(t|j)s",
"!**/*.module.ts",
"!**/main.ts"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
"testEnvironment": "node",
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
}
}
}
90 changes: 90 additions & 0 deletions services/identity-service/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import {
HealthCheckError,
HealthCheckService,
HealthIndicatorResult,
HttpHealthIndicator,
TerminusModule,
} from '@nestjs/terminus';
import { PrismaHealthIndicator } from './utils/prisma.health';
import { VaultHealthIndicator } from './utils/vault.health';
import { HealthCheckExecutor } from '@nestjs/terminus/dist/health-check/health-check-executor.service';
import {
ERROR_LOGGER,
getErrorLoggerProvider,
} from '@nestjs/terminus/dist/health-check/error-logger/error-logger.provider';
import { getLoggerProvider, TERMINUS_LOGGER } from '@nestjs/terminus/dist/health-check/logger/logger.provider';
import { HttpModule } from '@nestjs/axios';
import { ServiceUnavailableException } from '@nestjs/common';

describe('AppController', () => {
let appController: AppController;
let prismaHealthIndicator: PrismaHealthIndicator;
let vaultHealthIndicator: VaultHealthIndicator;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
HttpModule,
TerminusModule
],
controllers: [AppController],
providers: [
getLoggerProvider(),
getErrorLoggerProvider(),
HealthCheckExecutor,
HealthCheckError,
HealthCheckService,
HttpHealthIndicator,
{
provide: PrismaHealthIndicator,
useFactory: () => ({
isHealthy: jest.fn(),
}),
},
{
provide: VaultHealthIndicator,
useFactory: () => ({
isHealthy: jest.fn(),
}),
},
],
}).compile();

appController = module.get<AppController>(AppController);
prismaHealthIndicator = module.get<PrismaHealthIndicator>(PrismaHealthIndicator);
vaultHealthIndicator = module.get<VaultHealthIndicator>(VaultHealthIndicator);
});

beforeEach(async () => {
jest.restoreAllMocks();
})

describe('checkHealth', () => {
it('should return a health check result with all services healthy', async () => {
jest.spyOn(prismaHealthIndicator, 'isHealthy')
.mockResolvedValue(new Promise((resolve) => {
resolve({ db: { status: 'up' }} as HealthIndicatorResult);
}));
jest.spyOn(vaultHealthIndicator, 'isHealthy')
.mockResolvedValue(new Promise((resolve) => {
resolve({ vault: { status: 'up' }} as HealthIndicatorResult);
}));
const result = await appController.checkHealth();
expect(result.status).toEqual('ok');
expect(result.info.db.status).toEqual('up');
expect(result.info.vault.status).toEqual('up');
});

it('should return a health check result with one service unhealthy', async () => {
jest.spyOn(prismaHealthIndicator, 'isHealthy')
.mockRejectedValue(new HealthCheckError("Prisma health check failed", null));
jest.spyOn(vaultHealthIndicator, 'isHealthy')
.mockResolvedValue(new Promise((resolve) => {
resolve({ vault: { status: 'up' }} as HealthIndicatorResult);
}));
await expect(appController.checkHealth()).rejects.toThrow(ServiceUnavailableException);
});
});
});
81 changes: 81 additions & 0 deletions services/identity-service/src/auth/auth.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthGuard } from './auth.guard';
import { Reflector } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';

jest.mock('jwks-rsa', () => ({
__esModule: true,
default: jest.fn(),
}));

describe('AuthGuard', () => {
let guard: AuthGuard;
let reflector: Reflector;
let configService: ConfigService;
let originalEnv: NodeJS.ProcessEnv;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthGuard,
{
provide: Reflector,
useValue: {
get: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn(),
},
},
],
}).compile();

guard = module.get<AuthGuard>(AuthGuard);
reflector = module.get<Reflector>(Reflector);
configService = module.get<ConfigService>(ConfigService);
});

beforeEach(async () => {
originalEnv = { ...process.env };
process.env.ENABLE_AUTH = 'true';
jest.restoreAllMocks();
})

it('should be defined', () => {
expect(guard).toBeDefined();
});

describe('canActivate', () => {
it('should return true if isPublic is set to true', async () => {
jest.spyOn(reflector, 'get').mockReturnValue(true);
const result = await guard.canActivate({ getHandler: jest.fn() });
expect(result).toEqual(true);
});

it('should return true if ENABLE_AUTH is false', async () => {
process.env.ENABLE_AUTH = 'false';
jest.spyOn(reflector, 'get').mockReturnValue(false);
jest.spyOn(configService, 'get').mockReturnValue('false');
const result = await guard.canActivate({ getHandler: jest.fn() });
expect(result).toEqual(true);
});

it('should return false if no Bearer token found', async () => {
jest.spyOn(reflector, 'get').mockReturnValue(false);
jest.spyOn(configService, 'get').mockReturnValue('true');
const request = { headers: { authorization: 'InvalidToken' } };

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical test

The hard-coded value "InvalidToken" is used as
authorization header
.
const result = await guard.canActivate({ getHandler: jest.fn(), switchToHttp: () => ({ getRequest: () => request }) });
expect(result).toEqual(false);
});

// Add more test cases as needed to cover different scenarios
});

afterEach(() => {
// Restore the original process.env after the test
process.env = { ...originalEnv };
});
});
2 changes: 1 addition & 1 deletion services/identity-service/src/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class AuthGuard implements CanActivate {

if (process.env.ENABLE_AUTH === undefined) {
this.logger.warn('ENABLE_AUTH is not set, defaulting to true');
};
}
if (process.env.ENABLE_AUTH && process.env.ENABLE_AUTH.trim() === 'false') return true;

const request = context.switchToHttp().getRequest();
Expand Down
28 changes: 27 additions & 1 deletion services/identity-service/src/did/did.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { ConfigService } from '@nestjs/config';

describe('DidController', () => {
let controller: DidController;
const content = {
let content: any;
const defaultContent = {
"content": [
{
"alsoKnownAs": [
Expand Down Expand Up @@ -43,6 +44,11 @@ describe('DidController', () => {
controller = module.get<DidController>(DidController);
});

beforeEach(async () => {
content = JSON.parse(JSON.stringify(defaultContent));
jest.restoreAllMocks();
})

it('should be defined', () => {
expect(controller).toBeDefined();
});
Expand All @@ -55,6 +61,26 @@ describe('DidController', () => {
expect(dids[0].verificationMethod).toBeDefined();
});

it('should test throw DID generation Exception', async () => {
jest.spyOn((controller as any).didService, "generateDID")
.mockRejectedValue(new Error('generate failed'));
await expect(controller.generateDID(content)).rejects.toThrow(new InternalServerErrorException("generate failed"));
});

it('should test resolveDID', async () => {
jest.spyOn((controller as any).didService, "resolveDID")
.mockResolvedValue({ id: "1234"});
const result = await controller.resolveDID("1234");
expect(result.id).toEqual("1234");
});

it('should test resolveWebDID', async () => {
jest.spyOn((controller as any).didService, "resolveWebDID")
.mockResolvedValue({ id: "1234"});
const result = await controller.resolveWebDID("1234");
expect(result.id).toEqual("1234");
});

it('should test bulk DID generation', async () => {
const toGenerate = content;
for (let i = 0; i < 10; i++) {
Expand Down
66 changes: 58 additions & 8 deletions services/identity-service/src/did/did.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { Test, TestingModule } from '@nestjs/testing';
import { DidService } from './did.service';
import { PrismaService } from '../utils/prisma.service';
import { VaultService } from '../utils/vault.service';
import { GenerateDidDTO } from './dtos/GenerateDidRequest.dto';
import { GenerateDidDTO, VerificationKeyType } from './dtos/GenerateDidRequest.dto';
import { ConfigService } from '@nestjs/config';

describe('DidService', () => {
let service: DidService;
const doc: GenerateDidDTO =
let doc: GenerateDidDTO;
const defaultDoc: GenerateDidDTO =
{
"alsoKnownAs": [
"C4GT",
Expand All @@ -29,17 +30,19 @@ describe('DidService', () => {
"method": "C4GT"
}

beforeEach(async () => {
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DidService, PrismaService, VaultService, ConfigService],
}).compile();

// const app = module.createNestApplication();
// await app.init();

service = module.get<DidService>(DidService);
});

beforeEach(async () => {
doc = JSON.parse(JSON.stringify(defaultDoc));
jest.restoreAllMocks();
})

it('should be defined', () => {
expect(service).toBeDefined();
});
Expand All @@ -55,22 +58,62 @@ describe('DidService', () => {
});

it('should generate a DID with a default method', async () => {
const docWithoutMethod = doc;
delete docWithoutMethod.method;
delete doc.method;
const result = await service.generateDID(doc);
expect(result).toBeDefined();
expect(result.verificationMethod).toBeDefined();
expect(result.verificationMethod[0].publicKeyMultibase).toBeDefined();
expect(result.id.split(':')[1]).toEqual('rcw');
});

it('should generate a web DID with a given base url and verification key', async () => {
doc.method = "web";
doc.webDidBaseUrl = "https://registry.dev.example.com/identity";
doc.keyPairType = VerificationKeyType.Ed25519VerificationKey2018;
const result = await service.generateDID(doc);
expect(result).toBeDefined();
expect(result.id).toMatch(/did:web:registry.dev.example.com:*/i)
expect(result.verificationMethod).toBeDefined();
expect(result.verificationMethod[0].type).toStrictEqual(VerificationKeyType.Ed25519VerificationKey2018.toString());
});

it('should generate a web DID with RsaVerificationKey2018 verification key', async () => {
doc.method = "abc";
doc.keyPairType = VerificationKeyType.RsaVerificationKey2018;
const result = await service.generateDID(doc);
expect(result).toBeDefined();
expect(result.verificationMethod).toBeDefined();
expect(result.verificationMethod[0].type).toStrictEqual(VerificationKeyType.RsaVerificationKey2018.toString());
});

it('should generate a DID with a given ID', async () => {
doc.method = "web";
doc.id = "did:web:abc.com:given:1234"
const result = await service.generateDID(doc);
expect(result).toBeDefined();
expect(result.id).toMatch("did:web:abc.com:given:1234")
});

it('resolve a DID', async () => {
const result = await service.generateDID(doc);
const didToResolve = result.id;
const resolvedDid = await service.resolveDID(didToResolve);
expect(resolvedDid).toBeDefined();
expect(resolvedDid.id).toEqual(didToResolve);
expect(resolvedDid).toEqual(result);
await expect(service.resolveDID("did:abc:efg:hij")).rejects
.toThrow("DID: did:abc:efg:hij not found");
});

it('resolve a web DID for given id', async () => {
service.webDidPrefix = "did:web:abc.com:resolveweb:";
doc.method = "web";
const result = await service.generateDID(doc);
const didToResolve = result.id;
const resolvedDid = await service.resolveWebDID(didToResolve.split(service.webDidPrefix)[1]);
expect(resolvedDid).toBeDefined();
expect(resolvedDid.id).toEqual(didToResolve);
expect(resolvedDid).toEqual(result);
});

it("generate web did id test", () => {
Expand Down Expand Up @@ -104,4 +147,11 @@ describe('DidService', () => {
expect(() => service.getWebDidIdForId("abc"))
.toThrow("Web did base url not found");
});

// it("throw exception when signature algorithm is not found", () => {
// service.signingAlgorithm = "EdDsa";
// expect(() => service.generateDID(doc))
// .toThrow("Signature algorithm not found");
// });

});
6 changes: 4 additions & 2 deletions services/identity-service/src/did/did.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class DidService {
await this.init();
}
if(!this.keys[signingAlgorithm]) {
throw new NotFoundException("Signature suite not supported")
throw new NotFoundException("Signature algorithm not found")
}
return this.keys[signingAlgorithm];
}
Expand Down Expand Up @@ -191,7 +191,9 @@ export class DidService {

if(!artifact && id?.startsWith("did:web") && !id?.startsWith(this.webDidPrefix)) {
try {
return (await this.didResolver.resolve(id)).didDocument;
let doc = (await this.didResolver.resolve(id)).didDocument;
if(!doc) throw new Error("DID document is null");
return doc;
} catch (err) {
Logger.error(`Error fetching DID: ${id} from web, ${err}`);
throw new InternalServerErrorException(`Error fetching DID: ${id} from web`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
const { Service } = require('did-resolver');
type Service = typeof Service;

enum VerificationKeyType {
export enum VerificationKeyType {
Ed25519VerificationKey2020 = "Ed25519VerificationKey2020",
Ed25519VerificationKey2018 = "Ed25519VerificationKey2018",
RsaVerificationKey2018 = "RsaVerificationKey2018"
Expand Down

This file was deleted.

Loading

0 comments on commit 09e6c5c

Please sign in to comment.