mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
2ac6c76086
The 851-line services/adminService folds into the wrapper AdminService over DatabaseService plus injected Settings/Addons/Passkey/Packing/Auth/Permissions/ Notifications services; the auth, notifications and permissions bridge imports all become injections, PERMISSION_ACTIONS stays a plain const, and the mcp/sessionManager deep import keeps its anti-cycle comment. Ahead of the fold the 11 packing-template functions move to PackingService, which already owned all three template tables — resolving the admin-2 residual with no bridge. The pure and module-scoped half moves to admin.helpers.ts, the version cache staying module-scoped so admin.bridge's instance and the container singleton share one GitHub fetch. A 1-export admin.bridge.ts serves scheduler.ts's cron, the only out-of-container consumer. Recipe steps 3 and 4 are no-ops: no src/mcp/tools/ admin.ts has ever existed and the plugin host never imported the domain. Four lines are non-verbatim, all path re-anchoring one directory deeper, both resolved paths verified against the emitted dist layout. admin.dto.ts clears all twenty AdminController allow-list entries, retiring the three 'enabled must be a boolean' checks plus 'permissions object required' and 'Object body required' for the pipe envelope. The module e2e goes DI-native: the 3-method whole-module mock dies, 6 cases become 15 over real SQL. Quirk fixes on top, each pinned: the three places toggles go fail-closed with an append-only backfill migration; the template item routes honour :templateId; updateUser rejects an empty username/email; createInvite 404s an unresolvable trip_id; listOAuthSessions guards its scopes parse; GitHub fetches gain a timeout and size cap and failures cache briefly; updateOidcSettings/updateAddon/updateUser become transactional; admin.oauth_session.revoke is renamed to match its siblings; two mutations gain their missing audit rows; getAuditLog drops the _parse_error sentinel. Tests move with IDs preserved (ADMIN-SVC-001..069 incl. the pre-existing 029/030 gap and duplicated 069, VNOTIF-001..007), plus ADMIN-BR-001 and ADMIN-SVC-070..076.
223 lines
8.4 KiB
TypeScript
223 lines
8.4 KiB
TypeScript
/**
|
|
* Unit tests for AdminService.checkAndNotifyVersion() — VNOTIF-001 to
|
|
* VNOTIF-007, moved from tests/unit/services/versionNotification.test.ts with
|
|
* the 2026-08 admin fold, IDs preserved. Kept separate from admin.service.test.ts
|
|
* because it stubs global fetch and drives the module-scoped version cache per
|
|
* case, which would leak into the ADMIN-SVC-* suite. The notification path runs
|
|
* for real against the temp db's notifications table.
|
|
*/
|
|
import { runMigrations } from '../../../src/db/migrations';
|
|
import { createTables } from '../../../src/db/schema';
|
|
import { __clearVersionCacheForTests } from '../../../src/nest/admin/admin.helpers';
|
|
import { createAdmin } from '../../helpers/factories';
|
|
import { resetTestDb } from '../../helpers/test-db';
|
|
|
|
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
|
|
const { testDb, dbMock } = vi.hoisted(() => {
|
|
const Database = require('better-sqlite3');
|
|
const db = new Database(':memory:');
|
|
db.exec('PRAGMA journal_mode = WAL');
|
|
db.exec('PRAGMA foreign_keys = ON');
|
|
const mock = {
|
|
db,
|
|
closeDb: () => {},
|
|
reinitialize: () => {},
|
|
getPlaceWithTags: () => null,
|
|
canAccessTrip: () => null,
|
|
isOwner: () => false,
|
|
};
|
|
return { testDb: db, dbMock: mock };
|
|
});
|
|
|
|
vi.mock('../../../src/db/database', () => dbMock);
|
|
vi.mock('../../../src/config', () => ({
|
|
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
|
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
|
updateJwtSecret: () => {},
|
|
}));
|
|
vi.mock('../../../src/websocket', () => ({ broadcastToUser: vi.fn() }));
|
|
// Mock MCP to avoid session side-effects
|
|
vi.mock('../../../src/mcp', () => ({ revokeUserSessions: vi.fn(), invalidateMcpSessions: vi.fn() }));
|
|
vi.mock('../../../src/mcp/sessionManager', () => ({ revokeUserSessions: vi.fn(), revokeUserSessionsForClient: vi.fn() }));
|
|
|
|
import { DatabaseService } from '../../../src/nest/database/database.service';
|
|
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
|
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
|
import { SettingsService } from '../../../src/nest/settings/settings.service';
|
|
import { AtlasService } from '../../../src/nest/atlas/atlas.service';
|
|
import { AuthService } from '../../../src/nest/auth/auth.service';
|
|
import { PasskeyService } from '../../../src/nest/auth/passkey.service';
|
|
import { PackingService } from '../../../src/nest/packing/packing.service';
|
|
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
|
import { NotificationsService } from '../../../src/nest/notifications/notifications.service';
|
|
import { AdminService } from '../../../src/nest/admin/admin.service';
|
|
|
|
const dbs = new DatabaseService(testDb);
|
|
const realtime = new RealtimeService();
|
|
const permissions = new PermissionsService(dbs);
|
|
const auth = new AuthService(dbs, permissions, new AtlasService(dbs));
|
|
const svc = new AdminService(
|
|
dbs,
|
|
new SettingsService(dbs),
|
|
new AddonsService(dbs),
|
|
new PasskeyService(dbs, auth),
|
|
new PackingService(dbs, permissions, realtime),
|
|
auth,
|
|
permissions,
|
|
new NotificationsService(dbs, realtime),
|
|
);
|
|
const checkAndNotifyVersion = () => svc.checkAndNotifyVersion();
|
|
|
|
// Helper: mock the GitHub releases/latest endpoint
|
|
function mockGitHubLatest(tagName: string, ok = true): void {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue({
|
|
ok,
|
|
// fetchGithub reads text() and parses it itself (size cap), so stub both.
|
|
text: async () => JSON.stringify({ tag_name: tagName, html_url: `https://github.com/liketrek/TREK/releases/tag/${tagName}` }),
|
|
json: async () => ({ tag_name: tagName, html_url: `https://github.com/liketrek/TREK/releases/tag/${tagName}` }),
|
|
}),
|
|
);
|
|
}
|
|
|
|
function mockGitHubFetchFailure(): void {
|
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error')));
|
|
}
|
|
|
|
function getLastNotifiedVersion(): string | undefined {
|
|
return (
|
|
testDb.prepare('SELECT value FROM app_settings WHERE key = ?').get('last_notified_version') as
|
|
| { value: string }
|
|
| undefined
|
|
)?.value;
|
|
}
|
|
|
|
function getNotificationCount(): number {
|
|
return (testDb.prepare('SELECT COUNT(*) as c FROM notifications').get() as { c: number }).c;
|
|
}
|
|
|
|
beforeAll(() => {
|
|
createTables(testDb);
|
|
runMigrations(testDb);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
resetTestDb(testDb);
|
|
__clearVersionCacheForTests();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
afterAll(() => {
|
|
testDb.close();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// checkAndNotifyVersion
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe('checkAndNotifyVersion', () => {
|
|
it('VNOTIF-001 — does nothing when no update is available', async () => {
|
|
createAdmin(testDb);
|
|
// GitHub reports same version as package.json (or older) → update_available: false
|
|
const { version } = require('../../../package.json');
|
|
mockGitHubLatest(`v${version}`);
|
|
|
|
await checkAndNotifyVersion();
|
|
|
|
expect(getNotificationCount()).toBe(0);
|
|
expect(getLastNotifiedVersion()).toBeUndefined();
|
|
});
|
|
|
|
it('VNOTIF-002 — creates a navigate notification for all admins when update available', async () => {
|
|
const { user: admin1 } = createAdmin(testDb);
|
|
const { user: admin2 } = createAdmin(testDb);
|
|
mockGitHubLatest('v99.0.0');
|
|
|
|
await checkAndNotifyVersion();
|
|
|
|
const notifications = testDb.prepare('SELECT * FROM notifications ORDER BY id').all() as Array<{
|
|
recipient_id: number;
|
|
type: string;
|
|
scope: string;
|
|
}>;
|
|
expect(notifications.length).toBe(2);
|
|
const recipientIds = notifications.map((n) => n.recipient_id);
|
|
expect(recipientIds).toContain(admin1.id);
|
|
expect(recipientIds).toContain(admin2.id);
|
|
expect(notifications[0].type).toBe('navigate');
|
|
expect(notifications[0].scope).toBe('admin');
|
|
});
|
|
|
|
it('VNOTIF-003 — sets last_notified_version in app_settings after notifying', async () => {
|
|
createAdmin(testDb);
|
|
mockGitHubLatest('v99.1.0');
|
|
|
|
await checkAndNotifyVersion();
|
|
|
|
expect(getLastNotifiedVersion()).toBe('99.1.0');
|
|
});
|
|
|
|
it('VNOTIF-004 — does NOT create duplicate notification if last_notified_version matches', async () => {
|
|
createAdmin(testDb);
|
|
mockGitHubLatest('v99.2.0');
|
|
|
|
// First call notifies
|
|
await checkAndNotifyVersion();
|
|
const countAfterFirst = getNotificationCount();
|
|
expect(countAfterFirst).toBe(1);
|
|
|
|
// Second call with same version — should not create another
|
|
await checkAndNotifyVersion();
|
|
expect(getNotificationCount()).toBe(countAfterFirst);
|
|
});
|
|
|
|
it('VNOTIF-005 — creates new notification when last_notified_version is an older version', async () => {
|
|
createAdmin(testDb);
|
|
// Simulate having been notified about an older version
|
|
testDb
|
|
.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)')
|
|
.run('last_notified_version', '98.0.0');
|
|
mockGitHubLatest('v99.3.0');
|
|
|
|
await checkAndNotifyVersion();
|
|
|
|
expect(getNotificationCount()).toBe(1);
|
|
expect(getLastNotifiedVersion()).toBe('99.3.0');
|
|
});
|
|
|
|
it('VNOTIF-006 — notification has correct type, scope, and navigate_target', async () => {
|
|
createAdmin(testDb);
|
|
mockGitHubLatest('v99.4.0');
|
|
|
|
await checkAndNotifyVersion();
|
|
|
|
const notif = testDb.prepare('SELECT * FROM notifications LIMIT 1').get() as {
|
|
type: string;
|
|
scope: string;
|
|
navigate_target: string;
|
|
title_key: string;
|
|
text_key: string;
|
|
navigate_text_key: string;
|
|
};
|
|
expect(notif.type).toBe('navigate');
|
|
expect(notif.scope).toBe('admin');
|
|
expect(notif.navigate_target).toBe('/admin');
|
|
expect(notif.title_key).toBe('notif.version_available.title');
|
|
expect(notif.text_key).toBe('notif.version_available.text');
|
|
expect(notif.navigate_text_key).toBe('notif.action.view_admin');
|
|
});
|
|
|
|
it('VNOTIF-007 — silently handles GitHub API fetch failure (no crash, no notification)', async () => {
|
|
createAdmin(testDb);
|
|
mockGitHubFetchFailure();
|
|
|
|
// Should not throw
|
|
await expect(checkAndNotifyVersion()).resolves.toBeUndefined();
|
|
expect(getNotificationCount()).toBe(0);
|
|
expect(getLastNotifiedVersion()).toBeUndefined();
|
|
});
|
|
});
|