2024-05-27 22:16:53 -04:00
|
|
|
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk';
|
2024-05-26 18:15:52 -04:00
|
|
|
import { createUserDto } from 'src/fixtures';
|
2024-02-19 22:34:18 -05:00
|
|
|
import { errorDto } from 'src/responses';
|
2024-03-07 10:14:36 -05:00
|
|
|
import { app, asBearerAuth, utils } from 'src/utils';
|
2024-02-19 22:34:18 -05:00
|
|
|
import request from 'supertest';
|
2024-05-26 18:15:52 -04:00
|
|
|
import { beforeAll, describe, expect, it } from 'vitest';
|
2024-03-10 00:10:24 +01:00
|
|
|
|
2024-05-22 13:24:57 -04:00
|
|
|
describe('/users', () => {
|
2024-02-19 22:34:18 -05:00
|
|
|
let admin: LoginResponseDto;
|
2024-02-21 16:52:13 -05:00
|
|
|
let deletedUser: LoginResponseDto;
|
|
|
|
|
let nonAdmin: LoginResponseDto;
|
2024-02-19 22:34:18 -05:00
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
2024-03-07 10:14:36 -05:00
|
|
|
await utils.resetDatabase();
|
|
|
|
|
admin = await utils.adminSetup({ onboarding: false });
|
2024-02-21 16:52:13 -05:00
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
[deletedUser, nonAdmin] = await Promise.all([
|
2024-03-07 10:14:36 -05:00
|
|
|
utils.userSetup(admin.accessToken, createUserDto.user1),
|
|
|
|
|
utils.userSetup(admin.accessToken, createUserDto.user2),
|
2024-02-21 16:52:13 -05:00
|
|
|
]);
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
await deleteUserAdmin(
|
|
|
|
|
{ id: deletedUser.userId, userAdminDeleteDto: {} },
|
|
|
|
|
{ headers: asBearerAuth(admin.accessToken) },
|
|
|
|
|
);
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
|
2024-05-22 13:24:57 -04:00
|
|
|
describe('GET /users', () => {
|
2024-02-19 22:34:18 -05:00
|
|
|
it('should require authentication', async () => {
|
2024-05-22 13:24:57 -04:00
|
|
|
const { status, body } = await request(app).get('/users');
|
2024-02-19 22:34:18 -05:00
|
|
|
expect(status).toBe(401);
|
|
|
|
|
expect(body).toEqual(errorDto.unauthorized);
|
|
|
|
|
});
|
|
|
|
|
|
2024-02-21 16:52:13 -05:00
|
|
|
it('should get users', async () => {
|
2024-05-22 13:24:57 -04:00
|
|
|
const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
|
2024-02-19 22:34:18 -05:00
|
|
|
expect(status).toEqual(200);
|
2024-05-26 18:15:52 -04:00
|
|
|
expect(body).toHaveLength(2);
|
2024-02-21 16:52:13 -05:00
|
|
|
expect(body).toEqual(
|
|
|
|
|
expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({ email: 'admin@immich.cloud' }),
|
|
|
|
|
expect.objectContaining({ email: 'user2@immich.cloud' }),
|
2024-02-29 11:26:55 -05:00
|
|
|
]),
|
2024-02-21 16:52:13 -05:00
|
|
|
);
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-22 13:24:57 -04:00
|
|
|
describe('GET /users/me', () => {
|
2024-02-19 22:34:18 -05:00
|
|
|
it('should require authentication', async () => {
|
2024-05-22 13:24:57 -04:00
|
|
|
const { status, body } = await request(app).get(`/users/me`);
|
2024-02-19 22:34:18 -05:00
|
|
|
expect(status).toBe(401);
|
|
|
|
|
expect(body).toEqual(errorDto.unauthorized);
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
it('should not work for shared links', async () => {
|
|
|
|
|
const album = await utils.createAlbum(admin.accessToken, { albumName: 'Album' });
|
|
|
|
|
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
|
|
|
|
type: SharedLinkType.Album,
|
|
|
|
|
albumId: album.id,
|
|
|
|
|
});
|
|
|
|
|
const { status, body } = await request(app).get(`/users/me?key=${sharedLink.key}`);
|
|
|
|
|
expect(status).toBe(403);
|
|
|
|
|
expect(body).toEqual(errorDto.forbidden);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get my user', async () => {
|
2024-05-22 13:24:57 -04:00
|
|
|
const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`);
|
2024-02-19 22:34:18 -05:00
|
|
|
expect(status).toBe(200);
|
|
|
|
|
expect(body).toMatchObject({
|
|
|
|
|
id: admin.userId,
|
|
|
|
|
email: 'admin@immich.cloud',
|
2024-05-26 18:15:52 -04:00
|
|
|
quotaUsageInBytes: 0,
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
describe('PUT /users/me', () => {
|
2024-02-19 22:34:18 -05:00
|
|
|
it('should require authentication', async () => {
|
2024-05-26 18:15:52 -04:00
|
|
|
const { status, body } = await request(app).put(`/users/me`);
|
2024-02-19 22:34:18 -05:00
|
|
|
expect(status).toBe(401);
|
|
|
|
|
expect(body).toEqual(errorDto.unauthorized);
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-27 22:16:53 -04:00
|
|
|
for (const key of ['email', 'name']) {
|
2024-02-19 22:34:18 -05:00
|
|
|
it(`should not allow null ${key}`, async () => {
|
2024-05-26 18:15:52 -04:00
|
|
|
const dto = { [key]: null };
|
2024-02-19 22:34:18 -05:00
|
|
|
const { status, body } = await request(app)
|
2024-05-26 18:15:52 -04:00
|
|
|
.put(`/users/me`)
|
2024-02-19 22:34:18 -05:00
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
2024-05-26 18:15:52 -04:00
|
|
|
.send(dto);
|
2024-02-19 22:34:18 -05:00
|
|
|
expect(status).toBe(400);
|
|
|
|
|
expect(body).toEqual(errorDto.badRequest());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
it('should update first and last name', async () => {
|
|
|
|
|
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
|
|
|
|
|
2024-02-19 22:34:18 -05:00
|
|
|
const { status, body } = await request(app)
|
2024-05-26 18:15:52 -04:00
|
|
|
.put(`/users/me`)
|
|
|
|
|
.send({ name: 'Name' })
|
2024-02-19 22:34:18 -05:00
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
2024-05-26 18:15:52 -04:00
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
|
expect(body).toEqual({
|
|
|
|
|
...before,
|
|
|
|
|
updatedAt: expect.any(String),
|
|
|
|
|
name: 'Name',
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
/** @deprecated */
|
|
|
|
|
it('should allow a user to change their password (deprecated)', async () => {
|
|
|
|
|
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
|
2024-02-19 22:34:18 -05:00
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
expect(user.shouldChangePassword).toBe(true);
|
2024-03-10 00:10:24 +01:00
|
|
|
|
|
|
|
|
const { status, body } = await request(app)
|
2024-05-26 18:15:52 -04:00
|
|
|
.put(`/users/me`)
|
|
|
|
|
.send({ password: 'super-secret' })
|
|
|
|
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
2024-03-10 00:10:24 +01:00
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
|
expect(body).toMatchObject({
|
2024-05-26 18:15:52 -04:00
|
|
|
email: nonAdmin.userEmail,
|
|
|
|
|
shouldChangePassword: false,
|
2024-03-10 00:10:24 +01:00
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
|
2024-02-19 22:34:18 -05:00
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
expect(token.accessToken).toBeDefined();
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
it('should not allow user to change to a taken email', async () => {
|
2024-02-19 22:34:18 -05:00
|
|
|
const { status, body } = await request(app)
|
2024-05-26 18:15:52 -04:00
|
|
|
.put(`/users/me`)
|
|
|
|
|
.send({ email: 'admin@immich.cloud' })
|
|
|
|
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
2024-02-19 22:34:18 -05:00
|
|
|
|
|
|
|
|
expect(status).toBe(400);
|
2024-05-26 18:15:52 -04:00
|
|
|
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
it('should update my email', async () => {
|
|
|
|
|
const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
|
2024-02-19 22:34:18 -05:00
|
|
|
const { status, body } = await request(app)
|
2024-05-26 18:15:52 -04:00
|
|
|
.put(`/users/me`)
|
|
|
|
|
.send({ email: 'non-admin@immich.cloud' })
|
|
|
|
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
2024-02-19 22:34:18 -05:00
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
2024-05-26 18:15:52 -04:00
|
|
|
expect(body).toMatchObject({
|
2024-02-19 22:34:18 -05:00
|
|
|
...before,
|
2024-05-26 18:15:52 -04:00
|
|
|
email: 'non-admin@immich.cloud',
|
|
|
|
|
updatedAt: expect.anything(),
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
});
|
2024-05-26 18:15:52 -04:00
|
|
|
});
|
2024-02-19 22:34:18 -05:00
|
|
|
|
2024-05-27 22:16:53 -04:00
|
|
|
describe('PUT /users/me/preferences', () => {
|
|
|
|
|
it('should update memories enabled', async () => {
|
|
|
|
|
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
|
|
|
expect(before).toMatchObject({ memories: { enabled: true } });
|
|
|
|
|
|
|
|
|
|
const { status, body } = await request(app)
|
|
|
|
|
.put(`/users/me/preferences`)
|
|
|
|
|
.send({ memories: { enabled: false } })
|
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
|
expect(body).toMatchObject({ memories: { enabled: false } });
|
|
|
|
|
|
|
|
|
|
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
|
|
|
expect(after).toMatchObject({ memories: { enabled: false } });
|
|
|
|
|
});
|
2024-06-14 17:27:12 +02:00
|
|
|
|
|
|
|
|
it('should update avatar color', async () => {
|
|
|
|
|
const { status, body } = await request(app)
|
|
|
|
|
.put(`/users/me/preferences`)
|
|
|
|
|
.send({ avatar: { color: 'blue' } })
|
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
|
expect(body).toMatchObject({ avatar: { color: 'blue' } });
|
|
|
|
|
|
|
|
|
|
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
|
|
|
expect(after).toMatchObject({ avatar: { color: 'blue' } });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should require an integer for download archive size', async () => {
|
|
|
|
|
const { status, body } = await request(app)
|
|
|
|
|
.put(`/users/me/preferences`)
|
|
|
|
|
.send({ download: { archiveSize: 1_234_567.89 } })
|
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
|
|
|
|
|
|
|
|
expect(status).toBe(400);
|
|
|
|
|
expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number']));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should update download archive size', async () => {
|
|
|
|
|
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
|
|
|
expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } });
|
|
|
|
|
|
|
|
|
|
const { status, body } = await request(app)
|
|
|
|
|
.put(`/users/me/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 getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
|
|
|
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
|
|
|
|
|
});
|
2024-05-27 22:16:53 -04:00
|
|
|
});
|
|
|
|
|
|
2024-05-26 18:15:52 -04:00
|
|
|
describe('GET /users/:id', () => {
|
|
|
|
|
it('should require authentication', async () => {
|
|
|
|
|
const { status } = await request(app).get(`/users/${admin.userId}`);
|
|
|
|
|
expect(status).toEqual(401);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should get the user', async () => {
|
2024-02-19 22:34:18 -05:00
|
|
|
const { status, body } = await request(app)
|
2024-05-26 18:15:52 -04:00
|
|
|
.get(`/users/${admin.userId}`)
|
2024-02-19 22:34:18 -05:00
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
|
expect(body).toMatchObject({
|
2024-05-26 18:15:52 -04:00
|
|
|
id: admin.userId,
|
|
|
|
|
email: 'admin@immich.cloud',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(body).not.toMatchObject({
|
|
|
|
|
shouldChangePassword: expect.anything(),
|
|
|
|
|
storageLabel: expect.anything(),
|
2024-02-19 22:34:18 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|