Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TokenRefresh feature in reputation oracle integration #141

Merged
merged 6 commits into from
Jun 21, 2024
4 changes: 4 additions & 0 deletions packages/apps/human-app/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { ChainId } from '@human-protocol/sdk';
import { RegisterAddressController } from './modules/register-address/register-address.controller';
import { RegisterAddressModule } from './modules/register-address/register-address.module';
import { InterceptorModule } from './common/interceptors/interceptor.module';
import { TokenRefreshModule } from './modules/token-refresh/token-refresh.module';
import { TokenRefreshController } from './modules/token-refresh/token-refresh.controller';

@Module({
imports: [
Expand Down Expand Up @@ -93,6 +95,7 @@ import { InterceptorModule } from './common/interceptors/interceptor.module';
EscrowUtilsModule,
RegisterAddressModule,
InterceptorModule,
TokenRefreshModule,
],
controllers: [
AppController,
Expand All @@ -104,6 +107,7 @@ import { InterceptorModule } from './common/interceptors/interceptor.module';
StatisticsController,
HCaptchaController,
RegisterAddressController,
TokenRefreshController,
],
exports: [HttpModule],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export class GatewayConfigService {
method: HttpMethod.POST,
headers: this.JSON_HEADER,
},
[ReputationOracleEndpoints.TOKEN_REFRESH]: {
endpoint: '/auth/refresh',
method: HttpMethod.POST,
headers: this.JSON_HEADER,
},
} as Record<ReputationOracleEndpoints, GatewayEndpointConfig>,
},
[ExternalApiName.HCAPTCHA_LABELING_STATS]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export const gatewayConfigServiceMock = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
},
TOKEN_REFRESH: {
endpoint: '/auth/refresh',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
},
},
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum ReputationOracleEndpoints {
KYC_PROCEDURE_START = 'KYC_PROCEDURE_START',
ENABLE_LABELING = 'ENABLE_LABELING',
REGISTER_ADDRESS = 'REGISTER_ADDRESS',
TOKEN_REFRESH = 'TOKEN_REFRESH',
}
export enum HCaptchaLabelingStatsEndpoints {
USER_STATS = 'USER_STATS',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ import {
RegisterAddressData,
RegisterAddressResponse,
} from '../../modules/register-address/model/register-address.model';
import {
TokenRefreshCommand,
TokenRefreshData,
TokenRefreshResponse,
} from '../../modules/token-refresh/model/token-refresh.model';

@Injectable()
export class ReputationOracleGateway {
Expand Down Expand Up @@ -285,4 +290,17 @@ export class ReputationOracleGateway {
options,
);
}

async sendRefreshToken(command: TokenRefreshCommand) {
const data = this.mapper.map(
command,
TokenRefreshCommand,
TokenRefreshData,
);
const options = this.getEndpointOptions(
ReputationOracleEndpoints.TOKEN_REFRESH,
data,
);
return this.handleRequestToReputationOracle<TokenRefreshResponse>(options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PrepareSignatureData } from '../../modules/prepare-signature/model/prep
import { DisableOperatorData } from '../../modules/disable-operator/model/disable-operator.model';
import { RegisterAddressData } from '../../modules/register-address/model/register-address.model';
import { RestorePasswordData } from '../../modules/password-reset/model/restore-password.model';
import { TokenRefreshData } from '../../modules/token-refresh/model/token-refresh.model';
class Empty {}

export type RequestDataType =
Expand All @@ -23,4 +24,5 @@ export type RequestDataType =
| SigninOperatorData
| RegisterAddressData
| RestorePasswordData
| TokenRefreshData
| Empty;
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ import {
SigninWorkerCommand,
SigninWorkerData,
} from '../../modules/user-worker/model/worker-signin.model';
import {
EnableLabelingCommand,
} from '../../modules/h-captcha/model/enable-labeling.model';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused import

import { EnableLabelingCommand } from '../../modules/h-captcha/model/enable-labeling.model';
import {
PrepareSignatureCommand,
PrepareSignatureData,
Expand Down Expand Up @@ -54,6 +52,10 @@ import {
RegisterAddressCommand,
RegisterAddressData,
} from '../../modules/register-address/model/register-address.model';
import {
TokenRefreshCommand,
TokenRefreshData,
} from '../../modules/token-refresh/model/token-refresh.model';

@Injectable()
export class ReputationOracleProfile extends AutomapperProfile {
Expand Down Expand Up @@ -110,6 +112,15 @@ export class ReputationOracleProfile extends AutomapperProfile {
destination: new SnakeCaseNamingConvention(),
}),
);
createMap(
mapper,
TokenRefreshCommand,
TokenRefreshData,
namingConventions({
source: new CamelCaseNamingConvention(),
destination: new SnakeCaseNamingConvention(),
}),
);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const reputationOracleGatewayMock = {
sendKycProcedureStart: jest.fn(),
sendOperatorSignin: jest.fn(),
sendBlockchainAddressRegistration: jest.fn(),
sendRefreshToken: jest.fn(),
};
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ import {
registerAddressDataFixture,
registerAddressResponseFixture,
} from '../../../modules/register-address/spec/register-address.fixtures';
import {
TokenRefreshCommand,
TokenRefreshData,
} from '../../../modules/token-refresh/model/token-refresh.model';

const httpServiceMock = {
request: jest.fn(),
Expand Down Expand Up @@ -741,6 +745,64 @@ describe('ReputationOracleGateway', () => {
});
});

describe('sendRefeshToken', () => {
it('should successfully call the reputation oracle endpoint', async () => {
const command: TokenRefreshCommand = {
refreshToken: 'token',
};
const data: TokenRefreshData = {
refresh_token: command.refreshToken,
};
nock('https://example.com')
.post('/auth/refresh', {
...data,
})
.reply(201, '');

httpServiceMock.request.mockReturnValue(of({}));

await expect(service.sendRefreshToken(command)).resolves.not.toThrow();
expect(httpService.request).toHaveBeenCalled();
});

it('should handle http error response correctly', async () => {
jest
.spyOn(httpService, 'request')
.mockReturnValue(
throwError(
() =>
new HttpException(
{ message: 'Bad request' },
HttpStatus.BAD_REQUEST,
),
),
);

const command: TokenRefreshCommand = {
refreshToken: 'token',
};
await expect(service.sendRefreshToken(command)).rejects.toThrow(
new HttpException({ message: 'Bad request' }, HttpStatus.BAD_REQUEST),
);
});

it('should handle network or unknown errors correctly', async () => {
jest
.spyOn(httpService, 'request')
.mockReturnValue(throwError(() => new Error('Internal Server Error')));

const command: TokenRefreshCommand = {
refreshToken: 'token',
};
await expect(service.sendRefreshToken(command)).rejects.toThrow(
new HttpException(
'Internal Server Error',
HttpStatus.INTERNAL_SERVER_ERROR,
),
);
});
});

afterEach(() => {
nock.cleanAll();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { AutoMap } from '@automapper/classes';

export class TokenRefreshDto {
@AutoMap()
@ApiProperty({ example: 'string' })
@IsString()
refresh_token: string;
}

export class TokenRefreshCommand {
@AutoMap()
refreshToken: string;
}

export class TokenRefreshData {
@AutoMap()
refresh_token: string;
}

export class TokenRefreshResponse {
refresh_token: string;
access_token: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TokenRefreshController } from '../token-refresh.controller';
import { TokenRefreshService } from '../token-refresh.service';
import { Test, TestingModule } from '@nestjs/testing';
import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes';
import { TokenRefreshProfile } from '../token-refresh.mapper.profile';
import { tokenRefreshServiceMock } from './token-refresh.service.mock';
import { TokenRefreshDto } from '../model/token-refresh.model';

describe('RefreshTokenController', () => {
let controller: TokenRefreshController;
let service: TokenRefreshService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TokenRefreshController],
imports: [
AutomapperModule.forRoot({
strategyInitializer: classes(),
}),
],
providers: [TokenRefreshService, TokenRefreshProfile],
})
.overrideProvider(TokenRefreshService)
.useValue(tokenRefreshServiceMock)
.compile();

controller = module.get<TokenRefreshController>(TokenRefreshController);
service = module.get<TokenRefreshService>(TokenRefreshService);
});

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

describe('refresh token', () => {
it('should service a refresh token method with proper fields set', async () => {
const dto: TokenRefreshDto = {
refresh_token: 'token',
};
await controller.refreshToken(dto);
const expectedCommand = {
refreshToken: dto.refresh_token,
};
expect(service.refreshToken).toHaveBeenCalledWith(expectedCommand);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const tokenRefreshServiceMock = {
refreshToken: jest.fn(),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TokenRefreshService } from '../token-refresh.service';
import { ReputationOracleGateway } from '../../../integrations/reputation-oracle/reputation-oracle.gateway';
import { reputationOracleGatewayMock } from '../../../integrations/reputation-oracle/spec/reputation-oracle.gateway.mock';

describe('RefreshTokenService', () => {
let service: TokenRefreshService;
let reputationOracleGateway: ReputationOracleGateway;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TokenRefreshService, ReputationOracleGateway],
})
.overrideProvider(ReputationOracleGateway)
.useValue(reputationOracleGatewayMock)
.compile();

service = module.get<TokenRefreshService>(TokenRefreshService);
reputationOracleGateway = module.get<ReputationOracleGateway>(
ReputationOracleGateway,
);
});

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

describe('refresh token', () => {
it('should call reputation oracle gateway without doing anything else', async () => {
const command = {
refreshToken: 'token',
};
await service.refreshToken(command);
expect(reputationOracleGateway.sendRefreshToken).toHaveBeenCalledWith(
command,
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Body,
Controller,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { InjectMapper } from '@automapper/nestjs';
import { Mapper } from '@automapper/core';
import { TokenRefreshService } from './token-refresh.service';
import {
TokenRefreshCommand,
TokenRefreshDto,
} from './model/token-refresh.model';
import { TokenRefreshResponse } from './model/token-refresh.model';

@Controller()
export class TokenRefreshController {
constructor(
private readonly service: TokenRefreshService,
@InjectMapper() private readonly mapper: Mapper,
) {}

@ApiTags('Refresh-Token')
@Post('/auth/refresh')
@ApiOperation({ summary: 'Refresh token' })
@UsePipes(new ValidationPipe())
public refreshToken(
@Body() dto: TokenRefreshDto,
): Promise<TokenRefreshResponse> {
const command = this.mapper.map(dto, TokenRefreshDto, TokenRefreshCommand);
return this.service.refreshToken(command);
}
}
Loading
Loading