mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
105 lines
4.6 KiB
TypeScript
105 lines
4.6 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
vi.mock('@/lib/admin/session', () => ({
|
|
requireAdminAuth: vi.fn(async () => ({ session: { sub: 'admin' } })),
|
|
getClientIP: () => '127.0.0.1',
|
|
}));
|
|
const audit: Array<[string, Record<string, unknown>]> = [];
|
|
vi.mock('@/lib/admin/audit', () => ({ auditLog: vi.fn(async (a: string, d: Record<string, unknown>) => { audit.push([a, d]); }) }));
|
|
vi.mock('@/lib/admin/csp-frame-origins', async (orig) => ({
|
|
...(await orig<typeof import('@/lib/admin/csp-frame-origins')>()),
|
|
invalidateFrameOriginsCache: vi.fn(),
|
|
}));
|
|
|
|
let dir: string;
|
|
beforeEach(async () => {
|
|
dir = await mkdtemp(path.join(tmpdir(), 'bw-rollback-'));
|
|
vi.stubEnv('ADMIN_CONFIG_DIR', dir);
|
|
vi.resetModules();
|
|
audit.length = 0;
|
|
});
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const PLUGIN = {
|
|
id: 'demo', name: 'Demo', version: '1.0.0', author: 'a', description: '', type: 'hook',
|
|
permissions: ['email:read'], entrypoint: 'index.js', enabled: true, installedAt: '2026-01-01T00:00:00.000Z', updatedAt: 'x',
|
|
};
|
|
|
|
const call = async (kind: string, id: string, body: unknown) => {
|
|
const { POST } = await import('../[kind]/[id]/rollback/route');
|
|
return POST(
|
|
new NextRequest(`http://localhost/api/admin/extensions/${kind}/${id}/rollback`, {
|
|
method: 'POST', body: JSON.stringify(body), headers: { 'content-type': 'application/json' },
|
|
}),
|
|
{ params: Promise.resolve({ kind, id }) },
|
|
);
|
|
};
|
|
|
|
describe('POST /api/admin/extensions/[kind]/[id]/rollback', () => {
|
|
it('puts the previous version back and keeps the current enabled and forced flags', async () => {
|
|
const registry = await import('@/lib/admin/plugin-registry');
|
|
await registry.savePlugin(PLUGIN, 'v1()');
|
|
await registry.savePlugin({ ...PLUGIN, version: '1.1.0', permissions: ['email:read', 'email:send'], enabled: false, forceEnabled: false }, 'v2()');
|
|
|
|
const res = await call('plugin', 'demo', { version: '1.0.0' });
|
|
expect(res.status).toBe(200);
|
|
|
|
const now = (await registry.getPlugin('demo'))!;
|
|
expect(now.version).toBe('1.0.0');
|
|
expect(now.permissions).toEqual(['email:read']);
|
|
expect(now.enabled).toBe(false);
|
|
expect(now.forceEnabled).toBe(false);
|
|
expect(now.installedAt).toBe('2026-01-01T00:00:00.000Z');
|
|
expect(await registry.getPluginBundle('demo')).toBe('v1()');
|
|
expect(audit).toEqual([['plugin.rollback', { id: 'demo', from: '1.1.0', to: '1.0.0' }]]);
|
|
|
|
// The version that was replaced is now the one that can be gone back to.
|
|
const { GET } = await import('../[kind]/[id]/history/route');
|
|
const history = await (await GET(new NextRequest('http://localhost/x'), { params: Promise.resolve({ kind: 'plugin', id: 'demo' }) })).json();
|
|
expect(history.current.version).toBe('1.0.0');
|
|
expect(history.history.map((e: { version: string }) => e.version)).toEqual(['1.1.0']);
|
|
});
|
|
|
|
it('answers 404 when no copy of that version is kept', async () => {
|
|
const registry = await import('@/lib/admin/plugin-registry');
|
|
await registry.savePlugin(PLUGIN, 'v1()');
|
|
const res = await call('plugin', 'demo', { version: '0.9.0' });
|
|
expect(res.status).toBe(404);
|
|
expect((await registry.getPlugin('demo'))!.version).toBe('1.0.0');
|
|
});
|
|
|
|
it('answers 409 when that version is already installed', async () => {
|
|
const registry = await import('@/lib/admin/plugin-registry');
|
|
await registry.savePlugin(PLUGIN, 'v1()');
|
|
expect((await call('plugin', 'demo', { version: '1.0.0' })).status).toBe(409);
|
|
});
|
|
|
|
it('refuses an unknown kind and a bad id', async () => {
|
|
expect((await call('widget', 'demo', { version: '1.0.0' })).status).toBe(400);
|
|
expect((await call('plugin', '../demo', { version: '1.0.0' })).status).toBe(400);
|
|
});
|
|
|
|
it('rolls a theme back with its skin', async () => {
|
|
const registry = await import('@/lib/admin/plugin-registry');
|
|
const THEME = { id: 'night', name: 'Night', version: '1.0.0', author: 'a', description: '', variants: ['dark'], enabled: true, installedAt: 'x', updatedAt: 'x' };
|
|
await registry.saveTheme(THEME, 'body{}', ':root{--a:1}');
|
|
await registry.saveTheme({ ...THEME, version: '2.0.0' }, 'body{color:red}', null);
|
|
|
|
const res = await call('theme', 'night', { version: '1.0.0' });
|
|
expect(res.status).toBe(200);
|
|
expect((await registry.getTheme('night'))!.version).toBe('1.0.0');
|
|
expect(await registry.getThemeCSS('night')).toBe('body{}');
|
|
expect(await registry.getThemeSkin('night')).toBe(':root{--a:1}');
|
|
});
|
|
});
|