-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This migrates the validation of `Message` to `zod`: - Remove `MessageValidator` and associated schemas. - Add test-covered `MessageSchema`/`MessageConfirmationSchema` and infer types from them. - Propagate type requirements. - Update tests accordingly.
- Loading branch information
Showing
12 changed files
with
284 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 4 additions & 7 deletions
11
src/domain/messages/entities/message-confirmation.entity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,11 @@ | ||
import { MessageConfirmationSchema } from '@/domain/messages/entities/schemas/message.schema'; | ||
import { z } from 'zod'; | ||
|
||
export enum SignatureType { | ||
ContractSignature = 'CONTRACT_SIGNATURE', | ||
ApprovedHash = 'APPROVED_HASH', | ||
Eoa = 'EOA', | ||
EthSign = 'ETH_SIGN', | ||
} | ||
|
||
export interface MessageConfirmation { | ||
created: Date; | ||
modified: Date; | ||
owner: string; | ||
signature: string; | ||
signatureType: SignatureType; | ||
} | ||
export type MessageConfirmation = z.infer<typeof MessageConfirmationSchema>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,4 @@ | ||
import { MessageConfirmation } from '@/domain/messages/entities/message-confirmation.entity'; | ||
import { MessageSchema } from '@/domain/messages/entities/schemas/message.schema'; | ||
import { z } from 'zod'; | ||
|
||
export interface Message { | ||
created: Date; | ||
modified: Date; | ||
safe: string; | ||
messageHash: string; | ||
message: string | unknown; | ||
proposedBy: string; | ||
safeAppId: number | null; | ||
confirmations: MessageConfirmation[]; | ||
preparedSignature: string | null; | ||
} | ||
export type Message = z.infer<typeof MessageSchema>; |
220 changes: 220 additions & 0 deletions
220
src/domain/messages/entities/schemas/__tests__/message.schema.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
import { fakeJson } from '@/__tests__/faker'; | ||
import { pageBuilder } from '@/domain/entities/__tests__/page.builder'; | ||
import { messageConfirmationBuilder } from '@/domain/messages/entities/__tests__/message-confirmation.builder'; | ||
import { messageBuilder } from '@/domain/messages/entities/__tests__/message.builder'; | ||
import { SignatureType } from '@/domain/messages/entities/message-confirmation.entity'; | ||
import { | ||
MessageConfirmationSchema, | ||
MessagePageSchema, | ||
MessageSchema, | ||
} from '@/domain/messages/entities/schemas/message.schema'; | ||
import { faker } from '@faker-js/faker'; | ||
import { getAddress } from 'viem'; | ||
import { ZodError } from 'zod'; | ||
|
||
describe('Message schemas', () => { | ||
describe('MessageConfirmationSchema', () => { | ||
it('should validate a valid MessageConfirmation', () => { | ||
const messageConfirmation = messageConfirmationBuilder().build(); | ||
|
||
const result = MessageConfirmationSchema.safeParse(messageConfirmation); | ||
|
||
expect(result.success).toBe(true); | ||
}); | ||
|
||
it.each(['created' as const, 'modified' as const])( | ||
'should coerce %s to a date', | ||
(key) => { | ||
const messageConfirmation = messageConfirmationBuilder() | ||
.with(key, faker.date.recent().toISOString() as unknown as Date) | ||
.build(); | ||
|
||
const result = MessageConfirmationSchema.safeParse(messageConfirmation); | ||
|
||
expect(result.success && result.data[key]).toStrictEqual( | ||
new Date(messageConfirmation[key]), | ||
); | ||
}, | ||
); | ||
|
||
it('should checksum the owner', () => { | ||
const nonChecksummedAddress = faker.finance | ||
.ethereumAddress() | ||
.toLowerCase() as `0x${string}`; | ||
const messageConfirmation = messageConfirmationBuilder() | ||
.with('owner', nonChecksummedAddress) | ||
.build(); | ||
|
||
const result = MessageConfirmationSchema.safeParse(messageConfirmation); | ||
|
||
expect(result.success && result.data.owner).toBe( | ||
getAddress(nonChecksummedAddress), | ||
); | ||
}); | ||
|
||
it('should not allow non-hex signature', () => { | ||
const messageConfirmation = messageConfirmationBuilder() | ||
.with('signature', faker.string.numeric() as `0x${string}`) | ||
.build(); | ||
|
||
const result = MessageConfirmationSchema.safeParse(messageConfirmation); | ||
|
||
expect(!result.success && result.error).toStrictEqual( | ||
new ZodError([ | ||
{ | ||
code: 'custom', | ||
message: 'Invalid input', | ||
path: ['signature'], | ||
}, | ||
]), | ||
); | ||
}); | ||
|
||
it('should not allow invalid signature types', () => { | ||
const messageConfirmation = messageConfirmationBuilder() | ||
.with('signatureType', faker.lorem.word() as SignatureType) | ||
.build(); | ||
|
||
const result = MessageConfirmationSchema.safeParse(messageConfirmation); | ||
|
||
expect(!result.success && result.error).toStrictEqual( | ||
new ZodError([ | ||
{ | ||
received: messageConfirmation.signatureType, | ||
code: 'invalid_enum_value', | ||
options: ['CONTRACT_SIGNATURE', 'APPROVED_HASH', 'EOA', 'ETH_SIGN'], | ||
path: ['signatureType'], | ||
message: `Invalid enum value. Expected 'CONTRACT_SIGNATURE' | 'APPROVED_HASH' | 'EOA' | 'ETH_SIGN', received '${messageConfirmation.signatureType}'`, | ||
}, | ||
]), | ||
); | ||
}); | ||
}); | ||
|
||
describe('MessageSchema', () => { | ||
it('should validate a valid Message', () => { | ||
const message = messageBuilder().build(); | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect(result.success).toBe(true); | ||
}); | ||
|
||
it.each(['created' as const, 'modified' as const])( | ||
'should coerce %s to a date', | ||
(key) => { | ||
const message = messageBuilder().build(); | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect(result.success && result.data[key]).toStrictEqual( | ||
new Date(message[key]), | ||
); | ||
}, | ||
); | ||
|
||
it.each(['safe' as const, 'proposedBy' as const])( | ||
'should checksum the %s', | ||
(key) => { | ||
const nonChecksummedAddress = faker.finance | ||
.ethereumAddress() | ||
.toLowerCase() as `0x${string}`; | ||
const message = messageBuilder() | ||
.with(key, nonChecksummedAddress) | ||
.build(); | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect(result.success && result.data[key]).toBe( | ||
getAddress(nonChecksummedAddress), | ||
); | ||
}, | ||
); | ||
|
||
it.each(['messageHash' as const, 'preparedSignature' as const])( | ||
'should not allow non-hex %s', | ||
(key) => { | ||
const message = messageBuilder() | ||
.with(key, faker.string.numeric() as `0x${string}`) | ||
.build(); | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect(!result.success && result.error).toStrictEqual( | ||
new ZodError([ | ||
{ | ||
code: 'custom', | ||
message: 'Invalid input', | ||
path: [key], | ||
}, | ||
]), | ||
); | ||
}, | ||
); | ||
|
||
it.each([ | ||
['string', faker.lorem.sentence()], | ||
['object', JSON.parse(fakeJson())], | ||
])('should allow a %s message', (_, message) => { | ||
const result = MessageSchema.safeParse({ | ||
...messageBuilder().build(), | ||
message, | ||
}); | ||
|
||
expect(result.success).toBe(true); | ||
}); | ||
|
||
it.each(['safeAppId' as const, 'preparedSignature' as const])( | ||
'should allow undefined %s, defaulting to null', | ||
(key) => { | ||
const message = messageBuilder().build(); | ||
delete message[key]; | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect(result.success && result.data[key]).toBe(null); | ||
}, | ||
); | ||
|
||
it('should allow empty confirmations', () => { | ||
const message = messageBuilder().with('confirmations', []).build(); | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect(result.success).toBe(true); | ||
}); | ||
|
||
it.each([ | ||
'created' as const, | ||
'modified' as const, | ||
'safe' as const, | ||
'messageHash' as const, | ||
'message' as const, | ||
'proposedBy' as const, | ||
'confirmations' as const, | ||
])('should not allow %s to be undefined', (key) => { | ||
const message = messageBuilder().build(); | ||
delete message[key]; | ||
|
||
const result = MessageSchema.safeParse(message); | ||
|
||
expect( | ||
!result.success && | ||
result.error.issues.length === 1 && | ||
result.error.issues[0].path.length === 1 && | ||
result.error.issues[0].path[0] === key, | ||
).toBe(true); | ||
}); | ||
}); | ||
|
||
describe('MessagePageSchema', () => { | ||
it('should validate a valid Page<Message>', () => { | ||
const message = messageBuilder().build(); | ||
const messagePage = pageBuilder().with('results', [message]).build(); | ||
|
||
const result = MessagePageSchema.safeParse(messagePage); | ||
|
||
expect(result.success).toBe(true); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.