Skip to content

Commit

Permalink
[Fortune Recording Oracle] Added enums parsing (#2629)
Browse files Browse the repository at this point in the history
* Added enum parsing

* Updated solution error

* Lint fix

* Lint fix

* Updated interceptor

* Lint fix

* Updated imports
  • Loading branch information
eugenvoronov authored Oct 16, 2024
1 parent df7d76d commit f9aa731
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 16 deletions.
4 changes: 4 additions & 0 deletions packages/apps/fortune/recording-oracle/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import { EnvConfigModule } from './common/config/config.module';
provide: APP_INTERCEPTOR,
useClass: SnakeCaseInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: TransformEnumInterceptor,
},
],
imports: [
ConfigModule.forRoot({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
import 'reflect-metadata';

export function IsEnumCaseInsensitive(
enumType: any,
validationOptions?: ValidationOptions,
) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string) {
// Attach enum metadata to the property
Reflect.defineMetadata('custom:enum', enumType, object, propertyName);

// Register the validation logic using class-validator
registerDecorator({
name: 'isEnumWithMetadata',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
// Retrieve enum type from metadata
const enumType = Reflect.getMetadata(
'custom:enum',
args.object,
args.property,
);
if (!enumType) {
return false; // If no enum metadata is found, validation fails
}

// Validate value is part of the enum
const enumValues = Object.values(enumType);
return enumValues.includes(value);
},
defaultMessage(args: ValidationArguments) {
// Default message if validation fails
const enumType = Reflect.getMetadata(
'custom:enum',
args.object,
args.property,
);
const enumValues = Object.values(enumType).join(', ');
return `${args.property} must be a valid enum value. Valid values: [${enumValues}]`;
},
},
});
};
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './public';
export * from './enums';
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export enum JobRequestType {
FORTUNE = 'FORTUNE',
FORTUNE = 'fortune',
}

export enum SolutionError {
Duplicated = 'Duplicated',
CurseWord = 'CurseWord',
Duplicated = 'duplicated',
CurseWord = 'curse_word',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { TransformEnumInterceptor } from './transform-enum.interceptor';
import {
ExecutionContext,
CallHandler,
BadRequestException,
} from '@nestjs/common';
import { of } from 'rxjs';
import { IsNumber, IsString, Min } from 'class-validator';
import { JobRequestType } from '../../common/enums/job';
import { ApiProperty } from '@nestjs/swagger';
import { IsEnumCaseInsensitive } from '../decorators';

export class MockDto {
@ApiProperty({
enum: JobRequestType,
})
@IsEnumCaseInsensitive(JobRequestType)
public jobType: JobRequestType;

@ApiProperty()
@IsNumber()
@Min(0.5)
public amount: number;

@ApiProperty()
@IsString()
public address: string;
}

describe('TransformEnumInterceptor', () => {
let interceptor: TransformEnumInterceptor;
let executionContext: ExecutionContext;
let callHandler: CallHandler;

beforeEach(() => {
interceptor = new TransformEnumInterceptor();

// Mocking ExecutionContext and CallHandler
executionContext = {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
body: {
jobType: 'FORTUNE',
amount: 5,
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
},
query: {
jobType: 'FORTUNE',
},
}),
}),
getHandler: jest.fn().mockReturnValue({
name: 'create', // Assume the handler is named 'create'
}),
getClass: jest.fn().mockReturnValue({
prototype: {},
}),
} as unknown as ExecutionContext;

callHandler = {
handle: jest.fn().mockReturnValue(of({})),
};

// Mock Reflect.getMetadata to return DTO and Enum types
Reflect.getMetadata = jest.fn((metadataKey, target, propertyKey) => {
// Mock design:paramtypes to return MockDto as the parameter type
if (metadataKey === 'design:paramtypes') {
return [MockDto];
}

if (metadataKey === 'custom:enum' && propertyKey === 'jobType') {
return JobRequestType;
}
return undefined; // For non-enum properties, return undefined
}) as any;
});

it('should transform enum values in query params to lowercase', async () => {
// Run the interceptor
await interceptor.intercept(executionContext, callHandler).toPromise();

// Access the modified request query
const request = executionContext.switchToHttp().getRequest();

// Expectations
expect(request.query.jobType).toBe('fortune');
expect(request.query).toEqual({
jobType: 'fortune',
});
expect(callHandler.handle).toBeCalled(); // Ensure the handler is called
});

it('should throw an error if the query value is not a valid enum', async () => {
// Modify the request query to have an invalid enum value for jobType
executionContext.switchToHttp = jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
query: {
jobType: 'invalidEnum', // Invalid enum value for jobType
},
}),
});

try {
// Run the interceptor
await interceptor.intercept(executionContext, callHandler).toPromise();
} catch (err: any) {
// Expect an error to be thrown
expect(err).toBeInstanceOf(BadRequestException);
expect(err.response.statusCode).toBe(400);
expect(err.response.message).toContain('Validation failed');
}
});

it('should transform enum values to lowercase', async () => {
// Run the interceptor
await interceptor.intercept(executionContext, callHandler).toPromise();

// Access the modified request body
const request = executionContext.switchToHttp().getRequest();

// Expectations
expect(request.body.jobType).toBe('fortune'); // Should be transformed to lowercase
expect(request.body).toEqual({
jobType: 'fortune',
amount: 5,
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
});
expect(callHandler.handle).toBeCalled(); // Ensure the handler is called
});

it('should throw an error if the value is not a valid enum', async () => {
// Modify the request body to have an invalid enum value for jobType
executionContext.switchToHttp = jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
body: {
status: 'pending',
jobType: 'invalidEnum', // Invalid enum value for jobType
amount: 5,
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
},
}),
});

try {
// Run the interceptor
await interceptor.intercept(executionContext, callHandler).toPromise();
} catch (err: any) {
// Expect an error to be thrown
expect(err).toBeInstanceOf(BadRequestException);
expect(err.response.statusCode).toBe(400);
expect(err.response.message).toContain('Validation failed');
}
});

it('should not transform non-enum properties', async () => {
// Run the interceptor with a non-enum property (amount and address)
await interceptor.intercept(executionContext, callHandler).toPromise();

// Access the modified request body
const request = executionContext.switchToHttp().getRequest();

// Expectations
expect(request.body.amount).toBe(5); // Non-enum property should remain unchanged
expect(request.body.address).toBe(
'0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
); // Non-enum string should remain unchanged
expect(callHandler.handle).toBeCalled();
});

it('should handle nested objects with enums', async () => {
// Modify the request body to have a nested object with enum value
executionContext.switchToHttp = jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
body: {
transaction: {
jobType: 'FORTUNE',
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
},
amount: 5,
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
},
}),
});

// Run the interceptor
await interceptor.intercept(executionContext, callHandler).toPromise();

// Access the modified request body
const request = executionContext.switchToHttp().getRequest();

// Expectations
expect(request.body.transaction.jobType).toBe('fortune');
expect(request.body).toEqual({
transaction: {
jobType: 'fortune',
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
},
amount: 5,
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
});
expect(callHandler.handle).toHaveBeenCalled();
});
});
Loading

0 comments on commit f9aa731

Please sign in to comment.