diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index ee75d6070b8..70c32313f14 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -46,38 +46,6 @@ describe('/activities', () => { }); describe('GET /activities', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/activities'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require an albumId', async () => { - const { status, body } = await request(app) - .get('/activities') - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); - }); - - it('should reject an invalid albumId', async () => { - const { status, body } = await request(app) - .get('/activities') - .query({ albumId: uuidDto.invalid }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); - }); - - it('should reject an invalid assetId', async () => { - const { status, body } = await request(app) - .get('/activities') - .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))); - }); - it('should start off empty', async () => { const { status, body } = await request(app) .get('/activities') @@ -192,30 +160,6 @@ describe('/activities', () => { }); describe('POST /activities', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/activities'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require an albumId', async () => { - const { status, body } = await request(app) - .post('/activities') - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidDto.invalid }); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); - }); - - it('should require a comment when type is comment', async () => { - const { status, body } = await request(app) - .post('/activities') - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty'])); - }); - it('should add a comment to an album', async () => { const { status, body } = await request(app) .post('/activities') @@ -330,20 +274,6 @@ describe('/activities', () => { }); describe('DELETE /activities/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/activities/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should remove a comment from an album', async () => { const reaction = await createActivity({ albumId: album.id, diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index cede49f4690..65a94122fa9 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -9,7 +9,7 @@ import { LoginResponseDto, SharedLinkType, } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -128,28 +128,6 @@ describe('/albums', () => { }); describe('GET /albums', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should reject an invalid shared param', async () => { - const { status, body } = await request(app) - .get('/albums?shared=invalid') - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value'])); - }); - - it('should reject an invalid assetId param', async () => { - const { status, body } = await request(app) - .get('/albums?assetId=invalid') - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID'])); - }); - it("should not show other users' favorites", async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) @@ -323,12 +301,6 @@ describe('/albums', () => { }); describe('GET /albums/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return album info for own album', async () => { const { status, body } = await request(app) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`) @@ -421,12 +393,6 @@ describe('/albums', () => { }); describe('GET /albums/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums/statistics'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return total count of albums the user has access to', async () => { const { status, body } = await request(app) .get('/albums/statistics') @@ -438,12 +404,6 @@ describe('/albums', () => { }); describe('POST /albums', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should create an album', async () => { const { status, body } = await request(app) .post('/albums') @@ -471,12 +431,6 @@ describe('/albums', () => { }); describe('PUT /albums/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should be able to add own asset to own album', async () => { const asset = await utils.createAsset(user1.accessToken); const { status, body } = await request(app) @@ -526,14 +480,6 @@ describe('/albums', () => { }); describe('PATCH /albums/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .patch(`/albums/${uuidDto.notFound}`) - .send({ albumName: 'New album name' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should update an album', async () => { const album = await utils.createAlbum(user1.accessToken, { albumName: 'New album', @@ -576,15 +522,6 @@ describe('/albums', () => { }); describe('DELETE /albums/:id/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .delete(`/albums/${user1Albums[0].id}/assets`) - .send({ ids: [user1Asset1.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .delete(`/albums/${user1Albums[1].id}/assets`) @@ -679,13 +616,6 @@ describe('/albums', () => { }); }); - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should be able to add user to own album', async () => { const { status, body } = await request(app) .put(`/albums/${album.id}/users`) diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts index 17482766253..e86edddcdfa 100644 --- a/e2e/src/api/specs/api-key.e2e-spec.ts +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -1,5 +1,5 @@ import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -24,12 +24,6 @@ describe('/api-keys', () => { }); describe('POST /api-keys', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - 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' }); @@ -99,12 +93,6 @@ describe('/api-keys', () => { }); describe('GET /api-keys', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/api-keys'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should start off empty', async () => { const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toEqual([]); @@ -125,12 +113,6 @@ describe('/api-keys', () => { }); describe('GET /api-keys/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -140,14 +122,6 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.badRequest('API Key not found')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .get(`/api-keys/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should get api key details', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -165,12 +139,6 @@ describe('/api-keys', () => { }); describe('PUT /api-keys/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -181,15 +149,6 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.badRequest('API Key not found')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .put(`/api-keys/${uuidDto.invalid}`) - .send({ name: 'new name' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should update api key details', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -208,12 +167,6 @@ describe('/api-keys', () => { }); describe('DELETE /api-keys/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) @@ -223,14 +176,6 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.badRequest('API Key not found')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/api-keys/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should delete an api key', async () => { const { apiKey } = await create(user.accessToken, [Permission.All]); const { status } = await request(app) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 01129b32996..f5cf78bb8a6 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -22,24 +22,6 @@ import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/ import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -const makeUploadDto = (options?: { omit: string }): Record => { - const dto: Record = { - deviceAssetId: 'example-image', - deviceId: 'TEST', - fileCreatedAt: new Date().toISOString(), - fileModifiedAt: new Date().toISOString(), - isFavorite: 'testing', - duration: '0:00:00.000000', - }; - - const omit = options?.omit; - if (omit) { - delete dto[omit]; - } - - return dto; -}; - const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; @@ -160,13 +142,6 @@ describe('/asset', () => { }); describe('GET /assets/:id/original', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}/original`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should download the file', async () => { const response = await request(app) .get(`/assets/${user1Assets[0].id}/original`) @@ -178,20 +153,6 @@ describe('/asset', () => { }); describe('GET /assets/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}`); - expect(body).toEqual(errorDto.unauthorized); - expect(status).toBe(401); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .get(`/assets/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); - }); - it('should require access', async () => { const { status, body } = await request(app) .get(`/assets/${user2Assets[0].id}`) @@ -354,13 +315,6 @@ describe('/asset', () => { }); describe('GET /assets/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/assets/statistics'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return stats of all assets', async () => { const { status, body } = await request(app) .get('/assets/statistics') @@ -425,13 +379,6 @@ describe('/asset', () => { await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); - it('should require authentication', async () => { - const { status, body } = await request(app).get('/assets/random'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it.each(TEN_TIMES)('should return 1 random assets', async () => { const { status, body } = await request(app) .get('/assets/random') @@ -467,14 +414,6 @@ describe('/asset', () => { expect(status).toBe(200); expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]); }); - - it('should return error', async () => { - const { status } = await request(app) - .get('/assets/random?count=ABC') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(400); - }); }); describe('PUT /assets/:id', () => { @@ -619,28 +558,6 @@ describe('/asset', () => { expect(status).toEqual(200); }); - it('should reject invalid gps coordinates', async () => { - for (const test of [ - { latitude: 12 }, - { longitude: 12 }, - { latitude: 12, longitude: 'abc' }, - { latitude: 'abc', longitude: 12 }, - { latitude: null, longitude: 12 }, - { latitude: 12, longitude: null }, - { latitude: 91, longitude: 12 }, - { latitude: -91, longitude: 12 }, - { latitude: 12, longitude: -181 }, - { latitude: 12, longitude: 181 }, - ]) { - const { status, body } = await request(app) - .put(`/assets/${user1Assets[0].id}`) - .send(test) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - } - }); - it('should update gps data', async () => { const { status, body } = await request(app) .put(`/assets/${user1Assets[0].id}`) @@ -712,17 +629,6 @@ describe('/asset', () => { expect(status).toEqual(200); }); - it('should reject invalid rating', async () => { - for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { - const { status, body } = await request(app) - .put(`/assets/${user1Assets[0].id}`) - .send(test) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - } - }); - it('should return tagged people', async () => { const { status, body } = await request(app) .put(`/assets/${user1Assets[0].id}`) @@ -746,25 +652,6 @@ describe('/asset', () => { }); describe('DELETE /assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .delete(`/assets`) - .send({ ids: [uuidDto.notFound] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/assets`) - .send({ ids: [uuidDto.invalid] }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); - }); - it('should throw an error when the id is not found', async () => { const { status, body } = await request(app) .delete(`/assets`) @@ -877,13 +764,6 @@ describe('/asset', () => { }); describe('GET /assets/:id/thumbnail', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${locationAsset.id}/thumbnail`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should not include gps data for webp thumbnails', async () => { await utils.waitForWebsocketEvent({ event: 'assetUpload', @@ -919,13 +799,6 @@ describe('/asset', () => { }); describe('GET /assets/:id/original', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/assets/${locationAsset.id}/original`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should download the original', async () => { const { status, body, type } = await request(app) .get(`/assets/${locationAsset.id}/original`) @@ -946,43 +819,9 @@ describe('/asset', () => { }); }); - describe('PUT /assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/assets'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - describe('POST /assets', () => { beforeAll(setupTests, 30_000); - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/assets`); - expect(body).toEqual(errorDto.unauthorized); - expect(status).toBe(401); - }); - - it.each([ - { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } }, - { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } }, - { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } }, - { should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } }, - { should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } }, - { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } }, - { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } }, - { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } }, - ])('should $should', async ({ dto }) => { - const { status, body } = await request(app) - .post('/assets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .attach('assetData', makeRandomImage(), 'example.png') - .field(dto); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - }); - const tests = [ { input: 'formats/avif/8bit-sRGB.avif', diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 1031390ee9d..23f8c65fa83 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -3,7 +3,6 @@ import { DateTime } from 'luxon'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { Socket } from 'socket.io-client'; -import { errorDto } from 'src/responses'; import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -141,65 +140,6 @@ describe('/search', () => { }); describe('POST /search/metadata', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/search/metadata'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - const badTests = [ - { - should: 'should reject page as a string', - dto: { page: 'abc' }, - expected: ['page must not be less than 1', 'page must be an integer number'], - }, - { - should: 'should reject page as a decimal', - dto: { page: 1.5 }, - expected: ['page must be an integer number'], - }, - { - should: 'should reject page as a negative number', - dto: { page: -10 }, - expected: ['page must not be less than 1'], - }, - { - should: 'should reject page as 0', - dto: { page: 0 }, - expected: ['page must not be less than 1'], - }, - { - should: 'should reject size as a string', - dto: { size: 'abc' }, - expected: [ - 'size must not be greater than 1000', - 'size must not be less than 1', - 'size must be an integer number', - ], - }, - { - should: 'should reject an invalid size', - dto: { size: -1.5 }, - expected: ['size must not be less than 1', 'size must be an integer number'], - }, - ...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({ - should: `should reject ${value} not a boolean`, - dto: { [value]: 'immich' }, - expected: [`${value} must be a boolean value`], - })), - ]; - - for (const { should, dto, expected } of badTests) { - it(should, async () => { - const { status, body } = await request(app) - .post('/search/metadata') - .set('Authorization', `Bearer ${admin.accessToken}`) - .send(dto); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expected)); - }); - } - const searchTests = [ { should: 'should get my assets', @@ -454,14 +394,6 @@ describe('/search', () => { } }); - describe('POST /search/smart', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/search/smart'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - describe('POST /search/random', () => { beforeAll(async () => { await Promise.all([ @@ -476,13 +408,6 @@ describe('/search', () => { await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); - it('should require authentication', async () => { - const { status, body } = await request(app).post('/search/random').send({ size: 1 }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it.each(TEN_TIMES)('should return 1 random assets', async () => { const { status, body } = await request(app) .post('/search/random') @@ -512,12 +437,6 @@ describe('/search', () => { }); describe('GET /search/explore', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/explore'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get explore data', async () => { const { status, body } = await request(app) .get('/search/explore') @@ -528,12 +447,6 @@ describe('/search', () => { }); describe('GET /search/places', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/places'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get relevant places', async () => { const name = 'Paris'; @@ -552,12 +465,6 @@ describe('/search', () => { }); describe('GET /search/cities', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/cities'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get all cities', async () => { const { status, body } = await request(app) .get('/search/cities') @@ -576,12 +483,6 @@ describe('/search', () => { }); describe('GET /search/suggestions', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/search/suggestions'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get suggestions for country (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=country&includeNull=true') diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts new file mode 100644 index 00000000000..fdf1c34d06a --- /dev/null +++ b/server/src/controllers/activity.controller.spec.ts @@ -0,0 +1,81 @@ +import { ActivityController } from 'src/controllers/activity.controller'; +import { ActivityService } from 'src/services/activity.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(ActivityController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(ActivityController, [ + { provide: ActivityService, useValue: mockBaseService(ActivityService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /activities', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/activities'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require an albumId', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/activities'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + }); + + it('should reject an invalid albumId', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + }); + + it('should reject an invalid assetId', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get('/activities') + .query({ albumId: factory.uuid(), assetId: '123' }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + }); + }); + + describe('POST /activities', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/activities'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require an albumId', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/activities').send({ albumId: '123' }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + }); + + it('should require a comment when type is comment', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/activities') + .send({ albumId: factory.uuid(), type: 'comment', comment: null }); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['comment must be a string', 'comment should not be empty'])); + }); + }); + + describe('DELETE /activities/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/activities/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); +}); diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts new file mode 100644 index 00000000000..5aee4dfdd5a --- /dev/null +++ b/server/src/controllers/album.controller.spec.ts @@ -0,0 +1,86 @@ +import { AlbumController } from 'src/controllers/album.controller'; +import { AlbumService } from 'src/services/album.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AlbumController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(AlbumController, [{ provide: AlbumService, useValue: mockBaseService(AlbumService) }]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /albums', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/albums'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject an invalid shared param', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value'])); + }); + + it('should reject an invalid assetId param', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid'); + expect(status).toEqual(400); + expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID'])); + }); + }); + + describe('GET /albums/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /albums/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/albums/statistics'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /albums', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/albums').send({ albumName: 'New album' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /albums/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PATCH /albums/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).patch(`/albums/${factory.uuid()}`).send({ albumName: 'New album name' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /albums/:id/assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/albums/${factory.uuid()}/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT :id/users', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/users`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts new file mode 100644 index 00000000000..b481d1e63d9 --- /dev/null +++ b/server/src/controllers/api-key.controller.spec.ts @@ -0,0 +1,73 @@ +import { APIKeyController } from 'src/controllers/api-key.controller'; +import { ApiKeyService } from 'src/services/api-key.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(APIKeyController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(APIKeyController, [ + { provide: ApiKeyService, useValue: mockBaseService(ApiKeyService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /api-keys', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/api-keys').send({ name: 'API Key' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /api-keys', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/api-keys'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /api-keys/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/api-keys/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); + + describe('PUT /api-keys/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/api-keys/${factory.uuid()}`).send({ name: 'new name' }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/api-keys/123`).send({ name: 'new name' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); + + describe('DELETE /api-keys/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete(`/api-keys/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); +}); diff --git a/server/src/controllers/app.controller.spec.ts b/server/src/controllers/app.controller.spec.ts new file mode 100644 index 00000000000..4cc00e78c56 --- /dev/null +++ b/server/src/controllers/app.controller.spec.ts @@ -0,0 +1,49 @@ +import { AppController } from 'src/controllers/app.controller'; +import { SystemConfigService } from 'src/services/system-config.service'; +import request from 'supertest'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AppController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(AppController, [ + { provide: SystemConfigService, useValue: mockBaseService(SystemConfigService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /.well-known/immich', () => { + it('should not be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/.well-known/immich'); + expect(ctx.authenticate).not.toHaveBeenCalled(); + }); + + it('should return a 200 status code', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/.well-known/immich'); + expect(status).toBe(200); + expect(body).toEqual({ + api: { + endpoint: '/api', + }, + }); + }); + }); + + describe('GET /custom.css', () => { + it('should not be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/custom.css'); + expect(ctx.authenticate).not.toHaveBeenCalled(); + }); + + it('should reply with text/css', async () => { + const { status, headers } = await request(ctx.getHttpServer()).get('/custom.css'); + expect(status).toBe(200); + expect(headers['content-type']).toEqual('text/css; charset=utf-8'); + }); + }); +}); diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts new file mode 100644 index 00000000000..c674dc1f2ca --- /dev/null +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -0,0 +1,137 @@ +import { AssetMediaController } from 'src/controllers/asset-media.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { AssetMediaService } from 'src/services/asset-media.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +const makeUploadDto = (options?: { omit: string }): Record => { + const dto: Record = { + deviceAssetId: 'example-image', + deviceId: 'TEST', + fileCreatedAt: new Date().toISOString(), + fileModifiedAt: new Date().toISOString(), + isFavorite: 'testing', + duration: '0:00:00.000000', + }; + + const omit = options?.omit; + if (omit) { + delete dto[omit]; + } + + return dto; +}; + +describe(AssetMediaController.name, () => { + let ctx: ControllerContext; + const assetData = Buffer.from('123'); + const filename = 'example.png'; + + beforeAll(async () => { + ctx = await controllerSetup(AssetMediaController, [ + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + { provide: AssetMediaService, useValue: mockBaseService(AssetMediaService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post(`/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require `deviceAssetId`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'deviceAssetId' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `deviceId`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'deviceId' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `fileCreatedAt`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `fileModifiedAt`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'fileModifiedAt' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should require `duration`', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto({ omit: 'duration' }) }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should throw if `isFavorite` is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should throw if `isVisible` is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto(), isVisible: 'not-a-boolean' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + + it('should throw if `isArchived` is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/assets') + .attach('assetData', assetData, filename) + .field({ ...makeUploadDto(), isArchived: 'not-a-boolean' }); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + }); + }); + + // TODO figure out how to deal with `sendFile` + describe.skip('GET /assets/:id/original', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + // TODO figure out how to deal with `sendFile` + describe.skip('GET /assets/:id/thumbnail', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts new file mode 100644 index 00000000000..66d2d7c2063 --- /dev/null +++ b/server/src/controllers/asset.controller.spec.ts @@ -0,0 +1,118 @@ +import { AssetController } from 'src/controllers/asset.controller'; +import { AssetService } from 'src/services/asset.service'; +import request from 'supertest'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AssetController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('PUT /assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/assets`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /assets', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()) + .delete(`/assets`) + .send({ ids: [factory.uuid()] }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .delete(`/assets`) + .send({ ids: ['123'] }); + + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + }); + }); + + describe('GET /assets/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + }); + + describe('PUT /assets/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/123`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + }); + + it('should reject invalid gps coordinates', async () => { + for (const test of [ + { latitude: 12 }, + { longitude: 12 }, + { latitude: 12, longitude: 'abc' }, + { latitude: 'abc', longitude: 12 }, + { latitude: null, longitude: 12 }, + { latitude: 12, longitude: null }, + { latitude: 91, longitude: 12 }, + { latitude: -91, longitude: 12 }, + { latitude: 12, longitude: -181 }, + { latitude: 12, longitude: 181 }, + ]) { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + } + }); + + it('should reject invalid rating', async () => { + for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) { + const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); + expect(status).toBe(400); + expect(body).toEqual(factory.responses.badRequest()); + } + }); + }); + + describe('GET /assets/statistics', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/statistics`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /assets/random', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/random`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should not allow count to be a string', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/assets/random?count=ABC'); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest(['count must be a positive number', 'count must be an integer number']), + ); + }); + }); +}); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts new file mode 100644 index 00000000000..d8ee5ab27d8 --- /dev/null +++ b/server/src/controllers/auth.controller.spec.ts @@ -0,0 +1,60 @@ +import { AuthController } from 'src/controllers/auth.controller'; +import { AuthService } from 'src/services/auth.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(AuthController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(AuthService); + + beforeAll(async () => { + ctx = await controllerSetup(AuthController, [{ provide: AuthService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /auth/admin-sign-up', () => { + const name = 'admin'; + const email = 'admin@immich.cloud'; + const password = 'password'; + + it('should require an email address', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a password', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a name', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a valid email', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/auth/admin-sign-up') + .send({ name, email: 'immich', password }); + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should transform email to lower case', async () => { + service.adminSignUp.mockReset(); + const { status } = await request(ctx.getHttpServer()) + .post('/auth/admin-sign-up') + .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' }); + expect(status).toEqual(201); + expect(service.adminSignUp).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@immich.cloud' })); + }); + }); +}); diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts new file mode 100644 index 00000000000..4eac7d34517 --- /dev/null +++ b/server/src/controllers/notification.controller.spec.ts @@ -0,0 +1,64 @@ +import { NotificationController } from 'src/controllers/notification.controller'; +import { NotificationService } from 'src/services/notification.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(NotificationController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(NotificationController, [ + { provide: NotificationService, useValue: mockBaseService(NotificationService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /notifications', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/notifications'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it(`should reject an invalid notification level`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get(`/notifications`) + .query({ level: 'invalid' }) + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); + }); + }); + + describe('PUT /notifications', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/notifications'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /notifications/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/notifications/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + }); + }); + + describe('PUT /notifications/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() }); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts new file mode 100644 index 00000000000..2388f93e53f --- /dev/null +++ b/server/src/controllers/search.controller.spec.ts @@ -0,0 +1,201 @@ +import { SearchController } from 'src/controllers/search.controller'; +import { SearchService } from 'src/services/search.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(SearchController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(SearchController, [ + { provide: SearchService, useValue: mockBaseService(SearchService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('POST /search/metadata', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/search/metadata'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject page as a string', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['page must not be less than 1', 'page must be an integer number'])); + }); + + it('should reject page as a negative number', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: -10 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + }); + + it('should reject page as 0', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 0 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + }); + + it('should reject size as a string', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: 'abc' }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'size must not be greater than 1000', + 'size must not be less than 1', + 'size must be an integer number', + ]), + ); + }); + + it('should reject an invalid size', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1.5 }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number'])); + }); + + it('should reject an isArchived as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isArchived: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isArchived must be a boolean value'])); + }); + + it('should reject an isFavorite as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isFavorite: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + }); + + it('should reject an isEncoded as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isEncoded: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isEncoded must be a boolean value'])); + }); + + it('should reject an isOffline as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isOffline: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isOffline must be a boolean value'])); + }); + + it('should reject an isMotion as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ isMotion: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value'])); + }); + + it('should reject an isVisible as not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/metadata') + .send({ isVisible: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value'])); + }); + }); + + describe('POST /search/random', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/search/random'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should reject if withStacked is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/search/random') + .send({ withStacked: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value'])); + }); + + it('should reject if withPeople is not a boolean', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); + }); + }); + + describe('POST /search/smart', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).post('/search/smart'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a query', async () => { + const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string'])); + }); + }); + + describe('GET /search/explore', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/explore'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /search/person', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/person'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a name', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + }); + }); + + describe('GET /search/places', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/places'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a name', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + }); + }); + + describe('GET /search/cities', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/cities'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /search/suggestions', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/search/suggestions'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require a type', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest([ + 'type should not be empty', + expect.stringContaining('type must be one of the following values:'), + ]), + ); + }); + }); +}); diff --git a/server/src/controllers/server.controller.spec.ts b/server/src/controllers/server.controller.spec.ts new file mode 100644 index 00000000000..49edee061af --- /dev/null +++ b/server/src/controllers/server.controller.spec.ts @@ -0,0 +1,28 @@ +import { ServerController } from 'src/controllers/server.controller'; +import { ServerService } from 'src/services/server.service'; +import { VersionService } from 'src/services/version.service'; +import request from 'supertest'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(ServerController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(ServerController, [ + { provide: ServerService, useValue: mockBaseService(ServerService) }, + { provide: VersionService, useValue: mockBaseService(VersionService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /server/license', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/server/license'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/user.controller.spec.ts b/server/src/controllers/user.controller.spec.ts new file mode 100644 index 00000000000..bb3e709e687 --- /dev/null +++ b/server/src/controllers/user.controller.spec.ts @@ -0,0 +1,77 @@ +import { UserController } from 'src/controllers/user.controller'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { UserService } from 'src/services/user.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(UserController.name, () => { + let ctx: ControllerContext; + + beforeAll(async () => { + ctx = await controllerSetup(UserController, [ + { provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) }, + { provide: UserService, useValue: mockBaseService(UserService) }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + ctx.reset(); + }); + + describe('GET /users', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/users'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('GET /users/me', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/users/me'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /users/me', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/users/me'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + for (const key of ['email', 'name']) { + it(`should not allow null ${key}`, async () => { + const dto = { [key]: null }; + const { status, body } = await request(ctx.getHttpServer()) + .put(`/users/me`) + .set('Authorization', `Bearer token`) + .send(dto); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + } + }); + + describe('GET /users/:id', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/users/${factory.uuid()}`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('PUT /users/me/license', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).put('/users/me/license'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('DELETE /users/me/license', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).delete('/users/me/license'); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); +}); diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts index 0148f2e1e93..15673ab54ba 100644 --- a/server/test/medium/responses.ts +++ b/server/test/medium/responses.ts @@ -47,7 +47,6 @@ export const errorDto = { error: 'Bad Request', statusCode: 400, message: message ?? expect.anything(), - correlationId: expect.any(String), }), noPermission: { error: 'Bad Request', diff --git a/server/test/medium/specs/controllers/auth.controller.spec.ts b/server/test/medium/specs/controllers/auth.controller.spec.ts deleted file mode 100644 index ef2b904f48a..00000000000 --- a/server/test/medium/specs/controllers/auth.controller.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { AuthController } from 'src/controllers/auth.controller'; -import { AuthService } from 'src/services/auth.service'; -import request from 'supertest'; -import { errorDto } from 'test/medium/responses'; -import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; - -describe(AuthController.name, () => { - let app: TestControllerApp; - - beforeAll(async () => { - app = await createControllerTestApp(); - }); - - describe('POST /auth/admin-sign-up', () => { - const name = 'admin'; - const email = 'admin@immich.cloud'; - const password = 'password'; - - const invalid = [ - { - should: 'require an email address', - data: { name, password }, - }, - { - should: 'require a password', - data: { name, email }, - }, - { - should: 'require a name', - data: { email, password }, - }, - { - should: 'require a valid email', - data: { name, email: 'immich', password }, - }, - ]; - - for (const { should, data } of invalid) { - it(`should ${should}`, async () => { - const { status, body } = await request(app.getHttpServer()).post('/auth/admin-sign-up').send(data); - expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest()); - }); - } - - it('should transform email to lower case', async () => { - const { status } = await request(app.getHttpServer()) - .post('/auth/admin-sign-up') - .send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' }); - expect(status).toEqual(201); - expect(app.getMockedService(AuthService).adminSignUp).toHaveBeenCalledWith( - expect.objectContaining({ email: 'admin@immich.cloud' }), - ); - }); - }); - - afterAll(async () => { - await app.close(); - }); -}); diff --git a/server/test/medium/specs/controllers/notification.controller.spec.ts b/server/test/medium/specs/controllers/notification.controller.spec.ts deleted file mode 100644 index f4a0ec82d5a..00000000000 --- a/server/test/medium/specs/controllers/notification.controller.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { NotificationController } from 'src/controllers/notification.controller'; -import { AuthService } from 'src/services/auth.service'; -import { NotificationService } from 'src/services/notification.service'; -import request from 'supertest'; -import { errorDto } from 'test/medium/responses'; -import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; -import { factory } from 'test/small.factory'; - -describe(NotificationController.name, () => { - let realApp: TestControllerApp; - let mockApp: TestControllerApp; - - beforeEach(async () => { - realApp = await createControllerTestApp({ authType: 'real' }); - mockApp = await createControllerTestApp({ authType: 'mock' }); - }); - - describe('GET /notifications', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get('/notifications'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should call the service with an auth dto', async () => { - const auth = factory.auth({ user: factory.user() }); - mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth); - const service = mockApp.getMockedService(NotificationService); - - const { status } = await request(mockApp.getHttpServer()) - .get('/notifications') - .set('Authorization', `Bearer token`); - - expect(status).toBe(200); - expect(service.search).toHaveBeenCalledWith(auth, {}); - }); - - it(`should reject an invalid notification level`, async () => { - const auth = factory.auth({ user: factory.user() }); - mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth); - const service = mockApp.getMockedService(NotificationService); - - const { status, body } = await request(mockApp.getHttpServer()) - .get(`/notifications`) - .query({ level: 'invalid' }) - .set('Authorization', `Bearer token`); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); - expect(service.search).not.toHaveBeenCalled(); - }); - }); - - describe('PUT /notifications', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()) - .put(`/notifications`) - .send({ ids: [], readAt: new Date().toISOString() }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('GET /notifications/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get(`/notifications/${factory.uuid()}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('PUT /notifications/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()) - .put(`/notifications/${factory.uuid()}`) - .send({ readAt: factory.date() }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - afterAll(async () => { - await realApp.close(); - await mockApp.close(); - }); -}); diff --git a/server/test/medium/specs/controllers/user.controller.spec.ts b/server/test/medium/specs/controllers/user.controller.spec.ts deleted file mode 100644 index f4d90d54694..00000000000 --- a/server/test/medium/specs/controllers/user.controller.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { UserController } from 'src/controllers/user.controller'; -import { AuthService } from 'src/services/auth.service'; -import { UserService } from 'src/services/user.service'; -import request from 'supertest'; -import { errorDto } from 'test/medium/responses'; -import { createControllerTestApp, TestControllerApp } from 'test/medium/utils'; -import { factory } from 'test/small.factory'; - -describe(UserController.name, () => { - let realApp: TestControllerApp; - let mockApp: TestControllerApp; - - beforeAll(async () => { - realApp = await createControllerTestApp({ authType: 'real' }); - mockApp = await createControllerTestApp({ authType: 'mock' }); - }); - - describe('GET /users', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get('/users'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should call the service with an auth dto', async () => { - const user = factory.user(); - const authService = mockApp.getMockedService(AuthService); - const auth = factory.auth({ user }); - authService.authenticate.mockResolvedValue(auth); - - const userService = mockApp.getMockedService(UserService); - const { status } = await request(mockApp.getHttpServer()).get('/users').set('Authorization', `Bearer token`); - - expect(status).toBe(200); - expect(userService.search).toHaveBeenCalledWith(auth); - }); - }); - - describe('GET /users/me', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get(`/users/me`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('PUT /users/me', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).put(`/users/me`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - for (const key of ['email', 'name']) { - it(`should not allow null ${key}`, async () => { - const dto = { [key]: null }; - const { status, body } = await request(mockApp.getHttpServer()) - .put(`/users/me`) - .set('Authorization', `Bearer token`) - .send(dto); - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - }); - } - }); - - describe('GET /users/:id', () => { - it('should require authentication', async () => { - const { status } = await request(realApp.getHttpServer()).get(`/users/${factory.uuid()}`); - expect(status).toEqual(401); - }); - }); - - describe('GET /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(realApp.getHttpServer()).get('/users/me/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - }); - - describe('PUT /users/me/license', () => { - it('should require authentication', async () => { - const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`); - expect(status).toEqual(401); - }); - }); - - describe('DELETE /users/me/license', () => { - it('should require authentication', async () => { - const { status } = await request(realApp.getHttpServer()).put(`/users/me/license`); - expect(status).toEqual(401); - }); - }); - - afterAll(async () => { - await realApp.close(); - await mockApp.close(); - }); -}); diff --git a/server/test/medium/utils.ts b/server/test/medium/utils.ts deleted file mode 100644 index 030780b35b9..00000000000 --- a/server/test/medium/utils.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Provider } from '@nestjs/common'; -import { SchedulerRegistry } from '@nestjs/schedule'; -import { Test } from '@nestjs/testing'; -import { ClassConstructor } from 'class-transformer'; -import { ClsService } from 'nestjs-cls'; -import { middleware } from 'src/app.module'; -import { controllers } from 'src/controllers'; -import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; -import { LoggingRepository } from 'src/repositories/logging.repository'; -import { services } from 'src/services'; -import { ApiService } from 'src/services/api.service'; -import { AuthService } from 'src/services/auth.service'; -import { BaseService } from 'src/services/base.service'; -import { automock } from 'test/utils'; -import { Mocked } from 'vitest'; - -export const createControllerTestApp = async (options?: { authType?: 'mock' | 'real' }) => { - const { authType = 'mock' } = options || {}; - - const configMock = { getEnv: () => ({ noColor: true }) }; - const clsMock = { getId: vitest.fn().mockReturnValue('cls-id') }; - const loggerMock = automock(LoggingRepository, { args: [clsMock, configMock], strict: false }); - loggerMock.setContext.mockReturnValue(void 0); - loggerMock.error.mockImplementation((...args: any[]) => { - console.log('Logger.error was called with', ...args); - }); - - const mockBaseService = (service: ClassConstructor) => { - return automock(service, { args: [loggerMock], strict: false }); - }; - - const clsServiceMock = clsMock; - - const FAKE_MOCK = vitest.fn(); - - const providers: Provider[] = [ - ...middleware, - ...services.map((Service) => { - if ((authType === 'real' && Service === AuthService) || Service === ApiService) { - return Service; - } - return { provide: Service, useValue: mockBaseService(Service as ClassConstructor) }; - }), - GlobalExceptionFilter, - { provide: LoggingRepository, useValue: loggerMock }, - { provide: ClsService, useValue: clsServiceMock }, - ]; - - const moduleRef = await Test.createTestingModule({ - imports: [], - controllers: [...controllers], - providers, - }) - .useMocker((token) => { - if (token === LoggingRepository) { - return; - } - - if (token === SchedulerRegistry) { - return FAKE_MOCK; - } - - if (typeof token === 'function' && token.name.endsWith('Repository')) { - return FAKE_MOCK; - } - - if (typeof token === 'string' && token === 'KyselyModuleConnectionToken') { - return FAKE_MOCK; - } - }) - - .compile(); - - const app = moduleRef.createNestApplication(); - - await app.init(); - - const getMockedRepository = (token: ClassConstructor) => { - return app.get(token) as Mocked; - }; - - return { - getHttpServer: () => app.getHttpServer(), - getMockedService: (token: ClassConstructor) => { - if (authType === 'real' && token === AuthService) { - throw new Error('Auth type is real, cannot get mocked service'); - } - return app.get(token) as Mocked; - }, - getMockedRepository, - close: () => app.close(), - }; -}; - -export type TestControllerApp = { - getHttpServer: () => any; - getMockedService: (token: ClassConstructor) => Mocked; - getMockedRepository: (token: ClassConstructor) => Mocked; - close: () => Promise; -}; diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index d2742f7f80e..81ada65b688 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -315,4 +315,11 @@ export const factory = { }, uuid: newUuid, date: newDate, + responses: { + badRequest: (message: any = null) => ({ + error: 'Bad Request', + statusCode: 400, + message: message ?? expect.anything(), + }), + }, }; diff --git a/server/test/utils.ts b/server/test/utils.ts index 2c444f491e8..1f80c9bbe2e 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,3 +1,6 @@ +import { CallHandler, Provider, ValidationPipe } from '@nestjs/common'; +import { APP_GUARD, APP_PIPE } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; import { ClassConstructor } from 'class-transformer'; import { Kysely } from 'kysely'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; @@ -5,6 +8,9 @@ import { Writable } from 'node:stream'; import { PNG } from 'pngjs'; import postgres from 'postgres'; import { DB } from 'src/db'; +import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; +import { AuthGuard } from 'src/middleware/auth.guard'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -48,6 +54,7 @@ import { TrashRepository } from 'src/repositories/trash.repository'; import { UserRepository } from 'src/repositories/user.repository'; import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; import { ViewRepository } from 'src/repositories/view-repository'; +import { AuthService } from 'src/services/auth.service'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database'; @@ -64,7 +71,47 @@ import { newStorageRepositoryMock } from 'test/repositories/storage.repository.m import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { ITelemetryRepositoryMock, newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools'; -import { assert, Mocked, vitest } from 'vitest'; +import { assert, Mock, Mocked, vitest } from 'vitest'; + +export type ControllerContext = { + authenticate: Mock; + getHttpServer: () => any; + reset: () => void; + close: () => Promise; +}; + +export const controllerSetup = async (controller: ClassConstructor, providers: Provider[]) => { + const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; + const authenticate = vi.fn(); + const moduleRef = await Test.createTestingModule({ + controllers: [controller], + providers: [ + { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_GUARD, useClass: AuthGuard }, + { provide: LoggingRepository, useValue: LoggingRepository.create() }, + { provide: AuthService, useValue: { authenticate } }, + ...providers, + ], + }) + .overrideInterceptor(FileUploadInterceptor) + .useValue(noopInterceptor) + .overrideInterceptor(AssetUploadInterceptor) + .useValue(noopInterceptor) + .compile(); + const app = moduleRef.createNestApplication(); + await app.init(); + + return { + authenticate, + getHttpServer: () => app.getHttpServer(), + reset: () => { + authenticate.mockReset(); + }, + close: async () => { + await app.close(); + }, + }; +}; const mockFn = (label: string, { strict }: { strict: boolean }) => { const message = `Called a mock function without a mock implementation (${label})`; @@ -77,6 +124,10 @@ const mockFn = (label: string, { strict }: { strict: boolean }) => { }); }; +export const mockBaseService = (service: ClassConstructor) => { + return automock(service, { args: [{ setContext: () => {} }], strict: false }); +}; + export const automock = ( Dependency: ClassConstructor, options?: {