import { SystemMetadataKey } from 'src/enum'; import { StorageService } from 'src/services/storage.service'; import { ImmichStartupError } from 'src/utils/misc'; import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTestService, ServiceMocks } from 'test/utils'; describe(StorageService.name, () => { let sut: StorageService; let mocks: ServiceMocks; beforeEach(() => { ({ sut, mocks } = newTestService(StorageService)); }); it('should work', () => { expect(sut).toBeDefined(); }); describe('onBootstrap', () => { it('should enable mount folder checking', async () => { mocks.systemMetadata.get.mockResolvedValue(null); await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, { mountChecks: { backups: true, 'encoded-video': true, library: true, profile: true, thumbs: true, upload: true, }, }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/encoded-video'); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/library'); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/profile'); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/thumbs'); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/upload'); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/backups'); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer)); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer)); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer)); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer)); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer)); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer)); }); it('should enable mount folder checking for a new folder type', async () => { mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { backups: false, 'encoded-video': true, library: false, profile: true, thumbs: true, upload: true, }, }); await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.SystemFlags, { mountChecks: { backups: true, 'encoded-video': true, library: true, profile: true, thumbs: true, upload: true, }, }); expect(mocks.storage.mkdirSync).toHaveBeenCalledTimes(2); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/library'); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith('upload/backups'); expect(mocks.storage.createFile).toHaveBeenCalledTimes(2); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer)); expect(mocks.storage.createFile).toHaveBeenCalledWith('upload/backups/.immich', expect.any(Buffer)); }); it('should throw an error if .immich is missing', async () => { mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } }); mocks.storage.readFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); await expect(sut.onBootstrap()).rejects.toThrow('Failed to read'); expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); it('should throw an error if .immich is present but read-only', async () => { mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } }); mocks.storage.overwriteFile.mockRejectedValue( new Error("ENOENT: no such file or directory, open '/app/.immich'"), ); await expect(sut.onBootstrap()).rejects.toThrow('Failed to write'); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); it('should skip mount file creation if file already exists', async () => { const error = new Error('Error creating file') as any; error.code = 'EEXIST'; mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} }); mocks.storage.createFile.mockRejectedValue(error); await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.logger.warn).toHaveBeenCalledWith('Found existing mount file, skipping creation'); }); it('should throw an error if mount file could not be created', async () => { mocks.systemMetadata.get.mockResolvedValue({ mountChecks: {} }); mocks.storage.createFile.mockRejectedValue(new Error('Error creating file')); await expect(sut.onBootstrap()).rejects.toBeInstanceOf(ImmichStartupError); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); it('should startup if checks are disabled', async () => { mocks.systemMetadata.get.mockResolvedValue({ mountChecks: { upload: true } }); mocks.config.getEnv.mockReturnValue( mockEnvData({ storage: { ignoreMountCheckErrors: true }, }), ); mocks.storage.overwriteFile.mockRejectedValue( new Error("ENOENT: no such file or directory, open '/app/.immich'"), ); await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); }); describe('handleDeleteFiles', () => { it('should handle null values', async () => { await sut.handleDeleteFiles({ files: [undefined, null] }); expect(mocks.storage.unlink).not.toHaveBeenCalled(); }); it('should handle an error removing a file', async () => { mocks.storage.unlink.mockRejectedValue(new Error('something-went-wrong')); await sut.handleDeleteFiles({ files: ['path/to/something'] }); expect(mocks.storage.unlink).toHaveBeenCalledWith('path/to/something'); }); it('should remove the file', async () => { await sut.handleDeleteFiles({ files: ['path/to/something'] }); expect(mocks.storage.unlink).toHaveBeenCalledWith('path/to/something'); }); }); });