From 11e7533a4d6f46b37984a1f79d8c8bdc94ae69a7 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sun, 10 Mar 2024 00:10:24 +0100 Subject: [PATCH] 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 --- e2e/src/api/specs/asset.e2e-spec.ts | 10 +++--- e2e/src/api/specs/search.e2e-spec.ts | 4 +-- e2e/src/api/specs/trash.e2e-spec.ts | 2 +- e2e/src/api/specs/user.e2e-spec.ts | 47 ++++++++++++++++++++++------ e2e/src/fixtures.ts | 7 ++++- e2e/src/utils.ts | 30 +++++++++--------- 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 65ad094be6..a13bb58eb1 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -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([ utils.createAsset(user1.accessToken), @@ -693,7 +693,7 @@ describe('/asset', () => { 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); @@ -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); @@ -822,8 +822,8 @@ describe('/asset', () => { .set('Authorization', `Bearer ${admin.accessToken}`); await utils.waitForWebsocketEvent({ - event: 'upload', - assetId: locationAsset.id, + event: 'assetUpload', + id: locationAsset.id, }); expect(status).toBe(200); diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 19b1b68073..9c554abc56 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -76,7 +76,7 @@ describe('/search', () => { } 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') .send(dto) .set('Authorization', `Bearer ${admin.accessToken}`); - console.dir({ status, body }, { depth: 10 }); expect(status).toBe(200); expect(body.assets).toBeDefined(); expect(Array.isArray(body.assets.items)).toBe(true); - console.log({ assets: body.assets.items }); for (const [i, asset] of assets.entries()) { expect(body.assets.items[i]).toEqual(expect.objectContaining({ id: asset.id })); } diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index 3e6c2f1fc6..dc2cadc498 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -38,7 +38,7 @@ describe('/trash', () => { const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); 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) }); expect(after.length).toBe(0); diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index d448a605cd..911f25381a 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -1,27 +1,37 @@ import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk'; +import { Socket } from 'socket.io-client'; import { createUserDto, userDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; 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 deletedUser: LoginResponseDto; let userToDelete: LoginResponseDto; + let userToHardDelete: LoginResponseDto; let nonAdmin: LoginResponseDto; beforeAll(async () => { await utils.resetDatabase(); 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.user2), 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', () => { @@ -34,13 +44,14 @@ describe('/server-info', () => { it('should get users', async () => { const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(200); - expect(body).toHaveLength(4); + expect(body).toHaveLength(5); expect(body).toEqual( expect.arrayContaining([ expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'user1@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }), + expect.objectContaining({ email: 'user4@immich.cloud' }), ]), ); }); @@ -51,12 +62,13 @@ describe('/server-info', () => { .query({ isAll: true }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(3); + expect(body).toHaveLength(4); expect(body).toEqual( expect.arrayContaining([ expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'user2@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}`); expect(status).toBe(200); - expect(body).toHaveLength(4); + expect(body).toHaveLength(5); expect(body).toEqual( expect.arrayContaining([ expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'user1@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }), + expect.objectContaining({ email: 'user4@immich.cloud' }), ]), ); }); @@ -138,13 +151,13 @@ describe('/server-info', () => { .post(`/user`) .send({ isAdmin: true, - email: 'user4@immich.cloud', + email: 'user5@immich.cloud', password: 'password123', name: 'Immich', }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toMatchObject({ - email: 'user4@immich.cloud', + email: 'user5@immich.cloud', isAdmin: false, shouldChangePassword: true, }); @@ -188,6 +201,22 @@ describe('/server-info', () => { 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', () => { diff --git a/e2e/src/fixtures.ts b/e2e/src/fixtures.ts index 56070e6e34..031985c5fb 100644 --- a/e2e/src/fixtures.ts +++ b/e2e/src/fixtures.ts @@ -24,7 +24,7 @@ export const createUserDto = { create(key: string) { return { email: `${key}@immich.cloud`, - name: `User ${key}`, + name: `Generated User ${key}`, password: `password-${key}`, }; }, @@ -43,6 +43,11 @@ export const createUserDto = { name: 'User 3', password: 'password123', }, + user4: { + email: 'user4@immich.cloud', + name: 'User 4', + password: 'password123', + }, userQuota: { email: 'user-quota@immich.cloud', name: 'User Quota', diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index af86a608db..dde9ed22ce 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -36,8 +36,8 @@ import { makeRandomImage } from 'src/generators'; import request from 'supertest'; type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; -type EventType = 'upload' | 'delete'; -type WaitOptions = { event: EventType; assetId: string; timeout?: number }; +type EventType = 'assetUpload' | 'assetDelete' | 'userDelete'; +type WaitOptions = { event: EventType; id: string; timeout?: number }; type AdminSetupOptions = { onboarding?: boolean }; type AssetData = { bytes?: Buffer; filename: string }; @@ -78,20 +78,21 @@ export const immichCli = async (args: string[]) => { let client: pg.Client | null = null; const events: Record> = { - upload: new Set(), - delete: new Set(), + assetUpload: new Set(), + assetDelete: new Set(), + userDelete: new Set(), }; const callbacks: Record void> = {}; const execPromise = promisify(exec); -const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { - events[event].add(assetId); - const callback = callbacks[assetId]; +const onEvent = ({ event, id }: { event: EventType; id: string }) => { + events[event].add(id); + const callback = callbacks[id]; if (callback) { callback(); - delete callbacks[assetId]; + delete callbacks[id]; } }; @@ -166,8 +167,9 @@ export const utils = { return new Promise((resolve) => { websocket .on('connect', () => resolve(websocket)) - .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id })) - .on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId })) + .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id })) + .on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId })) + .on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId })) .connect(); }); }, @@ -182,17 +184,17 @@ export const utils = { } }, - waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise => { - console.log(`Waiting for ${event} [${assetId}]`); + waitForWebsocketEvent: async ({ event, id, timeout: ms }: WaitOptions): Promise => { + console.log(`Waiting for ${event} [${id}]`); const set = events[event]; - if (set.has(assetId)) { + if (set.has(id)) { return; } return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000); - callbacks[assetId] = () => { + callbacks[id] = () => { clearTimeout(timeout); resolve(); };