Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
365 changes: 365 additions & 0 deletions packages/root-cms/core/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
import {RootConfig} from '@blinkk/root';
import {describe, it, expect, beforeEach, vi} from 'vitest';
import * as schema from './schema.js';

// Mock Firebase Admin.
vi.mock('firebase-admin/app', () => ({
getApp: vi.fn(),
initializeApp: vi.fn(),
applicationDefault: vi.fn(),
}));

vi.mock('firebase-admin/firestore', () => ({
getFirestore: vi.fn(() => ({
doc: vi.fn(() => ({
get: vi.fn(() => ({exists: false, data: () => ({})})),
set: vi.fn(),
})),
})),
Timestamp: {
now: vi.fn(() => ({toMillis: () => 1234567890})),
},
FieldValue: {},
}));

// Mock project module.
vi.mock('./project.js', () => ({
getCollectionSchema: vi.fn(),
}));

describe('RootCMSClient Validation', () => {
let mockRootConfig: RootConfig;
let mockGetCollectionSchema: any;

beforeEach(async () => {
// Reset mocks.
vi.clearAllMocks();

// Setup mock root config.
mockRootConfig = {
rootDir: '/test',
plugins: [
{
name: 'root-cms',
getConfig: () => ({
id: 'test-project',
firebaseConfig: {
apiKey: 'test',
authDomain: 'test',
projectId: 'test-project',
storageBucket: 'test',
},
}),
getFirebaseApp: vi.fn(),
getFirestore: vi.fn(() => ({
doc: vi.fn(() => ({
get: vi.fn(() => ({exists: false, data: () => ({})})),
set: vi.fn(),
})),
})),
} as any,
],
} as any;

// Import after mocks are set up.
const projectModule = await import('./project.js');
mockGetCollectionSchema = projectModule.getCollectionSchema as any;
});

describe('getCollection', () => {
it('returns collection schema when it exists', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const testSchema = schema.define({
name: 'TestCollection',
fields: [schema.string({id: 'title'})],
});

mockGetCollectionSchema.mockResolvedValue(testSchema);

const result = await client.getCollection('TestCollection');

expect(mockGetCollectionSchema).toHaveBeenCalledWith('TestCollection');
expect(result).toEqual(testSchema);
});

it('returns null when collection does not exist', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

mockGetCollectionSchema.mockResolvedValue(null);

const result = await client.getCollection('NonExistent');

expect(result).toBeNull();
});
});

describe('saveDraftData with validation', () => {
it('validates and saves successfully with valid data', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const testSchema = schema.define({
name: 'Pages',
fields: [schema.string({id: 'title'}), schema.number({id: 'count'})],
});

mockGetCollectionSchema.mockResolvedValue(testSchema);

const validData = {
title: 'Test Page',
count: 42,
};

// Should not throw.
await expect(
client.saveDraftData('Pages/test', validData, {
validate: true,
})
).resolves.not.toThrow();
});

it('throws error with validation details when data is invalid', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const testSchema = schema.define({
name: 'Pages',
fields: [schema.string({id: 'title'}), schema.number({id: 'count'})],
});

mockGetCollectionSchema.mockResolvedValue(testSchema);

const invalidData = {
title: 123, // Should be string.
count: 'invalid', // Should be number.
};

await expect(
client.saveDraftData('Pages/test', invalidData, {
validate: true,
})
).rejects.toThrow(/Validation failed for Pages\/test/);
});

it('throws error when collection schema not found', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

mockGetCollectionSchema.mockResolvedValue(null);

await expect(
client.saveDraftData(
'Pages/test',
{title: 'Test'},
{
validate: true,
}
)
).rejects.toThrow(/Collection schema not found for: Pages/);
});

it('skips validation when validate option is false', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const invalidData = {
title: 123, // Invalid but validation is off.
};

// Should not call getCollectionSchema.
await client.saveDraftData('Pages/test', invalidData, {
validate: false,
});

expect(mockGetCollectionSchema).not.toHaveBeenCalled();
});

it('skips validation when validate option is not provided (backward compatibility)', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const invalidData = {
title: 123, // Invalid but validation is off by default.
};

// Should not call getCollectionSchema.
await client.saveDraftData('Pages/test', invalidData);

expect(mockGetCollectionSchema).not.toHaveBeenCalled();
});
});

describe('updateDraftData', () => {
it('updates a simple field path', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

// Mock getRawDoc to return existing data.
const mockGetRawDoc = vi.fn().mockResolvedValue({
sys: {},
fields: {
title: 'Old Title',
count: 10,
},
});
client.getRawDoc = mockGetRawDoc as any;

const mockSetRawDoc = vi.fn();
client.setRawDoc = mockSetRawDoc as any;

await client.updateDraftData('Pages/test', 'title', 'New Title');

// Verify the document was saved with updated title.
expect(mockSetRawDoc).toHaveBeenCalled();
const savedData = mockSetRawDoc.mock.calls[0][2];
expect(savedData.fields.title).toBe('New Title');
expect(savedData.fields.count).toBe(10); // Unchanged.
});

it('updates a nested field path', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const mockGetRawDoc = vi.fn().mockResolvedValue({
sys: {},
fields: {
hero: {
title: 'Old Hero Title',
subtitle: 'Subtitle',
},
},
});
client.getRawDoc = mockGetRawDoc as any;

const mockSetRawDoc = vi.fn();
client.setRawDoc = mockSetRawDoc as any;

await client.updateDraftData(
'Pages/test',
'hero.title',
'New Hero Title'
);

const savedData = mockSetRawDoc.mock.calls[0][2];
expect(savedData.fields.hero.title).toBe('New Hero Title');
expect(savedData.fields.hero.subtitle).toBe('Subtitle'); // Unchanged.
});

it('updates an array item by index', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const mockGetRawDoc = vi.fn().mockResolvedValue({
sys: {},
fields: {
content: {
_array: ['item1', 'item2'],
item1: {text: 'First item'},
item2: {text: 'Second item'},
},
},
});
client.getRawDoc = mockGetRawDoc as any;

const mockSaveDraftData = vi.fn();
client.saveDraftData = mockSaveDraftData as any;

await client.updateDraftData(
'Pages/test',
'content.0.text',
'Updated first item'
);

// Verify saveDraftData was called with the updated data.
expect(mockSaveDraftData).toHaveBeenCalled();
const savedData = mockSaveDraftData.mock.calls[0][1];
expect(savedData.content[0].text).toBe('Updated first item');
expect(savedData.content[1].text).toBe('Second item');
});

it('validates after update when validate: true', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const testSchema = schema.define({
name: 'Pages',
fields: [schema.string({id: 'title'}), schema.number({id: 'count'})],
});

mockGetCollectionSchema.mockResolvedValue(testSchema);

const mockGetRawDoc = vi.fn().mockResolvedValue({
sys: {},
fields: {
title: 'Old Title',
count: 10,
},
});
client.getRawDoc = mockGetRawDoc as any;

const mockSetRawDoc = vi.fn();
client.setRawDoc = mockSetRawDoc as any;

// Valid update.
await client.updateDraftData('Pages/test', 'title', 'New Title', {
validate: true,
});

expect(mockSetRawDoc).toHaveBeenCalled();
});

it('throws validation error when update results in invalid document', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const testSchema = schema.define({
name: 'Pages',
fields: [schema.string({id: 'title'}), schema.number({id: 'count'})],
});

mockGetCollectionSchema.mockResolvedValue(testSchema);

const mockGetRawDoc = vi.fn().mockResolvedValue({
sys: {},
fields: {
title: 'Old Title',
count: 10,
},
});
client.getRawDoc = mockGetRawDoc as any;

// Try to set count to invalid value.
await expect(
client.updateDraftData('Pages/test', 'count', 'invalid', {
validate: true,
})
).rejects.toThrow(/Validation failed for Pages\/test/);
});

it('skips validation when validate: false', async () => {
const {RootCMSClient} = await import('./client.js');
const client = new RootCMSClient(mockRootConfig);

const mockGetRawDoc = vi.fn().mockResolvedValue({
sys: {},
fields: {
title: 'Old Title',
},
});
client.getRawDoc = mockGetRawDoc as any;

const mockSetRawDoc = vi.fn();
client.setRawDoc = mockSetRawDoc as any;

await client.updateDraftData('Pages/test', 'title', 123, {
validate: false,
});

expect(mockGetCollectionSchema).not.toHaveBeenCalled();
expect(mockSetRawDoc).toHaveBeenCalled();
});
});
});
Loading