2024-02-19 18:03:51 +01:00
|
|
|
import {
|
2024-02-27 20:04:38 +01:00
|
|
|
AssetFileUploadResponseDto,
|
2024-02-28 10:21:31 +01:00
|
|
|
AssetResponseDto,
|
2024-02-21 22:52:13 +01:00
|
|
|
CreateAlbumDto,
|
2024-02-20 04:34:18 +01:00
|
|
|
CreateAssetDto,
|
2024-02-29 21:10:08 +01:00
|
|
|
CreateLibraryDto,
|
2024-02-20 04:34:18 +01:00
|
|
|
CreateUserDto,
|
2024-02-21 14:28:03 +01:00
|
|
|
PersonUpdateDto,
|
2024-02-21 22:52:13 +01:00
|
|
|
SharedLinkCreateDto,
|
2024-02-29 21:10:08 +01:00
|
|
|
ValidateLibraryDto,
|
2024-02-21 22:52:13 +01:00
|
|
|
createAlbum,
|
2024-02-19 23:25:57 +01:00
|
|
|
createApiKey,
|
2024-02-29 21:10:08 +01:00
|
|
|
createLibrary,
|
2024-02-21 14:28:03 +01:00
|
|
|
createPerson,
|
2024-02-21 22:52:13 +01:00
|
|
|
createSharedLink,
|
2024-02-20 04:34:18 +01:00
|
|
|
createUser,
|
2024-02-19 18:03:51 +01:00
|
|
|
defaults,
|
2024-02-27 20:04:38 +01:00
|
|
|
deleteAssets,
|
|
|
|
getAssetInfo,
|
2024-02-19 18:03:51 +01:00
|
|
|
login,
|
|
|
|
setAdminOnboarding,
|
|
|
|
signUpAdmin,
|
2024-02-21 14:28:03 +01:00
|
|
|
updatePerson,
|
2024-02-29 21:10:08 +01:00
|
|
|
validate,
|
2024-02-19 18:03:51 +01:00
|
|
|
} from '@immich/sdk';
|
2024-02-13 19:08:49 +01:00
|
|
|
import { BrowserContext } from '@playwright/test';
|
2024-02-28 10:21:31 +01:00
|
|
|
import { exec, spawn } from 'node:child_process';
|
2024-02-19 23:25:57 +01:00
|
|
|
import { access } from 'node:fs/promises';
|
2024-02-28 10:21:31 +01:00
|
|
|
import { tmpdir } from 'node:os';
|
2024-02-19 23:25:57 +01:00
|
|
|
import path from 'node:path';
|
2024-02-22 17:16:10 +01:00
|
|
|
import { promisify } from 'node:util';
|
2024-02-19 18:03:51 +01:00
|
|
|
import pg from 'pg';
|
2024-02-27 20:04:38 +01:00
|
|
|
import { io, type Socket } from 'socket.io-client';
|
2024-02-19 18:03:51 +01:00
|
|
|
import { loginDto, signupDto } from 'src/fixtures';
|
2024-02-29 18:07:01 +01:00
|
|
|
import { makeRandomImage } from 'src/generators';
|
2024-02-20 04:34:18 +01:00
|
|
|
import request from 'supertest';
|
2024-02-13 19:08:49 +01:00
|
|
|
|
2024-02-22 17:16:10 +01:00
|
|
|
const execPromise = promisify(exec);
|
|
|
|
|
2024-02-19 18:03:51 +01:00
|
|
|
export const app = 'http://127.0.0.1:2283/api';
|
2024-02-13 19:08:49 +01:00
|
|
|
|
2024-02-19 23:25:57 +01:00
|
|
|
const directoryExists = (directory: string) =>
|
|
|
|
access(directory)
|
|
|
|
.then(() => true)
|
|
|
|
.catch(() => false);
|
|
|
|
|
|
|
|
// TODO move test assets into e2e/assets
|
|
|
|
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
2024-02-29 21:10:08 +01:00
|
|
|
export const testAssetDirInternal = '/data/assets';
|
2024-02-28 10:21:31 +01:00
|
|
|
export const tempDir = tmpdir();
|
2024-02-19 23:25:57 +01:00
|
|
|
|
2024-02-22 17:16:10 +01:00
|
|
|
const serverContainerName = 'immich-e2e-server';
|
2024-02-27 20:04:38 +01:00
|
|
|
const mediaDir = '/usr/src/app/upload';
|
|
|
|
const dirs = [
|
|
|
|
`"${mediaDir}/thumbs"`,
|
|
|
|
`"${mediaDir}/upload"`,
|
|
|
|
`"${mediaDir}/library"`,
|
2024-02-28 10:21:31 +01:00
|
|
|
`"${mediaDir}/encoded-video"`,
|
2024-02-27 20:04:38 +01:00
|
|
|
].join(' ');
|
2024-02-22 17:16:10 +01:00
|
|
|
|
2024-02-19 23:25:57 +01:00
|
|
|
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
|
|
|
throw new Error(
|
2024-02-27 20:04:38 +01:00
|
|
|
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
|
2024-02-19 23:25:57 +01:00
|
|
|
);
|
|
|
|
}
|
2024-02-13 19:08:49 +01:00
|
|
|
|
2024-02-19 23:25:57 +01:00
|
|
|
export const asBearerAuth = (accessToken: string) => ({
|
2024-02-13 19:08:49 +01:00
|
|
|
Authorization: `Bearer ${accessToken}`,
|
|
|
|
});
|
|
|
|
|
2024-02-19 23:25:57 +01:00
|
|
|
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
|
|
|
|
2024-02-19 18:03:51 +01:00
|
|
|
let client: pg.Client | null = null;
|
|
|
|
|
2024-02-22 17:16:10 +01:00
|
|
|
export const fileUtils = {
|
|
|
|
reset: async () => {
|
2024-02-29 17:26:55 +01:00
|
|
|
await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
|
2024-02-22 17:16:10 +01:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2024-02-19 18:03:51 +01:00
|
|
|
export const dbUtils = {
|
2024-02-29 17:26:55 +01:00
|
|
|
createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
|
2024-02-21 14:28:03 +01:00
|
|
|
if (!client) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const vector = Array.from({ length: 512 }, Math.random);
|
|
|
|
const embedding = `[${vector.join(',')}]`;
|
|
|
|
|
2024-02-29 17:26:55 +01:00
|
|
|
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
|
|
|
|
assetId,
|
|
|
|
personId,
|
|
|
|
embedding,
|
|
|
|
]);
|
2024-02-21 14:28:03 +01:00
|
|
|
},
|
|
|
|
setPersonThumbnail: async (personId: string) => {
|
|
|
|
if (!client) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-02-29 17:26:55 +01:00
|
|
|
await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
|
2024-02-21 14:28:03 +01:00
|
|
|
},
|
|
|
|
reset: async (tables?: string[]) => {
|
2024-02-19 18:03:51 +01:00
|
|
|
try {
|
|
|
|
if (!client) {
|
2024-02-29 17:26:55 +01:00
|
|
|
client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich');
|
2024-02-19 18:03:51 +01:00
|
|
|
await client.connect();
|
|
|
|
}
|
2024-02-13 19:08:49 +01:00
|
|
|
|
2024-02-21 14:28:03 +01:00
|
|
|
tables = tables || [
|
2024-02-29 21:10:08 +01:00
|
|
|
'libraries',
|
2024-02-21 14:28:03 +01:00
|
|
|
'shared_links',
|
|
|
|
'person',
|
2024-02-19 23:25:57 +01:00
|
|
|
'albums',
|
|
|
|
'assets',
|
2024-02-21 14:28:03 +01:00
|
|
|
'asset_faces',
|
|
|
|
'activity',
|
2024-02-19 23:25:57 +01:00
|
|
|
'api_keys',
|
|
|
|
'user_token',
|
|
|
|
'users',
|
|
|
|
'system_metadata',
|
2024-02-21 14:28:03 +01:00
|
|
|
];
|
|
|
|
|
|
|
|
for (const table of tables) {
|
2024-02-19 18:03:51 +01:00
|
|
|
await client.query(`DELETE FROM ${table} CASCADE;`);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to reset database', error);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
teardown: async () => {
|
|
|
|
try {
|
|
|
|
if (client) {
|
|
|
|
await client.end();
|
|
|
|
client = null;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to teardown database', error);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
2024-02-19 23:25:57 +01:00
|
|
|
export interface CliResponse {
|
|
|
|
stdout: string;
|
|
|
|
stderr: string;
|
|
|
|
exitCode: number | null;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const immichCli = async (args: string[]) => {
|
|
|
|
let _resolve: (value: CliResponse) => void;
|
|
|
|
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
|
|
|
|
const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args];
|
|
|
|
const child = spawn('node', _args, {
|
|
|
|
stdio: 'pipe',
|
|
|
|
});
|
|
|
|
|
|
|
|
let stdout = '';
|
|
|
|
let stderr = '';
|
|
|
|
|
|
|
|
child.stdout.on('data', (data) => (stdout += data.toString()));
|
|
|
|
child.stderr.on('data', (data) => (stderr += data.toString()));
|
|
|
|
child.on('exit', (exitCode) => {
|
|
|
|
_resolve({
|
|
|
|
stdout: stdout.trim(),
|
|
|
|
stderr: stderr.trim(),
|
|
|
|
exitCode,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return deferred;
|
|
|
|
};
|
2024-02-13 19:08:49 +01:00
|
|
|
|
2024-02-20 04:34:18 +01:00
|
|
|
export interface AdminSetupOptions {
|
|
|
|
onboarding?: boolean;
|
|
|
|
}
|
|
|
|
|
2024-02-28 10:21:31 +01:00
|
|
|
export enum SocketEvent {
|
|
|
|
UPLOAD = 'upload',
|
|
|
|
DELETE = 'delete',
|
|
|
|
}
|
|
|
|
|
|
|
|
export type EventType = 'upload' | 'delete';
|
|
|
|
export interface WaitOptions {
|
|
|
|
event: EventType;
|
|
|
|
assetId: string;
|
|
|
|
timeout?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
const events: Record<EventType, Set<string>> = {
|
|
|
|
upload: new Set<string>(),
|
|
|
|
delete: new Set<string>(),
|
|
|
|
};
|
|
|
|
|
|
|
|
const callbacks: Record<string, () => void> = {};
|
|
|
|
|
|
|
|
const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => {
|
|
|
|
events[event].add(assetId);
|
|
|
|
const callback = callbacks[assetId];
|
|
|
|
if (callback) {
|
|
|
|
callback();
|
|
|
|
delete callbacks[assetId];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-02-27 20:04:38 +01:00
|
|
|
export const wsUtils = {
|
|
|
|
connect: async (accessToken: string) => {
|
|
|
|
const websocket = io('http://127.0.0.1:2283', {
|
|
|
|
path: '/api/socket.io',
|
|
|
|
transports: ['websocket'],
|
|
|
|
extraHeaders: { Authorization: `Bearer ${accessToken}` },
|
2024-02-28 10:21:31 +01:00
|
|
|
autoConnect: true,
|
2024-02-27 20:04:38 +01:00
|
|
|
forceNew: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
return new Promise<Socket>((resolve) => {
|
2024-02-28 10:21:31 +01:00
|
|
|
websocket
|
|
|
|
.on('connect', () => resolve(websocket))
|
2024-02-29 17:26:55 +01:00
|
|
|
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id }))
|
|
|
|
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId }))
|
2024-02-28 10:21:31 +01:00
|
|
|
.connect();
|
2024-02-27 20:04:38 +01:00
|
|
|
});
|
|
|
|
},
|
|
|
|
disconnect: (ws: Socket) => {
|
|
|
|
if (ws?.connected) {
|
|
|
|
ws.disconnect();
|
|
|
|
}
|
2024-02-28 10:21:31 +01:00
|
|
|
|
|
|
|
for (const set of Object.values(events)) {
|
|
|
|
set.clear();
|
|
|
|
}
|
2024-02-27 20:04:38 +01:00
|
|
|
},
|
2024-02-29 17:26:55 +01:00
|
|
|
waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
|
2024-02-28 10:21:31 +01:00
|
|
|
const set = events[event];
|
|
|
|
if (set.has(assetId)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
2024-02-29 17:26:55 +01:00
|
|
|
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000);
|
2024-02-28 10:21:31 +01:00
|
|
|
|
|
|
|
callbacks[assetId] = () => {
|
2024-02-27 20:04:38 +01:00
|
|
|
clearTimeout(timeout);
|
2024-02-28 10:21:31 +01:00
|
|
|
resolve();
|
|
|
|
};
|
2024-02-27 20:04:38 +01:00
|
|
|
});
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2024-02-29 18:07:01 +01:00
|
|
|
type AssetData = { bytes?: Buffer; filename: string };
|
|
|
|
|
2024-02-19 18:03:51 +01:00
|
|
|
export const apiUtils = {
|
2024-02-19 23:25:57 +01:00
|
|
|
setup: () => {
|
2024-02-27 20:04:38 +01:00
|
|
|
defaults.baseUrl = app;
|
2024-02-19 23:25:57 +01:00
|
|
|
},
|
2024-02-27 20:04:38 +01:00
|
|
|
|
2024-02-20 04:34:18 +01:00
|
|
|
adminSetup: async (options?: AdminSetupOptions) => {
|
|
|
|
options = options || { onboarding: true };
|
|
|
|
|
2024-02-19 18:03:51 +01:00
|
|
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
|
|
|
const response = await login({ loginCredentialDto: loginDto.admin });
|
2024-02-20 04:34:18 +01:00
|
|
|
if (options.onboarding) {
|
|
|
|
await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
|
|
|
|
}
|
2024-02-19 18:03:51 +01:00
|
|
|
return response;
|
|
|
|
},
|
2024-02-20 04:34:18 +01:00
|
|
|
userSetup: async (accessToken: string, dto: CreateUserDto) => {
|
2024-02-29 17:26:55 +01:00
|
|
|
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
|
2024-02-20 04:34:18 +01:00
|
|
|
return login({
|
|
|
|
loginCredentialDto: { email: dto.email, password: dto.password },
|
|
|
|
});
|
|
|
|
},
|
2024-02-19 23:25:57 +01:00
|
|
|
createApiKey: (accessToken: string) => {
|
2024-02-29 17:26:55 +01:00
|
|
|
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
|
2024-02-19 23:25:57 +01:00
|
|
|
},
|
2024-02-21 22:52:13 +01:00
|
|
|
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
|
2024-02-29 17:26:55 +01:00
|
|
|
createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
|
2024-02-20 04:34:18 +01:00
|
|
|
createAsset: async (
|
|
|
|
accessToken: string,
|
2024-02-29 18:07:01 +01:00
|
|
|
dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
|
2024-02-20 04:34:18 +01:00
|
|
|
) => {
|
2024-02-27 20:04:38 +01:00
|
|
|
const _dto = {
|
2024-02-20 04:34:18 +01:00
|
|
|
deviceAssetId: 'test-1',
|
|
|
|
deviceId: 'test',
|
|
|
|
fileCreatedAt: new Date().toISOString(),
|
|
|
|
fileModifiedAt: new Date().toISOString(),
|
2024-02-29 17:26:55 +01:00
|
|
|
...dto,
|
2024-02-20 04:34:18 +01:00
|
|
|
};
|
2024-02-27 20:04:38 +01:00
|
|
|
|
2024-02-29 18:07:01 +01:00
|
|
|
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
|
|
|
const filename = dto?.assetData?.filename || 'example.png';
|
2024-02-27 20:04:38 +01:00
|
|
|
|
|
|
|
const builder = request(app)
|
2024-02-20 04:34:18 +01:00
|
|
|
.post(`/asset/upload`)
|
2024-02-29 18:07:01 +01:00
|
|
|
.attach('assetData', assetData, filename)
|
2024-02-20 04:34:18 +01:00
|
|
|
.set('Authorization', `Bearer ${accessToken}`);
|
|
|
|
|
2024-02-27 20:04:38 +01:00
|
|
|
for (const [key, value] of Object.entries(_dto)) {
|
2024-02-29 17:26:55 +01:00
|
|
|
void builder.field(key, String(value));
|
2024-02-27 20:04:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const { body } = await builder;
|
|
|
|
|
|
|
|
return body as AssetFileUploadResponseDto;
|
2024-02-20 04:34:18 +01:00
|
|
|
},
|
2024-02-29 17:26:55 +01:00
|
|
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
2024-02-27 20:04:38 +01:00
|
|
|
deleteAssets: (accessToken: string, ids: string[]) =>
|
2024-02-29 17:26:55 +01:00
|
|
|
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
|
2024-02-27 20:04:38 +01:00
|
|
|
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
|
2024-02-21 14:28:03 +01:00
|
|
|
// TODO fix createPerson to accept a body
|
2024-02-29 17:26:55 +01:00
|
|
|
const person = await createPerson({ headers: asBearerAuth(accessToken) });
|
2024-02-27 20:04:38 +01:00
|
|
|
await dbUtils.setPersonThumbnail(person.id);
|
|
|
|
|
|
|
|
if (!dto) {
|
|
|
|
return person;
|
|
|
|
}
|
|
|
|
|
2024-02-29 17:26:55 +01:00
|
|
|
return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) });
|
2024-02-21 14:28:03 +01:00
|
|
|
},
|
2024-02-21 22:52:13 +01:00
|
|
|
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
|
2024-02-29 17:26:55 +01:00
|
|
|
createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
|
2024-02-29 21:10:08 +01:00
|
|
|
createLibrary: (accessToken: string, dto: CreateLibraryDto) =>
|
|
|
|
createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
|
|
|
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
|
|
|
|
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
|
2024-02-19 23:25:57 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export const cliUtils = {
|
|
|
|
login: async () => {
|
|
|
|
const admin = await apiUtils.adminSetup();
|
|
|
|
const key = await apiUtils.createApiKey(admin.accessToken);
|
|
|
|
await immichCli(['login-key', app, `${key.secret}`]);
|
|
|
|
return key.secret;
|
|
|
|
},
|
2024-02-19 18:03:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
export const webUtils = {
|
2024-02-20 05:39:49 +01:00
|
|
|
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
|
2024-02-13 19:08:49 +01:00
|
|
|
await context.addCookies([
|
|
|
|
{
|
|
|
|
name: 'immich_access_token',
|
2024-02-20 05:39:49 +01:00
|
|
|
value: accessToken,
|
2024-02-13 19:08:49 +01:00
|
|
|
domain: '127.0.0.1',
|
|
|
|
path: '/',
|
2024-02-29 17:26:55 +01:00
|
|
|
expires: 1_742_402_728,
|
2024-02-13 19:08:49 +01:00
|
|
|
httpOnly: true,
|
|
|
|
secure: false,
|
|
|
|
sameSite: 'Lax',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'immich_auth_type',
|
|
|
|
value: 'password',
|
|
|
|
domain: '127.0.0.1',
|
|
|
|
path: '/',
|
2024-02-29 17:26:55 +01:00
|
|
|
expires: 1_742_402_728,
|
2024-02-13 19:08:49 +01:00
|
|
|
httpOnly: true,
|
|
|
|
secure: false,
|
|
|
|
sameSite: 'Lax',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'immich_is_authenticated',
|
|
|
|
value: 'true',
|
|
|
|
domain: '127.0.0.1',
|
|
|
|
path: '/',
|
2024-02-29 17:26:55 +01:00
|
|
|
expires: 1_742_402_728,
|
2024-02-13 19:08:49 +01:00
|
|
|
httpOnly: false,
|
|
|
|
secure: false,
|
|
|
|
sameSite: 'Lax',
|
|
|
|
},
|
2024-02-19 18:03:51 +01:00
|
|
|
]),
|
2024-02-13 19:08:49 +01:00
|
|
|
};
|