refactor: e2e tests (#30870)
This commit is contained in:
@@ -1,54 +1,7 @@
|
|||||||
import { expect } from 'vitest';
|
import { expect } from 'vitest';
|
||||||
|
|
||||||
export const errorDto = {
|
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) => ({
|
badRequest: (message: any = null) => ({
|
||||||
message: message ?? expect.anything(),
|
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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { LoginResponseDto } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { app, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@@ -8,18 +7,16 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
describe('/admin/maintenance', () => {
|
describe('/admin/maintenance', () => {
|
||||||
let cookie: string | undefined;
|
let cookie: string | undefined;
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonAdmin: LoginResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
|
||||||
await utils.resetBackups(admin.accessToken);
|
await utils.resetBackups(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
// => outside of maintenance mode
|
// => outside of maintenance mode
|
||||||
|
|
||||||
describe('GET ~/server/config', async () => {
|
describe('GET /server/config', async () => {
|
||||||
it('should indicate we are out of maintenance mode', async () => {
|
it('should indicate we are out of maintenance mode', async () => {
|
||||||
const { status, body } = await request(app).get('/server/config');
|
const { status, body } = await request(app).get('/server/config');
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -49,24 +46,6 @@ describe('/admin/maintenance', () => {
|
|||||||
// => enter maintenance mode
|
// => enter maintenance mode
|
||||||
|
|
||||||
describe.sequential('POST /', () => {
|
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 () => {
|
it('should be a no-op if try to exit maintenance mode', async () => {
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.post('/admin/maintenance')
|
.post('/admin/maintenance')
|
||||||
@@ -132,7 +111,7 @@ describe('/admin/maintenance', () => {
|
|||||||
it('should fail without cookie or token in body', async () => {
|
it('should fail without cookie or token in body', async () => {
|
||||||
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
|
const { status, body } = await request(app).post('/admin/maintenance/login').send({});
|
||||||
expect(status).toBe(401);
|
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 () => {
|
it('should succeed with cookie', async () => {
|
||||||
|
|||||||
@@ -613,16 +613,6 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /albums/:id/assets', () => {
|
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 () => {
|
it('should be able to remove foreign asset from owned album', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/albums/${user2Albums[0].id}/assets`)
|
.delete(`/albums/${user2Albums[0].id}/assets`)
|
||||||
|
|||||||
@@ -24,13 +24,6 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /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 () => {
|
it('should work with apiKey.create', async () => {
|
||||||
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]);
|
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -113,15 +106,6 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /api-keys/:id', () => {
|
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 () => {
|
it('should get api key details', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -139,16 +123,6 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /api-keys/:id', () => {
|
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 () => {
|
it('should update api key details', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
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', () => {
|
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 () => {
|
it('should delete an api key', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
|
|||||||
@@ -151,14 +151,6 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/:id', () => {
|
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 () => {
|
it('should get the asset info', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/assets/${user1Assets[0].id}`)
|
.get(`/assets/${user1Assets[0].id}`)
|
||||||
@@ -306,15 +298,6 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /assets/:id', () => {
|
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 () => {
|
it('should favorite an asset', async () => {
|
||||||
const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id);
|
const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id);
|
||||||
expect(before.isFavorite).toBe(false);
|
expect(before.isFavorite).toBe(false);
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { LoginResponseDto, QueueCommand, QueueName, updateConfig } from '@immich
|
|||||||
import { cpSync, rmSync } from 'node:fs';
|
import { cpSync, rmSync } from 'node:fs';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
import { errorDto } from 'src/responses';
|
import { asBearerAuth, testAssetDir, utils } from 'src/utils';
|
||||||
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/jobs', () => {
|
describe('/jobs', () => {
|
||||||
@@ -50,12 +48,6 @@ describe('/jobs', () => {
|
|||||||
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
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 () => {
|
it('should queue metadata extraction for missing assets', async () => {
|
||||||
const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
|
const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { readFile } from 'node:fs/promises';
|
|||||||
import { basename, join } from 'node:path';
|
import { basename, join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, testAssetDir, utils } from 'src/utils';
|
import { app, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -55,12 +54,6 @@ describe('/map', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /map/markers', () => {
|
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 () => {
|
it('should get map markers for all non-archived assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/map/markers')
|
.get('/map/markers')
|
||||||
@@ -139,52 +132,6 @@ describe('/map', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /map/reverse-geocode', () => {
|
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 = [
|
const reverseGeocodeTestCases = [
|
||||||
{
|
{
|
||||||
name: 'Vaucluse',
|
name: 'Vaucluse',
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
getMemory,
|
getMemory,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
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 { beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -42,14 +41,6 @@ describe('/memories', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /memories/:id', () => {
|
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 () => {
|
it('should get the memory', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/memories/${userMemory.id}`)
|
.get(`/memories/${userMemory.id}`)
|
||||||
@@ -60,15 +51,6 @@ describe('/memories', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /memories/:id', () => {
|
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 () => {
|
it('should update the memory', async () => {
|
||||||
const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) });
|
const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) });
|
||||||
expect(before.isSaved).toBe(false);
|
expect(before.isSaved).toBe(false);
|
||||||
@@ -86,15 +68,6 @@ describe('/memories', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /memories/:id/assets', () => {
|
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 () => {
|
it('should require asset access', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/memories/${userMemory.id}/assets`)
|
.put(`/memories/${userMemory.id}/assets`)
|
||||||
@@ -121,15 +94,6 @@ describe('/memories', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /memories/:id/assets', () => {
|
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 () => {
|
it('should only remove assets in the memory', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/memories/${userMemory.id}/assets`)
|
.delete(`/memories/${userMemory.id}/assets`)
|
||||||
@@ -156,14 +120,6 @@ describe('/memories', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /memories/:id', () => {
|
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 () => {
|
it('should delete the memory', async () => {
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.delete(`/memories/${userMemory.id}`)
|
.delete(`/memories/${userMemory.id}`)
|
||||||
|
|||||||
@@ -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 () => {
|
it('should return a redirect uri', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/oauth/authorize')
|
.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 () => {
|
it(`should throw an error if the state is not provided`, async () => {
|
||||||
const { url } = await loginWithOAuth('oauth-auto-register');
|
const { url } = await loginWithOAuth('oauth-auto-register');
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send({ url });
|
const { status, body } = await request(app).post('/oauth/callback').send({ url });
|
||||||
@@ -380,16 +354,6 @@ describe(`/oauth`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(`POST /oauth/backchannel-logout`, () => {
|
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 () => {
|
it(`should throw an error if an invalid logout token is provided`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/oauth/backchannel-logout')
|
.post('/oauth/backchannel-logout')
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { LoginResponseDto } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, utils } from 'src/utils';
|
import { app, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -137,14 +136,6 @@ describe('/server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /server/statistics', () => {
|
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 () => {
|
it('should return the server stats', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/server/statistics')
|
.get('/server/statistics')
|
||||||
@@ -195,14 +186,6 @@ describe('/server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /server/license', () => {
|
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 () => {
|
it('should return the server license', async () => {
|
||||||
await request(app).put('/server/license').set('Authorization', `Bearer ${admin.accessToken}`).send(serverLicense);
|
await request(app).put('/server/license').set('Authorization', `Bearer ${admin.accessToken}`).send(serverLicense);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -217,14 +200,6 @@ describe('/server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /server/license', () => {
|
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 () => {
|
it('should delete the server license', async () => {
|
||||||
await request(app)
|
await request(app)
|
||||||
.delete('/server/license')
|
.delete('/server/license')
|
||||||
@@ -236,14 +211,6 @@ describe('/server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /server/license', () => {
|
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 () => {
|
it('should set the server license', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put('/server/license')
|
.put('/server/license')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk';
|
import { LoginResponseDto, getSessions, login, signUpAdmin } from '@immich/sdk';
|
||||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
|
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 { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
@@ -18,7 +18,18 @@ describe('/sessions', () => {
|
|||||||
it('should get a list of authorized devices', async () => {
|
it('should get a list of authorized devices', async () => {
|
||||||
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(200);
|
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)
|
const response = await request(app)
|
||||||
.post('/auth/validateToken')
|
.post('/auth/validateToken')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(response.body).toEqual(errorDto.invalidToken);
|
expect(response.body).toEqual({ message: 'Invalid user token' });
|
||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -212,21 +212,21 @@ describe('/shared-links', () => {
|
|||||||
.query({ key: linkWithAlbum.key + 'foo' });
|
.query({ key: linkWithAlbum.key + 'foo' });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
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 () => {
|
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 });
|
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithDeletedAlbum.key });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
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 () => {
|
it('should return unauthorized for password protected link', async () => {
|
||||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
|
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
|
||||||
|
|
||||||
expect(status).toBe(401);
|
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 () => {
|
it('should get data for correct password protected link', async () => {
|
||||||
@@ -306,16 +306,6 @@ describe('/shared-links', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /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 () => {
|
it('should require a valid asset id', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/shared-links')
|
.post('/shared-links')
|
||||||
@@ -403,16 +393,6 @@ describe('/shared-links', () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest('Invalid shared link type'));
|
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 () => {
|
it('should remove assets from a shared link (individual)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/shared-links/${linkWithAssets.id}/assets`)
|
.delete(`/shared-links/${linkWithAssets.id}/assets`)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk';
|
import { LoginResponseDto, searchStacks } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
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 { beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -8,34 +7,16 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
|||||||
describe('/stacks', () => {
|
describe('/stacks', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user2: LoginResponseDto;
|
|
||||||
let asset: AssetMediaResponseDto;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
|
|
||||||
[user1, user2] = await Promise.all([
|
user1 = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
|
||||||
]);
|
|
||||||
|
|
||||||
asset = await utils.createAsset(user1.accessToken);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /stacks', () => {
|
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 () => {
|
it('should create a stack', async () => {
|
||||||
const [asset1, asset2] = await Promise.all([
|
const [asset1, asset2] = await Promise.all([
|
||||||
utils.createAsset(user1.accessToken),
|
utils.createAsset(user1.accessToken),
|
||||||
|
|||||||
@@ -1,30 +1,17 @@
|
|||||||
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, utils } from 'src/utils';
|
import { app, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
describe('/server-info', () => {
|
describe('/server-info', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: 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 });
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /system-metadata/admin-onboarding', () => {
|
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 () => {
|
it('should set admin onboarding', async () => {
|
||||||
const config = await getServerConfig({});
|
const config = await getServerConfig({});
|
||||||
expect(config.isOnboarded).toBe(false);
|
expect(config.isOnboarded).toBe(false);
|
||||||
@@ -41,14 +28,6 @@ describe('/server-info', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /system-metadata/reverse-geocoding-state', () => {
|
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 () => {
|
it('should get the reverse geocoding state', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/system-metadata/reverse-geocoding-state')
|
.get('/system-metadata/reverse-geocoding-state')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
tagAssets,
|
tagAssets,
|
||||||
upsertTags,
|
upsertTags,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto } 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';
|
||||||
@@ -41,13 +41,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /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 () => {
|
it('should work with tag.create', async () => {
|
||||||
const { secret } = await utils.createApiKey(user.accessToken, [Permission.TagCreate]);
|
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' });
|
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', () => {
|
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 () => {
|
it('should start off empty', async () => {
|
||||||
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toEqual([]);
|
expect(body).toEqual([]);
|
||||||
@@ -157,13 +143,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /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 () => {
|
it('should upsert tags', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/tags`)
|
.put(`/tags`)
|
||||||
@@ -195,16 +174,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /tags/assets', () => {
|
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 () => {
|
it('should skip assets that are not owned by the user', async () => {
|
||||||
const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
|
const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
|
||||||
create(user.accessToken, { name: 'TagA' }),
|
create(user.accessToken, { name: 'TagA' }),
|
||||||
@@ -255,25 +224,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /tags/:id', () => {
|
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 () => {
|
it('should get tag details', async () => {
|
||||||
const tag = await create(user.accessToken, { name: 'TagA' });
|
const tag = await create(user.accessToken, { name: 'TagA' });
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -311,27 +261,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /tags/:id', () => {
|
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 () => {
|
it('should update a tag', async () => {
|
||||||
const tag = await create(user.accessToken, { name: 'tagA' });
|
const tag = await create(user.accessToken, { name: 'tagA' });
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -354,23 +283,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /tags/:id', () => {
|
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 () => {
|
it('should delete a tag', async () => {
|
||||||
const tag = await create(user.accessToken, { name: 'TagA' });
|
const tag = await create(user.accessToken, { name: 'TagA' });
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
@@ -404,27 +316,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /tags/:id/assets', () => {
|
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 () => {
|
it('should be able to tag own asset', async () => {
|
||||||
const tagA = await create(user.accessToken, { name: 'TagA' });
|
const tagA = await create(user.accessToken, { name: 'TagA' });
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -463,32 +354,6 @@ describe('/tags', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /tags/:id/assets', () => {
|
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 () => {
|
it('should be able to remove own asset from own tag', async () => {
|
||||||
const tagA = await create(user.accessToken, { name: 'TagA' });
|
const tagA = await create(user.accessToken, { name: 'TagA' });
|
||||||
await tagAssets(
|
await tagAssets(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
|
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { errorDto } from 'src/responses';
|
|
||||||
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
|
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -21,13 +20,6 @@ describe('/trash', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /trash/empty', () => {
|
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 () => {
|
it('should empty the trash', async () => {
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||||
@@ -142,13 +134,6 @@ describe('/trash', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /trash/restore', () => {
|
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 () => {
|
it('should restore all trashed assets', async () => {
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||||
@@ -198,13 +183,6 @@ describe('/trash', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /trash/restore/assets', () => {
|
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 () => {
|
it('should restore a trashed asset by id', async () => {
|
||||||
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
const { id: assetId } = await utils.createAsset(admin.accessToken);
|
||||||
await utils.deleteAssets(admin.accessToken, [assetId]);
|
await utils.deleteAssets(admin.accessToken, [assetId]);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
Permission,
|
|
||||||
QueueName,
|
QueueName,
|
||||||
createStack,
|
createStack,
|
||||||
deleteUserAdmin,
|
deleteUserAdmin,
|
||||||
@@ -10,8 +9,7 @@ import {
|
|||||||
login,
|
login,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
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 { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -46,14 +44,6 @@ describe('/admin/users', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /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 () => {
|
it('should hide deleted users by default', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/admin/users`)
|
.get(`/admin/users`)
|
||||||
@@ -88,15 +78,6 @@ describe('/admin/users', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /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 () => {
|
it('should accept `isAdmin`', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/admin/users`)
|
.post(`/admin/users`)
|
||||||
@@ -117,14 +98,6 @@ describe('/admin/users', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /admin/users/:id', () => {
|
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 () => {
|
it('should allow a non-admin to become an admin', async () => {
|
||||||
const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
|
const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -225,14 +198,6 @@ describe('/admin/users', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /admin/users/:id', () => {
|
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 () => {
|
it('should delete user', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/admin/users/${userToDelete.userId}`)
|
.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', () => {
|
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 () => {
|
it('should restore a user', async () => {
|
||||||
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
|
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
|
||||||
|
|
||||||
|
|||||||
@@ -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 { createUserDto } from 'src/fixtures';
|
||||||
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 { beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -15,19 +14,6 @@ describe('/users', () => {
|
|||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2);
|
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', () => {
|
describe('PUT /users/me', () => {
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
it('should allow a user to change their password (deprecated)', async () => {
|
it('should allow a user to change their password (deprecated)', async () => {
|
||||||
|
|||||||
@@ -10,6 +10,61 @@ const UNAUTHENTICATED_ADMIN_ROUTES = new Set([
|
|||||||
'POST admin/database-backups/start-restore',
|
'POST admin/database-backups/start-restore',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** Admin-only routes that live outside `admin/`, i.e. `@Authenticated({ admin: true })` */
|
||||||
|
const ADMIN_ROUTES = new Set([
|
||||||
|
'DELETE libraries/:id',
|
||||||
|
'DELETE queues/:name/jobs',
|
||||||
|
'DELETE server/license',
|
||||||
|
'GET jobs',
|
||||||
|
'GET libraries',
|
||||||
|
'GET libraries/:id',
|
||||||
|
'GET libraries/:id/statistics',
|
||||||
|
'GET queues',
|
||||||
|
'GET queues/:name',
|
||||||
|
'GET queues/:name/jobs',
|
||||||
|
'GET server/license',
|
||||||
|
'GET server/statistics',
|
||||||
|
'GET system-config',
|
||||||
|
'GET system-config/defaults',
|
||||||
|
'GET system-config/storage-template-options',
|
||||||
|
'GET system-metadata/admin-onboarding',
|
||||||
|
'GET system-metadata/reverse-geocoding-state',
|
||||||
|
'GET system-metadata/version-check-state',
|
||||||
|
'PATCH libraries/:id',
|
||||||
|
'POST jobs',
|
||||||
|
'POST libraries',
|
||||||
|
'POST libraries/:id/scan',
|
||||||
|
'POST libraries/:id/validate',
|
||||||
|
'POST system-metadata/admin-onboarding',
|
||||||
|
'PUT jobs/:name',
|
||||||
|
'PUT libraries/:id',
|
||||||
|
'PUT queues/:name',
|
||||||
|
'PUT server/license',
|
||||||
|
'PUT system-config',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Routes a shared link (`?key=`) is allowed to reach, i.e. `@Authenticated({ sharedLink: true })` */
|
||||||
|
const SHARED_LINK_ROUTES = new Set([
|
||||||
|
'DELETE assets/:id/video/stream/:sessionId',
|
||||||
|
'GET albums/:id',
|
||||||
|
'GET albums/:id/map-markers',
|
||||||
|
'GET assets/:id',
|
||||||
|
'GET assets/:id/original',
|
||||||
|
'GET assets/:id/thumbnail',
|
||||||
|
'GET assets/:id/video/playback',
|
||||||
|
'GET assets/:id/video/stream/:sessionId/:variantIndex/:filename',
|
||||||
|
'GET assets/:id/video/stream/:sessionId/:variantIndex/playlist.m3u8',
|
||||||
|
'GET assets/:id/video/stream/main.m3u8',
|
||||||
|
'GET shared-links/me',
|
||||||
|
'GET timeline/bucket',
|
||||||
|
'GET timeline/buckets',
|
||||||
|
'POST assets',
|
||||||
|
'POST download/archive',
|
||||||
|
'POST download/info',
|
||||||
|
'POST search/metadata',
|
||||||
|
'POST shared-links/login',
|
||||||
|
]);
|
||||||
|
|
||||||
const isAdminPermission = (permission: AuthenticatedOptions['permission']) =>
|
const isAdminPermission = (permission: AuthenticatedOptions['permission']) =>
|
||||||
typeof permission === 'string' && permission.startsWith('admin');
|
typeof permission === 'string' && permission.startsWith('admin');
|
||||||
|
|
||||||
@@ -45,9 +100,9 @@ const getRoutes = () => {
|
|||||||
|
|
||||||
describe('controllers', () => {
|
describe('controllers', () => {
|
||||||
const routes = getRoutes();
|
const routes = getRoutes();
|
||||||
const adminRoutes = routes.filter((route) => route.path === 'admin' || route.path.startsWith('admin/'));
|
|
||||||
|
|
||||||
it('should only allow non-admin access to bootstrap routes under admin/', () => {
|
it('should only allow non-admin access to bootstrap routes under admin/', () => {
|
||||||
|
const adminRoutes = routes.filter((route) => route.path === 'admin' || route.path.startsWith('admin/'));
|
||||||
const reachableByNonAdmins = adminRoutes.filter((route) => !route.auth?.admin).map((route) => route.id);
|
const reachableByNonAdmins = adminRoutes.filter((route) => !route.auth?.admin).map((route) => route.id);
|
||||||
|
|
||||||
expect(new Set(reachableByNonAdmins)).toEqual(UNAUTHENTICATED_ADMIN_ROUTES);
|
expect(new Set(reachableByNonAdmins)).toEqual(UNAUTHENTICATED_ADMIN_ROUTES);
|
||||||
@@ -59,6 +114,20 @@ describe('controllers', () => {
|
|||||||
expect(undeclared).toEqual([]);
|
expect(undeclared).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only allow shared link access to expected routes', () => {
|
||||||
|
const sharedLinkRoutes = routes.filter((route) => route.auth?.sharedLink).map((route) => route.id);
|
||||||
|
|
||||||
|
expect(new Set(sharedLinkRoutes)).toEqual(SHARED_LINK_ROUTES);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only require admin access on expected routes outside /admin', () => {
|
||||||
|
const adminRoutes = routes
|
||||||
|
.filter((route) => route.auth?.admin && route.path !== 'admin' && !route.path.startsWith('admin/'))
|
||||||
|
.map((route) => route.id);
|
||||||
|
|
||||||
|
expect(new Set(adminRoutes)).toEqual(ADMIN_ROUTES);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require admin access for routes with an admin permission', () => {
|
it('should require admin access for routes with an admin permission', () => {
|
||||||
const offenders = routes
|
const offenders = routes
|
||||||
.filter((route) => isAdminPermission(route.auth?.permission) && !route.auth?.admin)
|
.filter((route) => isAdminPermission(route.auth?.permission) && !route.auth?.admin)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { JobController } from 'src/controllers/job.controller';
|
||||||
|
import { JobService } from 'src/services/job.service';
|
||||||
|
import { QueueService } from 'src/services/queue.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(JobController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(JobService);
|
||||||
|
const queueService = mockBaseService(QueueService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(JobController, [
|
||||||
|
{ provide: JobService, useValue: service },
|
||||||
|
{ provide: QueueService, useValue: queueService },
|
||||||
|
]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
queueService.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /jobs/:name', () => {
|
||||||
|
it('should require a valid queue name', async () => {
|
||||||
|
const { status } = await request(ctx.getHttpServer())
|
||||||
|
.put('/jobs/invalid')
|
||||||
|
.send({ command: 'start', force: false });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(queueService.runCommandLegacy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid command', async () => {
|
||||||
|
const { status } = await request(ctx.getHttpServer())
|
||||||
|
.put('/jobs/metadataExtraction')
|
||||||
|
.send({ command: 'invalid', force: false });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(queueService.runCommandLegacy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,28 +20,6 @@ describe(LibraryController.name, () => {
|
|||||||
|
|
||||||
const id = factory.uuid();
|
const id = factory.uuid();
|
||||||
|
|
||||||
describe('authentication', () => {
|
|
||||||
const routes = [
|
|
||||||
{ method: 'get', path: '/libraries' },
|
|
||||||
{ method: 'post', path: '/libraries' },
|
|
||||||
{ method: 'get', path: `/libraries/${id}` },
|
|
||||||
{ method: 'put', path: `/libraries/${id}` },
|
|
||||||
{ method: 'patch', path: `/libraries/${id}` },
|
|
||||||
{ method: 'delete', path: `/libraries/${id}` },
|
|
||||||
{ method: 'post', path: `/libraries/${id}/validate` },
|
|
||||||
{ method: 'get', path: `/libraries/${id}/statistics` },
|
|
||||||
{ method: 'post', path: `/libraries/${id}/scan` },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
it.each(routes)('$method $path should be an admin route', async ({ method, path }) => {
|
|
||||||
await request(ctx.getHttpServer())[method](path).send({});
|
|
||||||
|
|
||||||
expect(ctx.authenticate).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ metadata: expect.objectContaining({ adminRoute: true }) }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('POST /libraries', () => {
|
describe('POST /libraries', () => {
|
||||||
it('should require an owner id', async () => {
|
it('should require an owner id', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer()).post('/libraries').send({});
|
const { status, body } = await request(ctx.getHttpServer()).post('/libraries').send({});
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { MapController } from 'src/controllers/map.controller';
|
||||||
|
import { MapService } from 'src/services/map.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(MapController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(MapService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(MapController, [{ provide: MapService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /map/reverse-geocode', () => {
|
||||||
|
it('should require a lat', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).get('/map/reverse-geocode').query({ lon: 123 });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
|
expect(service.reverseGeocode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a lat that is a number', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.get('/map/reverse-geocode')
|
||||||
|
.query({ lat: 'abc', lon: 123.456 });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
|
expect(service.reverseGeocode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a lat that is in range', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.get('/map/reverse-geocode')
|
||||||
|
.query({ lat: 91, lon: 123.456 });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
|
||||||
|
);
|
||||||
|
expect(service.reverseGeocode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a lon', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).get('/map/reverse-geocode').query({ lat: 75 });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
|
expect(service.reverseGeocode).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
|
import { AuthService } from 'src/services/auth.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(OAuthController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(AuthService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(OAuthController, [{ provide: AuthService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /oauth/authorize', () => {
|
||||||
|
it('should require a redirect uri', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/oauth/authorize').send({});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(service.authorize).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /oauth/callback', () => {
|
||||||
|
it('should require a url', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/oauth/callback').send({});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['url'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow an empty url', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/oauth/callback').send({ url: '' });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['url'], message: 'Too small: expected string to have >=1 characters' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /oauth/backchannel-logout', () => {
|
||||||
|
it('should require a logout token', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/oauth/backchannel-logout').send({});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -47,6 +47,15 @@ describe(SharedLinkController.name, () => {
|
|||||||
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null }));
|
expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ expiresAt: null }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require an albumId for share type Album', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post('/shared-links')
|
||||||
|
.send({ type: SharedLinkType.Album });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'albumId is required for type ALBUM' }]));
|
||||||
|
expect(service.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should not allow an albumId for share type Individual', async () => {
|
it('should not allow an albumId for share type Individual', async () => {
|
||||||
const { status, body } = await request(ctx.getHttpServer())
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
.post('/shared-links')
|
.post('/shared-links')
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { SystemMetadataController } from 'src/controllers/system-metadata.controller';
|
||||||
|
import { SystemMetadataService } from 'src/services/system-metadata.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(SystemMetadataController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(SystemMetadataService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(SystemMetadataController, [{ provide: SystemMetadataService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /system-metadata/admin-onboarding', () => {
|
||||||
|
it('should require isOnboarded', async () => {
|
||||||
|
const { status } = await request(ctx.getHttpServer()).post('/system-metadata/admin-onboarding').send({});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(service.updateAdminOnboarding).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { TrashController } from 'src/controllers/trash.controller';
|
||||||
|
import { TrashService } from 'src/services/trash.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||||
|
|
||||||
|
describe(TrashController.name, () => {
|
||||||
|
let ctx: ControllerContext;
|
||||||
|
const service = mockBaseService(TrashService);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
ctx = await controllerSetup(TrashController, [{ provide: TrashService, useValue: service }]);
|
||||||
|
return () => ctx.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetAllMocks();
|
||||||
|
ctx.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /trash/restore/assets', () => {
|
||||||
|
it('should require asset ids', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer()).post('/trash/restore/assets').send({});
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
factory.responses.validationError([
|
||||||
|
{ path: ['ids'], message: 'Invalid input: expected array, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(service.restoreAssets).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require valid asset ids', async () => {
|
||||||
|
const { status, body } = await request(ctx.getHttpServer())
|
||||||
|
.post('/trash/restore/assets')
|
||||||
|
.send({ ids: ['invalid'] });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }]));
|
||||||
|
expect(service.restoreAssets).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AlbumService } from 'src/services/album.service';
|
import { AlbumService } from 'src/services/album.service';
|
||||||
import { newMediumService } from 'test/medium.factory';
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
import { getKyselyDB } from 'test/utils';
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
let defaultDatabase: Kysely<DB>;
|
let defaultDatabase: Kysely<DB>;
|
||||||
@@ -12,7 +15,7 @@ let defaultDatabase: Kysely<DB>;
|
|||||||
const setup = (db?: Kysely<DB>) => {
|
const setup = (db?: Kysely<DB>) => {
|
||||||
return newMediumService(AlbumService, {
|
return newMediumService(AlbumService, {
|
||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
real: [AlbumRepository, UserRepository],
|
real: [AccessRepository, AlbumRepository, AssetRepository, UserRepository],
|
||||||
mock: [LoggingRepository],
|
mock: [LoggingRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -22,16 +25,35 @@ beforeAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(AlbumService.name, () => {
|
describe(AlbumService.name, () => {
|
||||||
|
describe('removeAssets', () => {
|
||||||
|
it('should not remove assets from an album of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { album } = await ctx.newAlbum({ ownerId: user.id }, [asset.id]);
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(factory.auth({ user: otherUser }), album.id, { ids: [asset.id] })).rejects.toThrow(
|
||||||
|
'Not found or no albumAsset.delete access',
|
||||||
|
);
|
||||||
|
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('database triggers', () => {
|
describe('database triggers', () => {
|
||||||
it('should cascade delete an album when the owner is deleted', async () => {
|
it('should cascade delete an album when the owner is deleted', async () => {
|
||||||
const { ctx } = setup();
|
const { ctx } = setup();
|
||||||
const { user } = await ctx.newUser();
|
const { user } = await ctx.newUser();
|
||||||
await ctx.newAlbum({ ownerId: user.id });
|
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||||
|
|
||||||
await ctx.get(UserRepository).delete({ id: user.id }, true);
|
await ctx.get(UserRepository).delete({ id: user.id }, true);
|
||||||
|
|
||||||
await expect(ctx.database.selectFrom('album').selectAll().execute()).resolves.toEqual([]);
|
await expect(ctx.database.selectFrom('album').selectAll().where('id', '=', album.id).execute()).resolves.toEqual(
|
||||||
await expect(ctx.database.selectFrom('album_user').selectAll().execute()).resolves.toEqual([]);
|
[],
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('album_user').selectAll().where('albumId', '=', album.id).execute(),
|
||||||
|
).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,44 @@ beforeAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(ApiKeyService.name, () => {
|
describe(ApiKeyService.name, () => {
|
||||||
|
describe('getById', () => {
|
||||||
|
it('should not return an api key of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { apiKey } = await sut.create(factory.auth({ user }), { permissions: [Permission.All] });
|
||||||
|
|
||||||
|
await expect(sut.getById(factory.auth({ user: otherUser }), apiKey.id)).rejects.toThrow('API Key not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should not update an api key of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { apiKey } = await sut.create(factory.auth({ user }), { permissions: [Permission.All] });
|
||||||
|
|
||||||
|
await expect(sut.update(factory.auth({ user: otherUser }), apiKey.id, { name: 'new name' })).rejects.toThrow(
|
||||||
|
'API Key not found',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should not delete an api key of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { apiKey } = await sut.create(factory.auth({ user }), { permissions: [Permission.All] });
|
||||||
|
|
||||||
|
await expect(sut.delete(factory.auth({ user: otherUser }), apiKey.id)).rejects.toThrow('API Key not found');
|
||||||
|
await expect(sut.getById(factory.auth({ user }), apiKey.id)).resolves.toEqual(
|
||||||
|
expect.objectContaining({ id: apiKey.id }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('rotate', () => {
|
describe('rotate', () => {
|
||||||
it('should not rotate an api key of another user', async () => {
|
it('should not rotate an api key of another user', async () => {
|
||||||
const { sut, ctx } = setup();
|
const { sut, ctx } = setup();
|
||||||
|
|||||||
@@ -45,6 +45,19 @@ beforeAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe(AssetService.name, () => {
|
describe(AssetService.name, () => {
|
||||||
|
describe('get', () => {
|
||||||
|
it('should not return an asset of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await expect(sut.get(factory.auth({ user: otherUser }), asset.id)).rejects.toThrow(
|
||||||
|
'Not found or no asset.read access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getStatistics', () => {
|
describe('getStatistics', () => {
|
||||||
it('should return stats as numbers, not strings', async () => {
|
it('should return stats as numbers, not strings', async () => {
|
||||||
const { sut, ctx } = setup();
|
const { sut, ctx } = setup();
|
||||||
@@ -334,6 +347,17 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
|
it('should not update an asset of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await expect(sut.update(factory.auth({ user: otherUser }), asset.id, {})).rejects.toThrow(
|
||||||
|
'Not found or no asset.update access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should automatically lock lockable columns', async () => {
|
it('should automatically lock lockable columns', async () => {
|
||||||
const { sut, ctx } = setup();
|
const { sut, ctx } = setup();
|
||||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||||
|
|||||||
@@ -34,7 +34,70 @@ const setup = (db?: Kysely<DB>) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A memory owned by one user, plus another user's auth to attempt access with */
|
||||||
|
const newMemoryOfAnotherUser = async (ctx: ReturnType<typeof setup>['ctx']) => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { memory } = await ctx.newMemory({ ownerId: user.id });
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
return { memory, asset, auth: factory.auth({ user }), otherAuth: factory.auth({ user: otherUser }) };
|
||||||
|
};
|
||||||
|
|
||||||
describe(MemoryService.name, () => {
|
describe(MemoryService.name, () => {
|
||||||
|
describe('get', () => {
|
||||||
|
it('should not return a memory of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { memory, otherAuth } = await newMemoryOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.get(otherAuth, memory.id)).rejects.toThrow('Not found or no memory.read access');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should not update a memory of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { memory, otherAuth } = await newMemoryOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.update(otherAuth, memory.id, { isSaved: true })).rejects.toThrow(
|
||||||
|
'Not found or no memory.update access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
it('should not remove a memory of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { memory, auth, otherAuth } = await newMemoryOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.remove(otherAuth, memory.id)).rejects.toThrow('Not found or no memory.delete access');
|
||||||
|
await expect(sut.get(auth, memory.id)).resolves.toEqual(expect.objectContaining({ id: memory.id }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addAssets', () => {
|
||||||
|
it('should not add assets to a memory of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { memory, asset, otherAuth } = await newMemoryOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.addAssets(otherAuth, memory.id, { ids: [asset.id] })).rejects.toThrow(
|
||||||
|
'Not found or no memory.read access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAssets', () => {
|
||||||
|
it('should not remove assets from a memory of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { memory, asset, otherAuth } = await newMemoryOfAnotherUser(ctx);
|
||||||
|
await ctx.newMemoryAsset({ memoryId: memory.id, assetId: asset.id });
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(otherAuth, memory.id, { ids: [asset.id] })).rejects.toThrow(
|
||||||
|
'Not found or no memory.update access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
defaultDatabase = await getKyselyDB();
|
defaultDatabase = await getKyselyDB();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { StackRepository } from 'src/repositories/stack.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
|
import { StackService } from 'src/services/stack.service';
|
||||||
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = (db?: Kysely<DB>) => {
|
||||||
|
return newMediumService(StackService, {
|
||||||
|
database: db || defaultDatabase,
|
||||||
|
real: [AccessRepository, AssetRepository, StackRepository],
|
||||||
|
mock: [EventRepository, LoggingRepository],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(StackService.name, () => {
|
||||||
|
describe('create', () => {
|
||||||
|
it('should not stack an asset of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const { asset: otherAsset } = await ctx.newAsset({ ownerId: otherUser.id });
|
||||||
|
|
||||||
|
await expect(sut.create(factory.auth({ user }), { assetIds: [asset.id, otherAsset.id] })).rejects.toThrow(
|
||||||
|
'Not found or no asset.update access',
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('stack').selectAll().where('ownerId', '=', user.id).execute(),
|
||||||
|
).resolves.toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,12 +22,61 @@ const setup = (db?: Kysely<DB>) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** A tag owned by one user, plus another user's auth to attempt access with */
|
||||||
|
const newTagOfAnotherUser = async (ctx: ReturnType<typeof setup>['ctx']) => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { user: otherUser } = await ctx.newUser();
|
||||||
|
const [tag] = await upsertTags(ctx.get(TagRepository), { userId: user.id, tags: ['tag-1'] });
|
||||||
|
|
||||||
|
return { tag, auth: factory.auth({ user }), otherAuth: factory.auth({ user: otherUser }) };
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
defaultDatabase = await getKyselyDB();
|
defaultDatabase = await getKyselyDB();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(TagService.name, () => {
|
describe(TagService.name, () => {
|
||||||
|
describe('get', () => {
|
||||||
|
it('should not return a tag of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { tag, otherAuth } = await newTagOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.get(otherAuth, tag.id)).rejects.toThrow('Not found or no tag.read access');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update', () => {
|
||||||
|
it('should not update a tag of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { tag, otherAuth } = await newTagOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.update(otherAuth, tag.id, { color: '#000000' })).rejects.toThrow(
|
||||||
|
'Not found or no tag.update access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remove', () => {
|
||||||
|
it('should not remove a tag of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { tag, auth, otherAuth } = await newTagOfAnotherUser(ctx);
|
||||||
|
|
||||||
|
await expect(sut.remove(otherAuth, tag.id)).rejects.toThrow('Not found or no tag.delete access');
|
||||||
|
await expect(sut.get(auth, tag.id)).resolves.toEqual(expect.objectContaining({ id: tag.id }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('addAssets', () => {
|
describe('addAssets', () => {
|
||||||
|
it('should not add assets to a tag of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { tag, otherAuth } = await newTagOfAnotherUser(ctx);
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: otherAuth.user.id });
|
||||||
|
|
||||||
|
await expect(sut.addAssets(otherAuth, tag.id, { ids: [asset.id] })).rejects.toThrow(
|
||||||
|
'Not found or no tag.asset access',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should lock exif column', async () => {
|
it('should lock exif column', async () => {
|
||||||
const { sut, ctx } = setup();
|
const { sut, ctx } = setup();
|
||||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
@@ -53,6 +102,22 @@ describe(TagService.name, () => {
|
|||||||
await expect(ctx.get(TagRepository).getAssetIds(tag.id, [asset.id])).resolves.toContain(asset.id);
|
await expect(ctx.get(TagRepository).getAssetIds(tag.id, [asset.id])).resolves.toContain(asset.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeAssets', () => {
|
||||||
|
it('should not remove assets from a tag of another user', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
|
const { tag, auth, otherAuth } = await newTagOfAnotherUser(ctx);
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||||
|
await sut.addAssets(auth, tag.id, { ids: [asset.id] });
|
||||||
|
|
||||||
|
await expect(sut.removeAssets(otherAuth, tag.id, { ids: [asset.id] })).rejects.toThrow(
|
||||||
|
'Not found or no tag.asset access',
|
||||||
|
);
|
||||||
|
await expect(ctx.get(TagRepository).getAssetIds(tag.id, [asset.id])).resolves.toContain(asset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deleteEmptyTags', () => {
|
describe('deleteEmptyTags', () => {
|
||||||
it('single tag exists, not connected to any assets, and is deleted', async () => {
|
it('single tag exists, not connected to any assets, and is deleted', async () => {
|
||||||
const { sut, ctx } = setup();
|
const { sut, ctx } = setup();
|
||||||
|
|||||||
Reference in New Issue
Block a user