diff --git a/e2e/src/api/specs/integrity.e2e-spec.ts b/e2e/src/api/specs/integrity.e2e-spec.ts new file mode 100644 index 0000000000..36ad8addae --- /dev/null +++ b/e2e/src/api/specs/integrity.e2e-spec.ts @@ -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; + + 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, + }), + ); + }); + }); +}); diff --git a/e2e/src/api/specs/maintenance.e2e-spec.ts b/e2e/src/api/specs/maintenance.e2e-spec.ts index 0fdc283c2a..9e010e60d4 100644 --- a/e2e/src/api/specs/maintenance.e2e-spec.ts +++ b/e2e/src/api/specs/maintenance.e2e-spec.ts @@ -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; - - 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 /', () => {