chore: added missing tests
This commit is contained in:
55
tests/cli/platform.test.ts
Normal file
55
tests/cli/platform.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { platformConfigPath } from '../../src/cli/platform';
|
||||
|
||||
const APP = 'Blogging Desktop Server';
|
||||
|
||||
describe('platformConfigPath', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
it('returns ~/Library/Application Support/<app> on macOS', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
const home = os.homedir();
|
||||
expect(platformConfigPath()).toBe(path.join(home, 'Library', 'Application Support', APP));
|
||||
});
|
||||
|
||||
it('returns %APPDATA%/<app> on Windows when APPDATA is set', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
process.env['APPDATA'] = '/mock/AppData/Roaming';
|
||||
expect(platformConfigPath()).toBe(path.join('/mock/AppData/Roaming', APP));
|
||||
});
|
||||
|
||||
it('falls back to ~/AppData/Roaming/<app> on Windows when APPDATA is unset', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
delete process.env['APPDATA'];
|
||||
const home = os.homedir();
|
||||
expect(platformConfigPath()).toBe(path.join(home, 'AppData', 'Roaming', APP));
|
||||
});
|
||||
|
||||
it('returns ~/.config/<app> on Linux', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
delete process.env['XDG_CONFIG_HOME'];
|
||||
const home = os.homedir();
|
||||
expect(platformConfigPath()).toBe(path.join(home, '.config', APP));
|
||||
});
|
||||
|
||||
it('honours XDG_CONFIG_HOME on Linux', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
process.env['XDG_CONFIG_HOME'] = '/custom/config';
|
||||
expect(platformConfigPath()).toBe(path.join('/custom/config', APP));
|
||||
});
|
||||
|
||||
it('treats unknown platforms the same as Linux', () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'freebsd' });
|
||||
delete process.env['XDG_CONFIG_HOME'];
|
||||
const home = os.homedir();
|
||||
expect(platformConfigPath()).toBe(path.join(home, '.config', APP));
|
||||
});
|
||||
});
|
||||
71
tests/engine/CliNotifier.test.ts
Normal file
71
tests/engine/CliNotifier.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { NoopNotifier, DbNotifier, type NotifyEntity, type NotifyAction } from '../../src/main/engine/CliNotifier';
|
||||
import { dbNotifications } from '../../src/main/database/schema';
|
||||
|
||||
describe('NoopNotifier', () => {
|
||||
it('resolves without side effects', async () => {
|
||||
const notifier = new NoopNotifier();
|
||||
await expect(notifier.notify('post', 'p-1', 'created')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('accepts every valid entity and action combination', async () => {
|
||||
const notifier = new NoopNotifier();
|
||||
const entities: NotifyEntity[] = ['post', 'media', 'script', 'template'];
|
||||
const actions: NotifyAction[] = ['created', 'updated', 'deleted'];
|
||||
|
||||
for (const entity of entities) {
|
||||
for (const action of actions) {
|
||||
await expect(notifier.notify(entity, 'id-1', action)).resolves.toBeUndefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('DbNotifier', () => {
|
||||
let valuesFn: ReturnType<typeof vi.fn>;
|
||||
let insertFn: ReturnType<typeof vi.fn>;
|
||||
let mockDb: { insert: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
valuesFn = vi.fn().mockResolvedValue(undefined);
|
||||
insertFn = vi.fn().mockReturnValue({ values: valuesFn });
|
||||
mockDb = { insert: insertFn };
|
||||
});
|
||||
|
||||
it('inserts a notification row into db_notifications', async () => {
|
||||
const notifier = new DbNotifier(mockDb);
|
||||
const beforeMs = Date.now();
|
||||
|
||||
await notifier.notify('post', 'post-42', 'created');
|
||||
|
||||
expect(insertFn).toHaveBeenCalledWith(dbNotifications);
|
||||
expect(valuesFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
const row = valuesFn.mock.calls[0][0];
|
||||
expect(row.entity).toBe('post');
|
||||
expect(row.entityId).toBe('post-42');
|
||||
expect(row.action).toBe('created');
|
||||
expect(row.fromCli).toBe(1);
|
||||
expect(row.seenAt).toBeNull();
|
||||
expect(row.createdAt).toBeGreaterThanOrEqual(beforeMs);
|
||||
expect(row.createdAt).toBeLessThanOrEqual(Date.now());
|
||||
});
|
||||
|
||||
it('inserts distinct rows for consecutive calls', async () => {
|
||||
const notifier = new DbNotifier(mockDb);
|
||||
|
||||
await notifier.notify('media', 'm-1', 'updated');
|
||||
await notifier.notify('script', 's-1', 'deleted');
|
||||
|
||||
expect(valuesFn).toHaveBeenCalledTimes(2);
|
||||
expect(valuesFn.mock.calls[0][0].entity).toBe('media');
|
||||
expect(valuesFn.mock.calls[1][0].entity).toBe('script');
|
||||
});
|
||||
|
||||
it('propagates database insertion errors', async () => {
|
||||
valuesFn.mockRejectedValueOnce(new Error('SQLITE_BUSY'));
|
||||
const notifier = new DbNotifier(mockDb);
|
||||
|
||||
await expect(notifier.notify('template', 't-1', 'created')).rejects.toThrow('SQLITE_BUSY');
|
||||
});
|
||||
});
|
||||
@@ -1,28 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { MCPServer, type MCPServerDependencies, DEFAULT_PAGE_SIZE, encodeCursor, decodeCursor } from '../../src/main/engine/MCPServer';
|
||||
|
||||
// Mock all engine singletons
|
||||
vi.mock('../../src/main/engine/PostEngine', () => ({
|
||||
getPostEngine: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/main/engine/MediaEngine', () => ({
|
||||
getMediaEngine: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/main/engine/ScriptEngine', () => ({
|
||||
getScriptEngine: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/main/engine/TemplateEngine', () => ({
|
||||
getTemplateEngine: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/main/engine/MetaEngine', () => ({
|
||||
getMetaEngine: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/main/engine/PostMediaEngine', () => ({
|
||||
getPostMediaEngine: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/main/engine/TagEngine', () => ({
|
||||
getTagEngine: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
function createMockPostEngine() {
|
||||
return {
|
||||
|
||||
@@ -445,4 +445,142 @@ describe('ScriptEngine', () => {
|
||||
expect(endFn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('draft lifecycle', () => {
|
||||
it('createDraftScript inserts a row with status draft and content in DB', async () => {
|
||||
const created = await scriptEngine.createDraftScript({
|
||||
title: 'Draft Macro',
|
||||
kind: 'macro',
|
||||
content: 'def render(ctx): return {"html": "<p>draft</p>"}',
|
||||
});
|
||||
|
||||
expect(created.status).toBe('draft');
|
||||
expect(created.content).toBe('def render(ctx): return {"html": "<p>draft</p>"}');
|
||||
expect(created.slug).toBe('draft_macro');
|
||||
expect(mockScripts.has(created.id)).toBe(true);
|
||||
|
||||
const row = mockScripts.get(created.id);
|
||||
expect(row.status).toBe('draft');
|
||||
expect(row.content).toBe('def render(ctx): return {"html": "<p>draft</p>"}');
|
||||
});
|
||||
|
||||
it('createDraftScript does not write a file to disk', async () => {
|
||||
const fsModule = await import('fs/promises');
|
||||
vi.mocked(fsModule.writeFile).mockClear();
|
||||
|
||||
await scriptEngine.createDraftScript({
|
||||
title: 'No File Draft',
|
||||
kind: 'utility',
|
||||
content: 'print("no file")',
|
||||
});
|
||||
|
||||
expect(fsModule.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('createDraftScript generates a unique slug', async () => {
|
||||
await scriptEngine.createDraftScript({
|
||||
title: 'Unique Slug',
|
||||
kind: 'macro',
|
||||
content: 'pass',
|
||||
});
|
||||
|
||||
vi.mocked((await import('uuid')).v4).mockReturnValueOnce('mock-script-id-2');
|
||||
|
||||
const second = await scriptEngine.createDraftScript({
|
||||
title: 'Unique Slug',
|
||||
kind: 'macro',
|
||||
content: 'pass',
|
||||
});
|
||||
|
||||
expect(second.slug).toBe('unique_slug_2');
|
||||
});
|
||||
|
||||
it('publishScript writes file and sets status to published', async () => {
|
||||
const draft = await scriptEngine.createDraftScript({
|
||||
title: 'Publish Me',
|
||||
kind: 'macro',
|
||||
content: 'def render(ctx): return {"html": "<p>publish</p>"}',
|
||||
});
|
||||
|
||||
const published = await scriptEngine.publishScript(draft.id);
|
||||
|
||||
expect(published).not.toBeNull();
|
||||
expect(published!.status).toBe('published');
|
||||
|
||||
const row = mockScripts.get(draft.id);
|
||||
expect(row.status).toBe('published');
|
||||
expect(row.content).toBeNull();
|
||||
|
||||
const fileContent = mockFiles.get(draft.filePath);
|
||||
expect(fileContent).toBeDefined();
|
||||
expect(fileContent).toContain('title: "Publish Me"');
|
||||
expect(fileContent).toContain('def render');
|
||||
});
|
||||
|
||||
it('publishScript returns null for non-existent script', async () => {
|
||||
const result = await scriptEngine.publishScript('nonexistent-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('publishScript calls notifier with updated action', async () => {
|
||||
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
|
||||
const notifiedEngine = new ScriptEngine(mockNotifier);
|
||||
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
|
||||
|
||||
const draft = await notifiedEngine.createDraftScript({
|
||||
title: 'Notified Publish',
|
||||
kind: 'macro',
|
||||
content: 'def render(ctx): pass',
|
||||
});
|
||||
|
||||
await notifiedEngine.publishScript(draft.id);
|
||||
|
||||
expect(mockNotifier.notify).toHaveBeenCalledWith('script', draft.id, 'updated');
|
||||
});
|
||||
|
||||
it('deleteDraftScript removes a draft row from the database', async () => {
|
||||
const draft = await scriptEngine.createDraftScript({
|
||||
title: 'Delete Draft',
|
||||
kind: 'utility',
|
||||
content: 'pass',
|
||||
});
|
||||
|
||||
const deleted = await scriptEngine.deleteDraftScript(draft.id);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(mockScripts.size).toBe(0);
|
||||
});
|
||||
|
||||
it('deleteDraftScript returns false for non-existent script', async () => {
|
||||
const result = await scriptEngine.deleteDraftScript('no-such-id');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('deleteDraftScript returns false for published scripts', async () => {
|
||||
const created = await scriptEngine.createScript({
|
||||
title: 'Published Script',
|
||||
kind: 'macro',
|
||||
content: 'def render(ctx): pass',
|
||||
});
|
||||
|
||||
const result = await scriptEngine.deleteDraftScript(created.id);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('deleteDraftScript calls notifier with deleted action', async () => {
|
||||
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
|
||||
const notifiedEngine = new ScriptEngine(mockNotifier);
|
||||
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
|
||||
|
||||
const draft = await notifiedEngine.createDraftScript({
|
||||
title: 'Notified Delete',
|
||||
kind: 'utility',
|
||||
content: 'pass',
|
||||
});
|
||||
|
||||
await notifiedEngine.deleteDraftScript(draft.id);
|
||||
|
||||
expect(mockNotifier.notify).toHaveBeenCalledWith('script', draft.id, 'deleted');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -517,4 +517,142 @@ describe('TemplateEngine', () => {
|
||||
expect(row.title).toBe(originalTitle);
|
||||
});
|
||||
});
|
||||
|
||||
describe('draft lifecycle', () => {
|
||||
it('createDraftTemplate inserts a row with status draft and content in DB', async () => {
|
||||
const created = await templateEngine.createDraftTemplate({
|
||||
title: 'Draft Post Layout',
|
||||
kind: 'post',
|
||||
content: '<main>{{ post.title }}</main>',
|
||||
});
|
||||
|
||||
expect(created.status).toBe('draft');
|
||||
expect(created.content).toBe('<main>{{ post.title }}</main>');
|
||||
expect(created.slug).toBe('draft_post_layout');
|
||||
expect(mockTemplates.has(created.id)).toBe(true);
|
||||
|
||||
const row = mockTemplates.get(created.id);
|
||||
expect(row.status).toBe('draft');
|
||||
expect(row.content).toBe('<main>{{ post.title }}</main>');
|
||||
});
|
||||
|
||||
it('createDraftTemplate does not write a file to disk', async () => {
|
||||
const fsModule = await import('fs/promises');
|
||||
vi.mocked(fsModule.writeFile).mockClear();
|
||||
|
||||
await templateEngine.createDraftTemplate({
|
||||
title: 'No File Draft',
|
||||
kind: 'list',
|
||||
content: '<main>{{ day_blocks }}</main>',
|
||||
});
|
||||
|
||||
expect(fsModule.writeFile).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('createDraftTemplate generates a unique slug', async () => {
|
||||
await templateEngine.createDraftTemplate({
|
||||
title: 'Unique Slug',
|
||||
kind: 'post',
|
||||
content: '<main>first</main>',
|
||||
});
|
||||
|
||||
vi.mocked((await import('uuid')).v4).mockReturnValueOnce('mock-template-id-2');
|
||||
|
||||
const second = await templateEngine.createDraftTemplate({
|
||||
title: 'Unique Slug',
|
||||
kind: 'post',
|
||||
content: '<main>second</main>',
|
||||
});
|
||||
|
||||
expect(second.slug).toBe('unique_slug_2');
|
||||
});
|
||||
|
||||
it('publishTemplate writes file and sets status to published', async () => {
|
||||
const draft = await templateEngine.createDraftTemplate({
|
||||
title: 'Publish Me',
|
||||
kind: 'post',
|
||||
content: '<main>{{ post.content }}</main>',
|
||||
});
|
||||
|
||||
const published = await templateEngine.publishTemplate(draft.id);
|
||||
|
||||
expect(published).not.toBeNull();
|
||||
expect(published!.status).toBe('published');
|
||||
|
||||
const row = mockTemplates.get(draft.id);
|
||||
expect(row.status).toBe('published');
|
||||
expect(row.content).toBeNull();
|
||||
|
||||
const fileContent = mockFiles.get(draft.filePath);
|
||||
expect(fileContent).toBeDefined();
|
||||
expect(fileContent).toContain('title: "Publish Me"');
|
||||
expect(fileContent).toContain('{{ post.content }}');
|
||||
});
|
||||
|
||||
it('publishTemplate returns null for non-existent template', async () => {
|
||||
const result = await templateEngine.publishTemplate('nonexistent-id');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('publishTemplate calls notifier with updated action', async () => {
|
||||
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
|
||||
const notifiedEngine = new TemplateEngine(mockNotifier);
|
||||
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
|
||||
|
||||
const draft = await notifiedEngine.createDraftTemplate({
|
||||
title: 'Notified Publish',
|
||||
kind: 'post',
|
||||
content: '<main>notify</main>',
|
||||
});
|
||||
|
||||
await notifiedEngine.publishTemplate(draft.id);
|
||||
|
||||
expect(mockNotifier.notify).toHaveBeenCalledWith('template', draft.id, 'updated');
|
||||
});
|
||||
|
||||
it('deleteDraftTemplate removes a draft row from the database', async () => {
|
||||
const draft = await templateEngine.createDraftTemplate({
|
||||
title: 'Delete Draft',
|
||||
kind: 'partial',
|
||||
content: '<footer>draft</footer>',
|
||||
});
|
||||
|
||||
const deleted = await templateEngine.deleteDraftTemplate(draft.id);
|
||||
|
||||
expect(deleted).toBe(true);
|
||||
expect(mockTemplates.size).toBe(0);
|
||||
});
|
||||
|
||||
it('deleteDraftTemplate returns false for non-existent template', async () => {
|
||||
const result = await templateEngine.deleteDraftTemplate('no-such-id');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('deleteDraftTemplate returns false for published templates', async () => {
|
||||
const created = await templateEngine.createTemplate({
|
||||
title: 'Published Template',
|
||||
kind: 'post',
|
||||
content: '<main>published</main>',
|
||||
});
|
||||
|
||||
const result = await templateEngine.deleteDraftTemplate(created.id);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('deleteDraftTemplate calls notifier with deleted action', async () => {
|
||||
const mockNotifier = { notify: vi.fn().mockResolvedValue(undefined) };
|
||||
const notifiedEngine = new TemplateEngine(mockNotifier);
|
||||
notifiedEngine.setProjectContext('default', '/mock/userData/projects/default');
|
||||
|
||||
const draft = await notifiedEngine.createDraftTemplate({
|
||||
title: 'Notified Delete',
|
||||
kind: 'partial',
|
||||
content: '<footer>notify</footer>',
|
||||
});
|
||||
|
||||
await notifiedEngine.deleteDraftTemplate(draft.id);
|
||||
|
||||
expect(mockNotifier.notify).toHaveBeenCalledWith('template', draft.id, 'deleted');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user