Skip to content

Commit ece8ef8

Browse files
authored
[Fortune Exchange Oracle] Added enums parsing (#2626)
* Implemented enum parsing for exchange oracle * Updated lint * Updated decorator and interceptor * Resolved conflicts * Add guards * Update decorator export * Deleted console.log
1 parent c515a06 commit ece8ef8

24 files changed

+614
-79
lines changed

packages/apps/fortune/exchange-oracle/server/src/app.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@ import { CronJobModule } from './modules/cron-job/cron-job.module';
1515
import { HealthModule } from './modules/health/health.module';
1616
import { EnvConfigModule } from './common/config/config.module';
1717
import { ScheduleModule } from '@nestjs/schedule';
18+
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
1819

1920
@Module({
2021
providers: [
2122
{
2223
provide: APP_INTERCEPTOR,
2324
useClass: SnakeCaseInterceptor,
2425
},
26+
{
27+
provide: APP_INTERCEPTOR,
28+
useClass: TransformEnumInterceptor,
29+
},
2530
JwtHttpStrategy,
2631
],
2732
imports: [
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
registerDecorator,
3+
ValidationOptions,
4+
ValidationArguments,
5+
} from 'class-validator';
6+
import 'reflect-metadata';
7+
8+
export function IsEnumCaseInsensitive(
9+
enumType: any,
10+
validationOptions?: ValidationOptions,
11+
) {
12+
// eslint-disable-next-line @typescript-eslint/ban-types
13+
return function (object: Object, propertyName: string) {
14+
// Attach enum metadata to the property
15+
Reflect.defineMetadata('custom:enum', enumType, object, propertyName);
16+
17+
// Register the validation logic using class-validator
18+
registerDecorator({
19+
name: 'isEnumWithMetadata',
20+
target: object.constructor,
21+
propertyName: propertyName,
22+
options: validationOptions,
23+
validator: {
24+
validate(value: any, args: ValidationArguments) {
25+
// Retrieve enum type from metadata
26+
const enumType = Reflect.getMetadata(
27+
'custom:enum',
28+
args.object,
29+
args.property,
30+
);
31+
if (!enumType) {
32+
return false; // If no enum metadata is found, validation fails
33+
}
34+
35+
// Validate value is part of the enum
36+
const enumValues = Object.values(enumType);
37+
return enumValues.includes(value);
38+
},
39+
defaultMessage(args: ValidationArguments) {
40+
// Default message if validation fails
41+
const enumType = Reflect.getMetadata(
42+
'custom:enum',
43+
args.object,
44+
args.property,
45+
);
46+
const enumValues = Object.values(enumType).join(', ');
47+
return `${args.property} must be a valid enum value. Valid values: [${enumValues}]`;
48+
},
49+
},
50+
});
51+
};
52+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './public';
2+
export * from './enums';

packages/apps/fortune/exchange-oracle/server/src/common/enums/job.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export enum JobStatus {
2-
ACTIVE = 'ACTIVE',
3-
COMPLETED = 'COMPLETED',
4-
CANCELED = 'CANCELED',
2+
ACTIVE = 'active',
3+
COMPLETED = 'completed',
4+
CANCELED = 'canceled',
55
}
66

77
export enum JobSortField {
@@ -22,12 +22,12 @@ export enum JobFieldName {
2222
}
2323

2424
export enum AssignmentStatus {
25-
ACTIVE = 'ACTIVE',
26-
VALIDATION = 'VALIDATION',
27-
COMPLETED = 'COMPLETED',
28-
EXPIRED = 'EXPIRED',
29-
CANCELED = 'CANCELED',
30-
REJECTED = 'REJECTED',
25+
ACTIVE = 'active',
26+
VALIDATION = 'validation',
27+
COMPLETED = 'completed',
28+
EXPIRED = 'expired',
29+
CANCELED = 'canceled',
30+
REJECTED = 'rejected',
3131
}
3232

3333
export enum AssignmentSortField {
@@ -40,5 +40,5 @@ export enum AssignmentSortField {
4040
}
4141

4242
export enum JobType {
43-
FORTUNE = 'FORTUNE',
43+
FORTUNE = 'fortune',
4444
}

packages/apps/fortune/exchange-oracle/server/src/common/enums/role.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ export enum AuthSignatureRole {
66
}
77

88
export enum Role {
9-
Worker = 'WORKER',
10-
HumanApp = 'HUMAN_APP',
9+
Worker = 'worker',
10+
HumanApp = 'human_app',
1111
}

packages/apps/fortune/exchange-oracle/server/src/common/enums/webhook.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export enum EventType {
88
}
99

1010
export enum WebhookStatus {
11-
PENDING = 'PENDING',
12-
COMPLETED = 'COMPLETED',
13-
FAILED = 'FAILED',
11+
PENDING = 'pending',
12+
COMPLETED = 'completed',
13+
FAILED = 'failed',
1414
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { TransformEnumInterceptor } from './transform-enum.interceptor';
2+
import {
3+
ExecutionContext,
4+
CallHandler,
5+
BadRequestException,
6+
} from '@nestjs/common';
7+
import { of } from 'rxjs';
8+
import { IsNumber, IsString, Min } from 'class-validator';
9+
import { JobType } from '../../common/enums/job';
10+
import { ApiProperty } from '@nestjs/swagger';
11+
import { IsEnumCaseInsensitive } from '../decorators/enums';
12+
13+
export class MockDto {
14+
@ApiProperty({
15+
enum: JobType,
16+
})
17+
@IsEnumCaseInsensitive(JobType)
18+
public jobType: JobType;
19+
20+
@ApiProperty()
21+
@IsNumber()
22+
@Min(0.5)
23+
public amount: number;
24+
25+
@ApiProperty()
26+
@IsString()
27+
public address: string;
28+
}
29+
30+
describe('TransformEnumInterceptor', () => {
31+
let interceptor: TransformEnumInterceptor;
32+
let executionContext: ExecutionContext;
33+
let callHandler: CallHandler;
34+
35+
beforeEach(() => {
36+
interceptor = new TransformEnumInterceptor();
37+
38+
// Mocking ExecutionContext and CallHandler
39+
executionContext = {
40+
switchToHttp: jest.fn().mockReturnValue({
41+
getRequest: jest.fn().mockReturnValue({
42+
body: {
43+
jobType: 'FORTUNE',
44+
amount: 5,
45+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
46+
},
47+
}),
48+
}),
49+
getHandler: jest.fn().mockReturnValue({
50+
name: 'create', // Assume the handler is named 'create'
51+
}),
52+
getClass: jest.fn().mockReturnValue({
53+
prototype: {},
54+
}),
55+
} as unknown as ExecutionContext;
56+
57+
callHandler = {
58+
handle: jest.fn().mockReturnValue(of({})),
59+
};
60+
61+
// Mock Reflect.getMetadata to return DTO and Enum types
62+
Reflect.getMetadata = jest.fn((metadataKey, target, propertyKey) => {
63+
// Mock design:paramtypes to return MockDto as the parameter type
64+
if (metadataKey === 'design:paramtypes') {
65+
return [MockDto];
66+
}
67+
68+
// Mock custom:enum to return the corresponding enum for each property
69+
if (metadataKey === 'custom:enum' && propertyKey === 'jobType') {
70+
return JobType;
71+
}
72+
return undefined; // For non-enum properties, return undefined
73+
}) as any;
74+
});
75+
76+
it('should transform enum values to lowercase', async () => {
77+
// Run the interceptor
78+
await interceptor.intercept(executionContext, callHandler).toPromise();
79+
80+
// Access the modified request body
81+
const request = executionContext.switchToHttp().getRequest();
82+
83+
// Expectations
84+
expect(request.body.jobType).toBe('fortune'); // Should be transformed to lowercase
85+
expect(request.body).toEqual({
86+
jobType: 'fortune',
87+
amount: 5,
88+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
89+
});
90+
expect(callHandler.handle).toBeCalled(); // Ensure the handler is called
91+
});
92+
93+
it('should throw an error if the value is not a valid enum', async () => {
94+
// Modify the request body to have an invalid enum value for jobType
95+
executionContext.switchToHttp = jest.fn().mockReturnValue({
96+
getRequest: jest.fn().mockReturnValue({
97+
body: {
98+
jobType: 'invalidEnum', // Invalid enum value for jobType
99+
amount: 5,
100+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
101+
},
102+
}),
103+
});
104+
105+
try {
106+
// Run the interceptor
107+
await interceptor.intercept(executionContext, callHandler).toPromise();
108+
} catch (err) {
109+
// Expect an error to be thrown
110+
expect(err).toBeInstanceOf(BadRequestException);
111+
expect(err.response.statusCode).toBe(400);
112+
expect(err.response.message).toContain('Validation failed');
113+
}
114+
});
115+
116+
it('should not transform non-enum properties', async () => {
117+
// Run the interceptor with a non-enum property (amount and address)
118+
await interceptor.intercept(executionContext, callHandler).toPromise();
119+
120+
// Access the modified request body
121+
const request = executionContext.switchToHttp().getRequest();
122+
123+
// Expectations
124+
expect(request.body.amount).toBe(5); // Non-enum property should remain unchanged
125+
expect(request.body.address).toBe(
126+
'0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
127+
); // Non-enum string should remain unchanged
128+
expect(callHandler.handle).toBeCalled();
129+
});
130+
131+
it('should handle nested objects with enums', async () => {
132+
// Modify the request body to have a nested object with enum value
133+
executionContext.switchToHttp = jest.fn().mockReturnValue({
134+
getRequest: jest.fn().mockReturnValue({
135+
body: {
136+
transaction: {
137+
jobType: 'FORTUNE',
138+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
139+
},
140+
amount: 5,
141+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
142+
},
143+
}),
144+
});
145+
146+
// Run the interceptor
147+
await interceptor.intercept(executionContext, callHandler).toPromise();
148+
149+
// Access the modified request body
150+
const request = executionContext.switchToHttp().getRequest();
151+
152+
// Expectations
153+
expect(request.body.transaction.jobType).toBe('fortune');
154+
expect(request.body).toEqual({
155+
transaction: {
156+
jobType: 'fortune',
157+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
158+
},
159+
amount: 5,
160+
address: '0xCf88b3f1992458C2f5a229573c768D0E9F70C44e',
161+
});
162+
expect(callHandler.handle).toHaveBeenCalled();
163+
});
164+
});

0 commit comments

Comments
 (0)