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:
parent
ec8fb0be83
commit
11e7533a4d
@ -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);
|
||||||
|
@ -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 }));
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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',
|
||||||
|
@ -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();
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user