1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

chore(server): user e2e: wait for user delete event (#7799)

* wait for user delete event

* fix update event names

* add test for hard deletion of user
This commit is contained in:
Daniel Dietzler 2024-03-10 00:10:24 +01:00 committed by GitHub
parent ec8fb0be83
commit 11e7533a4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 33 deletions

View File

@ -96,7 +96,7 @@ describe('/asset', () => {
}, },
}); });
await utils.waitForWebsocketEvent({ event: 'upload', assetId: locationAsset.id }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id: locationAsset.id });
user1Assets = await Promise.all([ user1Assets = await Promise.all([
utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken),
@ -693,7 +693,7 @@ describe('/asset', () => {
expect(duplicate).toBe(false); expect(duplicate).toBe(false);
await utils.waitForWebsocketEvent({ event: 'upload', assetId: id }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
const asset = await utils.getAssetInfo(admin.accessToken, id); const asset = await utils.getAssetInfo(admin.accessToken, id);
@ -795,7 +795,7 @@ describe('/asset', () => {
}, },
}); });
await utils.waitForWebsocketEvent({ event: 'upload', assetId: response.id }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
expect(response.duplicate).toBe(false); expect(response.duplicate).toBe(false);
@ -822,8 +822,8 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
await utils.waitForWebsocketEvent({ await utils.waitForWebsocketEvent({
event: 'upload', event: 'assetUpload',
assetId: locationAsset.id, id: locationAsset.id,
}); });
expect(status).toBe(200); expect(status).toBe(200);

View File

@ -76,7 +76,7 @@ describe('/search', () => {
} }
for (const asset of assets) { for (const asset of assets) {
await utils.waitForWebsocketEvent({ event: 'upload', assetId: asset.id }); await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
} }
[ [
@ -325,11 +325,9 @@ describe('/search', () => {
.post('/search/metadata') .post('/search/metadata')
.send(dto) .send(dto)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
console.dir({ status, body }, { depth: 10 });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toBeDefined(); expect(body.assets).toBeDefined();
expect(Array.isArray(body.assets.items)).toBe(true); expect(Array.isArray(body.assets.items)).toBe(true);
console.log({ assets: body.assets.items });
for (const [i, asset] of assets.entries()) { for (const [i, asset] of assets.entries()) {
expect(body.assets.items[i]).toEqual(expect.objectContaining({ id: asset.id })); expect(body.assets.items[i]).toEqual(expect.objectContaining({ id: asset.id }));
} }

View File

@ -38,7 +38,7 @@ describe('/trash', () => {
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'delete', assetId }); await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(after.length).toBe(0); expect(after.length).toBe(0);

View File

@ -1,27 +1,37 @@
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk'; import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, userDto } from 'src/fixtures'; import { createUserDto, userDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/user', () => {
let websocket: Socket;
describe('/server-info', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let deletedUser: LoginResponseDto; let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto; let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
let nonAdmin: LoginResponseDto; let nonAdmin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false }); admin = await utils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin, userToDelete] = await Promise.all([ [websocket, deletedUser, nonAdmin, userToDelete, userToHardDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1), utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2), utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3), utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]); ]);
await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) }); await deleteUser({ id: deletedUser.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
}); });
describe('GET /user', () => { describe('GET /user', () => {
@ -34,13 +44,14 @@ describe('/server-info', () => {
it('should get users', async () => { it('should get users', async () => {
const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(5);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }), expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]), ]),
); );
}); });
@ -51,12 +62,13 @@ describe('/server-info', () => {
.query({ isAll: true }) .query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(3); expect(body).toHaveLength(4);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]), ]),
); );
}); });
@ -68,13 +80,14 @@ describe('/server-info', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(5);
expect(body).toEqual( expect(body).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }), expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }),
expect.objectContaining({ email: 'user4@immich.cloud' }),
]), ]),
); );
}); });
@ -138,13 +151,13 @@ describe('/server-info', () => {
.post(`/user`) .post(`/user`)
.send({ .send({
isAdmin: true, isAdmin: true,
email: 'user4@immich.cloud', email: 'user5@immich.cloud',
password: 'password123', password: 'password123',
name: 'Immich', name: 'Immich',
}) })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({ expect(body).toMatchObject({
email: 'user4@immich.cloud', email: 'user5@immich.cloud',
isAdmin: false, isAdmin: false,
shouldChangePassword: true, shouldChangePassword: true,
}); });
@ -188,6 +201,22 @@ describe('/server-info', () => {
deletedAt: expect.any(String), deletedAt: expect.any(String),
}); });
}); });
it('should hard delete user', async () => {
const { status, body } = await request(app)
.delete(`/user/${userToHardDelete.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
});
}); });
describe('PUT /user', () => { describe('PUT /user', () => {

View File

@ -24,7 +24,7 @@ export const createUserDto = {
create(key: string) { create(key: string) {
return { return {
email: `${key}@immich.cloud`, email: `${key}@immich.cloud`,
name: `User ${key}`, name: `Generated User ${key}`,
password: `password-${key}`, password: `password-${key}`,
}; };
}, },
@ -43,6 +43,11 @@ export const createUserDto = {
name: 'User 3', name: 'User 3',
password: 'password123', password: 'password123',
}, },
user4: {
email: 'user4@immich.cloud',
name: 'User 4',
password: 'password123',
},
userQuota: { userQuota: {
email: 'user-quota@immich.cloud', email: 'user-quota@immich.cloud',
name: 'User Quota', name: 'User Quota',

View File

@ -36,8 +36,8 @@ import { makeRandomImage } from 'src/generators';
import request from 'supertest'; import request from 'supertest';
type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'upload' | 'delete'; type EventType = 'assetUpload' | 'assetDelete' | 'userDelete';
type WaitOptions = { event: EventType; assetId: string; timeout?: number }; type WaitOptions = { event: EventType; id: string; timeout?: number };
type AdminSetupOptions = { onboarding?: boolean }; type AdminSetupOptions = { onboarding?: boolean };
type AssetData = { bytes?: Buffer; filename: string }; type AssetData = { bytes?: Buffer; filename: string };
@ -78,20 +78,21 @@ export const immichCli = async (args: string[]) => {
let client: pg.Client | null = null; let client: pg.Client | null = null;
const events: Record<EventType, Set<string>> = { const events: Record<EventType, Set<string>> = {
upload: new Set<string>(), assetUpload: new Set<string>(),
delete: new Set<string>(), assetDelete: new Set<string>(),
userDelete: new Set<string>(),
}; };
const callbacks: Record<string, () => void> = {}; const callbacks: Record<string, () => void> = {};
const execPromise = promisify(exec); const execPromise = promisify(exec);
const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { const onEvent = ({ event, id }: { event: EventType; id: string }) => {
events[event].add(assetId); events[event].add(id);
const callback = callbacks[assetId]; const callback = callbacks[id];
if (callback) { if (callback) {
callback(); callback();
delete callbacks[assetId]; delete callbacks[id];
} }
}; };
@ -166,8 +167,9 @@ export const utils = {
return new Promise<Socket>((resolve) => { return new Promise<Socket>((resolve) => {
websocket websocket
.on('connect', () => resolve(websocket)) .on('connect', () => resolve(websocket))
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id })) .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id }))
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId })) .on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId }))
.on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId }))
.connect(); .connect();
}); });
}, },
@ -182,17 +184,17 @@ export const utils = {
} }
}, },
waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => { waitForWebsocketEvent: async ({ event, id, timeout: ms }: WaitOptions): Promise<void> => {
console.log(`Waiting for ${event} [${assetId}]`); console.log(`Waiting for ${event} [${id}]`);
const set = events[event]; const set = events[event];
if (set.has(assetId)) { if (set.has(id)) {
return; return;
} }
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000); const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
callbacks[assetId] = () => { callbacks[id] = () => {
clearTimeout(timeout); clearTimeout(timeout);
resolve(); resolve();
}; };