godcrm/backend/services/__tests__/WidgetLibraryService.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

623 lines
24 KiB
JavaScript

/**
* WidgetLibraryService Tests - TDD (ADR-073)
* Testing widget library operations for the Widget Picker system
*/
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import {
getLibraryWidgets,
getFavorites,
getRecent,
toggleFavorite,
trackUsage,
addFromLibrary,
registerWidget,
unregisterWidget
} from '../WidgetLibraryService.js';
import { createWidget, getWidgetById } from '../WidgetService.js';
import { dbAll, dbGet, dbRun, destroyAdapter, resetAdapter, toBool } from '../../database/connection.js';
// Helper functions (matching existing test patterns)
async function createTestUser(suffix = '') {
const uniqueEmail = `test-widgetlib-${Date.now()}${suffix}@hltrn.cc`;
const result = await dbRun(
'INSERT INTO users (email, password_hash, name, encryption_key_encrypted, email_verified) VALUES (?, ?, ?, ?, ?)',
[uniqueEmail, 'hash', 'Test User', 'encrypted_key', 1]
);
return result.lastInsertRowid;
}
async function createTestSpace(ownerId, name = 'Test Space') {
const result = await dbRun(
'INSERT INTO spaces (owner_id, name, type) VALUES (?, ?, ?)',
[ownerId, name, 'business']
);
return result.lastInsertRowid;
}
async function createTestProject(ownerId, spaceId) {
const result = await dbRun(
'INSERT INTO projects (owner_id, space_id, name, type) VALUES (?, ?, ?, ?)',
[ownerId, spaceId, 'Test Project', 'business']
);
return result.lastInsertRowid;
}
async function createTestDashboard(projectId) {
const result = await dbRun(
'INSERT INTO dashboards (project_id, name, is_default) VALUES (?, ?, ?)',
[projectId, 'Test Dashboard', toBool(true)]
);
return result.lastInsertRowid;
}
async function createTestTable(spaceId, name = 'Test Table') {
const result = await dbRun(
'INSERT INTO universal_tables (space_id, name, type) VALUES (?, ?, ?)',
[spaceId, name, 'data']
);
return result.lastInsertRowid;
}
async function createTestWidget(dashboardId, options = {}) {
const {
presetName = 'table_view',
title = `Widget ${presetName}`,
tableId = null
} = options;
const config = tableId ? { table_id: tableId } : {};
return await createWidget({
dashboard_id: dashboardId,
widget_type: 'preset',
preset_name: presetName,
title,
config,
position: { x: 0, y: 0, w: 6, h: 4 }
});
}
async function registerWidgetInLibrary(widgetId, spaceId, options = {}) {
const {
is_public = false,
is_template = false,
tags = null
} = options;
const result = await dbRun(
'INSERT INTO widget_library (widget_id, space_id, is_public, is_template, tags) VALUES (?, ?, ?, ?, ?)',
[widgetId, spaceId, is_public ? 1 : 0, is_template ? 1 : 0, tags ? `{${tags.join(',')}}` : null]
);
return result.lastInsertRowid;
}
describe('WidgetLibraryService - ADR-073', () => {
let userId;
let spaceId;
let projectId;
let dashboardId;
beforeEach(async () => {
process.env.TEST_MODE = 'true';
process.env.SKIP_DEV_USER = 'true';
await resetAdapter();
userId = await createTestUser();
spaceId = await createTestSpace(userId);
projectId = await createTestProject(userId, spaceId);
dashboardId = await createTestDashboard(projectId);
});
afterEach(async () => {
await destroyAdapter();
});
// ============================================================
// getLibraryWidgets
// ============================================================
describe('getLibraryWidgets', () => {
test('should return widgets from current space', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
const result = await getLibraryWidgets(spaceId, { userId });
expect(result.items).toHaveLength(1);
expect(result.items[0].widget_id).toBe(widget.id);
expect(result.items[0].is_own_space).toBe(true);
});
test('should include public widgets from other spaces when include_public=true', async () => {
// Create another space with a public widget
const otherUserId = await createTestUser('-other');
const otherSpaceId = await createTestSpace(otherUserId, 'Other Space');
const otherProjectId = await createTestProject(otherUserId, otherSpaceId);
const otherDashboardId = await createTestDashboard(otherProjectId);
const publicWidget = await createTestWidget(otherDashboardId, { title: 'Public Widget' });
await registerWidgetInLibrary(publicWidget.id, otherSpaceId, { is_public: true });
// Create local widget
const localWidget = await createTestWidget(dashboardId, { title: 'Local Widget' });
await registerWidgetInLibrary(localWidget.id, spaceId);
const result = await getLibraryWidgets(spaceId, { userId, include_public: true });
expect(result.items.length).toBeGreaterThanOrEqual(2);
const widgetIds = result.items.map(w => w.widget_id);
expect(widgetIds).toContain(publicWidget.id);
expect(widgetIds).toContain(localWidget.id);
});
test('should not include non-public widgets from other spaces', async () => {
// Create another space with a private widget
const otherUserId = await createTestUser('-other2');
const otherSpaceId = await createTestSpace(otherUserId, 'Other Space 2');
const otherProjectId = await createTestProject(otherUserId, otherSpaceId);
const otherDashboardId = await createTestDashboard(otherProjectId);
const privateWidget = await createTestWidget(otherDashboardId, { title: 'Private Widget' });
await registerWidgetInLibrary(privateWidget.id, otherSpaceId, { is_public: false });
const result = await getLibraryWidgets(spaceId, { userId, include_public: true });
const widgetIds = result.items.map(w => w.widget_id);
expect(widgetIds).not.toContain(privateWidget.id);
});
test('should filter by category favorites', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Widget 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Widget 2' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
// Add widget1 to favorites
await toggleFavorite(userId, widget1.id);
const result = await getLibraryWidgets(spaceId, { userId, category: 'favorites' });
expect(result.items).toHaveLength(1);
expect(result.items[0].widget_id).toBe(widget1.id);
});
test('should filter by category recent', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Widget 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Widget 2' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
// Track usage of widget1
await trackUsage(userId, widget1.id);
const result = await getLibraryWidgets(spaceId, { userId, category: 'recent' });
expect(result.items).toHaveLength(1);
expect(result.items[0].widget_id).toBe(widget1.id);
});
test('should filter by category this_space', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
const result = await getLibraryWidgets(spaceId, { userId, category: 'this_space' });
expect(result.items).toHaveLength(1);
expect(result.items[0].is_own_space).toBe(true);
});
test('should search by title', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Sales Dashboard' });
const widget2 = await createTestWidget(dashboardId, { title: 'Tasks Board' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
const result = await getLibraryWidgets(spaceId, { userId, search: 'Sales' });
expect(result.items).toHaveLength(1);
expect(result.items[0].title).toBe('Sales Dashboard');
});
test('should search by tags', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Widget 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Widget 2' });
await registerWidgetInLibrary(widget1.id, spaceId, { tags: ['kanban', 'sales'] });
await registerWidgetInLibrary(widget2.id, spaceId, { tags: ['table', 'contacts'] });
const result = await getLibraryWidgets(spaceId, { userId, search: 'kanban' });
expect(result.items).toHaveLength(1);
expect(result.items[0].widget_id).toBe(widget1.id);
});
test('should include is_favorite flag for current user', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
await toggleFavorite(userId, widget.id);
const result = await getLibraryWidgets(spaceId, { userId });
expect(result.items[0].is_favorite).toBe(true);
});
test('should paginate results', async () => {
// Create 5 widgets
for (let i = 0; i < 5; i++) {
const widget = await createTestWidget(dashboardId, { title: `Widget ${i}` });
await registerWidgetInLibrary(widget.id, spaceId);
}
const page1 = await getLibraryWidgets(spaceId, { userId, limit: 2, offset: 0 });
const page2 = await getLibraryWidgets(spaceId, { userId, limit: 2, offset: 2 });
expect(page1.items).toHaveLength(2);
expect(page2.items).toHaveLength(2);
expect(page1.total).toBe(5);
expect(page2.total).toBe(5);
});
test('should return categories counts', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Widget 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Widget 2' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
await toggleFavorite(userId, widget1.id);
await trackUsage(userId, widget2.id);
const result = await getLibraryWidgets(spaceId, { userId });
expect(result.categories).toBeDefined();
expect(result.categories.favorites).toBe(1);
expect(result.categories.recent).toBe(1);
expect(result.categories.this_space).toBe(2);
});
});
// ============================================================
// toggleFavorite
// ============================================================
describe('toggleFavorite', () => {
test('should add to favorites if not favorited', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
const result = await toggleFavorite(userId, widget.id);
expect(result.is_favorite).toBe(true);
// Verify in database
const fav = await dbGet('SELECT * FROM user_widget_favorites WHERE user_id = ? AND widget_id = ?', [userId, widget.id]);
expect(fav).toBeDefined();
});
test('should remove from favorites if already favorited', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
// Add to favorites first
await toggleFavorite(userId, widget.id);
// Then toggle again
const result = await toggleFavorite(userId, widget.id);
expect(result.is_favorite).toBe(false);
// Verify removed from database
const fav = await dbGet('SELECT * FROM user_widget_favorites WHERE user_id = ? AND widget_id = ?', [userId, widget.id]);
expect(fav).toBeFalsy();
});
test('should return new favorite status', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
const result1 = await toggleFavorite(userId, widget.id);
expect(result1.is_favorite).toBe(true);
const result2 = await toggleFavorite(userId, widget.id);
expect(result2.is_favorite).toBe(false);
});
test('should throw error for non-existent widget', async () => {
await expect(toggleFavorite(userId, 99999))
.rejects.toThrow('Widget not found');
});
});
// ============================================================
// trackUsage
// ============================================================
describe('trackUsage', () => {
test('should increment use_count', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
const before = await dbGet('SELECT use_count FROM widget_library WHERE widget_id = ?', [widget.id]);
expect(before.use_count).toBe(0);
await trackUsage(userId, widget.id);
const after = await dbGet('SELECT use_count FROM widget_library WHERE widget_id = ?', [widget.id]);
expect(after.use_count).toBe(1);
});
test('should update last_used_at', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
const before = await dbGet('SELECT last_used_at FROM widget_library WHERE widget_id = ?', [widget.id]);
expect(before.last_used_at).toBeNull();
await trackUsage(userId, widget.id);
const after = await dbGet('SELECT last_used_at FROM widget_library WHERE widget_id = ?', [widget.id]);
expect(after.last_used_at).not.toBeNull();
});
test('should add entry to user_widget_history', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
await trackUsage(userId, widget.id);
const history = await dbGet('SELECT * FROM user_widget_history WHERE user_id = ? AND widget_id = ?', [userId, widget.id]);
expect(history).toBeDefined();
expect(history.accessed_at).not.toBeNull();
});
test('should track multiple usages', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
await trackUsage(userId, widget.id);
await trackUsage(userId, widget.id);
await trackUsage(userId, widget.id);
const lib = await dbGet('SELECT use_count FROM widget_library WHERE widget_id = ?', [widget.id]);
expect(lib.use_count).toBe(3);
const history = await dbAll('SELECT * FROM user_widget_history WHERE user_id = ? AND widget_id = ?', [userId, widget.id]);
expect(history.length).toBe(3);
});
test('should throw error for non-existent widget', async () => {
await expect(trackUsage(userId, 99999))
.rejects.toThrow('Widget not found');
});
});
// ============================================================
// getFavorites
// ============================================================
describe('getFavorites', () => {
test('should return user favorites', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Fav 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Not Fav' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
await toggleFavorite(userId, widget1.id);
const favorites = await getFavorites(userId);
expect(favorites).toHaveLength(1);
expect(favorites[0].widget_id).toBe(widget1.id);
});
test('should return empty array when no favorites', async () => {
const favorites = await getFavorites(userId);
expect(favorites).toEqual([]);
});
});
// ============================================================
// getRecent
// ============================================================
describe('getRecent', () => {
test('should return recently used widgets', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Recent 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Not Recent' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
await trackUsage(userId, widget1.id);
const recent = await getRecent(userId);
expect(recent).toHaveLength(1);
expect(recent[0].widget_id).toBe(widget1.id);
});
test('should respect limit parameter', async () => {
// Create and track usage for 5 widgets
for (let i = 0; i < 5; i++) {
const widget = await createTestWidget(dashboardId, { title: `Widget ${i}` });
await registerWidgetInLibrary(widget.id, spaceId);
await trackUsage(userId, widget.id);
}
const recent = await getRecent(userId, 3);
expect(recent).toHaveLength(3);
});
test('should return empty array when no recent widgets', async () => {
const recent = await getRecent(userId);
expect(recent).toEqual([]);
});
test('should order by most recent first', async () => {
const widget1 = await createTestWidget(dashboardId, { title: 'Widget 1' });
const widget2 = await createTestWidget(dashboardId, { title: 'Widget 2' });
await registerWidgetInLibrary(widget1.id, spaceId);
await registerWidgetInLibrary(widget2.id, spaceId);
await trackUsage(userId, widget1.id);
// Small delay to ensure different timestamps
await new Promise(resolve => setTimeout(resolve, 10));
await trackUsage(userId, widget2.id);
const recent = await getRecent(userId);
// Most recent (widget2) should be first
expect(recent[0].widget_id).toBe(widget2.id);
});
});
// ============================================================
// addFromLibrary
// ============================================================
describe('addFromLibrary', () => {
test('should create reference widget when mode=reference', async () => {
const sourceWidget = await createTestWidget(dashboardId, { title: 'Source Widget', presetName: 'kanban_board' });
await registerWidgetInLibrary(sourceWidget.id, spaceId);
// Create another dashboard to add to
const targetDashboardId = (await dbRun(
'INSERT INTO dashboards (project_id, name, is_default) VALUES (?, ?, ?)',
[projectId, 'Target Dashboard', 0]
)).lastInsertRowid;
const result = await addFromLibrary(targetDashboardId, sourceWidget.id, 'reference', { x: 0, y: 0, w: 6, h: 4 }, userId);
expect(result.widget).toBeDefined();
expect(result.widget.source_widget_id).toBe(sourceWidget.id);
expect(result.widget.dashboard_id).toBe(targetDashboardId);
expect(result.mode_used).toBe('reference');
});
test('should create copied widget when mode=copy', async () => {
const sourceWidget = await createTestWidget(dashboardId, { title: 'Source Widget', presetName: 'table_view' });
await registerWidgetInLibrary(sourceWidget.id, spaceId);
const targetDashboardId = (await dbRun(
'INSERT INTO dashboards (project_id, name, is_default) VALUES (?, ?, ?)',
[projectId, 'Target Dashboard 2', 0]
)).lastInsertRowid;
const result = await addFromLibrary(targetDashboardId, sourceWidget.id, 'copy', { x: 0, y: 0, w: 6, h: 4 }, userId);
expect(result.widget).toBeDefined();
expect(result.widget.source_widget_id).toBeNull();
expect(result.widget.title).toBe('Source Widget');
expect(result.widget.preset_name).toBe('table_view');
expect(result.mode_used).toBe('copy');
});
test('should track usage on add', async () => {
const sourceWidget = await createTestWidget(dashboardId, { title: 'Source Widget' });
await registerWidgetInLibrary(sourceWidget.id, spaceId);
const targetDashboardId = (await dbRun(
'INSERT INTO dashboards (project_id, name, is_default) VALUES (?, ?, ?)',
[projectId, 'Target Dashboard 3', 0]
)).lastInsertRowid;
await addFromLibrary(targetDashboardId, sourceWidget.id, 'reference', { x: 0, y: 0, w: 6, h: 4 }, userId);
const lib = await dbGet('SELECT use_count FROM widget_library WHERE widget_id = ?', [sourceWidget.id]);
expect(lib.use_count).toBe(1);
});
test('should throw error for non-existent source widget', async () => {
await expect(addFromLibrary(dashboardId, 99999, 'reference', { x: 0, y: 0, w: 6, h: 4 }, userId))
.rejects.toThrow('Source widget not found');
});
test('should throw error for non-existent dashboard', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
await expect(addFromLibrary(99999, widget.id, 'reference', { x: 0, y: 0, w: 6, h: 4 }, userId))
.rejects.toThrow('Dashboard not found');
});
test('should throw error for invalid mode', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidgetInLibrary(widget.id, spaceId);
await expect(addFromLibrary(dashboardId, widget.id, 'invalid', { x: 0, y: 0, w: 6, h: 4 }, userId))
.rejects.toThrow('Invalid mode');
});
});
// ============================================================
// registerWidget
// ============================================================
describe('registerWidget', () => {
test('should register widget in library', async () => {
const widget = await createTestWidget(dashboardId);
const result = await registerWidget(widget.id, spaceId);
expect(result).toBeDefined();
expect(result.widget_id).toBe(widget.id);
expect(result.space_id).toBe(spaceId);
});
test('should set is_public flag', async () => {
const widget = await createTestWidget(dashboardId);
const result = await registerWidget(widget.id, spaceId, { is_public: true });
expect(result.is_public).toBeTruthy();
});
test('should set tags', async () => {
const widget = await createTestWidget(dashboardId);
const result = await registerWidget(widget.id, spaceId, { tags: ['kanban', 'sales'] });
const tags = typeof result.tags === 'string' ? JSON.parse(result.tags) : result.tags;
expect(tags).toEqual(['kanban', 'sales']);
});
test('should throw error for non-existent widget', async () => {
await expect(registerWidget(99999, spaceId))
.rejects.toThrow('Widget not found');
});
test('should throw error for non-existent space', async () => {
const widget = await createTestWidget(dashboardId);
await expect(registerWidget(widget.id, 99999))
.rejects.toThrow('Space not found');
});
test('should throw error if widget already in library', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidget(widget.id, spaceId);
await expect(registerWidget(widget.id, spaceId))
.rejects.toThrow('already in library');
});
});
// ============================================================
// unregisterWidget
// ============================================================
describe('unregisterWidget', () => {
test('should remove widget from library', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidget(widget.id, spaceId);
await unregisterWidget(widget.id);
const lib = await dbGet('SELECT * FROM widget_library WHERE widget_id = ?', [widget.id]);
expect(lib).toBeFalsy();
});
test('should not delete the widget itself', async () => {
const widget = await createTestWidget(dashboardId);
await registerWidget(widget.id, spaceId);
await unregisterWidget(widget.id);
const w = await getWidgetById(widget.id);
expect(w).toBeDefined();
});
test('should throw error for widget not in library', async () => {
const widget = await createTestWidget(dashboardId);
await expect(unregisterWidget(widget.id))
.rejects.toThrow('Widget not in library');
});
});
});