Pretty format webhook payload example + unify expected body validation (#13034)
## Webhook Expected Body is automatically pretty formatted https://github.com/user-attachments/assets/0ca7d621-0c6e-4bef-903f-859efd02f9cc ## Expected body fields can't contain spaces in keys' name https://github.com/user-attachments/assets/b68d36a6-acd4-4ba2-a99a-5857e30c8582 Closes https://github.com/twentyhq/core-team-issues/issues/1117
This commit is contained in:
committed by
GitHub
parent
92576aec0f
commit
43c3d114bb
+214
@@ -0,0 +1,214 @@
|
||||
import { parseAndValidateVariableFriendlyStringifiedJson } from '../parseAndValidateVariableFriendlyStringifiedJson';
|
||||
|
||||
describe('parseAndValidateVariableFriendlyStringifiedJson', () => {
|
||||
describe('Valid JSON with variable-friendly keys', () => {
|
||||
it('should accept empty object', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson('{}');
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({});
|
||||
});
|
||||
|
||||
it('should accept object with camelCase keys', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"camelCaseKey": "value", "anotherKey": 123}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
camelCaseKey: 'value',
|
||||
anotherKey: 123,
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept object with snake_case keys', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"snake_case_key": "value", "another_key": true}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
snake_case_key: 'value',
|
||||
another_key: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept keys with dashes and special characters', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"key-with-dashes": "value", "key.with.dots": "another", "key$symbol": 42}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
'key-with-dashes': 'value',
|
||||
'key.with.dots': 'another',
|
||||
key$symbol: 42,
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept numeric keys', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"123": "numeric", "456key": "mixed"}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
'123': 'numeric',
|
||||
'456key': 'mixed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept complex nested values', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"user": {"name": "John", "age": 30}, "items": [1, 2, 3]}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
user: { name: 'John', age: 30 },
|
||||
items: [1, 2, 3],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid keys with whitespace', () => {
|
||||
it('should reject key with space', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"key with space": "value"}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBe('JSON keys cannot contain spaces');
|
||||
});
|
||||
|
||||
it('should reject key with leading space', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{" leadingSpace": "value"}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBe('JSON keys cannot contain spaces');
|
||||
});
|
||||
|
||||
it('should reject key with trailing space', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"trailingSpace ": "value"}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBe('JSON keys cannot contain spaces');
|
||||
});
|
||||
|
||||
it('should reject key with tab character', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"key\\twith\\ttab": "value"}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBe('JSON keys cannot contain spaces');
|
||||
});
|
||||
|
||||
it('should reject when one of multiple keys has space', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"validKey": "value", "invalid key": "another"}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toBe('JSON keys cannot contain spaces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Malformed JSON', () => {
|
||||
it('should reject invalid JSON syntax', () => {
|
||||
const result =
|
||||
parseAndValidateVariableFriendlyStringifiedJson('{"key": value}');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain('Unexpected token');
|
||||
});
|
||||
|
||||
it('should reject unclosed object', () => {
|
||||
const result =
|
||||
parseAndValidateVariableFriendlyStringifiedJson('{"key": "value"');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain(
|
||||
"SyntaxError: Expected ',' or '}' after property value in JSON at position 15 (line 1 column 16)",
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject empty string', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson('');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain('Unexpected end of JSON input');
|
||||
});
|
||||
|
||||
it('should reject non-JSON string', () => {
|
||||
const result =
|
||||
parseAndValidateVariableFriendlyStringifiedJson('not json at all');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
expect(result.error).toContain('Unexpected token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-object JSON values', () => {
|
||||
it('should reject JSON array', () => {
|
||||
const result =
|
||||
parseAndValidateVariableFriendlyStringifiedJson('[1, 2, 3]');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject JSON string', () => {
|
||||
const result =
|
||||
parseAndValidateVariableFriendlyStringifiedJson('"just a string"');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject JSON number', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson('42');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject JSON boolean', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson('true');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject JSON null', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson('null');
|
||||
|
||||
expect(result.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should accept empty string key', () => {
|
||||
const result =
|
||||
parseAndValidateVariableFriendlyStringifiedJson('{"": "value"}');
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({ '': 'value' });
|
||||
});
|
||||
|
||||
it('should handle deeply nested objects', () => {
|
||||
const result = parseAndValidateVariableFriendlyStringifiedJson(
|
||||
'{"level1": {"level2": {"level3": "deep"}}}',
|
||||
);
|
||||
|
||||
expect(result.isValid).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
level1: {
|
||||
level2: {
|
||||
level3: 'deep',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z
|
||||
.record(z.any())
|
||||
.refine((data) => Object.keys(data).every((key) => !key.match(/\s/)), {
|
||||
message: 'JSON keys cannot contain spaces',
|
||||
});
|
||||
|
||||
export const parseAndValidateVariableFriendlyStringifiedJson = (
|
||||
expectedJson: string,
|
||||
) => {
|
||||
let value: unknown;
|
||||
|
||||
try {
|
||||
value = JSON.parse(expectedJson);
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: String(error),
|
||||
} as const;
|
||||
}
|
||||
|
||||
const parsingResult = schema.safeParse(value);
|
||||
|
||||
if (parsingResult.success) {
|
||||
return {
|
||||
isValid: true,
|
||||
data: parsingResult.data,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
error: parsingResult.error.issues[0].message,
|
||||
} as const;
|
||||
};
|
||||
Reference in New Issue
Block a user