test: backup restore service tests

This commit is contained in:
izzy
2025-11-20 15:24:56 +00:00
parent 26587dd690
commit 31410c3c20

View File

@@ -1,16 +1,19 @@
import { UnauthorizedException } from '@nestjs/common';
import { SignJWT } from 'jose';
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
import { DateTime } from 'luxon';
import { PassThrough, Readable } from 'node:stream';
import { StorageCore } from 'src/cores/storage.core';
import { MaintenanceAction, StorageFolder, SystemMetadataKey } from 'src/enum';
import { MaintenanceEphemeralStateRepository } from 'src/maintenance/maintenance-ephemeral-state.repository';
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
import { automock, getMocks, ServiceMocks } from 'test/utils';
import { automock, AutoMocked, getMocks, mockDuplex, mockSpawn, ServiceMocks } from 'test/utils';
describe(MaintenanceWorkerService.name, () => {
let sut: MaintenanceWorkerService;
let mocks: ServiceMocks;
let maintenanceWebsocketRepositoryMock: MaintenanceWebsocketRepository;
let maintenanceEphemeralStateRepositoryMock: MaintenanceEphemeralStateRepository;
let maintenanceWebsocketRepositoryMock: AutoMocked<MaintenanceWebsocketRepository>;
let maintenanceEphemeralStateRepositoryMock: AutoMocked<MaintenanceEphemeralStateRepository>;
beforeEach(() => {
mocks = getMocks();
@@ -132,6 +135,8 @@ describe(MaintenanceWorkerService.name, () => {
},
});
maintenanceEphemeralStateRepositoryMock.getSecret.mockReturnValue('secret');
const jwt = await new SignJWT({ _mockValue: true })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
@@ -164,4 +169,149 @@ describe(MaintenanceWorkerService.name, () => {
});
});
});
/**
* Actions
*/
describe('action: start', () => {
it('should not do anything', async () => {
await sut.runAction({
action: MaintenanceAction.Start,
});
expect(mocks.logger.log).toHaveBeenCalledTimes(0);
});
});
describe('action: restore database', () => {
beforeEach(() => {
function* mockData() {
yield '';
}
mocks.database.tryLock.mockResolvedValueOnce(true);
mocks.storage.readdir.mockResolvedValue([]);
mocks.process.spawn.mockReturnValue(mockSpawn(0, 'data', ''));
mocks.process.createSpawnDuplexStream.mockImplementation(() => mockDuplex('command', 0, 'data', ''));
mocks.storage.rename.mockResolvedValue();
mocks.storage.unlink.mockResolvedValue();
mocks.storage.createPlainReadStream.mockReturnValue(Readable.from(mockData()));
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
mocks.storage.createGzip.mockReturnValue(new PassThrough());
mocks.storage.createGunzip.mockReturnValue(new PassThrough());
});
it('should update maintenance mode state', async () => {
maintenanceEphemeralStateRepositoryMock.getSecret.mockReturnValue('secret');
await sut.runAction({
action: MaintenanceAction.RestoreDatabase,
restoreBackupFilename: 'filename',
});
expect(mocks.database.tryLock).toHaveBeenCalled();
expect(mocks.logger.log).toHaveBeenCalledWith('Running maintenance action restore_database');
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, {
isMaintenanceMode: true,
secret: 'secret',
action: {
action: 'start',
},
});
});
it('should fail to restore invalid backup', async () => {
await sut.runAction({
action: MaintenanceAction.RestoreDatabase,
restoreBackupFilename: 'filename',
});
expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenCalledWith({
action: MaintenanceAction.RestoreDatabase,
error: 'Error: Invalid backup file format!',
});
});
it('should successfully run a backup', async () => {
await sut.runAction({
action: MaintenanceAction.RestoreDatabase,
restoreBackupFilename: 'development-filename',
});
expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenCalledWith({
action: MaintenanceAction.RestoreDatabase,
progress: expect.any(Number),
});
expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenLastCalledWith({
action: 'end',
});
});
it('should fail if backup creation fails', async () => {
mocks.process.createSpawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error'));
await sut.runAction({
action: MaintenanceAction.RestoreDatabase,
restoreBackupFilename: 'development-filename',
});
expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenLastCalledWith({
action: MaintenanceAction.RestoreDatabase,
error: 'Error: pg_dump non-zero exit code (1)\nerror',
});
});
it('should fail if restore itself fails', async () => {
mocks.process.createSpawnDuplexStream
.mockReturnValueOnce(mockDuplex('pg_dump', 0, 'data', ''))
.mockReturnValueOnce(mockDuplex('gzip', 0, 'data', ''))
.mockReturnValueOnce(mockDuplex('psql', 1, '', 'error'));
await sut.runAction({
action: MaintenanceAction.RestoreDatabase,
restoreBackupFilename: 'development-filename',
});
expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenLastCalledWith({
action: MaintenanceAction.RestoreDatabase,
error: 'Error: psql non-zero exit code (1)\nerror',
});
});
});
/**
* Backups
*/
describe('listBackups', () => {
it('should give us all valid and failed backups', async () => {
mocks.storage.readdir.mockResolvedValue([
`immich-db-backup-${DateTime.fromISO('2025-07-25T11:02:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz.tmp`,
`immich-db-backup-${DateTime.fromISO('2025-07-27T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`,
'immich-db-backup-1753789649000.sql.gz',
`immich-db-backup-${DateTime.fromISO('2025-07-29T11:01:16Z').toFormat("yyyyLLdd'T'HHmmss")}-v1.234.5-pg14.5.sql.gz`,
]);
await expect(sut.listBackups()).resolves.toMatchObject({
backups: [
'immich-db-backup-20250729T110116-v1.234.5-pg14.5.sql.gz',
'immich-db-backup-20250727T110116-v1.234.5-pg14.5.sql.gz',
'immich-db-backup-1753789649000.sql.gz',
],
failedBackups: ['immich-db-backup-20250725T110216-v1.234.5-pg14.5.sql.gz.tmp'],
});
});
});
describe('deleteBackup', () => {
it('should unlink the target file', async () => {
await sut.deleteBackup('filename');
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
expect(mocks.storage.unlink).toHaveBeenCalledWith(`${StorageCore.getBaseFolder(StorageFolder.Backups)}/filename`);
});
});
});