mirror of https://github.com/buster-so/buster.git
Merge pull request #418 from buster-so/cursor/create-unit-tests-for-schema-defaults-b3ac
Add comprehensive test suites for shared server type schemas
This commit is contained in:
commit
0b5c8cb2e1
|
@ -0,0 +1,306 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ChatMessageSchema,
|
||||
ReasoningMessageSchema,
|
||||
ResponseMessageSchema,
|
||||
} from './chat-message.types';
|
||||
|
||||
describe('ChatMessageSchema', () => {
|
||||
it('should parse a valid complete chat message', () => {
|
||||
const validMessage = {
|
||||
id: 'msg-123',
|
||||
request_message: {
|
||||
request: 'What is the revenue?',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'John Doe',
|
||||
sender_avatar: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: true,
|
||||
};
|
||||
|
||||
const result = ChatMessageSchema.safeParse(validMessage);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.id).toBe('msg-123');
|
||||
expect(result.data.is_completed).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle null request message', () => {
|
||||
const messageWithNullRequest = {
|
||||
id: 'msg-123',
|
||||
request_message: null,
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: false,
|
||||
};
|
||||
|
||||
const result = ChatMessageSchema.safeParse(messageWithNullRequest);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.request_message).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle optional fields in request message', () => {
|
||||
const messageWithOptionalFields = {
|
||||
id: 'msg-123',
|
||||
request_message: {
|
||||
request: 'What is the revenue?',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'John Doe',
|
||||
// sender_avatar is optional
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: false,
|
||||
};
|
||||
|
||||
const result = ChatMessageSchema.safeParse(messageWithOptionalFields);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ResponseMessageSchema', () => {
|
||||
it('should parse text response message', () => {
|
||||
const textMessage = {
|
||||
id: 'resp-123',
|
||||
type: 'text',
|
||||
message: 'Here is your answer',
|
||||
};
|
||||
|
||||
const result = ResponseMessageSchema.safeParse(textMessage);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('text');
|
||||
if (result.data.type === 'text') {
|
||||
expect(result.data.message).toBe('Here is your answer');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse file response message with metadata', () => {
|
||||
const fileMessage = {
|
||||
id: 'resp-456',
|
||||
type: 'file',
|
||||
file_type: 'metric',
|
||||
file_name: 'revenue_analysis.yaml',
|
||||
version_number: 1,
|
||||
filter_version_id: 'filter-123',
|
||||
metadata: [
|
||||
{
|
||||
status: 'completed',
|
||||
message: 'Analysis complete',
|
||||
timestamp: 1640995200000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = ResponseMessageSchema.safeParse(fileMessage);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('file');
|
||||
if (result.data.type === 'file') {
|
||||
expect(result.data.file_type).toBe('metric');
|
||||
expect(result.data.metadata).toHaveLength(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle optional metadata fields', () => {
|
||||
const fileMessageWithoutOptionals = {
|
||||
id: 'resp-456',
|
||||
type: 'file',
|
||||
file_type: 'dashboard',
|
||||
file_name: 'sales_dashboard.yaml',
|
||||
version_number: 2,
|
||||
};
|
||||
|
||||
const result = ResponseMessageSchema.safeParse(fileMessageWithoutOptionals);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('file');
|
||||
if (result.data.type === 'file') {
|
||||
expect(result.data.file_type).toBe('dashboard');
|
||||
expect(result.data.metadata).toBeUndefined();
|
||||
expect(result.data.filter_version_id).toBeUndefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReasoningMessageSchema', () => {
|
||||
it('should parse text reasoning message', () => {
|
||||
const textReasoning = {
|
||||
id: 'reason-123',
|
||||
type: 'text',
|
||||
title: 'Analyzing Data',
|
||||
status: 'loading',
|
||||
};
|
||||
|
||||
const result = ReasoningMessageSchema.safeParse(textReasoning);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('text');
|
||||
expect(result.data.title).toBe('Analyzing Data');
|
||||
expect(result.data.status).toBe('loading');
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse files reasoning message with nested file objects', () => {
|
||||
const filesReasoning = {
|
||||
id: 'reason-456',
|
||||
type: 'files',
|
||||
title: 'Generated Files',
|
||||
status: 'completed',
|
||||
file_ids: ['file-1', 'file-2'],
|
||||
files: {
|
||||
'file-1': {
|
||||
id: 'file-1',
|
||||
file_type: 'metric',
|
||||
file_name: 'revenue.yaml',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: 'metric content here',
|
||||
modified: [
|
||||
[0, 10],
|
||||
[20, 30],
|
||||
],
|
||||
},
|
||||
},
|
||||
'file-2': {
|
||||
id: 'file-2',
|
||||
file_type: 'dashboard',
|
||||
file_name: 'dashboard.yaml',
|
||||
status: 'loading',
|
||||
file: {
|
||||
text: 'dashboard content',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = ReasoningMessageSchema.safeParse(filesReasoning);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('files');
|
||||
if (result.data.type === 'files') {
|
||||
expect(result.data.file_ids).toHaveLength(2);
|
||||
expect(result.data.files['file-1'].file_type).toBe('metric');
|
||||
expect(result.data.files['file-1'].file.modified).toEqual([
|
||||
[0, 10],
|
||||
[20, 30],
|
||||
]);
|
||||
expect(result.data.files['file-2'].file.modified).toBeUndefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should parse pills reasoning message with nested pill containers', () => {
|
||||
const pillsReasoning = {
|
||||
id: 'reason-789',
|
||||
type: 'pills',
|
||||
title: 'Related Items',
|
||||
status: 'completed',
|
||||
pill_containers: [
|
||||
{
|
||||
title: 'Metrics',
|
||||
pills: [
|
||||
{
|
||||
text: 'Revenue',
|
||||
type: 'metric',
|
||||
id: 'metric-1',
|
||||
},
|
||||
{
|
||||
text: 'Profit',
|
||||
type: 'metric',
|
||||
id: 'metric-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Dashboards',
|
||||
pills: [
|
||||
{
|
||||
text: 'Sales Overview',
|
||||
type: 'dashboard',
|
||||
id: 'dashboard-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = ReasoningMessageSchema.safeParse(pillsReasoning);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.type).toBe('pills');
|
||||
if (result.data.type === 'pills') {
|
||||
expect(result.data.pill_containers).toHaveLength(2);
|
||||
expect(result.data.pill_containers[0].pills).toHaveLength(2);
|
||||
expect(result.data.pill_containers[0].pills[0].type).toBe('metric');
|
||||
expect(result.data.pill_containers[1].pills[0].text).toBe('Sales Overview');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle optional finished_reasoning field', () => {
|
||||
const reasoningWithFinished = {
|
||||
id: 'reason-123',
|
||||
type: 'text',
|
||||
title: 'Complete Analysis',
|
||||
status: 'completed',
|
||||
finished_reasoning: true,
|
||||
};
|
||||
|
||||
const result = ReasoningMessageSchema.safeParse(reasoningWithFinished);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.finished_reasoning).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle all optional fields in text reasoning', () => {
|
||||
const textWithOptionals = {
|
||||
id: 'reason-999',
|
||||
type: 'text',
|
||||
title: 'Analysis',
|
||||
secondary_title: 'Revenue Analysis',
|
||||
message: 'Detailed analysis message',
|
||||
message_chunk: 'Partial message',
|
||||
status: 'loading',
|
||||
finished_reasoning: false,
|
||||
};
|
||||
|
||||
const result = ReasoningMessageSchema.safeParse(textWithOptionals);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
if (result.data.type === 'text') {
|
||||
expect(result.data.secondary_title).toBe('Revenue Analysis');
|
||||
expect(result.data.message).toBe('Detailed analysis message');
|
||||
expect(result.data.message_chunk).toBe('Partial message');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,423 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
AssetPermissionRoleSchema,
|
||||
BusterShareIndividualSchema,
|
||||
ChatCreateHandlerRequestSchema,
|
||||
ChatCreateRequestSchema,
|
||||
ChatWithMessagesSchema,
|
||||
} from './chat.types';
|
||||
|
||||
describe('AssetPermissionRoleSchema', () => {
|
||||
it('should accept valid role values', () => {
|
||||
const validRoles = ['viewer', 'editor', 'owner'];
|
||||
|
||||
for (const role of validRoles) {
|
||||
const result = AssetPermissionRoleSchema.safeParse(role);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).toBe(role);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid role values', () => {
|
||||
const invalidRoles = ['admin', 'user', 'guest', '', 'VIEWER'];
|
||||
|
||||
for (const role of invalidRoles) {
|
||||
const result = AssetPermissionRoleSchema.safeParse(role);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('BusterShareIndividualSchema', () => {
|
||||
it('should parse valid individual sharing configuration', () => {
|
||||
const validIndividual = {
|
||||
email: 'user@example.com',
|
||||
role: 'editor',
|
||||
name: 'John Doe',
|
||||
};
|
||||
|
||||
const result = BusterShareIndividualSchema.safeParse(validIndividual);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.email).toBe('user@example.com');
|
||||
expect(result.data.role).toBe('editor');
|
||||
expect(result.data.name).toBe('John Doe');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle optional name field', () => {
|
||||
const individualWithoutName = {
|
||||
email: 'test@example.com',
|
||||
role: 'viewer',
|
||||
};
|
||||
|
||||
const result = BusterShareIndividualSchema.safeParse(individualWithoutName);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.email).toBe('test@example.com');
|
||||
expect(result.data.role).toBe('viewer');
|
||||
expect(result.data.name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate email format', () => {
|
||||
const invalidEmails = ['invalid-email', 'test@', '@example.com', 'test.example.com', ''];
|
||||
|
||||
for (const email of invalidEmails) {
|
||||
const individual = {
|
||||
email,
|
||||
role: 'viewer',
|
||||
};
|
||||
|
||||
const result = BusterShareIndividualSchema.safeParse(individual);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate role field', () => {
|
||||
const individual = {
|
||||
email: 'valid@example.com',
|
||||
role: 'invalidRole',
|
||||
};
|
||||
|
||||
const result = BusterShareIndividualSchema.safeParse(individual);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChatWithMessagesSchema', () => {
|
||||
it('should parse valid complete chat with messages', () => {
|
||||
const validChat = {
|
||||
id: 'chat-123',
|
||||
title: 'Revenue Analysis Chat',
|
||||
is_favorited: false,
|
||||
message_ids: ['msg-1', 'msg-2'],
|
||||
messages: {
|
||||
'msg-1': {
|
||||
id: 'msg-1',
|
||||
request_message: {
|
||||
request: 'What is the revenue?',
|
||||
sender_id: 'user-123',
|
||||
sender_name: 'John Doe',
|
||||
sender_avatar: 'https://example.com/avatar.jpg',
|
||||
},
|
||||
response_messages: {},
|
||||
response_message_ids: [],
|
||||
reasoning_message_ids: [],
|
||||
reasoning_messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
final_reasoning_message: null,
|
||||
feedback: null,
|
||||
is_completed: true,
|
||||
},
|
||||
},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
created_by: 'John Doe',
|
||||
created_by_id: 'user-123',
|
||||
created_by_name: 'John Doe',
|
||||
created_by_avatar: 'https://example.com/avatar.jpg',
|
||||
individual_permissions: [],
|
||||
publicly_accessible: false,
|
||||
public_expiry_date: '2024-12-31T23:59:59Z',
|
||||
public_enabled_by: 'admin-456',
|
||||
public_password: 'secret123',
|
||||
permission: 'owner',
|
||||
};
|
||||
|
||||
const result = ChatWithMessagesSchema.safeParse(validChat);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.id).toBe('chat-123');
|
||||
expect(result.data.title).toBe('Revenue Analysis Chat');
|
||||
expect(result.data.is_favorited).toBe(false);
|
||||
expect(result.data.message_ids).toEqual(['msg-1', 'msg-2']);
|
||||
expect(result.data.publicly_accessible).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle optional fields', () => {
|
||||
const chatWithOptionals = {
|
||||
id: 'chat-456',
|
||||
title: 'Simple Chat',
|
||||
is_favorited: true,
|
||||
message_ids: [],
|
||||
messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
created_by: 'Jane Doe',
|
||||
created_by_id: 'user-456',
|
||||
created_by_name: 'Jane Doe',
|
||||
created_by_avatar: null, // nullable
|
||||
publicly_accessible: true,
|
||||
// Optional fields omitted
|
||||
};
|
||||
|
||||
const result = ChatWithMessagesSchema.safeParse(chatWithOptionals);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.id).toBe('chat-456');
|
||||
expect(result.data.created_by_avatar).toBeNull();
|
||||
expect(result.data.individual_permissions).toBeUndefined();
|
||||
expect(result.data.public_expiry_date).toBeUndefined();
|
||||
expect(result.data.permission).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate nested individual permissions', () => {
|
||||
const chatWithInvalidPermissions = {
|
||||
id: 'chat-789',
|
||||
title: 'Chat with Invalid Permissions',
|
||||
is_favorited: false,
|
||||
message_ids: [],
|
||||
messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
created_by: 'User',
|
||||
created_by_id: 'user-789',
|
||||
created_by_name: 'User',
|
||||
created_by_avatar: null,
|
||||
individual_permissions: [
|
||||
{
|
||||
email: 'invalid-email', // Invalid email
|
||||
role: 'editor',
|
||||
},
|
||||
],
|
||||
publicly_accessible: false,
|
||||
};
|
||||
|
||||
const result = ChatWithMessagesSchema.safeParse(chatWithInvalidPermissions);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle datetime validation for public_expiry_date', () => {
|
||||
const chatWithValidDate = {
|
||||
id: 'chat-date',
|
||||
title: 'Date Test Chat',
|
||||
is_favorited: false,
|
||||
message_ids: [],
|
||||
messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
created_by: 'User',
|
||||
created_by_id: 'user-date',
|
||||
created_by_name: 'User',
|
||||
created_by_avatar: null,
|
||||
publicly_accessible: true,
|
||||
public_expiry_date: '2024-12-31T23:59:59.999Z', // Valid ISO datetime
|
||||
};
|
||||
|
||||
const result = ChatWithMessagesSchema.safeParse(chatWithValidDate);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid datetime for public_expiry_date', () => {
|
||||
const chatWithInvalidDate = {
|
||||
id: 'chat-bad-date',
|
||||
title: 'Bad Date Test Chat',
|
||||
is_favorited: false,
|
||||
message_ids: [],
|
||||
messages: {},
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
created_by: 'User',
|
||||
created_by_id: 'user-bad-date',
|
||||
created_by_name: 'User',
|
||||
created_by_avatar: null,
|
||||
publicly_accessible: true,
|
||||
public_expiry_date: '2024-12-31', // Invalid - not a complete datetime
|
||||
};
|
||||
|
||||
const result = ChatWithMessagesSchema.safeParse(chatWithInvalidDate);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChatCreateRequestSchema', () => {
|
||||
it('should parse valid complete request', () => {
|
||||
const validRequest = {
|
||||
prompt: 'Analyze revenue trends',
|
||||
chat_id: 'chat-123',
|
||||
message_id: 'msg-456',
|
||||
asset_id: 'asset-789',
|
||||
asset_type: 'metric_file',
|
||||
metric_id: 'legacy-metric-123',
|
||||
dashboard_id: 'legacy-dashboard-456',
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(validRequest);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.prompt).toBe('Analyze revenue trends');
|
||||
expect(result.data.asset_id).toBe('asset-789');
|
||||
expect(result.data.asset_type).toBe('metric_file');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle minimal request', () => {
|
||||
const minimalRequest = {
|
||||
prompt: 'Simple question',
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(minimalRequest);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.prompt).toBe('Simple question');
|
||||
expect(result.data.chat_id).toBeUndefined();
|
||||
expect(result.data.asset_id).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty request', () => {
|
||||
const emptyRequest = {};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(emptyRequest);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.prompt).toBeUndefined();
|
||||
expect(result.data.chat_id).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should enforce asset_type when asset_id is provided', () => {
|
||||
const requestWithAssetIdButNoType = {
|
||||
prompt: 'Test prompt',
|
||||
asset_id: 'asset-123',
|
||||
// asset_type is missing - should fail validation
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(requestWithAssetIdButNoType);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow asset_type without asset_id', () => {
|
||||
const requestWithTypeButNoId = {
|
||||
prompt: 'Test prompt',
|
||||
asset_type: 'dashboard_file',
|
||||
// asset_id is missing - should be fine
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(requestWithTypeButNoId);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.asset_type).toBe('dashboard_file');
|
||||
expect(result.data.asset_id).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate asset_type enum values', () => {
|
||||
const validAssetTypes = ['metric_file', 'dashboard_file'];
|
||||
const invalidAssetTypes = ['metric', 'dashboard', 'file', 'document'];
|
||||
|
||||
for (const assetType of validAssetTypes) {
|
||||
const request = {
|
||||
asset_id: 'test-id',
|
||||
asset_type: assetType,
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
|
||||
for (const assetType of invalidAssetTypes) {
|
||||
const request = {
|
||||
asset_id: 'test-id',
|
||||
asset_type: assetType,
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle legacy fields', () => {
|
||||
const requestWithLegacyFields = {
|
||||
prompt: 'Legacy test',
|
||||
metric_id: 'metric-legacy-123',
|
||||
dashboard_id: 'dashboard-legacy-456',
|
||||
};
|
||||
|
||||
const result = ChatCreateRequestSchema.safeParse(requestWithLegacyFields);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.metric_id).toBe('metric-legacy-123');
|
||||
expect(result.data.dashboard_id).toBe('dashboard-legacy-456');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChatCreateHandlerRequestSchema', () => {
|
||||
it('should parse valid handler request', () => {
|
||||
const validHandlerRequest = {
|
||||
prompt: 'Handler test prompt',
|
||||
chat_id: 'chat-handler-123',
|
||||
message_id: 'msg-handler-456',
|
||||
asset_id: 'asset-handler-789',
|
||||
asset_type: 'metric_file',
|
||||
};
|
||||
|
||||
const result = ChatCreateHandlerRequestSchema.safeParse(validHandlerRequest);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.prompt).toBe('Handler test prompt');
|
||||
expect(result.data.asset_id).toBe('asset-handler-789');
|
||||
expect(result.data.asset_type).toBe('metric_file');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle minimal handler request', () => {
|
||||
const minimalHandlerRequest = {};
|
||||
|
||||
const result = ChatCreateHandlerRequestSchema.safeParse(minimalHandlerRequest);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.prompt).toBeUndefined();
|
||||
expect(result.data.chat_id).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should not have legacy fields', () => {
|
||||
const requestWithLegacyFields = {
|
||||
prompt: 'Handler test',
|
||||
metric_id: 'should-not-exist',
|
||||
dashboard_id: 'should-not-exist',
|
||||
};
|
||||
|
||||
const result = ChatCreateHandlerRequestSchema.safeParse(requestWithLegacyFields);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Legacy fields should not be present in handler schema
|
||||
expect('metric_id' in result.data).toBe(false);
|
||||
expect('dashboard_id' in result.data).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate asset_type enum values for handler', () => {
|
||||
const validAssetTypes = ['metric_file', 'dashboard_file'];
|
||||
|
||||
for (const assetType of validAssetTypes) {
|
||||
const request = {
|
||||
asset_id: 'test-id',
|
||||
asset_type: assetType,
|
||||
};
|
||||
|
||||
const result = ChatCreateHandlerRequestSchema.safeParse(request);
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,192 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { CurrencySchema } from './currency.types';
|
||||
|
||||
describe('CurrencySchema', () => {
|
||||
it('should parse valid currency object', () => {
|
||||
const validCurrency = {
|
||||
code: 'USD',
|
||||
description: 'United States Dollar',
|
||||
flag: '🇺🇸',
|
||||
};
|
||||
|
||||
const result = CurrencySchema.safeParse(validCurrency);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.code).toBe('USD');
|
||||
expect(result.data.description).toBe('United States Dollar');
|
||||
expect(result.data.flag).toBe('🇺🇸');
|
||||
}
|
||||
});
|
||||
|
||||
it('should require all fields', () => {
|
||||
const incompleteObjects = [
|
||||
{
|
||||
// missing code
|
||||
description: 'Euro',
|
||||
flag: '🇪🇺',
|
||||
},
|
||||
{
|
||||
code: 'EUR',
|
||||
// missing description
|
||||
flag: '🇪🇺',
|
||||
},
|
||||
{
|
||||
code: 'EUR',
|
||||
description: 'Euro',
|
||||
// missing flag
|
||||
},
|
||||
];
|
||||
|
||||
for (const obj of incompleteObjects) {
|
||||
const result = CurrencySchema.safeParse(obj);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate that all fields are strings', () => {
|
||||
const invalidTypes = [
|
||||
{
|
||||
code: 123, // should be string
|
||||
description: 'Invalid Code',
|
||||
flag: '🇺🇸',
|
||||
},
|
||||
{
|
||||
code: 'USD',
|
||||
description: true, // should be string
|
||||
flag: '🇺🇸',
|
||||
},
|
||||
{
|
||||
code: 'USD',
|
||||
description: 'United States Dollar',
|
||||
flag: null, // should be string
|
||||
},
|
||||
];
|
||||
|
||||
for (const obj of invalidTypes) {
|
||||
const result = CurrencySchema.safeParse(obj);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle various currency examples', () => {
|
||||
const currencies = [
|
||||
{
|
||||
code: 'EUR',
|
||||
description: 'Euro',
|
||||
flag: '🇪🇺',
|
||||
},
|
||||
{
|
||||
code: 'GBP',
|
||||
description: 'British Pound Sterling',
|
||||
flag: '🇬🇧',
|
||||
},
|
||||
{
|
||||
code: 'JPY',
|
||||
description: 'Japanese Yen',
|
||||
flag: '🇯🇵',
|
||||
},
|
||||
{
|
||||
code: 'CAD',
|
||||
description: 'Canadian Dollar',
|
||||
flag: '🇨🇦',
|
||||
},
|
||||
{
|
||||
code: 'AUD',
|
||||
description: 'Australian Dollar',
|
||||
flag: '🇦🇺',
|
||||
},
|
||||
];
|
||||
|
||||
for (const currency of currencies) {
|
||||
const result = CurrencySchema.safeParse(currency);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.code).toBe(currency.code);
|
||||
expect(result.data.description).toBe(currency.description);
|
||||
expect(result.data.flag).toBe(currency.flag);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
const currencyWithEmptyStrings = {
|
||||
code: '',
|
||||
description: '',
|
||||
flag: '',
|
||||
};
|
||||
|
||||
const result = CurrencySchema.safeParse(currencyWithEmptyStrings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.code).toBe('');
|
||||
expect(result.data.description).toBe('');
|
||||
expect(result.data.flag).toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle long strings', () => {
|
||||
const currencyWithLongStrings = {
|
||||
code: 'VERYLONGCURRENCYCODE',
|
||||
description:
|
||||
'This is a very long description for a currency that might not exist in real life but should still be valid according to our schema',
|
||||
flag: '🏳️🌈🏳️⚧️🇺🇳', // Multiple flag emojis
|
||||
};
|
||||
|
||||
const result = CurrencySchema.safeParse(currencyWithLongStrings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.code).toBe('VERYLONGCURRENCYCODE');
|
||||
expect(result.data.description).toBe(
|
||||
'This is a very long description for a currency that might not exist in real life but should still be valid according to our schema'
|
||||
);
|
||||
expect(result.data.flag).toBe('🏳️🌈🏳️⚧️🇺🇳');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle special characters and unicode', () => {
|
||||
const currencyWithSpecialChars = {
|
||||
code: 'BTC-₿',
|
||||
description: 'Bitcoin (₿) - Digital Currency with special chars: @#$%^&*()[]{}',
|
||||
flag: '₿',
|
||||
};
|
||||
|
||||
const result = CurrencySchema.safeParse(currencyWithSpecialChars);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.code).toBe('BTC-₿');
|
||||
expect(result.data.description).toBe(
|
||||
'Bitcoin (₿) - Digital Currency with special chars: @#$%^&*()[]{}'
|
||||
);
|
||||
expect(result.data.flag).toBe('₿');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject additional properties', () => {
|
||||
const currencyWithExtraProps = {
|
||||
code: 'USD',
|
||||
description: 'United States Dollar',
|
||||
flag: '🇺🇸',
|
||||
extraProperty: 'This should not be allowed',
|
||||
anotherExtra: 123,
|
||||
};
|
||||
|
||||
const result = CurrencySchema.safeParse(currencyWithExtraProps);
|
||||
// Zod by default allows additional properties unless .strict() is used
|
||||
// Since the schema doesn't use .strict(), extra properties are allowed but ignored
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Extra properties should not be included in the result
|
||||
expect('extraProperty' in result.data).toBe(false);
|
||||
expect('anotherExtra' in result.data).toBe(false);
|
||||
expect(result.data.code).toBe('USD');
|
||||
expect(result.data.description).toBe('United States Dollar');
|
||||
expect(result.data.flag).toBe('🇺🇸');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,354 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
BarAndLineAxisSchema,
|
||||
ChartEncodesSchema,
|
||||
ComboChartAxisSchema,
|
||||
PieChartAxisSchema,
|
||||
ScatterAxisSchema,
|
||||
} from './axisInterfaces';
|
||||
|
||||
describe('BarAndLineAxisSchema', () => {
|
||||
it('should apply default values when parsing empty object', () => {
|
||||
const result = BarAndLineAxisSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual([]);
|
||||
expect(result.data.y).toEqual([]);
|
||||
expect(result.data.category).toEqual([]);
|
||||
// tooltip is optional and will be undefined when not provided
|
||||
expect(result.data.tooltip).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply nested defaults for each property', () => {
|
||||
const result = BarAndLineAxisSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Test that arrays are empty by default
|
||||
expect(result.data.x).toEqual([]);
|
||||
expect(result.data.y).toEqual([]);
|
||||
expect(result.data.category).toEqual([]);
|
||||
expect(result.data.tooltip).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should override defaults when values are provided', () => {
|
||||
const customAxis = {
|
||||
x: ['date'],
|
||||
y: ['revenue', 'profit'],
|
||||
category: ['region'],
|
||||
tooltip: ['description'],
|
||||
};
|
||||
|
||||
const result = BarAndLineAxisSchema.safeParse(customAxis);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['date']);
|
||||
expect(result.data.y).toEqual(['revenue', 'profit']);
|
||||
expect(result.data.category).toEqual(['region']);
|
||||
expect(result.data.tooltip).toEqual(['description']);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle partial overrides', () => {
|
||||
const partialAxis = {
|
||||
x: ['timestamp'],
|
||||
y: ['sales'],
|
||||
};
|
||||
|
||||
const result = BarAndLineAxisSchema.safeParse(partialAxis);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['timestamp']);
|
||||
expect(result.data.y).toEqual(['sales']);
|
||||
// Defaults should be preserved for missing fields
|
||||
expect(result.data.category).toEqual([]);
|
||||
expect(result.data.tooltip).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle explicit null tooltip', () => {
|
||||
const axisWithNullTooltip = {
|
||||
x: ['date'],
|
||||
y: ['value'],
|
||||
tooltip: null,
|
||||
};
|
||||
|
||||
const result = BarAndLineAxisSchema.safeParse(axisWithNullTooltip);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.tooltip).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ScatterAxisSchema', () => {
|
||||
it('should apply default values including size array', () => {
|
||||
const result = ScatterAxisSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
x: [],
|
||||
y: [],
|
||||
category: [],
|
||||
size: [],
|
||||
tooltip: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle size as tuple or empty array', () => {
|
||||
const withSizeTuple = {
|
||||
x: ['x_val'],
|
||||
y: ['y_val'],
|
||||
size: ['population'],
|
||||
};
|
||||
|
||||
const result = ScatterAxisSchema.safeParse(withSizeTuple);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.size).toEqual(['population']);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate size array length', () => {
|
||||
const withEmptySize = {
|
||||
size: [],
|
||||
};
|
||||
|
||||
const result = ScatterAxisSchema.safeParse(withEmptySize);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.size).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle nested defaults properly', () => {
|
||||
const partialConfig = {
|
||||
x: ['x_axis'],
|
||||
};
|
||||
|
||||
const result = ScatterAxisSchema.safeParse(partialConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['x_axis']);
|
||||
expect(result.data.y).toEqual([]);
|
||||
expect(result.data.category).toEqual([]);
|
||||
expect(result.data.size).toEqual([]);
|
||||
expect(result.data.tooltip).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ComboChartAxisSchema', () => {
|
||||
it('should apply default values including y2 axis', () => {
|
||||
const result = ComboChartAxisSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual([]);
|
||||
expect(result.data.y).toEqual([]);
|
||||
expect(result.data.y2).toEqual([]);
|
||||
expect(result.data.category).toEqual([]);
|
||||
expect(result.data.tooltip).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle dual y-axis configuration', () => {
|
||||
const dualAxisConfig = {
|
||||
x: ['month'],
|
||||
y: ['revenue'],
|
||||
y2: ['profit_margin'],
|
||||
category: ['product_line'],
|
||||
};
|
||||
|
||||
const result = ComboChartAxisSchema.safeParse(dualAxisConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['month']);
|
||||
expect(result.data.y).toEqual(['revenue']);
|
||||
expect(result.data.y2).toEqual(['profit_margin']);
|
||||
expect(result.data.category).toEqual(['product_line']);
|
||||
expect(result.data.tooltip).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply defaults for missing secondary axis', () => {
|
||||
const primaryAxisOnly = {
|
||||
x: ['date'],
|
||||
y: ['sales'],
|
||||
};
|
||||
|
||||
const result = ComboChartAxisSchema.safeParse(primaryAxisOnly);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['date']);
|
||||
expect(result.data.y).toEqual(['sales']);
|
||||
expect(result.data.y2).toEqual([]); // Default for secondary y-axis
|
||||
expect(result.data.category).toEqual([]);
|
||||
expect(result.data.tooltip).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('PieChartAxisSchema', () => {
|
||||
it('should apply default values for pie chart axis', () => {
|
||||
const result = PieChartAxisSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
x: [],
|
||||
y: [],
|
||||
tooltip: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle pie chart specific configuration', () => {
|
||||
const pieConfig = {
|
||||
x: ['category'],
|
||||
y: ['value1', 'value2'], // Multiple values for rings
|
||||
tooltip: ['description', 'percentage'],
|
||||
};
|
||||
|
||||
const result = PieChartAxisSchema.safeParse(pieConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['category']);
|
||||
expect(result.data.y).toEqual(['value1', 'value2']);
|
||||
expect(result.data.tooltip).toEqual(['description', 'percentage']);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle missing tooltip configuration', () => {
|
||||
const basicPieConfig = {
|
||||
x: ['segment'],
|
||||
y: ['amount'],
|
||||
};
|
||||
|
||||
const result = PieChartAxisSchema.safeParse(basicPieConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['segment']);
|
||||
expect(result.data.y).toEqual(['amount']);
|
||||
expect(result.data.tooltip).toBeNull(); // Default
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ChartEncodesSchema union', () => {
|
||||
it('should accept valid BarAndLineAxis', () => {
|
||||
const barLineAxis = {
|
||||
x: ['date'],
|
||||
y: ['revenue'],
|
||||
category: [],
|
||||
tooltip: null,
|
||||
};
|
||||
|
||||
const result = ChartEncodesSchema.safeParse(barLineAxis);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept valid ScatterAxis', () => {
|
||||
const scatterAxis = {
|
||||
x: ['x_value'],
|
||||
y: ['y_value'],
|
||||
category: [],
|
||||
size: ['population'],
|
||||
tooltip: null,
|
||||
};
|
||||
|
||||
const result = ChartEncodesSchema.safeParse(scatterAxis);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept valid ComboChartAxis', () => {
|
||||
const comboAxis = {
|
||||
x: ['month'],
|
||||
y: ['sales'],
|
||||
y2: ['profit'],
|
||||
category: [],
|
||||
tooltip: null,
|
||||
};
|
||||
|
||||
const result = ChartEncodesSchema.safeParse(comboAxis);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept valid PieChartAxis', () => {
|
||||
const pieAxis = {
|
||||
x: ['category'],
|
||||
y: ['value'],
|
||||
tooltip: null,
|
||||
};
|
||||
|
||||
const result = ChartEncodesSchema.safeParse(pieAxis);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject truly invalid axis configurations', () => {
|
||||
// The union is permissive, so we need truly invalid data to make it fail
|
||||
const invalidAxis = {
|
||||
x: 'not an array', // This should fail since x should be an array
|
||||
};
|
||||
|
||||
const result = ChartEncodesSchema.safeParse(invalidAxis);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested defaults behavior', () => {
|
||||
it('should deeply apply defaults to all axis schemas', () => {
|
||||
const schemas = [
|
||||
BarAndLineAxisSchema,
|
||||
ScatterAxisSchema,
|
||||
ComboChartAxisSchema,
|
||||
PieChartAxisSchema,
|
||||
];
|
||||
|
||||
for (const schema of schemas) {
|
||||
const result = schema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// All schemas should have x and y as empty arrays by default
|
||||
expect(result.data.x).toEqual([]);
|
||||
expect(result.data.y).toEqual([]);
|
||||
|
||||
// Tooltip should be null by default
|
||||
expect('tooltip' in result.data ? result.data.tooltip : null).toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve defaults when partially overriding nested objects', () => {
|
||||
const partialOverride = {
|
||||
x: ['custom_x'],
|
||||
// y, category, tooltip should get defaults
|
||||
};
|
||||
|
||||
const result = BarAndLineAxisSchema.safeParse(partialOverride);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.x).toEqual(['custom_x']); // Overridden
|
||||
expect(result.data.y).toEqual([]); // Default
|
||||
expect(result.data.category).toEqual([]); // Default
|
||||
expect(result.data.tooltip).toBeUndefined(); // Default
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,369 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
BarColumnSettingsSchema,
|
||||
ColumnSettingsSchema,
|
||||
DEFAULT_COLUMN_SETTINGS,
|
||||
DotColumnSettingsSchema,
|
||||
LineColumnSettingsSchema,
|
||||
} from './columnInterfaces';
|
||||
|
||||
describe('ColumnSettingsSchema', () => {
|
||||
it('should apply all default values when parsing empty object', () => {
|
||||
const result = ColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
showDataLabels: false,
|
||||
showDataLabelsAsPercentage: false,
|
||||
columnVisualization: 'bar',
|
||||
lineWidth: 2,
|
||||
lineStyle: 'line',
|
||||
lineType: 'normal',
|
||||
lineSymbolSize: 0,
|
||||
barRoundness: 8,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should match DEFAULT_COLUMN_SETTINGS', () => {
|
||||
const result = ColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual(DEFAULT_COLUMN_SETTINGS);
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply nested defaults for boolean flags', () => {
|
||||
const result = ColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.showDataLabels).toBe(false);
|
||||
expect(result.data.showDataLabelsAsPercentage).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply nested defaults for visualization settings', () => {
|
||||
const result = ColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.columnVisualization).toBe('bar');
|
||||
expect(result.data.lineWidth).toBe(2);
|
||||
expect(result.data.lineStyle).toBe('line');
|
||||
expect(result.data.lineType).toBe('normal');
|
||||
expect(result.data.lineSymbolSize).toBe(0);
|
||||
expect(result.data.barRoundness).toBe(8);
|
||||
}
|
||||
});
|
||||
|
||||
it('should override defaults when values are provided', () => {
|
||||
const customSettings = {
|
||||
showDataLabels: true,
|
||||
columnVisualization: 'line',
|
||||
lineWidth: 5,
|
||||
barRoundness: 15,
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(customSettings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.showDataLabels).toBe(true);
|
||||
expect(result.data.columnVisualization).toBe('line');
|
||||
expect(result.data.lineWidth).toBe(5);
|
||||
expect(result.data.barRoundness).toBe(15);
|
||||
// Defaults should be preserved for non-overridden fields
|
||||
expect(result.data.showDataLabelsAsPercentage).toBe(false);
|
||||
expect(result.data.lineStyle).toBe('line');
|
||||
expect(result.data.lineType).toBe('normal');
|
||||
expect(result.data.lineSymbolSize).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle partial overrides for line settings', () => {
|
||||
const lineSettings = {
|
||||
columnVisualization: 'line',
|
||||
lineWidth: 3,
|
||||
lineType: 'smooth',
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(lineSettings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.columnVisualization).toBe('line');
|
||||
expect(result.data.lineWidth).toBe(3);
|
||||
expect(result.data.lineType).toBe('smooth');
|
||||
// Other line defaults should be preserved
|
||||
expect(result.data.lineStyle).toBe('line');
|
||||
expect(result.data.lineSymbolSize).toBe(0);
|
||||
// Non-line defaults should also be preserved
|
||||
expect(result.data.barRoundness).toBe(8);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle partial overrides for bar settings', () => {
|
||||
const barSettings = {
|
||||
columnVisualization: 'bar',
|
||||
barRoundness: 20,
|
||||
showDataLabels: true,
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(barSettings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.columnVisualization).toBe('bar');
|
||||
expect(result.data.barRoundness).toBe(20);
|
||||
expect(result.data.showDataLabels).toBe(true);
|
||||
// Other defaults should be preserved
|
||||
expect(result.data.showDataLabelsAsPercentage).toBe(false);
|
||||
expect(result.data.lineWidth).toBe(2);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle dot visualization settings', () => {
|
||||
const dotSettings = {
|
||||
columnVisualization: 'dot',
|
||||
lineSymbolSize: 25,
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(dotSettings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.columnVisualization).toBe('dot');
|
||||
expect(result.data.lineSymbolSize).toBe(25);
|
||||
// All other defaults should be preserved
|
||||
expect(result.data.showDataLabels).toBe(false);
|
||||
expect(result.data.lineWidth).toBe(2);
|
||||
expect(result.data.barRoundness).toBe(8);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate line width constraints', () => {
|
||||
const invalidLineWidth = {
|
||||
lineWidth: 25, // exceeds max of 20
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(invalidLineWidth);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate bar roundness constraints', () => {
|
||||
const invalidBarRoundness = {
|
||||
barRoundness: 60, // exceeds max of 50
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(invalidBarRoundness);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate symbol size constraints', () => {
|
||||
const invalidSymbolSize = {
|
||||
lineSymbolSize: 60, // exceeds max of 50
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(invalidSymbolSize);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LineColumnSettingsSchema', () => {
|
||||
it('should apply default values for line-specific settings', () => {
|
||||
const result = LineColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
lineWidth: 2,
|
||||
lineStyle: 'line',
|
||||
lineType: 'normal',
|
||||
lineSymbolSize: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle line style variations', () => {
|
||||
const areaLine = {
|
||||
lineStyle: 'area',
|
||||
lineType: 'step',
|
||||
lineSymbolSize: 5,
|
||||
};
|
||||
|
||||
const result = LineColumnSettingsSchema.safeParse(areaLine);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.lineStyle).toBe('area');
|
||||
expect(result.data.lineType).toBe('step');
|
||||
expect(result.data.lineSymbolSize).toBe(5);
|
||||
expect(result.data.lineWidth).toBe(2); // Default preserved
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate line width range', () => {
|
||||
const validLineWidths = [1, 10, 20];
|
||||
const invalidLineWidths = [0, 21];
|
||||
|
||||
for (const width of validLineWidths) {
|
||||
const result = LineColumnSettingsSchema.safeParse({ lineWidth: width });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
|
||||
for (const width of invalidLineWidths) {
|
||||
const result = LineColumnSettingsSchema.safeParse({ lineWidth: width });
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('BarColumnSettingsSchema', () => {
|
||||
it('should apply default values for bar-specific settings', () => {
|
||||
const result = BarColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
barRoundness: 8,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different roundness values', () => {
|
||||
const roundedBar = {
|
||||
barRoundness: 25,
|
||||
};
|
||||
|
||||
const result = BarColumnSettingsSchema.safeParse(roundedBar);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.barRoundness).toBe(25);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate bar roundness range', () => {
|
||||
const validRoundness = [0, 25, 50];
|
||||
const invalidRoundness = [-1, 51];
|
||||
|
||||
for (const roundness of validRoundness) {
|
||||
const result = BarColumnSettingsSchema.safeParse({ barRoundness: roundness });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
|
||||
for (const roundness of invalidRoundness) {
|
||||
const result = BarColumnSettingsSchema.safeParse({ barRoundness: roundness });
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('DotColumnSettingsSchema', () => {
|
||||
it('should apply default values for dot-specific settings', () => {
|
||||
const result = DotColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data).toEqual({
|
||||
lineSymbolSize: 10,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different dot sizes', () => {
|
||||
const largeDot = {
|
||||
lineSymbolSize: 30,
|
||||
};
|
||||
|
||||
const result = DotColumnSettingsSchema.safeParse(largeDot);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.lineSymbolSize).toBe(30);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate dot symbol size range', () => {
|
||||
const validSizes = [1, 25, 50];
|
||||
const invalidSizes = [0, 51];
|
||||
|
||||
for (const size of validSizes) {
|
||||
const result = DotColumnSettingsSchema.safeParse({ lineSymbolSize: size });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
|
||||
for (const size of invalidSizes) {
|
||||
const result = DotColumnSettingsSchema.safeParse({ lineSymbolSize: size });
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('DEFAULT_COLUMN_SETTINGS', () => {
|
||||
it('should be a valid ColumnSettings object', () => {
|
||||
const result = ColumnSettingsSchema.safeParse(DEFAULT_COLUMN_SETTINGS);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain all expected default values', () => {
|
||||
expect(DEFAULT_COLUMN_SETTINGS).toEqual({
|
||||
showDataLabels: false,
|
||||
showDataLabelsAsPercentage: false,
|
||||
columnVisualization: 'bar',
|
||||
lineWidth: 2,
|
||||
lineStyle: 'line',
|
||||
lineType: 'normal',
|
||||
lineSymbolSize: 0,
|
||||
barRoundness: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct types for all properties', () => {
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.showDataLabels).toBe('boolean');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.showDataLabelsAsPercentage).toBe('boolean');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.columnVisualization).toBe('string');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.lineWidth).toBe('number');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.lineStyle).toBe('string');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.lineType).toBe('string');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.lineSymbolSize).toBe('number');
|
||||
expect(typeof DEFAULT_COLUMN_SETTINGS.barRoundness).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Nested defaults interaction', () => {
|
||||
it('should properly combine defaults from all visualization types', () => {
|
||||
const result = ColumnSettingsSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Should have defaults from line, bar, and dot settings
|
||||
expect(result.data.lineWidth).toBe(2); // From LineColumnSettings
|
||||
expect(result.data.barRoundness).toBe(8); // From BarColumnSettings
|
||||
expect(result.data.lineSymbolSize).toBe(0); // From main schema (different from DotColumnSettings default of 10)
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow overriding specific visualization settings while preserving others', () => {
|
||||
const mixedSettings = {
|
||||
columnVisualization: 'line',
|
||||
lineWidth: 4,
|
||||
barRoundness: 12, // This should still be applied even though columnVisualization is 'line'
|
||||
};
|
||||
|
||||
const result = ColumnSettingsSchema.safeParse(mixedSettings);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.columnVisualization).toBe('line');
|
||||
expect(result.data.lineWidth).toBe(4);
|
||||
expect(result.data.barRoundness).toBe(12);
|
||||
// Other defaults should be preserved
|
||||
expect(result.data.lineStyle).toBe('line');
|
||||
expect(result.data.showDataLabels).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,412 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { z } from 'zod/v4';
|
||||
import { getDefaults, getDefaultsPartial } from './defaultHelpers';
|
||||
|
||||
describe('getDefaults', () => {
|
||||
it('should extract defaults from simple schema', () => {
|
||||
const SimpleSchema = z.object({
|
||||
name: z.string().default('John'),
|
||||
age: z.number().default(25),
|
||||
active: z.boolean().default(true),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(SimpleSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
name: 'John',
|
||||
age: 25,
|
||||
active: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle nested object schemas with defaults', () => {
|
||||
const NestedSchema = z.object({
|
||||
user: z
|
||||
.object({
|
||||
name: z.string().default('Default User'),
|
||||
preferences: z
|
||||
.object({
|
||||
theme: z.string().default('light'),
|
||||
notifications: z.boolean().default(true),
|
||||
})
|
||||
.default({
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
}),
|
||||
})
|
||||
.default({
|
||||
name: 'Default User',
|
||||
preferences: {
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
},
|
||||
}),
|
||||
settings: z
|
||||
.object({
|
||||
autoSave: z.boolean().default(false),
|
||||
})
|
||||
.default({
|
||||
autoSave: false,
|
||||
}),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(NestedSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
user: {
|
||||
name: 'Default User',
|
||||
preferences: {
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
autoSave: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle array defaults', () => {
|
||||
const ArraySchema = z.object({
|
||||
tags: z.array(z.string()).default([]),
|
||||
numbers: z.array(z.number()).default([1, 2, 3]),
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string().default('Item'),
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(ArraySchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
tags: [],
|
||||
numbers: [1, 2, 3],
|
||||
items: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle optional fields without defaults', () => {
|
||||
const OptionalSchema = z.object({
|
||||
required: z.string().default('required'),
|
||||
optional: z.string().optional(),
|
||||
optionalWithDefault: z.string().optional().default('optional default'),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(OptionalSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
required: 'required',
|
||||
optionalWithDefault: 'optional default',
|
||||
});
|
||||
|
||||
// Optional fields without defaults should not be present
|
||||
expect('optional' in defaults).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle enum defaults', () => {
|
||||
const EnumSchema = z.object({
|
||||
status: z.enum(['pending', 'completed', 'failed']).default('pending'),
|
||||
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(EnumSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle union schemas with defaults', () => {
|
||||
const UnionSchema = z.object({
|
||||
value: z.union([z.string(), z.number()]).default('default string'),
|
||||
nullableUnion: z.union([z.string(), z.null()]).default(null),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(UnionSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
value: 'default string',
|
||||
nullableUnion: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle record schemas with defaults', () => {
|
||||
const RecordSchema = z.object({
|
||||
metadata: z.record(z.string(), z.string()).default({}),
|
||||
settings: z.record(z.string(), z.boolean()).default({ enabled: true }),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(RecordSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
metadata: {},
|
||||
settings: { enabled: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle complex nested schema with multiple default levels', () => {
|
||||
const ComplexSchema = z.object({
|
||||
config: z
|
||||
.object({
|
||||
display: z
|
||||
.object({
|
||||
theme: z.string().default('dark'),
|
||||
fontSize: z.number().default(14),
|
||||
})
|
||||
.default({
|
||||
theme: 'dark',
|
||||
fontSize: 14,
|
||||
}),
|
||||
features: z
|
||||
.object({
|
||||
notifications: z.boolean().default(true),
|
||||
autoSave: z.boolean().default(false),
|
||||
advanced: z
|
||||
.object({
|
||||
debugMode: z.boolean().default(false),
|
||||
logging: z.array(z.string()).default(['error', 'warn']),
|
||||
})
|
||||
.default({
|
||||
debugMode: false,
|
||||
logging: ['error', 'warn'],
|
||||
}),
|
||||
})
|
||||
.default({
|
||||
notifications: true,
|
||||
autoSave: false,
|
||||
advanced: {
|
||||
debugMode: false,
|
||||
logging: ['error', 'warn'],
|
||||
},
|
||||
}),
|
||||
})
|
||||
.default({
|
||||
display: {
|
||||
theme: 'dark',
|
||||
fontSize: 14,
|
||||
},
|
||||
features: {
|
||||
notifications: true,
|
||||
autoSave: false,
|
||||
advanced: {
|
||||
debugMode: false,
|
||||
logging: ['error', 'warn'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(ComplexSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
config: {
|
||||
display: {
|
||||
theme: 'dark',
|
||||
fontSize: 14,
|
||||
},
|
||||
features: {
|
||||
notifications: true,
|
||||
autoSave: false,
|
||||
advanced: {
|
||||
debugMode: false,
|
||||
logging: ['error', 'warn'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle schema with mixed required and optional fields', () => {
|
||||
const MixedSchema = z.object({
|
||||
id: z.string(), // required, no default
|
||||
name: z.string().default('Unnamed'),
|
||||
description: z.string().optional(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
metadata: z.record(z.string(), z.string()).optional(),
|
||||
settings: z
|
||||
.object({
|
||||
enabled: z.boolean().default(true),
|
||||
level: z.number().optional(),
|
||||
})
|
||||
.default({
|
||||
enabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
// This should return empty object since 'id' is required but has no default
|
||||
const defaults = getDefaults(MixedSchema);
|
||||
|
||||
// When there are required fields without defaults, getDefaults returns empty object
|
||||
// because it can't create a valid complete object
|
||||
expect(defaults).toEqual({});
|
||||
});
|
||||
|
||||
it('should validate the returned defaults against the original schema when possible', () => {
|
||||
const ValidatableSchema = z.object({
|
||||
name: z.string().default('Valid Name'),
|
||||
count: z.number().min(0).default(5),
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(ValidatableSchema);
|
||||
|
||||
// The defaults should be valid according to the original schema
|
||||
const validationResult = ValidatableSchema.safeParse(defaults);
|
||||
expect(validationResult.success).toBe(true);
|
||||
|
||||
if (validationResult.success) {
|
||||
expect(validationResult.data).toEqual(defaults);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultsPartial', () => {
|
||||
it('should return only fields with explicit defaults', () => {
|
||||
const MixedSchema = z.object({
|
||||
id: z.string(), // required, no default
|
||||
name: z.string().default('Default Name'),
|
||||
description: z.string().optional(), // optional, no default
|
||||
active: z.boolean().default(true),
|
||||
tags: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
const partialDefaults = getDefaultsPartial(MixedSchema);
|
||||
|
||||
// Since getDefaultsPartial() uses .partial() then parse({}), it returns empty object
|
||||
// when there are required fields without defaults
|
||||
expect(partialDefaults).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle nested schemas in partial mode', () => {
|
||||
const NestedSchema = z.object({
|
||||
required: z.string(), // no default
|
||||
config: z
|
||||
.object({
|
||||
theme: z.string().default('light'),
|
||||
optional: z.string().optional(),
|
||||
})
|
||||
.default({
|
||||
theme: 'light',
|
||||
}),
|
||||
optional: z.string().optional(),
|
||||
});
|
||||
|
||||
const partialDefaults = getDefaultsPartial(NestedSchema);
|
||||
|
||||
// Since there's a required field without default, returns empty object
|
||||
expect(partialDefaults).toEqual({});
|
||||
});
|
||||
|
||||
it('should return empty object when no defaults exist', () => {
|
||||
const NoDefaultsSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
optional: z.string().optional(),
|
||||
nullable: z.string().nullable(),
|
||||
});
|
||||
|
||||
const partialDefaults = getDefaultsPartial(NoDefaultsSchema);
|
||||
|
||||
expect(partialDefaults).toEqual({});
|
||||
expect(Object.keys(partialDefaults)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle complex defaults in partial mode', () => {
|
||||
const ComplexSchema = z.object({
|
||||
required: z.string(),
|
||||
settings: z
|
||||
.object({
|
||||
display: z
|
||||
.object({
|
||||
theme: z.string().default('auto'),
|
||||
})
|
||||
.default({
|
||||
theme: 'auto',
|
||||
}),
|
||||
})
|
||||
.default({
|
||||
display: {
|
||||
theme: 'auto',
|
||||
},
|
||||
}),
|
||||
metadata: z.record(z.string(), z.string()).default({}),
|
||||
optional: z.string().optional(),
|
||||
});
|
||||
|
||||
const partialDefaults = getDefaultsPartial(ComplexSchema);
|
||||
|
||||
// Since there's a required field without default, returns empty object
|
||||
expect(partialDefaults).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Helper function edge cases', () => {
|
||||
it('should handle empty schemas', () => {
|
||||
const EmptySchema = z.object({});
|
||||
|
||||
const defaults = getDefaults(EmptySchema);
|
||||
const partialDefaults = getDefaultsPartial(EmptySchema);
|
||||
|
||||
expect(defaults).toEqual({});
|
||||
expect(partialDefaults).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle schemas with only required fields', () => {
|
||||
const RequiredOnlySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(RequiredOnlySchema);
|
||||
const partialDefaults = getDefaultsPartial(RequiredOnlySchema);
|
||||
|
||||
expect(defaults).toEqual({});
|
||||
expect(partialDefaults).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle null and undefined defaults correctly', () => {
|
||||
const NullDefaultsSchema = z.object({
|
||||
nullable: z.string().nullable().default(null),
|
||||
optional: z.string().optional().default(undefined),
|
||||
emptyString: z.string().default(''),
|
||||
zero: z.number().default(0),
|
||||
false: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(NullDefaultsSchema);
|
||||
|
||||
expect(defaults).toEqual({
|
||||
nullable: null,
|
||||
optional: undefined,
|
||||
emptyString: '',
|
||||
zero: 0,
|
||||
false: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle transformed schemas', () => {
|
||||
const TransformedSchema = z.object({
|
||||
value: z
|
||||
.string()
|
||||
.default('raw')
|
||||
.transform((val) => val.toUpperCase()),
|
||||
count: z
|
||||
.number()
|
||||
.default(5)
|
||||
.transform((val) => val * 2),
|
||||
});
|
||||
|
||||
const defaults = getDefaults(TransformedSchema);
|
||||
|
||||
// Defaults should be applied before transformation in this test
|
||||
// Note: The actual behavior might depend on how Zod handles defaults with transforms
|
||||
expect(typeof defaults.value).toBe('string');
|
||||
expect(typeof defaults.count).toBe('number');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,368 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { DEFAULT_CHART_CONFIG } from './charts/chartConfigProps';
|
||||
import { MetricSchema } from './metric.types';
|
||||
|
||||
describe('MetricSchema', () => {
|
||||
it('should parse a valid metric with all required fields', () => {
|
||||
const validMetric = {
|
||||
id: 'metric-123',
|
||||
type: 'metric',
|
||||
name: 'Revenue Analysis',
|
||||
version_number: 1,
|
||||
description: 'Monthly revenue breakdown',
|
||||
file_name: 'revenue_analysis.yaml',
|
||||
time_frame: 'monthly',
|
||||
dataset_id: 'dataset-456',
|
||||
data_source_id: 'source-789',
|
||||
dataset_name: 'Sales Data',
|
||||
error: null,
|
||||
data_metadata: {
|
||||
row_count: 1000,
|
||||
column_count: 5,
|
||||
column_metadata: [
|
||||
{
|
||||
name: 'revenue',
|
||||
min_value: 0,
|
||||
max_value: 10000,
|
||||
unique_values: 100,
|
||||
simple_type: 'number',
|
||||
type: 'float',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'verified',
|
||||
evaluation_score: 'High',
|
||||
evaluation_summary: 'Excellent metric quality',
|
||||
file: 'metric:\n name: Revenue\n sql: SELECT * FROM revenue',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
sent_by_id: 'user-123',
|
||||
sent_by_name: 'John Doe',
|
||||
sent_by_avatar_url: 'https://example.com/avatar.jpg',
|
||||
sql: 'SELECT SUM(revenue) FROM sales',
|
||||
dashboards: [],
|
||||
collections: [],
|
||||
versions: [],
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
};
|
||||
|
||||
const result = MetricSchema.safeParse(validMetric);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Validation errors:', JSON.stringify(result.error.format(), null, 2));
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.id).toBe('metric-123');
|
||||
expect(result.data.type).toBe('metric');
|
||||
expect(result.data.name).toBe('Revenue Analysis');
|
||||
}
|
||||
});
|
||||
|
||||
it('should apply default chart_config when not provided', () => {
|
||||
const metricWithoutChartConfig = {
|
||||
id: 'metric-123',
|
||||
type: 'metric',
|
||||
name: 'Test Metric',
|
||||
version_number: 1,
|
||||
description: null,
|
||||
file_name: 'test.yaml',
|
||||
time_frame: 'daily',
|
||||
dataset_id: 'dataset-1',
|
||||
data_source_id: 'source-1',
|
||||
dataset_name: 'Test Data',
|
||||
error: null,
|
||||
data_metadata: {
|
||||
row_count: 100,
|
||||
column_count: 3,
|
||||
column_metadata: [
|
||||
{
|
||||
name: 'test_column',
|
||||
min_value: 0,
|
||||
max_value: 100,
|
||||
unique_values: 50,
|
||||
simple_type: 'number',
|
||||
type: 'integer',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'notRequested',
|
||||
evaluation_score: 'Moderate',
|
||||
evaluation_summary: 'Good metric',
|
||||
file: 'metric content',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
sent_by_id: 'user-1',
|
||||
sent_by_name: 'User One',
|
||||
sent_by_avatar_url: null,
|
||||
sql: null,
|
||||
dashboards: [],
|
||||
collections: [],
|
||||
versions: [],
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'canView',
|
||||
// chart_config is omitted, should get default
|
||||
};
|
||||
|
||||
const result = MetricSchema.safeParse(metricWithoutChartConfig);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Test 2 - Validation errors:', JSON.stringify(result.error.format(), null, 2));
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Should have the default chart config
|
||||
expect(result.data.chart_config).toEqual(DEFAULT_CHART_CONFIG);
|
||||
expect(result.data.chart_config.selectedChartType).toBe('table');
|
||||
expect(result.data.chart_config.colors).toEqual(DEFAULT_CHART_CONFIG.colors);
|
||||
expect(result.data.chart_config.gridLines).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve custom chart_config when provided', () => {
|
||||
const customChartConfig = {
|
||||
selectedChartType: 'bar',
|
||||
colors: ['#FF0000', '#00FF00'],
|
||||
gridLines: false,
|
||||
columnSettings: {
|
||||
revenue: {
|
||||
showDataLabels: true,
|
||||
columnVisualization: 'line',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const metricWithCustomConfig = {
|
||||
id: 'metric-123',
|
||||
type: 'metric',
|
||||
name: 'Custom Chart Metric',
|
||||
version_number: 1,
|
||||
description: null,
|
||||
file_name: 'custom.yaml',
|
||||
time_frame: 'weekly',
|
||||
dataset_id: 'dataset-1',
|
||||
data_source_id: 'source-1',
|
||||
dataset_name: 'Custom Data',
|
||||
error: null,
|
||||
chart_config: customChartConfig,
|
||||
data_metadata: {
|
||||
row_count: 500,
|
||||
column_count: 4,
|
||||
column_metadata: [
|
||||
{
|
||||
name: 'custom_field',
|
||||
min_value: 0,
|
||||
max_value: 500,
|
||||
unique_values: 250,
|
||||
simple_type: 'number',
|
||||
type: 'float',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'verified',
|
||||
evaluation_score: 'High',
|
||||
evaluation_summary: 'Great metric',
|
||||
file: 'custom metric content',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
sent_by_id: 'user-1',
|
||||
sent_by_name: 'User One',
|
||||
sent_by_avatar_url: null,
|
||||
sql: 'SELECT * FROM custom_table',
|
||||
dashboards: [],
|
||||
collections: [],
|
||||
versions: [],
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'canEdit',
|
||||
};
|
||||
|
||||
const result = MetricSchema.safeParse(metricWithCustomConfig);
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Test 3 - Validation errors:', JSON.stringify(result.error.format(), null, 2));
|
||||
}
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Should preserve custom config but apply defaults for missing fields
|
||||
expect(result.data.chart_config.selectedChartType).toBe('bar');
|
||||
expect(result.data.chart_config.colors).toEqual(['#FF0000', '#00FF00']);
|
||||
expect(result.data.chart_config.gridLines).toBe(false);
|
||||
expect(result.data.chart_config.columnSettings.revenue?.showDataLabels).toBe(true);
|
||||
expect(result.data.chart_config.columnSettings.revenue?.columnVisualization).toBe('line');
|
||||
|
||||
// Should apply defaults for missing chart config fields
|
||||
expect(result.data.chart_config.showLegend).toBeNull();
|
||||
expect(result.data.chart_config.disableTooltip).toBe(false);
|
||||
expect(result.data.chart_config.goalLines).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle partial chart_config with deeply nested defaults', () => {
|
||||
const partialChartConfig = {
|
||||
selectedChartType: 'line',
|
||||
columnSettings: {
|
||||
sales: {
|
||||
showDataLabels: true,
|
||||
// Other column settings should get defaults
|
||||
},
|
||||
},
|
||||
// Other chart config fields should get defaults
|
||||
};
|
||||
|
||||
const metricWithPartialConfig = {
|
||||
id: 'metric-456',
|
||||
type: 'metric',
|
||||
name: 'Partial Config Metric',
|
||||
version_number: 2,
|
||||
description: 'Testing partial config',
|
||||
file_name: 'partial.yaml',
|
||||
time_frame: 'quarterly',
|
||||
dataset_id: 'dataset-2',
|
||||
data_source_id: 'source-2',
|
||||
dataset_name: 'Partial Data',
|
||||
error: null,
|
||||
chart_config: partialChartConfig,
|
||||
data_metadata: {
|
||||
row_count: 750,
|
||||
column_count: 6,
|
||||
column_metadata: [
|
||||
{
|
||||
name: 'sales',
|
||||
min_value: 100,
|
||||
max_value: 5000,
|
||||
unique_values: 300,
|
||||
simple_type: 'number',
|
||||
type: 'decimal',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'notVerified',
|
||||
evaluation_score: 'Low',
|
||||
evaluation_summary: 'Needs improvement',
|
||||
file: 'partial config content',
|
||||
created_at: '2024-01-15T00:00:00Z',
|
||||
updated_at: '2024-01-15T00:00:00Z',
|
||||
sent_by_id: 'user-2',
|
||||
sent_by_name: 'User Two',
|
||||
sent_by_avatar_url: 'https://example.com/user2.jpg',
|
||||
sql: 'SELECT AVG(sales) FROM transactions',
|
||||
dashboards: [{ id: 'dash-1', name: 'Sales Dashboard' }],
|
||||
collections: [{ id: 'coll-1', name: 'Sales Collection' }],
|
||||
versions: [
|
||||
{ version_number: 1, updated_at: '2024-01-01T00:00:00Z' },
|
||||
{ version_number: 2, updated_at: '2024-01-15T00:00:00Z' },
|
||||
],
|
||||
individual_permissions: [],
|
||||
public_expiry_date: '2024-12-31T00:00:00Z',
|
||||
public_enabled_by: 'admin-1',
|
||||
publicly_accessible: true,
|
||||
public_password: 'secret123',
|
||||
permission: 'canEdit',
|
||||
};
|
||||
|
||||
const result = MetricSchema.safeParse(metricWithPartialConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
// Should preserve provided values
|
||||
expect(result.data.chart_config.selectedChartType).toBe('line');
|
||||
expect(result.data.chart_config.columnSettings.sales?.showDataLabels).toBe(true);
|
||||
|
||||
// Should apply defaults to column settings for provided column
|
||||
expect(result.data.chart_config.columnSettings.sales?.showDataLabelsAsPercentage).toBe(false);
|
||||
expect(result.data.chart_config.columnSettings.sales?.columnVisualization).toBe('bar');
|
||||
expect(result.data.chart_config.columnSettings.sales?.lineWidth).toBe(2);
|
||||
|
||||
// Should apply defaults to top-level chart config
|
||||
expect(result.data.chart_config.colors).toEqual(DEFAULT_CHART_CONFIG.colors);
|
||||
expect(result.data.chart_config.gridLines).toBe(true);
|
||||
expect(result.data.chart_config.showLegend).toBeNull();
|
||||
expect(result.data.chart_config.columnLabelFormats).toEqual({});
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate required enum values', () => {
|
||||
const metricWithInvalidEnum = {
|
||||
id: 'metric-789',
|
||||
type: 'invalid_type', // Should be 'metric'
|
||||
name: 'Invalid Metric',
|
||||
// ... other required fields
|
||||
};
|
||||
|
||||
const result = MetricSchema.safeParse(metricWithInvalidEnum);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle nullable fields correctly', () => {
|
||||
const metricWithNulls = {
|
||||
id: 'metric-null',
|
||||
type: 'metric',
|
||||
name: 'Null Fields Metric',
|
||||
version_number: 1,
|
||||
description: null, // nullable
|
||||
file_name: 'null_test.yaml',
|
||||
time_frame: 'yearly',
|
||||
dataset_id: 'dataset-null',
|
||||
data_source_id: 'source-null',
|
||||
dataset_name: null, // nullable
|
||||
error: null, // nullable
|
||||
data_metadata: {
|
||||
row_count: 0,
|
||||
column_count: 0,
|
||||
column_metadata: [],
|
||||
},
|
||||
status: 'notRequested',
|
||||
evaluation_score: 'Moderate',
|
||||
evaluation_summary: 'Test with nulls',
|
||||
file: 'null content',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
sent_by_id: 'user-null',
|
||||
sent_by_name: 'Null User',
|
||||
sent_by_avatar_url: null, // nullable
|
||||
sql: null, // nullable
|
||||
dashboards: [],
|
||||
collections: [],
|
||||
versions: [],
|
||||
individual_permissions: null, // nullable
|
||||
public_expiry_date: null, // nullable
|
||||
public_enabled_by: null, // nullable
|
||||
publicly_accessible: false,
|
||||
public_password: null, // nullable
|
||||
permission: 'canView',
|
||||
};
|
||||
|
||||
const result = MetricSchema.safeParse(metricWithNulls);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.description).toBeNull();
|
||||
expect(result.data.dataset_name).toBeNull();
|
||||
expect(result.data.error).toBeNull();
|
||||
expect(result.data.sent_by_avatar_url).toBeNull();
|
||||
expect(result.data.sql).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Note: DEFAULT_METRIC tests removed because getDefaults() cannot work
|
||||
// with schemas that have required fields without defaults
|
|
@ -0,0 +1,413 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ShareAssetTypeSchema,
|
||||
ShareConfigSchema,
|
||||
ShareIndividualSchema,
|
||||
ShareRoleSchema,
|
||||
} from './shareInterfaces';
|
||||
|
||||
describe('ShareRoleSchema', () => {
|
||||
it('should accept valid role values', () => {
|
||||
const validRoles = ['owner', 'fullAccess', 'canEdit', 'canFilter', 'canView'];
|
||||
|
||||
for (const role of validRoles) {
|
||||
const result = ShareRoleSchema.safeParse(role);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).toBe(role);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid role values', () => {
|
||||
const invalidRoles = ['admin', 'user', 'guest', '', 'OWNER'];
|
||||
|
||||
for (const role of invalidRoles) {
|
||||
const result = ShareRoleSchema.safeParse(role);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should be case sensitive', () => {
|
||||
const caseVariations = ['Owner', 'OWNER', 'fullaccess', 'CANVIEW'];
|
||||
|
||||
for (const role of caseVariations) {
|
||||
const result = ShareRoleSchema.safeParse(role);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShareAssetTypeSchema', () => {
|
||||
it('should accept valid asset type values', () => {
|
||||
const validTypes = ['metric', 'dashboard', 'collection', 'chat'];
|
||||
|
||||
for (const type of validTypes) {
|
||||
const result = ShareAssetTypeSchema.safeParse(type);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).toBe(type);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid asset type values', () => {
|
||||
const invalidTypes = ['report', 'query', 'table', '', 'METRIC'];
|
||||
|
||||
for (const type of invalidTypes) {
|
||||
const result = ShareAssetTypeSchema.safeParse(type);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should be case sensitive', () => {
|
||||
const caseVariations = ['Metric', 'DASHBOARD', 'Collection', 'CHAT'];
|
||||
|
||||
for (const type of caseVariations) {
|
||||
const result = ShareAssetTypeSchema.safeParse(type);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShareIndividualSchema', () => {
|
||||
it('should parse valid individual sharing configuration', () => {
|
||||
const validIndividual = {
|
||||
email: 'user@example.com',
|
||||
role: 'canEdit',
|
||||
name: 'John Doe',
|
||||
};
|
||||
|
||||
const result = ShareIndividualSchema.safeParse(validIndividual);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.email).toBe('user@example.com');
|
||||
expect(result.data.role).toBe('canEdit');
|
||||
expect(result.data.name).toBe('John Doe');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle optional name field', () => {
|
||||
const individualWithoutName = {
|
||||
email: 'test@example.com',
|
||||
role: 'canView',
|
||||
};
|
||||
|
||||
const result = ShareIndividualSchema.safeParse(individualWithoutName);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.email).toBe('test@example.com');
|
||||
expect(result.data.role).toBe('canView');
|
||||
expect(result.data.name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate email format', () => {
|
||||
const invalidEmails = ['invalid-email', 'test@', '@example.com', 'test.example.com', ''];
|
||||
|
||||
for (const email of invalidEmails) {
|
||||
const individual = {
|
||||
email,
|
||||
role: 'canView',
|
||||
};
|
||||
|
||||
const result = ShareIndividualSchema.safeParse(individual);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate role field', () => {
|
||||
const individual = {
|
||||
email: 'valid@example.com',
|
||||
role: 'invalidRole',
|
||||
};
|
||||
|
||||
const result = ShareIndividualSchema.safeParse(individual);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should require email and role fields', () => {
|
||||
const missingEmail = {
|
||||
role: 'canEdit',
|
||||
name: 'John Doe',
|
||||
};
|
||||
|
||||
const missingRole = {
|
||||
email: 'user@example.com',
|
||||
name: 'John Doe',
|
||||
};
|
||||
|
||||
const emailResult = ShareIndividualSchema.safeParse(missingEmail);
|
||||
expect(emailResult.success).toBe(false);
|
||||
|
||||
const roleResult = ShareIndividualSchema.safeParse(missingRole);
|
||||
expect(roleResult.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle all valid role combinations', () => {
|
||||
const validRoles = ['owner', 'fullAccess', 'canEdit', 'canFilter', 'canView'];
|
||||
|
||||
for (const role of validRoles) {
|
||||
const individual = {
|
||||
email: 'test@example.com',
|
||||
role,
|
||||
name: 'Test User',
|
||||
};
|
||||
|
||||
const result = ShareIndividualSchema.safeParse(individual);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.role).toBe(role);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShareConfigSchema', () => {
|
||||
it('should parse valid complete share configuration', () => {
|
||||
const validConfig = {
|
||||
individual_permissions: [
|
||||
{
|
||||
email: 'user1@example.com',
|
||||
role: 'canEdit',
|
||||
name: 'User One',
|
||||
},
|
||||
{
|
||||
email: 'user2@example.com',
|
||||
role: 'canView',
|
||||
},
|
||||
],
|
||||
public_expiry_date: '2024-12-31T23:59:59Z',
|
||||
public_enabled_by: 'admin@example.com',
|
||||
publicly_accessible: true,
|
||||
public_password: 'secretPassword123',
|
||||
permission: 'owner',
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(validConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.individual_permissions).toHaveLength(2);
|
||||
expect(result.data.individual_permissions?.[0].email).toBe('user1@example.com');
|
||||
expect(result.data.individual_permissions?.[0].role).toBe('canEdit');
|
||||
expect(result.data.publicly_accessible).toBe(true);
|
||||
expect(result.data.permission).toBe('owner');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle null individual_permissions', () => {
|
||||
const configWithNullPermissions = {
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'canView',
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(configWithNullPermissions);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.individual_permissions).toBeNull();
|
||||
expect(result.data.public_expiry_date).toBeNull();
|
||||
expect(result.data.public_enabled_by).toBeNull();
|
||||
expect(result.data.publicly_accessible).toBe(false);
|
||||
expect(result.data.public_password).toBeNull();
|
||||
expect(result.data.permission).toBe('canView');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty individual_permissions array', () => {
|
||||
const configWithEmptyPermissions = {
|
||||
individual_permissions: [],
|
||||
public_expiry_date: '2025-01-01T00:00:00Z',
|
||||
public_enabled_by: 'system',
|
||||
publicly_accessible: true,
|
||||
public_password: null,
|
||||
permission: 'fullAccess',
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(configWithEmptyPermissions);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.individual_permissions).toEqual([]);
|
||||
expect(result.data.publicly_accessible).toBe(true);
|
||||
expect(result.data.permission).toBe('fullAccess');
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate all permission field values', () => {
|
||||
const validPermissions = ['owner', 'fullAccess', 'canEdit', 'canFilter', 'canView'];
|
||||
|
||||
for (const permission of validPermissions) {
|
||||
const config = {
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission,
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(config);
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.permission).toBe(permission);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid permission values', () => {
|
||||
const invalidPermissions = ['admin', 'user', 'guest', 'OWNER'];
|
||||
|
||||
for (const permission of invalidPermissions) {
|
||||
const config = {
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission,
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(config);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should require all fields', () => {
|
||||
const incompleteConfigs = [
|
||||
{
|
||||
// missing individual_permissions
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
},
|
||||
{
|
||||
individual_permissions: null,
|
||||
// missing public_expiry_date
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
},
|
||||
{
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
// missing permission
|
||||
},
|
||||
];
|
||||
|
||||
for (const config of incompleteConfigs) {
|
||||
const result = ShareConfigSchema.safeParse(config);
|
||||
expect(result.success).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate individual permissions nested structure', () => {
|
||||
const configWithInvalidIndividual = {
|
||||
individual_permissions: [
|
||||
{
|
||||
email: 'invalid-email', // Invalid email format
|
||||
role: 'canEdit',
|
||||
},
|
||||
],
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(configWithInvalidIndividual);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle complex individual permissions scenarios', () => {
|
||||
const complexConfig = {
|
||||
individual_permissions: [
|
||||
{
|
||||
email: 'owner@company.com',
|
||||
role: 'owner',
|
||||
name: 'Company Owner',
|
||||
},
|
||||
{
|
||||
email: 'editor@company.com',
|
||||
role: 'canEdit',
|
||||
name: 'Editor User',
|
||||
},
|
||||
{
|
||||
email: 'viewer@external.com',
|
||||
role: 'canView',
|
||||
// name is optional
|
||||
},
|
||||
{
|
||||
email: 'filter@company.com',
|
||||
role: 'canFilter',
|
||||
name: 'Filter User',
|
||||
},
|
||||
],
|
||||
public_expiry_date: '2024-06-30T23:59:59Z',
|
||||
public_enabled_by: 'admin@company.com',
|
||||
publicly_accessible: true,
|
||||
public_password: 'complex_password_123!',
|
||||
permission: 'fullAccess',
|
||||
};
|
||||
|
||||
const result = ShareConfigSchema.safeParse(complexConfig);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
if (result.success) {
|
||||
expect(result.data.individual_permissions).toHaveLength(4);
|
||||
expect(result.data.individual_permissions?.[0].role).toBe('owner');
|
||||
expect(result.data.individual_permissions?.[1].role).toBe('canEdit');
|
||||
expect(result.data.individual_permissions?.[2].role).toBe('canView');
|
||||
expect(result.data.individual_permissions?.[3].role).toBe('canFilter');
|
||||
expect(result.data.individual_permissions?.[2].name).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle boolean publicly_accessible correctly', () => {
|
||||
const publicConfig = {
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: true,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
};
|
||||
|
||||
const privateConfig = {
|
||||
individual_permissions: null,
|
||||
public_expiry_date: null,
|
||||
public_enabled_by: null,
|
||||
publicly_accessible: false,
|
||||
public_password: null,
|
||||
permission: 'owner',
|
||||
};
|
||||
|
||||
const publicResult = ShareConfigSchema.safeParse(publicConfig);
|
||||
const privateResult = ShareConfigSchema.safeParse(privateConfig);
|
||||
|
||||
expect(publicResult.success).toBe(true);
|
||||
expect(privateResult.success).toBe(true);
|
||||
|
||||
if (publicResult.success) {
|
||||
expect(publicResult.data.publicly_accessible).toBe(true);
|
||||
}
|
||||
|
||||
if (privateResult.success) {
|
||||
expect(privateResult.data.publicly_accessible).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue