test: e2e maintenance spec

This commit is contained in:
izzy 2025-11-20 16:22:30 +00:00
parent 9d4ad11cff
commit ed4a850a01
No known key found for this signature in database
GPG Key ID: 5059F398521BB0F6

View File

@ -1,9 +1,9 @@
import { LoginResponseDto } from '@immich/sdk';
import { LoginResponseDto, ManualJobName } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/admin/maintenance', () => {
let cookie: string | undefined;
@ -14,6 +14,7 @@ describe('/admin/maintenance', () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
await utils.resetBackups(admin.accessToken);
});
// => outside of maintenance mode
@ -26,6 +27,16 @@ describe('/admin/maintenance', () => {
});
});
describe('GET /status', async () => {
it('to always indicate we are not in maintenance mode', async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
expect(body).toEqual({
action: 'end',
});
});
});
describe('POST /login', async () => {
it('should not work out of maintenance mode', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({ token: 'token' });
@ -34,6 +45,69 @@ describe('/admin/maintenance', () => {
});
});
describe('GET /backups/list', async () => {
it('should succeed and be empty', async () => {
const { status, body } = await request(app)
.get('/admin/maintenance/backups/list')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
backups: [],
failedBackups: [],
});
});
it('should contain a created backup', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.BackupDatabase,
});
await expect
.poll(
async () => {
const { status, body } = await request(app)
.get('/admin/maintenance/backups/list')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
return body;
},
{
interval: 5e2,
timeout: 1e4,
},
)
.toEqual(
expect.objectContaining({
backups: [expect.stringMatching(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/)],
}),
);
});
});
describe('DELETE /backups/:filename', async () => {
it('should delete backup', async () => {
const filename = await utils.createBackup(admin.accessToken);
const { status } = await request(app)
.delete(`/admin/maintenance/backups/${filename}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
const { status: listStatus, body } = await request(app)
.get('/admin/maintenance/backups/list')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(listStatus).toBe(200);
expect(body).toEqual(
expect.objectContaining({
backups: [],
}),
);
});
});
// => enter maintenance mode
describe.sequential('POST /', () => {
@ -69,6 +143,7 @@ describe('/admin/maintenance', () => {
.send({
action: 'start',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
@ -79,7 +154,8 @@ describe('/admin/maintenance', () => {
await expect
.poll(
async () => {
const { body } = await request(app).get('/server/config');
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
@ -102,6 +178,16 @@ describe('/admin/maintenance', () => {
});
});
describe('GET /status', async () => {
it('to indicate we are in maintenance mode', async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
expect(body).toEqual({
action: 'start',
});
});
});
describe('POST /login', async () => {
it('should fail without cookie or token in body', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
@ -158,7 +244,8 @@ describe('/admin/maintenance', () => {
await expect
.poll(
async () => {
const { body } = await request(app).get('/server/config');
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
@ -169,4 +256,189 @@ describe('/admin/maintenance', () => {
.toBeFalsy();
});
});
// => action: restore database flow
describe.sequential('POST /start/restore', () => {
afterAll(async () => {
await request(app).post('/admin/maintenance/end').set('cookie', cookie!).send();
await utils.poll(
() => request(app).get('/server/config'),
({ status, body }) => status === 200 && !body.maintenanceMode,
);
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
it.sequential('should not work when the server is configured', async () => {
const { status, body } = await request(app).post('/admin/maintenance/start/restore').send();
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('The server already has an admin'));
});
it.sequential('should enter maintenance mode in "database restore mode"', async () => {
await utils.resetDatabase(); // reset database before running this test
const { status, headers } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
action: 'restore_database',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 5e2,
timeout: 1e4,
},
)
.toBeTruthy();
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status2).toBe(200);
expect(body).toEqual({
operation: 'restore-database-flow',
});
});
});
// => action: restore database
describe.sequential('POST /backups/restore', () => {
beforeAll(async () => {
await utils.disconnectDatabase();
});
afterAll(async () => {
await utils.connectDatabase();
});
it.sequential('should restore a backup', { timeout: 6e4 }, async () => {
const filename = await utils.createBackup(admin.accessToken);
const { status } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
action: 'restore_database',
restoreBackupFilename: filename,
});
expect(status).toBe(201);
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 5e2,
timeout: 1e4,
},
)
.toBeTruthy();
const { status: status2, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status2).toBe(200);
expect(body).toEqual(
expect.objectContaining({
operation: 'restore-database',
}),
);
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 5e2,
timeout: 6e4,
},
)
.toBeFalsy();
});
it.sequential('fail to restore a corrupted backup', { timeout: 6e4 }, async () => {
await utils.prepareTestBackup('corrupted.sql');
const { status, headers } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
action: 'restore_database',
restoreBackupFilename: 'development-corrupted.sql.gz',
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
return body.maintenanceMode;
},
{
interval: 5e2,
timeout: 1e4,
},
)
.toBeTruthy();
await expect
.poll(
async () => {
const { status, body } = await request(app).get('/admin/maintenance/status').send({ token: 'token' });
expect(status).toBe(200);
return body;
},
{
interval: 5e2,
timeout: 1e4,
},
)
.toEqual(
expect.objectContaining({
operation: 'restore-database',
error: 'Something went wrong, see logs!',
}),
);
const { status: status2, body: body2 } = await request(app)
.get('/admin/maintenance/status')
.set('cookie', cookie!)
.send({ token: 'token' });
expect(status2).toBe(200);
expect(body2).toEqual(
expect.objectContaining({
operation: 'restore-database',
error: expect.stringContaining('IM CORRUPTED'),
}),
);
await request(app).post('/admin/maintenance/end').set('cookie', cookie!).send();
await utils.poll(
() => request(app).get('/server/config'),
({ status, body }) => status === 200 && !body.maintenanceMode,
);
});
});
});