mirror of
https://github.com/immich-app/immich.git
synced 2026-04-25 02:29:51 -04:00
test: web e2e tests
This commit is contained in:
parent
5f5d3ea0ba
commit
4ded06dbb7
@ -44,7 +44,7 @@
|
||||
"exiftool-vendored": "^35.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
"orchestration-ui": "0.1.57",
|
||||
"orchestration-ui": "0.1.61",
|
||||
"pg": "^8.11.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.7.4",
|
||||
|
||||
@ -90,7 +90,7 @@ describe('/yucca', () => {
|
||||
beforeAll(async () => {
|
||||
await sdk.importRecoveryKey(
|
||||
{
|
||||
recoveryKey: '0'.repeat(32),
|
||||
recoveryKey: '0'.repeat(64),
|
||||
},
|
||||
requestOpts,
|
||||
);
|
||||
@ -285,7 +285,7 @@ describe('/yucca', () => {
|
||||
|
||||
await sdk.importRecoveryKey(
|
||||
{
|
||||
recoveryKey: '0'.repeat(32),
|
||||
recoveryKey: '0'.repeat(64),
|
||||
},
|
||||
maintenanceRequestOpts,
|
||||
);
|
||||
|
||||
141
e2e/src/specs/maintenance/web/yucca-backups.e2e-spec.ts
Normal file
141
e2e/src/specs/maintenance/web/yucca-backups.e2e-spec.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { LoginResponseDto, confirmRecoveryKey, importRecoveryKey, resetOrchestrator } from '@immich/sdk';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { asBearerAuth, baseUrl, utils } from 'src/utils';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Yucca Backups', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let socket: Socket;
|
||||
|
||||
const waitForTaskEnd = () =>
|
||||
new Promise<void>((resolve) => {
|
||||
const listener = (msg: string) => {
|
||||
try {
|
||||
const payload = JSON.parse(msg);
|
||||
if (payload.type === 'TaskEnd') {
|
||||
socket.offAny(listener);
|
||||
resolve();
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
socket.onAny(listener);
|
||||
});
|
||||
|
||||
test.beforeAll(async () => {
|
||||
utils.initSdk();
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
const headers = asBearerAuth(admin.accessToken);
|
||||
await resetOrchestrator({ headers });
|
||||
await importRecoveryKey({ importRecoveryKeyRequest: { recoveryKey: '0'.repeat(64) } }, { headers });
|
||||
await confirmRecoveryKey({ headers });
|
||||
await utils.mkdir('/local-backend');
|
||||
|
||||
socket = io(baseUrl, {
|
||||
path: '/api/yucca/socket.io',
|
||||
transports: ['websocket'],
|
||||
extraHeaders: headers,
|
||||
forceNew: true,
|
||||
});
|
||||
await new Promise<void>((resolve) => socket.on('connect', () => resolve()));
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
socket?.close();
|
||||
});
|
||||
|
||||
test('onboarding configures a local backend', async ({ context, page }) => {
|
||||
test.setTimeout(30_000);
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
await page.goto('/backups');
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog.filter({ hasText: 'Backup options' })).toBeVisible();
|
||||
await dialog.getByText('Local Folder').click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Create local backend' })).toBeVisible();
|
||||
await dialog.getByLabel('Path').fill('/local-backend');
|
||||
await dialog.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Configure Your Immich Backup' })).toBeVisible();
|
||||
await dialog.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(dialog).toHaveCount(0);
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Repositories' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('manually triggers a backup and waits for completion', async ({ context, page }) => {
|
||||
test.setTimeout(60_000);
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
await page.goto('/backups/repositories');
|
||||
const backupNow = page.getByRole('button', { name: 'Backup Now' });
|
||||
await expect(backupNow).toBeVisible();
|
||||
|
||||
const taskEnd = waitForTaskEnd();
|
||||
await backupNow.click();
|
||||
await expect(page.getByRole('dialog').filter({ hasText: 'Log Output' })).toBeVisible();
|
||||
|
||||
await taskEnd;
|
||||
});
|
||||
|
||||
test('resets immich and restores from the local yucca backup', async ({ context, page }) => {
|
||||
test.setTimeout(120_000);
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
await utils.resetBackups(admin.accessToken);
|
||||
await utils.createBackup(admin.accessToken);
|
||||
|
||||
await resetOrchestrator({ headers: asBearerAuth(admin.accessToken) });
|
||||
await utils.resetDatabase();
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Restore from backup' }).click();
|
||||
|
||||
try {
|
||||
await page.waitForURL('/maintenance**');
|
||||
} catch {
|
||||
await page.goto('/maintenance');
|
||||
await page.waitForURL('/maintenance**');
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'FUTO Backups' }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog.filter({ hasText: 'Import recovery key' })).toBeVisible();
|
||||
await dialog.getByLabel('Recovery Key').fill('0'.repeat(64));
|
||||
await dialog.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Where would you like to restore from?' })).toBeVisible();
|
||||
await dialog.getByText('Local Folder').click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Create local backend' })).toBeVisible();
|
||||
await dialog.getByLabel('Path').fill('/local-backend');
|
||||
await dialog.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Select Restore Point' })).toBeVisible();
|
||||
await dialog.getByRole('button', { name: 'Select' }).first().click();
|
||||
|
||||
await expect(dialog.filter({ hasText: /Restore from/ })).toBeVisible();
|
||||
await dialog.getByRole('button', { name: 'Restore' }).first().click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Confirm restore from snapshot' })).toBeVisible();
|
||||
await dialog.getByRole('button', { name: 'Restore' }).click();
|
||||
|
||||
await expect(dialog.filter({ hasText: 'Restoring' })).toBeVisible();
|
||||
await expect(dialog.filter({ hasText: 'Restoring' })).toBeHidden({ timeout: 60_000 });
|
||||
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
await page.getByRole('button', { name: 'Restore', exact: true }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Restore' }).click();
|
||||
|
||||
await page.waitForURL('/maintenance?**');
|
||||
await page.waitForURL('/photos', { timeout: 90_000 });
|
||||
});
|
||||
});
|
||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@ -253,8 +253,8 @@ importers:
|
||||
specifier: ^3.4.4
|
||||
version: 3.7.2
|
||||
orchestration-ui:
|
||||
specifier: 0.1.57
|
||||
version: 0.1.57(svelte@5.55.1)
|
||||
specifier: 0.1.61
|
||||
version: 0.1.61(svelte@5.55.1)
|
||||
pg:
|
||||
specifier: ^8.11.3
|
||||
version: 8.20.0
|
||||
@ -524,8 +524,8 @@ importers:
|
||||
specifier: ^6.3.3
|
||||
version: 6.8.2
|
||||
orchestration-api:
|
||||
specifier: 0.1.57
|
||||
version: 0.1.57(@nestjs/platform-express@11.1.17)(class-transformer@0.5.1)(reflect-metadata@0.2.2)
|
||||
specifier: 0.1.61
|
||||
version: 0.1.61(@nestjs/platform-express@11.1.17)(class-transformer@0.5.1)(reflect-metadata@0.2.2)
|
||||
pg:
|
||||
specifier: ^8.11.3
|
||||
version: 8.20.0
|
||||
@ -816,8 +816,8 @@ importers:
|
||||
specifier: ^5.6.2
|
||||
version: 5.21.0
|
||||
orchestration-ui:
|
||||
specifier: 0.1.57
|
||||
version: 0.1.57(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
|
||||
specifier: 0.1.61
|
||||
version: 0.1.61(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
|
||||
pmtiles:
|
||||
specifier: ^4.3.0
|
||||
version: 4.4.0
|
||||
@ -9675,11 +9675,11 @@ packages:
|
||||
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
orchestration-api@0.1.57:
|
||||
resolution: {integrity: sha512-rXsiIurhkliJ/5fbLz6kTg7VBrgaq6MrNZJQwdnDvsw1FLZCANY1KUh1WwSkgI5OLmb/lAcyLE+vO6UeMkXgyg==}
|
||||
orchestration-api@0.1.61:
|
||||
resolution: {integrity: sha512-RClFa0Xtlyyg1VaO9bUKWGVW34xgHNbHv/UOmybz2EWl2kTA+nZA4ZzoRBN/LbAuXsnTnlevsqJqTkqK1vCkTg==}
|
||||
|
||||
orchestration-ui@0.1.57:
|
||||
resolution: {integrity: sha512-vvpVItUdFpj5PKgIvkKMKv5gLpygc4b1rFtXBE4P0+rtFLklw6QTuVrIXzmxU/t2fMwP8J93ZPRSTP05t0MzBA==}
|
||||
orchestration-ui@0.1.61:
|
||||
resolution: {integrity: sha512-BvefDhMN4AwujszsXY3J0hDLmQM+x1qAbgWfb/KfKbjq+OB+hh6+8o4DMa2xCWsw80A8M6/cgKutYGvUa8FKJw==}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
@ -12639,8 +12639,8 @@ packages:
|
||||
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yucca-api-client@0.1.57:
|
||||
resolution: {integrity: sha512-faaYkjeO2IPvmvxYHrGefvGm7zE69sMk0fjizxd8/TOV9WztC2mWPDDwRIn9SZngg8AMekhg1xHakkWhMjttyQ==}
|
||||
yucca-api-client@0.1.61:
|
||||
resolution: {integrity: sha512-JNo95RWc4Zb3WslT50yN3l2BWIfXeNXnYJv6ULn40qDY1nlpZPGorYS2QlIxVLSz6ypmyf1Ia1vQVtzA75L5/w==}
|
||||
|
||||
yup@1.7.1:
|
||||
resolution: {integrity: sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==}
|
||||
@ -23009,7 +23009,7 @@ snapshots:
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
orchestration-api@0.1.57(@nestjs/platform-express@11.1.17)(class-transformer@0.5.1)(reflect-metadata@0.2.2):
|
||||
orchestration-api@0.1.61(@nestjs/platform-express@11.1.17)(class-transformer@0.5.1)(reflect-metadata@0.2.2):
|
||||
dependencies:
|
||||
'@futo-org/restic-wrapper': 1.1.2
|
||||
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@ -23030,7 +23030,7 @@ snapshots:
|
||||
rxjs: 7.8.2
|
||||
socket.io: 4.8.3
|
||||
tail: 2.2.6
|
||||
yucca-api-client: 0.1.57
|
||||
yucca-api-client: 0.1.61
|
||||
transitivePeerDependencies:
|
||||
- '@nestjs/microservices'
|
||||
- '@nestjs/platform-express'
|
||||
@ -23040,7 +23040,7 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
orchestration-ui@0.1.57(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1):
|
||||
orchestration-ui@0.1.61(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1):
|
||||
dependencies:
|
||||
'@immich/ui': 0.59.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
|
||||
'@mdi/js': 7.4.47
|
||||
@ -23052,14 +23052,14 @@ snapshots:
|
||||
luxon: 3.7.2
|
||||
socket.io-client: 4.8.3
|
||||
svelte: 5.55.1
|
||||
yucca-api-client: 0.1.57
|
||||
yucca-api-client: 0.1.61
|
||||
transitivePeerDependencies:
|
||||
- '@sveltejs/kit'
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
orchestration-ui@0.1.57(svelte@5.55.1):
|
||||
orchestration-ui@0.1.61(svelte@5.55.1):
|
||||
dependencies:
|
||||
'@immich/ui': 0.59.0(svelte@5.55.1)
|
||||
'@mdi/js': 7.4.47
|
||||
@ -23071,7 +23071,7 @@ snapshots:
|
||||
luxon: 3.7.2
|
||||
socket.io-client: 4.8.3
|
||||
svelte: 5.55.1
|
||||
yucca-api-client: 0.1.57
|
||||
yucca-api-client: 0.1.61
|
||||
transitivePeerDependencies:
|
||||
- '@sveltejs/kit'
|
||||
- bufferutil
|
||||
@ -26594,7 +26594,7 @@ snapshots:
|
||||
|
||||
yoctocolors@2.1.2: {}
|
||||
|
||||
yucca-api-client@0.1.57:
|
||||
yucca-api-client@0.1.61:
|
||||
dependencies:
|
||||
'@oazapfts/runtime': 1.2.0
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
"nestjs-zod": "^5.3.0",
|
||||
"nodemailer": "^8.0.0",
|
||||
"openid-client": "^6.3.3",
|
||||
"orchestration-api": "0.1.57",
|
||||
"orchestration-api": "0.1.61",
|
||||
"pg": "^8.11.3",
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"picomatch": "^4.0.2",
|
||||
|
||||
@ -10,7 +10,7 @@ import { OrchestrationApiModule } from 'orchestration-api/dist';
|
||||
import { commandsAndQuestions } from 'src/commands';
|
||||
import { IWorker } from 'src/constants';
|
||||
import { controllers } from 'src/controllers';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { ImmichEnvironment, ImmichWorker } from 'src/enum';
|
||||
import { MaintenanceAuthGuard } from 'src/maintenance/maintenance-auth.guard';
|
||||
import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository';
|
||||
import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-websocket.repository';
|
||||
@ -60,7 +60,9 @@ const apiMiddleware = [
|
||||
];
|
||||
|
||||
const configRepository = new ConfigRepository();
|
||||
const { bull, cls, database, otel } = configRepository.getEnv();
|
||||
const { bull, cls, database, environment, otel } = configRepository.getEnv();
|
||||
const isYuccaDevelopmentMode =
|
||||
environment === ImmichEnvironment.Development || environment === ImmichEnvironment.Testing;
|
||||
|
||||
const commonImports = [
|
||||
ClsModule.forRoot(cls.config),
|
||||
@ -121,6 +123,7 @@ export class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
statePath: '/data/yucca', // TODO
|
||||
requireWsAuth: true,
|
||||
requireLock: true,
|
||||
developmentMode: isYuccaDevelopmentMode,
|
||||
}),
|
||||
],
|
||||
controllers: [...controllers],
|
||||
@ -138,6 +141,7 @@ export class ApiModule extends BaseModule {}
|
||||
externalBaseUrl: 'https://my.immich.app',
|
||||
requireWsAuth: true,
|
||||
requireLock: true,
|
||||
developmentMode: isYuccaDevelopmentMode,
|
||||
}),
|
||||
],
|
||||
controllers: [MaintenanceWorkerController],
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.4.4",
|
||||
"maplibre-gl": "^5.6.2",
|
||||
"orchestration-ui": "0.1.57",
|
||||
"orchestration-ui": "0.1.61",
|
||||
"pmtiles": "^4.3.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"simple-icons": "^16.0.0",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user