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

[Reputation Oracle] refactor: web3service and dependent configs #3115

Merged
merged 12 commits into from
Feb 27, 2025
38 changes: 21 additions & 17 deletions packages/apps/reputation-oracle/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { QualificationModule } from './modules/qualification/qualification.modul
import { EscrowCompletionModule } from './modules/escrow-completion/escrow-completion.module';
import { WebhookIncomingModule } from './modules/webhook/webhook-incoming.module';
import { WebhookOutgoingModule } from './modules/webhook/webhook-outgoing.module';
import { UserModule } from './modules/user/user.module';
import { EmailModule } from './modules/email/module';
import { StorageModule } from './modules/storage/storage.module';
import Environment from './utils/environment';

@Module({
Expand All @@ -42,35 +44,37 @@ import Environment from './utils/environment';
],
imports: [
ScheduleModule.forRoot(),
ServeStaticModule.forRoot({
rootPath: join(
__dirname,
'../../../../../../',
'node_modules/swagger-ui-dist',
),
}),
ConfigModule.forRoot({
/**
* First value found takes precendece
*/
envFilePath: [`.env.${Environment.name}`, '.env'],
validationSchema: envValidator,
}),
EnvConfigModule,
DatabaseModule,
HealthModule,
ReputationModule,
WebhookIncomingModule,
WebhookOutgoingModule,
Web3Module,
HCaptchaModule,
AuthModule,
KycModule,
ServeStaticModule.forRoot({
rootPath: join(
__dirname,
'../../../../../../',
'node_modules/swagger-ui-dist',
),
}),
CronJobModule,
EmailModule,
EscrowCompletionModule,
HealthModule,
KycModule,
PayoutModule,
EnvConfigModule,
HCaptchaModule,
QualificationModule,
EscrowCompletionModule,
EmailModule,
ReputationModule,
StorageModule,
UserModule,
Web3Module,
WebhookIncomingModule,
WebhookOutgoingModule,
],
})
export class AppModule {}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
export enum Web3Env {
TESTNET = 'testnet',
MAINNET = 'mainnet',
LOCALHOST = 'localhost',
}

export enum SignatureType {
SIGNUP = 'signup',
SIGNIN = 'signin',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,104 +1,204 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
import { SignatureAuthGuard } from './signature.auth';
import { signMessage } from '../../utils/web3';
import { ChainId, EscrowUtils } from '@human-protocol/sdk';
import { MOCK_ADDRESS, MOCK_PRIVATE_KEY } from '../../../test/constants';
import { AuthSignatureRole } from '../enums/role';

jest.mock('@human-protocol/sdk', () => ({
...jest.requireActual('@human-protocol/sdk'),
EscrowUtils: {
getEscrow: jest.fn(),
},
}));

describe('SignatureAuthGuard', () => {
let guard: SignatureAuthGuard;
import { EscrowUtils } from '@human-protocol/sdk';
import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: SignatureAuthGuard,
useValue: new SignatureAuthGuard([
AuthSignatureRole.JobLauncher,
AuthSignatureRole.Exchange,
AuthSignatureRole.Recording,
]),
},
],
}).compile();

guard = module.get<SignatureAuthGuard>(SignatureAuthGuard);
EscrowUtils.getEscrow = jest.fn().mockResolvedValueOnce({
launcher: MOCK_ADDRESS,
exchangeOracle: MOCK_ADDRESS,
reputationOracle: MOCK_ADDRESS,
});
});
import {
generateContractAddress,
generateEthWallet,
generateTestnetChainId,
} from '../../../test/fixtures/web3';
import {
createExecutionContextMock,
ExecutionContextMock,
} from '../../../test/mock-creators/nest';
import { signMessage } from '../../utils/web3';

import { AuthSignatureRole, SignatureAuthGuard } from './signature.auth';

it('should be defined', () => {
expect(guard).toBeDefined();
describe('SignatureAuthGuard', () => {
it('should throw if empty roles provided in constructor', async () => {
let thrownError;
try {
new SignatureAuthGuard([]);
} catch (error) {
thrownError = error;
}
expect(thrownError).toBeInstanceOf(Error);
expect(thrownError.message).toBe(
'At least one auth signature role should be provided',
);
});

describe('canActivate', () => {
let context: ExecutionContext;
let mockRequest: any;
let executionContextMock: ExecutionContextMock;
let body: {
chain_id: number;
escrow_address: string;
};

beforeEach(() => {
mockRequest = {
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn().mockReturnThis(),
headers: {},
body: {},
originalUrl: '',
executionContextMock = createExecutionContextMock();
body = {
chain_id: generateTestnetChainId(),
escrow_address: generateContractAddress(),
};
context = {
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn(() => mockRequest),
} as any as ExecutionContext;
});

it('should return true if signature is verified', async () => {
const body = {
escrow_address: MOCK_ADDRESS,
chain_id: ChainId.LOCALHOST,
};
const signature = await signMessage(body, MOCK_PRIVATE_KEY);
mockRequest.headers['human-signature'] = signature;
mockRequest.body = body;
const result = await guard.canActivate(context as any);
expect(result).toBeTruthy();
expect(EscrowUtils.getEscrow).toHaveBeenCalledWith(
ChainId.LOCALHOST,
MOCK_ADDRESS,
it.each([
{
name: 'launcher',
role: AuthSignatureRole.JOB_LAUNCHER,
},
{
name: 'exchangeOracle',
role: AuthSignatureRole.EXCHANGE_ORACLE,
},
{
name: 'recordingOracle',
role: AuthSignatureRole.RECORDING_ORACLE,
},
])(
'should return true if signature is verified for "$role" role',
async ({ name, role }) => {
const guard = new SignatureAuthGuard([role]);

const { privateKey, address } = generateEthWallet();
EscrowUtils.getEscrow = jest.fn().mockResolvedValueOnce({
[name]: address,
});

const signature = await signMessage(body, privateKey);

const request = {
headers: {
'human-signature': signature,
},
body,
};
executionContextMock.__getRequest.mockReturnValueOnce(request);

const result = await guard.canActivate(
executionContextMock as unknown as ExecutionContext,
);

expect(result).toBeTruthy();
expect(EscrowUtils.getEscrow).toHaveBeenCalledWith(
body.chain_id,
body.escrow_address,
);
},
);

it('should throw unauthorized exception if signature is not verified', async () => {
const guard = new SignatureAuthGuard([AuthSignatureRole.JOB_LAUNCHER]);

const { privateKey, address } = generateEthWallet();
EscrowUtils.getEscrow = jest.fn().mockResolvedValueOnce({
launcher: address,
});
const signature = await signMessage(
{
'same-signer': 'different-message',
},
privateKey,
);

const request = {
headers: {
'human-signature': signature,
},
body,
};
executionContextMock.__getRequest.mockReturnValueOnce(request);

let thrownError;
try {
await guard.canActivate(
executionContextMock as unknown as ExecutionContext,
);
} catch (error) {
thrownError = error;
}
expect(thrownError).toBeInstanceOf(HttpException);
expect(thrownError.message).toBe('Invalid web3 signature');
expect(thrownError.status).toBe(HttpStatus.UNAUTHORIZED);
});

it('should throw unauthorized exception if signature is not verified', async () => {
let catchedError;
it('should throw unauthorized exception if not initialized for signer role', async () => {
const guard = new SignatureAuthGuard([
AuthSignatureRole.RECORDING_ORACLE,
]);

const { privateKey, address } = generateEthWallet();
EscrowUtils.getEscrow = jest.fn().mockResolvedValueOnce({
launcher: address,
exchangeOracle: address,
});

const signature = await signMessage(body, privateKey);

const request = {
headers: {
'human-signature': signature,
},
body,
};
executionContextMock.__getRequest.mockReturnValueOnce(request);

let thrownError;
try {
await guard.canActivate(context as any);
await guard.canActivate(
executionContextMock as unknown as ExecutionContext,
);
} catch (error) {
catchedError = error;
thrownError = error;
}
expect(catchedError).toBeInstanceOf(HttpException);
expect(catchedError).toHaveProperty('message', 'Invalid web3 signature');
expect(catchedError).toHaveProperty('status', HttpStatus.UNAUTHORIZED);
expect(thrownError).toBeInstanceOf(HttpException);
expect(thrownError.message).toBe('Invalid web3 signature');
expect(thrownError.status).toBe(HttpStatus.UNAUTHORIZED);
});

it('should throw unauthorized exception for unrecognized oracle type', async () => {
mockRequest.originalUrl = '/some/random/path';
let catchedError;
it('should throw unauthorized exception if escrow lacks oracle addresses', async () => {
const guard = new SignatureAuthGuard([
AuthSignatureRole.JOB_LAUNCHER,
AuthSignatureRole.EXCHANGE_ORACLE,
AuthSignatureRole.RECORDING_ORACLE,
]);

const { privateKey } = generateEthWallet();
EscrowUtils.getEscrow = jest.fn().mockResolvedValueOnce({
launcher: '',
exchangeOracle: '',
recordingOracle: '',
});

const signature = await signMessage(body, privateKey);

const request = {
headers: {
'human-signature': signature,
},
body,
};
executionContextMock.__getRequest.mockReturnValueOnce(request);

let thrownError;
try {
await guard.canActivate(context as any);
await guard.canActivate(
executionContextMock as unknown as ExecutionContext,
);
} catch (error) {
catchedError = error;
thrownError = error;
}
expect(catchedError).toBeInstanceOf(HttpException);
expect(catchedError).toHaveProperty('message', 'Invalid web3 signature');
expect(catchedError).toHaveProperty('status', HttpStatus.UNAUTHORIZED);
expect(thrownError).toBeInstanceOf(HttpException);
expect(thrownError.message).toBe('Invalid web3 signature');
expect(thrownError.status).toBe(HttpStatus.UNAUTHORIZED);
});
});
});
Loading