feat(phase3): phase 7 - integration & polish
Fix all build/type/lint errors, write 46 new tests (222 total across
20 files), regenerate Prisma client, update seed with user preferences.
Fix SvelteSet usage, add {#each} keys, clean unused imports.
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock child_process for Docker discovery
|
||||
vi.mock('node:child_process', () => ({
|
||||
exec: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('node:util', () => ({
|
||||
promisify: (fn: unknown) => fn
|
||||
}));
|
||||
|
||||
// Mock appService for discoverAll
|
||||
vi.mock('../appService.js', () => ({
|
||||
findAll: vi.fn()
|
||||
}));
|
||||
|
||||
import { exec } from 'node:child_process';
|
||||
import { findAll as findAllApps } from '../appService.js';
|
||||
import { discoverDocker, discoverTraefik, discoverAll } from '../discoveryService.js';
|
||||
|
||||
const mockExec = exec as unknown as ReturnType<typeof vi.fn>;
|
||||
const mockFindAllApps = findAllApps as ReturnType<typeof vi.fn>;
|
||||
|
||||
describe('discoveryService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('discoverDocker', () => {
|
||||
it('returns services from running Docker containers', async () => {
|
||||
const containers = [
|
||||
{
|
||||
Id: 'abc123',
|
||||
Names: ['/gitea'],
|
||||
Image: 'gitea/gitea:latest',
|
||||
Ports: [{ IP: '0.0.0.0', PrivatePort: 3000, PublicPort: 3000, Type: 'tcp' }],
|
||||
Labels: { 'org.opencontainers.image.description': 'Self-hosted Git' },
|
||||
State: 'running'
|
||||
}
|
||||
];
|
||||
|
||||
mockExec.mockResolvedValue({ stdout: JSON.stringify(containers) });
|
||||
|
||||
const result = await discoverDocker('/var/run/docker.sock');
|
||||
|
||||
expect(result.services).toHaveLength(1);
|
||||
expect(result.services[0].name).toBe('gitea');
|
||||
expect(result.services[0].url).toBe('http://localhost:3000');
|
||||
expect(result.services[0].source).toBe('docker');
|
||||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
|
||||
it('extracts URL from Traefik labels in Docker containers', async () => {
|
||||
const containers = [
|
||||
{
|
||||
Id: 'def456',
|
||||
Names: ['/myapp'],
|
||||
Image: 'myapp:latest',
|
||||
Ports: [],
|
||||
Labels: {
|
||||
'traefik.http.routers.myapp.rule': 'Host(`myapp.example.com`)',
|
||||
'traefik.http.routers.myapp.entrypoints': 'websecure'
|
||||
},
|
||||
State: 'running'
|
||||
}
|
||||
];
|
||||
|
||||
mockExec.mockResolvedValue({ stdout: JSON.stringify(containers) });
|
||||
|
||||
const result = await discoverDocker('/var/run/docker.sock');
|
||||
|
||||
expect(result.services).toHaveLength(1);
|
||||
expect(result.services[0].url).toBe('https://myapp.example.com');
|
||||
});
|
||||
|
||||
it('skips containers without accessible URLs', async () => {
|
||||
const containers = [
|
||||
{
|
||||
Id: 'nop123',
|
||||
Names: ['/background-worker'],
|
||||
Image: 'worker:latest',
|
||||
Ports: [],
|
||||
Labels: {},
|
||||
State: 'running'
|
||||
}
|
||||
];
|
||||
|
||||
mockExec.mockResolvedValue({ stdout: JSON.stringify(containers) });
|
||||
|
||||
const result = await discoverDocker('/var/run/docker.sock');
|
||||
|
||||
expect(result.services).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('returns error when Docker socket is inaccessible', async () => {
|
||||
mockExec.mockRejectedValue(new Error('connect ENOENT /var/run/docker.sock'));
|
||||
|
||||
const result = await discoverDocker('/var/run/docker.sock');
|
||||
|
||||
expect(result.services).toHaveLength(0);
|
||||
expect(result.error).toContain('ENOENT');
|
||||
});
|
||||
});
|
||||
|
||||
describe('discoverTraefik', () => {
|
||||
it('returns services from Traefik routers', async () => {
|
||||
const routers = [
|
||||
{
|
||||
name: 'myapp@docker',
|
||||
rule: 'Host(`myapp.example.com`)',
|
||||
service: 'myapp@docker',
|
||||
entryPoints: ['websecure']
|
||||
}
|
||||
];
|
||||
const services = [
|
||||
{
|
||||
name: 'myapp@docker',
|
||||
loadBalancer: { servers: [{ url: 'http://172.17.0.2:8080' }] }
|
||||
}
|
||||
];
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((url: string) => {
|
||||
if (url.includes('/api/http/routers')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(routers)
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(services)
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const result = await discoverTraefik('http://traefik.local:8080');
|
||||
|
||||
expect(result.services).toHaveLength(1);
|
||||
expect(result.services[0].name).toBe('myapp');
|
||||
expect(result.services[0].url).toBe('https://myapp.example.com');
|
||||
expect(result.services[0].source).toBe('traefik');
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns error on Traefik API failure', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() =>
|
||||
Promise.resolve({ ok: false, status: 500 })
|
||||
)
|
||||
);
|
||||
|
||||
const result = await discoverTraefik('http://traefik.local:8080');
|
||||
|
||||
expect(result.services).toHaveLength(0);
|
||||
expect(result.error).toContain('500');
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns error when fetch throws', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.reject(new Error('Network error')))
|
||||
);
|
||||
|
||||
const result = await discoverTraefik('http://traefik.local:8080');
|
||||
|
||||
expect(result.services).toHaveLength(0);
|
||||
expect(result.error).toContain('Network error');
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
});
|
||||
|
||||
describe('discoverAll', () => {
|
||||
it('marks already-registered services', async () => {
|
||||
mockExec.mockResolvedValue({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
Id: 'c1',
|
||||
Names: ['/gitea'],
|
||||
Image: 'gitea/gitea',
|
||||
Ports: [{ IP: '0.0.0.0', PrivatePort: 3000, PublicPort: 3000, Type: 'tcp' }],
|
||||
Labels: {},
|
||||
State: 'running'
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
mockFindAllApps.mockResolvedValue([
|
||||
{ id: 'a1', name: 'Gitea', url: 'http://localhost:3000' }
|
||||
]);
|
||||
|
||||
const result = await discoverAll({ dockerSocketPath: '/var/run/docker.sock' });
|
||||
|
||||
expect(result.services).toHaveLength(1);
|
||||
expect(result.services[0].alreadyRegistered).toBe(true);
|
||||
});
|
||||
|
||||
it('deduplicates by URL preferring Traefik', async () => {
|
||||
mockExec.mockResolvedValue({
|
||||
stdout: JSON.stringify([
|
||||
{
|
||||
Id: 'c1',
|
||||
Names: ['/app'],
|
||||
Image: 'app:latest',
|
||||
Ports: [],
|
||||
Labels: {
|
||||
'traefik.http.routers.app.rule': 'Host(`app.example.com`)'
|
||||
},
|
||||
State: 'running'
|
||||
}
|
||||
])
|
||||
});
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn((url: string) => {
|
||||
if (url.includes('/api/http/routers')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'app@docker',
|
||||
rule: 'Host(`app.example.com`)',
|
||||
service: 'app@docker',
|
||||
entryPoints: ['web']
|
||||
}
|
||||
])
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||
})
|
||||
);
|
||||
|
||||
mockFindAllApps.mockResolvedValue([]);
|
||||
|
||||
const result = await discoverAll({
|
||||
dockerSocketPath: '/var/run/docker.sock',
|
||||
traefikApiUrl: 'http://traefik.local:8080'
|
||||
});
|
||||
|
||||
// Should deduplicate: both Docker (via label) and Traefik discover http://app.example.com
|
||||
const urls = result.services.map((s) => s.url);
|
||||
const unique = new Set(urls);
|
||||
expect(urls.length).toBe(unique.size);
|
||||
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('returns empty when no sources configured', async () => {
|
||||
mockFindAllApps.mockResolvedValue([]);
|
||||
|
||||
const result = await discoverAll({});
|
||||
|
||||
expect(result.services).toHaveLength(0);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('../../prisma.js', () => ({
|
||||
prisma: {
|
||||
app: { findMany: vi.fn() },
|
||||
board: { findMany: vi.fn() },
|
||||
group: { findMany: vi.fn() },
|
||||
systemSettings: { upsert: vi.fn() }
|
||||
}
|
||||
}));
|
||||
|
||||
import { prisma } from '../../prisma.js';
|
||||
import { exportAllData } from '../exportService.js';
|
||||
|
||||
const mockApp = prisma.app as unknown as { findMany: ReturnType<typeof vi.fn> };
|
||||
const mockBoard = prisma.board as unknown as { findMany: ReturnType<typeof vi.fn> };
|
||||
const mockGroup = prisma.group as unknown as { findMany: ReturnType<typeof vi.fn> };
|
||||
const mockSettings = prisma.systemSettings as unknown as {
|
||||
upsert: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
describe('exportService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('exportAllData', () => {
|
||||
it('returns export data with version and timestamp', async () => {
|
||||
mockApp.findMany.mockResolvedValue([]);
|
||||
mockBoard.findMany.mockResolvedValue([]);
|
||||
mockGroup.findMany.mockResolvedValue([]);
|
||||
mockSettings.upsert.mockResolvedValue({
|
||||
authMode: 'local',
|
||||
registrationEnabled: true,
|
||||
defaultTheme: 'dark',
|
||||
defaultPrimaryColor: '#6366f1',
|
||||
healthcheckDefaults: '{}'
|
||||
});
|
||||
|
||||
const result = await exportAllData();
|
||||
|
||||
expect(result.version).toBe('1.0');
|
||||
expect(result.exportedAt).toBeTruthy();
|
||||
expect(result.apps).toEqual([]);
|
||||
expect(result.boards).toEqual([]);
|
||||
expect(result.groups).toEqual([]);
|
||||
expect(result.settings).toEqual({
|
||||
authMode: 'local',
|
||||
registrationEnabled: true,
|
||||
defaultTheme: 'dark',
|
||||
defaultPrimaryColor: '#6366f1',
|
||||
healthcheckDefaults: '{}'
|
||||
});
|
||||
});
|
||||
|
||||
it('maps apps to export format stripping internal fields', async () => {
|
||||
mockApp.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'a1',
|
||||
name: 'Gitea',
|
||||
url: 'https://git.local',
|
||||
icon: 'gitea',
|
||||
iconType: 'simple',
|
||||
description: 'Self-hosted Git',
|
||||
category: 'dev',
|
||||
tags: 'git,code',
|
||||
healthcheckEnabled: true,
|
||||
healthcheckInterval: 300,
|
||||
healthcheckMethod: 'GET',
|
||||
healthcheckExpectedStatus: 200,
|
||||
healthcheckTimeout: 5000,
|
||||
createdById: 'u1',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
]);
|
||||
mockBoard.findMany.mockResolvedValue([]);
|
||||
mockGroup.findMany.mockResolvedValue([]);
|
||||
mockSettings.upsert.mockResolvedValue({
|
||||
authMode: 'local',
|
||||
registrationEnabled: true,
|
||||
defaultTheme: 'dark',
|
||||
defaultPrimaryColor: '#6366f1',
|
||||
healthcheckDefaults: '{}'
|
||||
});
|
||||
|
||||
const result = await exportAllData();
|
||||
|
||||
expect(result.apps).toHaveLength(1);
|
||||
expect(result.apps[0]).toEqual({
|
||||
name: 'Gitea',
|
||||
url: 'https://git.local',
|
||||
icon: 'gitea',
|
||||
iconType: 'simple',
|
||||
description: 'Self-hosted Git',
|
||||
category: 'dev',
|
||||
tags: 'git,code',
|
||||
healthcheckEnabled: true,
|
||||
healthcheckInterval: 300,
|
||||
healthcheckMethod: 'GET',
|
||||
healthcheckExpectedStatus: 200,
|
||||
healthcheckTimeout: 5000
|
||||
});
|
||||
// Internal fields should not be present
|
||||
expect((result.apps[0] as Record<string, unknown>).id).toBeUndefined();
|
||||
expect((result.apps[0] as Record<string, unknown>).createdById).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps boards with nested sections and widgets', async () => {
|
||||
mockApp.findMany.mockResolvedValue([]);
|
||||
mockBoard.findMany.mockResolvedValue([
|
||||
{
|
||||
name: 'Dashboard',
|
||||
icon: null,
|
||||
description: 'Main board',
|
||||
isDefault: true,
|
||||
isGuestAccessible: false,
|
||||
backgroundConfig: null,
|
||||
sections: [
|
||||
{
|
||||
title: 'Apps',
|
||||
icon: null,
|
||||
order: 0,
|
||||
isExpandedByDefault: true,
|
||||
widgets: [
|
||||
{
|
||||
type: 'app',
|
||||
order: 0,
|
||||
config: '{}',
|
||||
app: { name: 'Gitea' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
mockGroup.findMany.mockResolvedValue([]);
|
||||
mockSettings.upsert.mockResolvedValue({
|
||||
authMode: 'local',
|
||||
registrationEnabled: true,
|
||||
defaultTheme: 'dark',
|
||||
defaultPrimaryColor: '#6366f1',
|
||||
healthcheckDefaults: '{}'
|
||||
});
|
||||
|
||||
const result = await exportAllData();
|
||||
|
||||
expect(result.boards).toHaveLength(1);
|
||||
expect(result.boards[0].name).toBe('Dashboard');
|
||||
expect(result.boards[0].sections).toHaveLength(1);
|
||||
expect(result.boards[0].sections[0].widgets).toHaveLength(1);
|
||||
expect(result.boards[0].sections[0].widgets[0].appName).toBe('Gitea');
|
||||
});
|
||||
|
||||
it('maps groups stripping internal fields', async () => {
|
||||
mockApp.findMany.mockResolvedValue([]);
|
||||
mockBoard.findMany.mockResolvedValue([]);
|
||||
mockGroup.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'g1',
|
||||
name: 'Admins',
|
||||
description: 'Admin users',
|
||||
isDefault: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
]);
|
||||
mockSettings.upsert.mockResolvedValue({
|
||||
authMode: 'local',
|
||||
registrationEnabled: true,
|
||||
defaultTheme: 'dark',
|
||||
defaultPrimaryColor: '#6366f1',
|
||||
healthcheckDefaults: '{}'
|
||||
});
|
||||
|
||||
const result = await exportAllData();
|
||||
|
||||
expect(result.groups).toHaveLength(1);
|
||||
expect(result.groups[0]).toEqual({
|
||||
name: 'Admins',
|
||||
description: 'Admin users',
|
||||
isDefault: false
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Build a transaction mock that mirrors prisma's nested structure
|
||||
const txMock = {
|
||||
app: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn() },
|
||||
group: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn() },
|
||||
board: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn() },
|
||||
section: { create: vi.fn(), deleteMany: vi.fn() },
|
||||
widget: { create: vi.fn() },
|
||||
systemSettings: { upsert: vi.fn() }
|
||||
};
|
||||
|
||||
vi.mock('../../prisma.js', () => ({
|
||||
prisma: {
|
||||
$transaction: vi.fn(async (fn: (tx: typeof txMock) => Promise<void>) => {
|
||||
await fn(txMock);
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
import { validateImportData, importData } from '../importService.js';
|
||||
import type { ExportData } from '../exportService.js';
|
||||
|
||||
function buildValidExportData(overrides: Partial<ExportData> = {}): ExportData {
|
||||
return {
|
||||
version: '1.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
apps: [],
|
||||
boards: [],
|
||||
groups: [],
|
||||
settings: {
|
||||
authMode: 'local',
|
||||
registrationEnabled: true,
|
||||
defaultTheme: 'dark',
|
||||
defaultPrimaryColor: '#6366f1',
|
||||
healthcheckDefaults: '{}'
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('importService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('validateImportData', () => {
|
||||
it('accepts valid export data', () => {
|
||||
const data = buildValidExportData();
|
||||
const result = validateImportData(data);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects data missing required fields', () => {
|
||||
const result = validateImportData({ version: '1.0' });
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects non-object input', () => {
|
||||
const result = validateImportData('not an object');
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('importData', () => {
|
||||
it('creates new apps when none exist', async () => {
|
||||
const data = buildValidExportData({
|
||||
apps: [
|
||||
{
|
||||
name: 'TestApp',
|
||||
url: 'https://test.local',
|
||||
icon: null,
|
||||
iconType: 'lucide',
|
||||
description: null,
|
||||
category: null,
|
||||
tags: '',
|
||||
healthcheckEnabled: false,
|
||||
healthcheckInterval: 300,
|
||||
healthcheckMethod: 'GET',
|
||||
healthcheckExpectedStatus: 200,
|
||||
healthcheckTimeout: 5000
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
txMock.app.findFirst.mockResolvedValue(null);
|
||||
txMock.app.create.mockResolvedValue({ id: 'new-app-id', name: 'TestApp' });
|
||||
|
||||
const result = await importData(data, 'skip');
|
||||
|
||||
expect(result.apps.created).toBe(1);
|
||||
expect(result.apps.skipped).toBe(0);
|
||||
expect(txMock.app.create).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('skips existing apps in skip mode', async () => {
|
||||
const data = buildValidExportData({
|
||||
apps: [
|
||||
{
|
||||
name: 'Existing',
|
||||
url: 'https://existing.local',
|
||||
icon: null,
|
||||
iconType: 'lucide',
|
||||
description: null,
|
||||
category: null,
|
||||
tags: '',
|
||||
healthcheckEnabled: false,
|
||||
healthcheckInterval: 300,
|
||||
healthcheckMethod: 'GET',
|
||||
healthcheckExpectedStatus: 200,
|
||||
healthcheckTimeout: 5000
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
txMock.app.findFirst.mockResolvedValue({ id: 'existing-id', name: 'Existing' });
|
||||
|
||||
const result = await importData(data, 'skip');
|
||||
|
||||
expect(result.apps.skipped).toBe(1);
|
||||
expect(result.apps.created).toBe(0);
|
||||
expect(txMock.app.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('overwrites existing apps in overwrite mode', async () => {
|
||||
const data = buildValidExportData({
|
||||
apps: [
|
||||
{
|
||||
name: 'Existing',
|
||||
url: 'https://updated.local',
|
||||
icon: null,
|
||||
iconType: 'lucide',
|
||||
description: 'updated',
|
||||
category: null,
|
||||
tags: '',
|
||||
healthcheckEnabled: true,
|
||||
healthcheckInterval: 60,
|
||||
healthcheckMethod: 'GET',
|
||||
healthcheckExpectedStatus: 200,
|
||||
healthcheckTimeout: 5000
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
txMock.app.findFirst.mockResolvedValue({ id: 'existing-id', name: 'Existing' });
|
||||
|
||||
const result = await importData(data, 'overwrite');
|
||||
|
||||
expect(result.apps.updated).toBe(1);
|
||||
expect(txMock.app.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('creates new groups', async () => {
|
||||
const data = buildValidExportData({
|
||||
groups: [{ name: 'NewGroup', description: 'A group', isDefault: false }]
|
||||
});
|
||||
|
||||
txMock.group.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await importData(data, 'skip');
|
||||
|
||||
expect(result.groups.created).toBe(1);
|
||||
expect(txMock.group.create).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('creates boards with sections and widgets', async () => {
|
||||
const data = buildValidExportData({
|
||||
boards: [
|
||||
{
|
||||
name: 'Board1',
|
||||
icon: null,
|
||||
description: null,
|
||||
isDefault: false,
|
||||
isGuestAccessible: false,
|
||||
backgroundConfig: null,
|
||||
sections: [
|
||||
{
|
||||
title: 'Section1',
|
||||
icon: null,
|
||||
order: 0,
|
||||
isExpandedByDefault: true,
|
||||
widgets: [
|
||||
{ type: 'note', order: 0, config: '{}', appName: null }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
txMock.board.findFirst.mockResolvedValue(null);
|
||||
txMock.board.create.mockResolvedValue({ id: 'board-id' });
|
||||
txMock.section.create.mockResolvedValue({ id: 'section-id' });
|
||||
|
||||
const result = await importData(data, 'skip');
|
||||
|
||||
expect(result.boards.created).toBe(1);
|
||||
expect(txMock.section.create).toHaveBeenCalledOnce();
|
||||
expect(txMock.widget.create).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('imports settings when provided', async () => {
|
||||
const data = buildValidExportData({
|
||||
settings: {
|
||||
authMode: 'both',
|
||||
registrationEnabled: false,
|
||||
defaultTheme: 'light',
|
||||
defaultPrimaryColor: '#ff0000',
|
||||
healthcheckDefaults: '{}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await importData(data, 'skip');
|
||||
|
||||
expect(result.settingsUpdated).toBe(true);
|
||||
expect(txMock.systemSettings.upsert).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user