test: split integrity out of maintenance

pull/24205/head
izzy 2025-12-17 14:42:07 +00:00
parent 8fdec465c5
commit 7d71f99783
No known key found for this signature in database
2 changed files with 205 additions and 188 deletions

View File

@ -0,0 +1,202 @@
import { IntegrityReportType, LoginResponseDto, ManualJobName, QueueName } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { createUserDto } from 'src/fixtures';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
const assetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
describe('/admin/maintenance', () => {
let cookie: string | undefined;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('POST /integrity/summary (& jobs)', async () => {
let baseline: Record<IntegrityReportType, number>;
beforeAll(async () => {
await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'asset.jpg',
bytes: await readFile(assetFilepath),
},
});
await utils.copyFolder(`/data/upload/${admin.userId}`, `/data/upload/${admin.userId}-bak`);
});
afterEach(async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.copyFolder(`/data/upload/${admin.userId}-bak`, `/data/upload/${admin.userId}`);
});
it.sequential('may report issues', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityOrphanFiles,
});
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual({
missing_file: 0,
orphan_file: expect.any(Number),
checksum_mismatch: 0,
});
baseline = body;
});
it.sequential('should detect an orphan file (job: check orphan files)', async () => {
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityOrphanFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
orphan_file: baseline.orphan_file + 1,
}),
);
});
it.sequential('should detect outdated orphan file reports (job: refresh orphan files)', async () => {
// these should not be detected:
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan2.png`);
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan3.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityOrphanFilesRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
orphan_file: baseline.orphan_file,
}),
);
});
it.sequential('should detect a missing file and not a checksum mismatch (job: check missing files)', async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 1,
checksum_mismatch: 0,
}),
);
});
it.sequential('should detect outdated missing file reports (job: refresh missing files)', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFilesRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 0,
checksum_mismatch: 0,
}),
);
});
it.sequential('should detect a checksum mismatch (job: check file checksums)', async () => {
await utils.truncateFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 1,
}),
);
});
it.sequential('should detect outdated checksum mismatch reports (job: refresh file checksums)', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatchRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 0,
}),
);
});
});
});

View File

@ -1,12 +1,9 @@
import { IntegrityReportType, LoginResponseDto, ManualJobName, QueueName } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
const assetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
import { beforeAll, describe, expect, it } from 'vitest';
describe('/admin/maintenance', () => {
let cookie: string | undefined;
@ -37,188 +34,6 @@ describe('/admin/maintenance', () => {
});
});
describe('POST /integrity/summary (& jobs)', async () => {
let baseline: Record<IntegrityReportType, number>;
beforeAll(async () => {
await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'asset.jpg',
bytes: await readFile(assetFilepath),
},
});
await utils.copyFolder(`/data/upload/${admin.userId}`, `/data/upload/${admin.userId}-bak`);
});
afterEach(async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.copyFolder(`/data/upload/${admin.userId}-bak`, `/data/upload/${admin.userId}`);
});
it.sequential('may report issues', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityOrphanFiles,
});
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual({
missing_file: 0,
orphan_file: expect.any(Number),
checksum_mismatch: 0,
});
baseline = body;
});
it.sequential('should detect an orphan file (job: check orphan files)', async () => {
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan1.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityOrphanFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
orphan_file: baseline.orphan_file + 1,
}),
);
});
it.sequential('should detect outdated orphan file reports (job: refresh orphan files)', async () => {
// these should not be detected:
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan2.png`);
await utils.putTextFile('orphan', `/data/upload/${admin.userId}/orphan3.png`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityOrphanFilesRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
orphan_file: baseline.orphan_file,
}),
);
});
it.sequential('should detect a missing file and not a checksum mismatch (job: check missing files)', async () => {
await utils.deleteFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFiles,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 1,
checksum_mismatch: 0,
}),
);
});
it.sequential('should detect outdated missing file reports (job: refresh missing files)', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityMissingFilesRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
missing_file: 0,
checksum_mismatch: 0,
}),
);
});
it.sequential('should detect a checksum mismatch (job: check file checksums)', async () => {
await utils.truncateFolder(`/data/upload/${admin.userId}`);
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatch,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 1,
}),
);
});
it.sequential('should detect outdated checksum mismatch reports (job: refresh file checksums)', async () => {
await utils.createJob(admin.accessToken, {
name: ManualJobName.IntegrityChecksumMismatchRefresh,
});
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
const { status, body } = await request(app)
.get('/admin/maintenance/integrity/summary')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
checksum_mismatch: 0,
}),
);
});
});
// => enter maintenance mode
describe.sequential('POST /', () => {