/* * Copyright (c) 2026 by Christian Kellner. * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ import { vi, describe, it, expect, beforeEach } from 'vitest'; describe('services/storage/backupRestoreService.js - precheck & filename', () => { let svc; let setZipState; let calls; beforeEach(async () => { calls = { logger: { info: [], warn: [], error: [] } }; // Mock AdmZip with configurable state via globalThis (avoid mock export name pitfalls) globalThis.__ADM_ZIP_STATE__ = { hasDb: false, meta: null }; setZipState = (s) => { globalThis.__ADM_ZIP_STATE__ = { ...globalThis.__ADM_ZIP_STATE__, ...s }; }; class MockAdmZip { constructor() {} getEntry(name) { const state = globalThis.__ADM_ZIP_STATE__ || {}; if (name === 'listings.db') { if (state.hasDb) return { getData: () => Buffer.from('db') }; return null; } if (name === 'fredy-backup.json') { if (state.meta) return { getData: () => Buffer.from(JSON.stringify(state.meta)) }; return null; } return null; } getEntries() { const state = globalThis.__ADM_ZIP_STATE__ || {}; const arr = []; if (state.hasDb) arr.push({ entryName: 'listings.db', getData: () => Buffer.from('db') }); return arr; } } const admZipMock = { default: MockAdmZip }; // Also expose for service via globalThis escape hatch globalThis.__TEST_ADM_ZIP__ = MockAdmZip; const path = await import('node:path'); const ROOT = path.resolve('.'); // Mocks for dependencies const migratePath = path.join(ROOT, 'lib', 'services', 'storage', 'migrations', 'migrate.js'); const sqlitePath = path.join(ROOT, 'lib', 'services', 'storage', 'SqliteConnection.js'); const loggerPath = path.join(ROOT, 'lib', 'services', 'logger.js'); const utilsPath = path.join(ROOT, 'lib', 'utils.js'); const migrateMock = { listMigrationFiles: () => [{ id: 10 }], runMigrations: async () => {}, }; const sqliteMock = { default: { getConnection: () => ({ backup: async () => {} }), close: () => {}, tableExists: () => false, query: () => [], withTransaction: (cb) => cb({ prepare: () => ({ run: () => {} }) }), }, computeDbPath: async () => ({ dir: '/tmp', dbPath: '/tmp/listings.db' }), }; const loggerMock = { info: (...a) => calls.logger.info.push(a), warn: (...a) => calls.logger.warn.push(a), error: (...a) => calls.logger.error.push(a), }; const utilsMock = { getPackageVersion: async () => '16.2.0' }; vi.resetModules(); vi.doMock('adm-zip', () => admZipMock); vi.doMock(migratePath, () => migrateMock); vi.doMock(sqlitePath, () => sqliteMock); vi.doMock(loggerPath, () => loggerMock); vi.doMock(utilsPath, () => utilsMock); const mod = await import(path.join(ROOT, 'lib', 'services', 'storage', 'backupRestoreService.js')); svc = mod; }); it('precheck: empty upload yields danger', async () => { const res = await svc.precheckRestore(Buffer.alloc(0)); expect(res.compatible).toBe(false); expect(res.severity).toBe('danger'); expect(res.message).toContain('Empty upload'); expect(res.requiredMigration).toBe(10); }); it('precheck: missing listings.db yields danger', async () => { setZipState({ hasDb: false, meta: { dbMigration: 9 } }); const res = await svc.precheckRestore(Buffer.from('dummy')); expect(res.compatible).toBe(false); expect(res.severity).toBe('danger'); expect(res.message).toMatch(/missing the database file/i); }); it('precheck: older backup is compatible with warning', async () => { setZipState({ hasDb: true, meta: { dbMigration: 5, fredyVersion: '16.0.0' } }); const res = await svc.precheckRestore(Buffer.from('zip')); expect(res.compatible).toBe(true); expect(res.severity).toBe('warning'); expect(res.message).toMatch(/automatic migrations/i); expect(res.backupMigration).toBe(5); expect(res.requiredMigration).toBe(10); }); it('precheck: equal backup is compatible with info', async () => { setZipState({ hasDb: true, meta: { dbMigration: 10 } }); const res = await svc.precheckRestore(Buffer.from('zip')); expect(res.compatible).toBe(true); expect(res.severity).toBe('info'); }); it('precheck: newer backup yields danger', async () => { setZipState({ hasDb: true, meta: { dbMigration: 11 } }); const res = await svc.precheckRestore(Buffer.from('zip')); expect(res.compatible).toBe(false); expect(res.severity).toBe('danger'); }); it('buildBackupFileName: matches pattern and includes version', async () => { const name = await svc.buildBackupFileName(); expect(name).toMatch(/^\d{4}-\d{2}-\d{2}-FredyBackup-/); expect(name).toContain('16.2.0'); expect(name).toMatch(/\.zip$/); }); });