mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			384 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			384 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|   JobName,
 | |
|   LoginResponseDto,
 | |
|   createStack,
 | |
|   deleteUserAdmin,
 | |
|   getMyUser,
 | |
|   getUserAdmin,
 | |
|   getUserPreferencesAdmin,
 | |
|   login,
 | |
| } from '@immich/sdk';
 | |
| import { Socket } from 'socket.io-client';
 | |
| import { createUserDto, uuidDto } from 'src/fixtures';
 | |
| import { errorDto } from 'src/responses';
 | |
| import { app, asBearerAuth, utils } from 'src/utils';
 | |
| import request from 'supertest';
 | |
| import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 | |
| 
 | |
| describe('/admin/users', () => {
 | |
|   let websocket: Socket;
 | |
| 
 | |
|   let admin: LoginResponseDto;
 | |
|   let nonAdmin: LoginResponseDto;
 | |
|   let deletedUser: LoginResponseDto;
 | |
|   let userToDelete: LoginResponseDto;
 | |
| 
 | |
|   beforeAll(async () => {
 | |
|     await utils.resetDatabase();
 | |
|     admin = await utils.adminSetup({ onboarding: false });
 | |
| 
 | |
|     [websocket, nonAdmin, deletedUser, userToDelete] = 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),
 | |
|     ]);
 | |
| 
 | |
|     await deleteUserAdmin(
 | |
|       { id: deletedUser.userId, userAdminDeleteDto: {} },
 | |
|       { headers: asBearerAuth(admin.accessToken) },
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   afterAll(() => {
 | |
|     utils.disconnectWebsocket(websocket);
 | |
|   });
 | |
| 
 | |
|   describe('GET /admin/users', () => {
 | |
|     it('should require authentication', async () => {
 | |
|       const { status, body } = await request(app).get(`/admin/users`);
 | |
|       expect(status).toBe(401);
 | |
|       expect(body).toEqual(errorDto.unauthorized);
 | |
|     });
 | |
| 
 | |
|     it('should require authorization', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .get(`/admin/users`)
 | |
|         .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
 | |
|       expect(status).toBe(403);
 | |
|       expect(body).toEqual(errorDto.forbidden);
 | |
|     });
 | |
| 
 | |
|     it('should hide deleted users by default', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .get(`/admin/users`)
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toHaveLength(3);
 | |
|       expect(body).toEqual(
 | |
|         expect.arrayContaining([
 | |
|           expect.objectContaining({ email: admin.userEmail }),
 | |
|           expect.objectContaining({ email: nonAdmin.userEmail }),
 | |
|           expect.objectContaining({ email: userToDelete.userEmail }),
 | |
|         ]),
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it('should include deleted users', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .get(`/admin/users?withDeleted=true`)
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toHaveLength(4);
 | |
|       expect(body).toEqual(
 | |
|         expect.arrayContaining([
 | |
|           expect.objectContaining({ email: admin.userEmail }),
 | |
|           expect.objectContaining({ email: nonAdmin.userEmail }),
 | |
|           expect.objectContaining({ email: userToDelete.userEmail }),
 | |
|           expect.objectContaining({ email: deletedUser.userEmail }),
 | |
|         ]),
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('POST /admin/users', () => {
 | |
|     it('should require authentication', async () => {
 | |
|       const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1);
 | |
|       expect(status).toBe(401);
 | |
|       expect(body).toEqual(errorDto.unauthorized);
 | |
|     });
 | |
| 
 | |
|     it('should require authorization', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .post(`/admin/users`)
 | |
|         .set('Authorization', `Bearer ${nonAdmin.accessToken}`)
 | |
|         .send(createUserDto.user1);
 | |
|       expect(status).toBe(403);
 | |
|       expect(body).toEqual(errorDto.forbidden);
 | |
|     });
 | |
| 
 | |
|     for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
 | |
|       it(`should not allow null ${key}`, async () => {
 | |
|         const { status, body } = await request(app)
 | |
|           .post(`/admin/users`)
 | |
|           .set('Authorization', `Bearer ${admin.accessToken}`)
 | |
|           .send({ ...createUserDto.user1, [key]: null });
 | |
|         expect(status).toBe(400);
 | |
|         expect(body).toEqual(errorDto.badRequest());
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     it('should accept `isAdmin`', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .post(`/admin/users`)
 | |
|         .send({
 | |
|           isAdmin: true,
 | |
|           email: 'user5@immich.cloud',
 | |
|           password: 'password123',
 | |
|           name: 'Immich',
 | |
|         })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
|       expect(body).toMatchObject({
 | |
|         email: 'user5@immich.cloud',
 | |
|         isAdmin: true,
 | |
|         shouldChangePassword: true,
 | |
|       });
 | |
|       expect(status).toBe(201);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('PUT /admin/users/:id', () => {
 | |
|     it('should require authentication', async () => {
 | |
|       const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`);
 | |
|       expect(status).toBe(401);
 | |
|       expect(body).toEqual(errorDto.unauthorized);
 | |
|     });
 | |
| 
 | |
|     it('should require authorization', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${uuidDto.notFound}`)
 | |
|         .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
 | |
|       expect(status).toBe(403);
 | |
|       expect(body).toEqual(errorDto.forbidden);
 | |
|     });
 | |
| 
 | |
|     for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
 | |
|       it(`should not allow null ${key}`, async () => {
 | |
|         const { status, body } = await request(app)
 | |
|           .put(`/admin/users/${uuidDto.notFound}`)
 | |
|           .set('Authorization', `Bearer ${admin.accessToken}`)
 | |
|           .send({ [key]: null });
 | |
|         expect(status).toBe(400);
 | |
|         expect(body).toEqual(errorDto.badRequest());
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     it('should allow a non-admin to become an admin', async () => {
 | |
|       const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${user.userId}`)
 | |
|         .send({ isAdmin: true })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({ isAdmin: true });
 | |
|     });
 | |
| 
 | |
|     it('ignores updates to profileImagePath', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${admin.userId}`)
 | |
|         .send({ profileImagePath: 'invalid.jpg' })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
 | |
|     });
 | |
| 
 | |
|     it('should update first and last name', async () => {
 | |
|       const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | |
| 
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${admin.userId}`)
 | |
|         .send({ name: 'Name' })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toEqual({
 | |
|         ...before,
 | |
|         updatedAt: expect.any(String),
 | |
|         name: 'Name',
 | |
|       });
 | |
|       expect(before.updatedAt).not.toEqual(body.updatedAt);
 | |
|     });
 | |
| 
 | |
|     it('should update password', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${nonAdmin.userId}`)
 | |
|         .send({ password: 'super-secret' })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({ email: nonAdmin.userEmail });
 | |
| 
 | |
|       const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
 | |
|       expect(token.accessToken).toBeDefined();
 | |
| 
 | |
|       const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
 | |
|       expect(user).toMatchObject({ email: nonAdmin.userEmail });
 | |
|     });
 | |
| 
 | |
|     it('should update the avatar color', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${admin.userId}`)
 | |
|         .send({ avatarColor: 'orange' })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({ avatarColor: 'orange' });
 | |
| 
 | |
|       const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | |
|       expect(after).toMatchObject({ avatarColor: 'orange' });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('PUT /admin/users/:id/preferences', () => {
 | |
|     it('should require authentication', async () => {
 | |
|       const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`);
 | |
|       expect(status).toBe(401);
 | |
|       expect(body).toEqual(errorDto.unauthorized);
 | |
|     });
 | |
| 
 | |
|     it('should update memories enabled', async () => {
 | |
|       const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | |
|       expect(before).toMatchObject({ memories: { enabled: true } });
 | |
| 
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${admin.userId}/preferences`)
 | |
|         .send({ memories: { enabled: false } })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({ memories: { enabled: false } });
 | |
| 
 | |
|       const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | |
|       expect(after).toMatchObject({ memories: { enabled: false } });
 | |
|     });
 | |
| 
 | |
|     it('should update download archive size', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .put(`/admin/users/${admin.userId}/preferences`)
 | |
|         .send({ download: { archiveSize: 1_234_567 } })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });
 | |
| 
 | |
|       const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | |
|       expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('DELETE /admin/users/:id', () => {
 | |
|     it('should require authentication', async () => {
 | |
|       const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);
 | |
|       expect(status).toBe(401);
 | |
|       expect(body).toEqual(errorDto.unauthorized);
 | |
|     });
 | |
| 
 | |
|     it('should require authorization', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .delete(`/admin/users/${userToDelete.userId}`)
 | |
|         .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
 | |
|       expect(status).toBe(403);
 | |
|       expect(body).toEqual(errorDto.forbidden);
 | |
|     });
 | |
| 
 | |
|     it('should delete user', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .delete(`/admin/users/${userToDelete.userId}`)
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({
 | |
|         id: userToDelete.userId,
 | |
|         updatedAt: expect.any(String),
 | |
|         deletedAt: expect.any(String),
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it('should hard delete a user', async () => {
 | |
|       const user = await utils.userSetup(admin.accessToken, createUserDto.create('hard-delete-1'));
 | |
| 
 | |
|       const { status, body } = await request(app)
 | |
|         .delete(`/admin/users/${user.userId}`)
 | |
|         .send({ force: true })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({
 | |
|         id: user.userId,
 | |
|         updatedAt: expect.any(String),
 | |
|         deletedAt: expect.any(String),
 | |
|       });
 | |
| 
 | |
|       await utils.waitForWebsocketEvent({ event: 'userDelete', id: user.userId, timeout: 5000 });
 | |
|     });
 | |
| 
 | |
|     it('should hard delete a user with stacked assets', async () => {
 | |
|       const user = await utils.userSetup(admin.accessToken, createUserDto.create('hard-delete-1'));
 | |
| 
 | |
|       const [asset1, asset2] = await Promise.all([
 | |
|         utils.createAsset(user.accessToken),
 | |
|         utils.createAsset(user.accessToken),
 | |
|       ]);
 | |
| 
 | |
|       await createStack(
 | |
|         { stackCreateDto: { assetIds: [asset1.id, asset2.id] } },
 | |
|         { headers: asBearerAuth(user.accessToken) },
 | |
|       );
 | |
| 
 | |
|       await utils.waitForQueueFinish(admin.accessToken, JobName.BackgroundTask);
 | |
| 
 | |
|       const { status, body } = await request(app)
 | |
|         .delete(`/admin/users/${user.userId}`)
 | |
|         .send({ force: true })
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
| 
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toMatchObject({
 | |
|         id: user.userId,
 | |
|         updatedAt: expect.any(String),
 | |
|         deletedAt: expect.any(String),
 | |
|       });
 | |
| 
 | |
|       await utils.waitForWebsocketEvent({ event: 'userDelete', id: user.userId, timeout: 5000 });
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('POST /admin/users/:id/restore', () => {
 | |
|     it('should require authentication', async () => {
 | |
|       const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`);
 | |
|       expect(status).toBe(401);
 | |
|       expect(body).toEqual(errorDto.unauthorized);
 | |
|     });
 | |
| 
 | |
|     it('should require authorization', async () => {
 | |
|       const { status, body } = await request(app)
 | |
|         .post(`/admin/users/${userToDelete.userId}/restore`)
 | |
|         .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
 | |
|       expect(status).toBe(403);
 | |
|       expect(body).toEqual(errorDto.forbidden);
 | |
|     });
 | |
| 
 | |
|     it('should restore a user', async () => {
 | |
|       const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
 | |
| 
 | |
|       await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
 | |
| 
 | |
|       const { status, body } = await request(app)
 | |
|         .post(`/admin/users/${user.userId}/restore`)
 | |
|         .set('Authorization', `Bearer ${admin.accessToken}`);
 | |
|       expect(status).toBe(200);
 | |
|       expect(body).toEqual(
 | |
|         expect.objectContaining({
 | |
|           id: user.userId,
 | |
|           email: user.userEmail,
 | |
|           status: 'active',
 | |
|           deletedAt: null,
 | |
|         }),
 | |
|       );
 | |
|     });
 | |
|   });
 | |
| });
 |