refactor: e2e tests (#30870)

This commit is contained in:
Jason Rasmussen 2026-08-19 14:31:57 -04:00 committed by GitHub
parent c63824bcea
commit 7cd0a7d30c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 618 additions and 648 deletions

View file

@ -1,54 +1,7 @@
import { expect } from 'vitest';
export const errorDto = {
unauthorized: {
message: 'Authentication required',
},
unauthorizedWithMessage: (message: string) => ({
message,
}),
forbidden: {
message: expect.any(String),
},
missingPermission: (permission: string) => ({
message: `Missing required permission: ${permission}`,
}),
wrongPassword: {
message: 'Wrong password',
},
invalidToken: {
message: 'Invalid user token',
},
invalidShareKey: {
message: 'Invalid share key',
},
passwordRequired: {
message: 'Password required',
},
badRequest: (message: any = null) => ({
message: message ?? expect.anything(),
}),
validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>) => ({
message: 'Validation failed',
errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
}),
noPermission: {
message: expect.stringContaining('Not found or no'),
},
incorrectLogin: {
message: 'Incorrect email or password',
},
};
export const deviceDto = {
current: {
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
current: true,
isPendingSyncReset: false,
deviceOS: '',
deviceType: '',
appVersion: null,
},
};

View file

@ -1,5 +1,4 @@
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
@ -8,18 +7,16 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/admin/maintenance', () => {
let cookie: string | undefined;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
await utils.resetBackups(admin.accessToken);
});
// => outside of maintenance mode
describe('GET ~/server/config', async () => {
describe('GET /server/config', async () => {
it('should indicate we are out of maintenance mode', async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
@ -49,24 +46,6 @@ describe('/admin/maintenance', () => {
// => enter maintenance mode
describe.sequential('POST /', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/admin/maintenance').send({
active: false,
action: 'end',
});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ action: 'end' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should be a no-op if try to exit maintenance mode', async () => {
const { status } = await request(app)
.post('/admin/maintenance')
@ -132,7 +111,7 @@ describe('/admin/maintenance', () => {
it('should fail without cookie or token in body', async () => {
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorizedWithMessage('Missing JWT Token'));
expect(body).toEqual({ message: 'Missing JWT Token' });
});
it('should succeed with cookie', async () => {

View file

@ -613,16 +613,6 @@ describe('/albums', () => {
});
describe('DELETE /albums/:id/assets', () => {
it('should require authorization', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should be able to remove foreign asset from owned album', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`)

View file

@ -24,13 +24,6 @@ describe('/api-keys', () => {
});
describe('POST /api-keys', () => {
it('should not work without permission', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('apiKey.create'));
});
it('should work with apiKey.create', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]);
const { status, body } = await request(app)
@ -113,15 +106,6 @@ describe('/api-keys', () => {
});
describe('GET /api-keys/:id', () => {
it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.get(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('API Key not found'));
});
it('should get api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
@ -139,16 +123,6 @@ describe('/api-keys', () => {
});
describe('PUT /api-keys/:id', () => {
it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.put(`/api-keys/${apiKey.id}`)
.send({ name: 'new name', permissions: [Permission.All] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('API Key not found'));
});
it('should update api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
@ -169,26 +143,7 @@ describe('/api-keys', () => {
});
});
describe('POST /api-keys/:id/rotate', () => {
it('should not work without permission', async () => {
const { apiKey } = await create(user.accessToken, [Permission.ApiKeyUpdate]);
const { secret } = await create(user.accessToken, [Permission.ApiKeyUpdate]);
const { status, body } = await request(app).post(`/api-keys/${apiKey.id}/rotate`).set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('apiKey.rotate'));
});
});
describe('DELETE /api-keys/:id', () => {
it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.delete(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('API Key not found'));
});
it('should delete an api key', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status } = await request(app)

View file

@ -151,14 +151,6 @@ describe('/asset', () => {
});
describe('GET /assets/:id', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.get(`/assets/${user2Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should get the asset info', async () => {
const { status, body } = await request(app)
.get(`/assets/${user1Assets[0].id}`)
@ -306,15 +298,6 @@ describe('/asset', () => {
});
describe('PUT /assets/:id', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/assets/${user2Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should favorite an asset', async () => {
const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id);
expect(before.isFavorite).toBe(false);

View file

@ -2,9 +2,7 @@ import { LoginResponseDto, QueueCommand, QueueName, updateConfig } from '@immich
import { cpSync, rmSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { asBearerAuth, testAssetDir, utils } from 'src/utils';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('/jobs', () => {
@ -50,12 +48,6 @@ describe('/jobs', () => {
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
});
it('should require authentication', async () => {
const { status, body } = await request(app).put('/jobs/metadataExtraction');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should queue metadata extraction for missing assets', async () => {
const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;

View file

@ -3,7 +3,6 @@ import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@ -55,12 +54,6 @@ describe('/map', () => {
});
describe('GET /map/markers', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/markers');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/map/markers')
@ -139,52 +132,6 @@ describe('/map', () => {
});
describe('GET /map/reverse-geocode', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/reverse-geocode');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should throw an error if a lat is not provided', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lon=123')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
});
it('should throw an error if a lat is not a number', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lat=abc&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
});
it('should throw an error if a lat is out of range', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lat=91&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
);
});
it('should throw an error if a lon is not provided', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lat=75')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
);
});
const reverseGeocodeTestCases = [
{
name: 'Vaucluse',

View file

@ -7,7 +7,6 @@ import {
getMemory,
} from '@immich/sdk';
import { createUserDto } 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';
@ -42,14 +41,6 @@ describe('/memories', () => {
});
describe('GET /memories/:id', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.get(`/memories/${userMemory.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should get the memory', async () => {
const { status, body } = await request(app)
.get(`/memories/${userMemory.id}`)
@ -60,15 +51,6 @@ describe('/memories', () => {
});
describe('PUT /memories/:id', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}`)
.send({ isSaved: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should update the memory', async () => {
const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) });
expect(before.isSaved).toBe(false);
@ -86,15 +68,6 @@ describe('/memories', () => {
});
describe('PUT /memories/:id/assets', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}/assets`)
.send({ ids: [userAsset1.id] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require asset access', async () => {
const { status, body } = await request(app)
.put(`/memories/${userMemory.id}/assets`)
@ -121,15 +94,6 @@ describe('/memories', () => {
});
describe('DELETE /memories/:id/assets', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}/assets`)
.send({ ids: [userAsset1.id] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should only remove assets in the memory', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}/assets`)
@ -156,14 +120,6 @@ describe('/memories', () => {
});
describe('DELETE /memories/:id', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.delete(`/memories/${userMemory.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should delete the memory', async () => {
const { status } = await request(app)
.delete(`/memories/${userMemory.id}`)

View file

@ -102,16 +102,6 @@ describe(`/oauth`, () => {
});
});
it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/authorize').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
]),
);
});
it('should return a redirect uri', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
@ -165,22 +155,6 @@ describe(`/oauth`, () => {
});
});
it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
);
});
it(`should throw an error if the url is empty`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
);
});
it(`should throw an error if the state is not provided`, async () => {
const { url } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
@ -380,16 +354,6 @@ describe(`/oauth`, () => {
});
describe(`POST /oauth/backchannel-logout`, () => {
it(`should throw an error if the logout_token is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
]),
);
});
it(`should throw an error if an invalid logout token is provided`, async () => {
const { status, body } = await request(app)
.post('/oauth/backchannel-logout')

View file

@ -1,6 +1,5 @@
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@ -137,14 +136,6 @@ describe('/server', () => {
});
describe('GET /server/statistics', () => {
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/server/statistics')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should return the server stats', async () => {
const { status, body } = await request(app)
.get('/server/statistics')
@ -195,14 +186,6 @@ describe('/server', () => {
});
describe('GET /server/license', () => {
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/server/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should return the server license', async () => {
await request(app).put('/server/license').set('Authorization', `Bearer ${admin.accessToken}`).send(serverLicense);
const { status, body } = await request(app)
@ -217,14 +200,6 @@ describe('/server', () => {
});
describe('DELETE /server/license', () => {
it('should only work for admins', async () => {
const { status, body } = await request(app)
.delete('/server/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should delete the server license', async () => {
await request(app)
.delete('/server/license')
@ -236,14 +211,6 @@ describe('/server', () => {
});
describe('PUT /server/license', () => {
it('should only work for admins', async () => {
const { status, body } = await request(app)
.put('/server/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should set the server license', async () => {
const { status, body } = await request(app)
.put('/server/license')

View file

@ -1,6 +1,6 @@
import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import { deviceDto, errorDto } from 'src/responses';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeEach, describe, expect, it } from 'vitest';
@ -18,7 +18,18 @@ describe('/sessions', () => {
it('should get a list of authorized devices', async () => {
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([deviceDto.current]);
expect(body).toEqual([
{
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
current: true,
isPendingSyncReset: false,
deviceOS: '',
deviceType: '',
appVersion: null,
},
]);
});
});
@ -56,7 +67,7 @@ describe('/sessions', () => {
const response = await request(app)
.post('/auth/validateToken')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.body).toEqual(errorDto.invalidToken);
expect(response.body).toEqual({ message: 'Invalid user token' });
expect(response.status).toBe(401);
});
});

View file

@ -212,21 +212,21 @@ describe('/shared-links', () => {
.query({ key: linkWithAlbum.key + 'foo' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey);
expect(body).toEqual({ message: 'Invalid share key' });
});
it('should return unauthorized if target has been soft deleted', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey);
expect(body).toEqual({ message: 'Invalid share key' });
});
it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
expect(status).toBe(401);
expect(body).toEqual(errorDto.passwordRequired);
expect(body).toEqual({ message: 'Password required' });
});
it('should get data for correct password protected link', async () => {
@ -306,16 +306,6 @@ describe('/shared-links', () => {
});
describe('POST /shared-links', () => {
it('should require an asset/album id', async () => {
const { status, body } = await request(app)
.post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album });
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'albumId is required for type ALBUM' }]));
});
it('should require a valid asset id', async () => {
const { status, body } = await request(app)
.post('/shared-links')
@ -403,16 +393,6 @@ describe('/shared-links', () => {
expect(body).toEqual(errorDto.badRequest('Invalid shared link type'));
});
it('should reject guests removing assets from an individual shared link', async () => {
const { status, body } = await request(app)
.delete(`/shared-links/${linkWithAssets.id}/assets`)
.query({ key: linkWithAssets.key })
.send({ assetIds: [asset1.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should remove assets from a shared link (individual)', async () => {
const { status, body } = await request(app)
.delete(`/shared-links/${linkWithAssets.id}/assets`)

View file

@ -1,6 +1,5 @@
import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk';
import { LoginResponseDto, searchStacks } from '@immich/sdk';
import { createUserDto } 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';
@ -8,34 +7,16 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/stacks', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let asset: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
[user1, user2] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
]);
asset = await utils.createAsset(user1.accessToken);
user1 = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('POST /stacks', () => {
it('should require access', async () => {
const user2Asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset.id, user2Asset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should create a stack', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),

View file

@ -1,30 +1,17 @@
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/server-info', () => {
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('POST /system-metadata/admin-onboarding', () => {
it('should only work for admins', async () => {
const { status, body } = await request(app)
.post('/system-metadata/admin-onboarding')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ isOnboarded: true });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should set admin onboarding', async () => {
const config = await getServerConfig({});
expect(config.isOnboarded).toBe(false);
@ -41,14 +28,6 @@ describe('/server-info', () => {
});
describe('GET /system-metadata/reverse-geocoding-state', () => {
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/system-metadata/reverse-geocoding-state')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get the reverse geocoding state', async () => {
const { status, body } = await request(app)
.get('/system-metadata/reverse-geocoding-state')

View file

@ -9,7 +9,7 @@ import {
tagAssets,
upsertTags,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
@ -41,13 +41,6 @@ describe('/tags', () => {
});
describe('POST /tags', () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.create'));
});
it('should work with tag.create', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.TagCreate]);
const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
@ -111,13 +104,6 @@ describe('/tags', () => {
});
describe('GET /tags', () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).get('/tags').set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});
it('should start off empty', async () => {
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]);
@ -157,13 +143,6 @@ describe('/tags', () => {
});
describe('PUT /tags', () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.create'));
});
it('should upsert tags', async () => {
const { status, body } = await request(app)
.put(`/tags`)
@ -195,16 +174,6 @@ describe('/tags', () => {
});
describe('PUT /tags/assets', () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put('/tags/assets')
.set('x-api-key', secret)
.send({ assetIds: [], tagIds: [] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});
it('should skip assets that are not owned by the user', async () => {
const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
create(user.accessToken, { name: 'TagA' }),
@ -255,25 +224,6 @@ describe('/tags', () => {
});
describe('GET /tags/:id', () => {
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.get(`/tags/${tag.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.get(`/tags/${uuidDto.notFound}`)
.set('x-api-key', secret)
.send({ assetIds: [], tagIds: [] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});
it('should get tag details', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
@ -311,27 +261,6 @@ describe('/tags', () => {
});
describe('PUT /tags/:id', () => {
it('should require authorization', async () => {
const tag = await create(admin.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.send({ color: '#000000' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.set('x-api-key', secret)
.send({ color: '#000000' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.update'));
});
it('should update a tag', async () => {
const tag = await create(user.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
@ -354,23 +283,6 @@ describe('/tags', () => {
});
describe('DELETE /tags/:id', () => {
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.delete(`/tags/${tag.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).delete(`/tags/${tag.id}`).set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.delete'));
});
it('should delete a tag', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status } = await request(app)
@ -404,27 +316,6 @@ describe('/tags', () => {
});
describe('PUT /tags/:id/assets', () => {
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}/assets`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put(`/tags/${tag.id}/assets`)
.set('x-api-key', secret)
.send({ ids: [userAsset.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});
it('should be able to tag own asset', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
@ -463,32 +354,6 @@ describe('/tags', () => {
});
describe('DELETE /tags/:id/assets', () => {
it('should require authorization', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(
{ id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
{ headers: asBearerAuth(user.accessToken) },
);
const { status, body } = await request(app)
.delete(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.delete(`/tags/${tag.id}/assets`)
.set('x-api-key', secret)
.send({ ids: [userAsset.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});
it('should be able to remove own asset from own tag', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(

View file

@ -1,7 +1,6 @@
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
import { existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@ -21,13 +20,6 @@ describe('/trash', () => {
});
describe('POST /trash/empty', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/trash/empty');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should empty the trash', async () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
@ -142,13 +134,6 @@ describe('/trash', () => {
});
describe('POST /trash/restore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/trash/restore');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should restore all trashed assets', async () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
@ -198,13 +183,6 @@ describe('/trash', () => {
});
describe('POST /trash/restore/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/trash/restore/assets');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should restore a trashed asset by id', async () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);

View file

@ -1,6 +1,5 @@
import {
LoginResponseDto,
Permission,
QueueName,
createStack,
deleteUserAdmin,
@ -10,8 +9,7 @@ import {
login,
} from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { createUserDto } from 'src/fixtures';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@ -46,14 +44,6 @@ describe('/admin/users', () => {
});
describe('GET /admin/users', () => {
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`)
@ -88,15 +78,6 @@ describe('/admin/users', () => {
});
describe('POST /admin/users', () => {
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);
});
it('should accept `isAdmin`', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
@ -117,14 +98,6 @@ describe('/admin/users', () => {
});
describe('PUT /admin/users/:id', () => {
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);
});
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)
@ -225,14 +198,6 @@ describe('/admin/users', () => {
});
describe('DELETE /admin/users/:id', () => {
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}`)
@ -296,34 +261,7 @@ describe('/admin/users', () => {
});
});
describe('GET /admin/users/:id/calendar-heatmap', () => {
it('should require admin permissions', async () => {
const { status, body } = await request(app)
.get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should require the AdminUserRead permission', async () => {
const { secret } = await utils.createApiKey(admin.accessToken, [Permission.UserRead]);
const { status, body } = await request(app)
.get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`)
.set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
});
describe('POST /admin/users/:id/restore', () => {
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'));

View file

@ -1,6 +1,5 @@
import { LoginResponseDto, SharedLinkType, getMyUser, login } from '@immich/sdk';
import { LoginResponseDto, getMyUser, login } from '@immich/sdk';
import { createUserDto } 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';
@ -15,19 +14,6 @@ describe('/users', () => {
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2);
});
describe('GET /users/me', () => {
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);
});
});
describe('PUT /users/me', () => {
/** @deprecated */
it('should allow a user to change their password (deprecated)', async () => {