chore: linting (#7532)

* chore: linting

* fix: broken tests

* fix: formatting
This commit is contained in:
Jason Rasmussen 2024-02-29 11:26:55 -05:00 committed by GitHub
parent 09a7291527
commit af0de1a768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 2480 additions and 548 deletions

View File

@ -16,4 +16,4 @@ max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
quote_type = double quote_type = single

View File

@ -35,7 +35,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: "recursive" submodules: 'recursive'
- name: Run e2e tests - name: Run e2e tests
run: make server-e2e-jobs run: make server-e2e-jobs
@ -184,7 +184,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: "recursive" submodules: 'recursive'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@ -194,25 +194,40 @@ jobs:
- name: Run setup typescript-sdk - name: Run setup typescript-sdk
run: npm ci && npm run build run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Run setup cli - name: Run setup cli
run: npm ci && npm run build run: npm ci && npm run build
working-directory: ./cli working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
if: ${{ !cancelled() }}
- name: Run linter
run: npm run lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: npx playwright install --with-deps chromium run: npx playwright install --with-deps chromium
if: ${{ !cancelled() }}
- name: Docker build - name: Docker build
run: docker compose build run: docker compose build
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli) - name: Run e2e tests (api & cli)
run: npm run test run: npm run test
if: ${{ !cancelled() }}
- name: Run e2e tests (web) - name: Run e2e tests (web)
run: npx playwright test run: npx playwright test
if: ${{ !cancelled() }}
mobile-unit-tests: mobile-unit-tests:
name: Mobile name: Mobile
@ -222,8 +237,8 @@ jobs:
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: 'stable'
flutter-version: "3.16.9" flutter-version: '3.16.9'
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
@ -241,7 +256,7 @@ jobs:
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
python-version: 3.11 python-version: 3.11
cache: "poetry" cache: 'poetry'
- name: Install dependencies - name: Install dependencies
run: | run: |
poetry install --with dev --with cpu poetry install --with dev --with cpu

View File

@ -2,7 +2,7 @@
# - https://immich.app/docs/developer/setup # - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting # - https://immich.app/docs/developer/troubleshooting
version: "3.8" version: '3.8'
name: immich-dev name: immich-dev
@ -30,7 +30,7 @@ x-server-build: &server-common
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
command: [ "/usr/src/app/bin/immich-dev", "immich" ] command: ['/usr/src/app/bin/immich-dev', 'immich']
<<: *server-common <<: *server-common
ports: ports:
- 3001:3001 - 3001:3001
@ -41,7 +41,7 @@ services:
immich-microservices: immich-microservices:
container_name: immich_microservices container_name: immich_microservices
command: [ "/usr/src/app/bin/immich-dev", "microservices" ] command: ['/usr/src/app/bin/immich-dev', 'microservices']
<<: *server-common <<: *server-common
# extends: # extends:
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml
@ -57,7 +57,7 @@ services:
image: immich-web-dev:latest image: immich-web-dev:latest
build: build:
context: ../web context: ../web
command: [ "/usr/src/app/bin/immich-web" ] command: ['/usr/src/app/bin/immich-web']
env_file: env_file:
- .env - .env
ports: ports:

View File

@ -1,4 +1,4 @@
version: "3.8" version: '3.8'
name: immich-prod name: immich-prod
@ -17,7 +17,7 @@ x-server-build: &server-common
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
command: [ "start.sh", "immich" ] command: ['start.sh', 'immich']
<<: *server-common <<: *server-common
ports: ports:
- 2283:3001 - 2283:3001
@ -27,7 +27,7 @@ services:
immich-microservices: immich-microservices:
container_name: immich_microservices container_name: immich_microservices
command: [ "start.sh", "microservices" ] command: ['start.sh', 'microservices']
<<: *server-common <<: *server-common
# extends: # extends:
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml

View File

@ -1,4 +1,4 @@
version: "3.8" version: '3.8'
# #
# WARNING: Make sure to use the docker-compose.yml of the current release: # WARNING: Make sure to use the docker-compose.yml of the current release:
@ -14,7 +14,7 @@ services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ] command: ['start.sh', 'immich']
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
@ -33,7 +33,7 @@ services:
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
# file: hwaccel.transcoding.yml # file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
command: [ "start.sh", "microservices" ] command: ['start.sh', 'microservices']
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro

31
e2e/.eslintrc.cjs Normal file
View File

@ -0,0 +1,31 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
root: true,
env: {
node: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
curly: 2,
'prettier/prettier': 0,
'unicorn/prevent-abbreviations': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off',
'unicorn/prefer-event-target': 'off',
'unicorn/no-thenable': 'off',
},
};

16
e2e/.prettierignore Normal file
View File

@ -0,0 +1,16 @@
.DS_Store
node_modules
/build
/package
.env
.env.*
!.env.example
*.md
*.json
coverage
dist
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

8
e2e/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true,
"organizeImportsSkipDestructiveCodeActions": true,
"plugins": ["prettier-plugin-organize-imports"]
}

View File

@ -1,4 +1,4 @@
version: "3.8" version: '3.8'
name: immich-e2e name: immich-e2e
@ -23,14 +23,14 @@ x-server-build: &server-common
services: services:
immich-server: immich-server:
container_name: immich-e2e-server container_name: immich-e2e-server
command: [ "./start.sh", "immich" ] command: ['./start.sh', 'immich']
<<: *server-common <<: *server-common
ports: ports:
- 2283:3001 - 2283:3001
immich-microservices: immich-microservices:
container_name: immich-e2e-microservices container_name: immich-e2e-microservices
command: [ "./start.sh", "microservices" ] command: ['./start.sh', 'microservices']
<<: *server-common <<: *server-common
redis: redis:

2185
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,11 @@
"scripts": { "scripts": {
"test": "vitest --config vitest.config.ts", "test": "vitest --config vitest.config.ts",
"test:web": "npx playwright test", "test:web": "npx playwright test",
"start:web": "npx playwright test --ui" "start:web": "npx playwright test --ui",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"lint:fix": "npm run lint -- --fix"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -20,10 +24,18 @@
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.0", "@vitest/coverage-v8": "^1.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^51.0.1",
"exiftool-vendored": "^24.5.0", "exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"pg": "^8.11.3", "pg": "^8.11.3",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"typescript": "^5.3.3", "typescript": "^5.3.3",

View File

@ -20,10 +20,7 @@ describe('/activity', () => {
let album: AlbumResponseDto; let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) => const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
create( create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) });
{ activityCreateDto: dto },
{ headers: asBearerAuth(accessToken || admin.accessToken) },
);
beforeAll(async () => { beforeAll(async () => {
apiUtils.setup(); apiUtils.setup();
@ -56,13 +53,9 @@ describe('/activity', () => {
}); });
it('should require an albumId', async () => { it('should require an albumId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
.get('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
);
}); });
it('should reject an invalid albumId', async () => { it('should reject an invalid albumId', async () => {
@ -71,9 +64,7 @@ describe('/activity', () => {
.query({ albumId: uuidDto.invalid }) .query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
);
}); });
it('should reject an invalid assetId', async () => { it('should reject an invalid assetId', async () => {
@ -82,9 +73,7 @@ describe('/activity', () => {
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
);
}); });
it('should start off empty', async () => { it('should start off empty', async () => {
@ -160,9 +149,7 @@ describe('/activity', () => {
}); });
it('should filter by userId', async () => { it('should filter by userId', async () => {
const [reaction] = await Promise.all([ const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const response1 = await request(app) const response1 = await request(app)
.get('/activity') .get('/activity')
@ -215,9 +202,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid }); .send({ albumId: uuidDto.invalid });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
);
}); });
it('should require a comment when type is comment', async () => { it('should require a comment when type is comment', async () => {
@ -226,12 +211,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); .send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
errorDto.badRequest([
'comment must be a string',
'comment should not be empty',
]),
);
}); });
it('should add a comment to an album', async () => { it('should add a comment to an album', async () => {
@ -271,9 +251,7 @@ describe('/activity', () => {
}); });
it('should return a 200 for a duplicate like on the album', async () => { it('should return a 200 for a duplicate like on the album', async () => {
const [reaction] = await Promise.all([ const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activity')
@ -356,9 +334,7 @@ describe('/activity', () => {
describe('DELETE /activity/:id', () => { describe('DELETE /activity/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
`/activity/${uuidDto.notFound}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -420,9 +396,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${nonOwner.accessToken}`); .set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest('Not found or no activity.delete access'));
errorDto.badRequest('Not found or no activity.delete access'),
);
}); });
it('should let a non-owner remove their own comment', async () => { it('should let a non-owner remove their own comment', async () => {

View File

@ -93,10 +93,7 @@ describe('/album', () => {
}), }),
]); ]);
await deleteUser( await deleteUser({ id: user3.userId }, { headers: asBearerAuth(admin.accessToken) });
{ id: user3.userId },
{ headers: asBearerAuth(admin.accessToken) },
);
}); });
describe('GET /album', () => { describe('GET /album', () => {
@ -111,9 +108,7 @@ describe('/album', () => {
.get('/album?shared=invalid') .get('/album?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
errorDto.badRequest(['shared must be a boolean value']),
);
}); });
it('should reject an invalid assetId param', async () => { it('should reject an invalid assetId param', async () => {
@ -153,9 +148,7 @@ describe('/album', () => {
}); });
it('should return the album collection including owned and shared', async () => { it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
.get('/album')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(3); expect(body).toHaveLength(3);
expect(body).toEqual( expect(body).toEqual(
@ -250,9 +243,7 @@ describe('/album', () => {
describe('GET /album/:id', () => { describe('GET /album/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
`/album/${user1Albums[0].id}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -326,9 +317,7 @@ describe('/album', () => {
describe('POST /album', () => { describe('POST /album', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
.post('/album')
.send({ albumName: 'New album' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -360,9 +349,7 @@ describe('/album', () => {
describe('PUT /album/:id/assets', () => { describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put( const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
`/album/${user1Albums[0].id}/assets`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -375,9 +362,7 @@ describe('/album', () => {
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([ expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
expect.objectContaining({ id: asset.id, success: true }),
]);
}); });
it('should be able to add own asset to shared album', async () => { it('should be able to add own asset to shared album', async () => {
@ -388,9 +373,7 @@ describe('/album', () => {
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([ expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
expect.objectContaining({ id: asset.id, success: true }),
]);
}); });
}); });
@ -473,9 +456,7 @@ describe('/album', () => {
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([ expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
}); });
it('should be able to remove own asset from shared album', async () => { it('should be able to remove own asset from shared album', async () => {
@ -485,9 +466,7 @@ describe('/album', () => {
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([ expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
}); });
}); });
@ -501,9 +480,7 @@ describe('/album', () => {
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
.put(`/album/${user1Albums[0].id}/users`)
.send({ sharedUserIds: [] });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);

View File

@ -13,21 +13,15 @@ import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures'; import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { import { apiUtils, app, dbUtils, tempDir, testAssetDir, wsUtils } from 'src/utils';
apiUtils,
app,
dbUtils,
tempDir,
testAssetDir,
wsUtils,
} 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';
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const sha1 = (bytes: Buffer) => const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64');
createHash('sha1').update(bytes).digest('base64');
const readTags = async (bytes: Buffer, filename: string) => { const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename); const filepath = join(tempDir, filename);
@ -83,7 +77,6 @@ describe('/asset', () => {
user1.accessToken, user1.accessToken,
{ {
isFavorite: true, isFavorite: true,
isExternal: true,
isReadOnly: true, isReadOnly: true,
fileCreatedAt: yesterday.toISO(), fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(), fileModifiedAt: yesterday.toISO(),
@ -96,6 +89,10 @@ describe('/asset', () => {
user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]); user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
for (const asset of [...user1Assets, ...user2Assets]) {
expect(asset.duplicate).toBe(false);
}
await Promise.all([ await Promise.all([
// stats // stats
apiUtils.createAsset(userStats.accessToken), apiUtils.createAsset(userStats.accessToken),
@ -126,9 +123,7 @@ describe('/asset', () => {
describe('GET /asset/:id', () => { describe('GET /asset/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/asset/${uuidDto.notFound}`);
`/asset/${uuidDto.notFound}`,
);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401); expect(status).toBe(401);
}); });
@ -163,9 +158,7 @@ describe('/asset', () => {
assetIds: [user1Assets[0].id], assetIds: [user1Assets[0].id],
}); });
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id }); expect(body).toMatchObject({ id: user1Assets[0].id });
}); });
@ -195,9 +188,7 @@ describe('/asset', () => {
assetIds: [user1Assets[0].id], assetIds: [user1Assets[0].id],
}); });
const data = await request(app).get( const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
);
expect(data.status).toBe(200); expect(data.status).toBe(200);
expect(data.body).toMatchObject({ people: [] }); expect(data.body).toMatchObject({ people: [] });
}); });
@ -280,7 +271,7 @@ describe('/asset', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it.each(Array(10))('should return 1 random assets', async () => { it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/asset/random') .get('/asset/random')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
@ -290,14 +281,9 @@ describe('/asset', () => {
const assets: AssetResponseDto[] = body; const assets: AssetResponseDto[] = body;
expect(assets.length).toBe(1); expect(assets.length).toBe(1);
expect(assets[0].ownerId).toBe(user1.userId); expect(assets[0].ownerId).toBe(user1.userId);
// assets owned by user1
expect([user1Assets.map(({ id }) => id)]).toContain(assets[0].id);
// assets owned by user2
expect([user1Assets.map(({ id }) => id)]).not.toContain(assets[0].id);
}); });
it.each(Array(10))('should return 2 random assets', async () => { it.each(TEN_TIMES)('should return 2 random assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/asset/random?count=2') .get('/asset/random?count=2')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
@ -309,24 +295,18 @@ describe('/asset', () => {
for (const asset of assets) { for (const asset of assets) {
expect(asset.ownerId).toBe(user1.userId); expect(asset.ownerId).toBe(user1.userId);
// assets owned by user1
expect([user1Assets.map(({ id }) => id)]).toContain(asset.id);
// assets owned by user2
expect([user2Assets.map(({ id }) => id)]).not.toContain(asset.id);
} }
}); });
it.each(Array(10))( it.each(TEN_TIMES)(
'should return 1 asset if there are 10 assets in the database but user 2 only has 1', 'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
async () => { async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/[]asset/random') .get('/asset/random')
.set('Authorization', `Bearer ${user2.accessToken}`); .set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([ expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
expect.objectContaining({ id: user2Assets[0].id }),
]);
}, },
); );
@ -341,9 +321,7 @@ describe('/asset', () => {
describe('PUT /asset/:id', () => { describe('PUT /asset/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put( const { status, body } = await request(app).put(`/asset/:${uuidDto.notFound}`);
`/asset/:${uuidDto.notFound}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -365,10 +343,7 @@ describe('/asset', () => {
}); });
it('should favorite an asset', async () => { it('should favorite an asset', async () => {
const before = await apiUtils.getAssetInfo( const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
user1.accessToken,
user1Assets[0].id,
);
expect(before.isFavorite).toBe(false); expect(before.isFavorite).toBe(false);
const { status, body } = await request(app) const { status, body } = await request(app)
@ -380,10 +355,7 @@ describe('/asset', () => {
}); });
it('should archive an asset', async () => { it('should archive an asset', async () => {
const before = await apiUtils.getAssetInfo( const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
user1.accessToken,
user1Assets[0].id,
);
expect(before.isArchived).toBe(false); expect(before.isArchived).toBe(false);
const { status, body } = await request(app) const { status, body } = await request(app)
@ -497,9 +469,7 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
errorDto.badRequest(['each value in ids must be a UUID']),
);
}); });
it('should throw an error when the id is not found', async () => { it('should throw an error when the id is not found', async () => {
@ -509,9 +479,7 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest('Not found or no asset.delete access'));
errorDto.badRequest('Not found or no asset.delete access'),
);
}); });
it('should move an asset to the trash', async () => { it('should move an asset to the trash', async () => {
@ -714,16 +682,10 @@ describe('/asset', () => {
expect(response.duplicate).toBe(false); expect(response.duplicate).toBe(false);
const asset = await apiUtils.getAssetInfo( const asset = await apiUtils.getAssetInfo(admin.accessToken, response.id);
admin.accessToken,
response.id,
);
expect(asset.livePhotoVideoId).toBeDefined(); expect(asset.livePhotoVideoId).toBeDefined();
const video = await apiUtils.getAssetInfo( const video = await apiUtils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
admin.accessToken,
asset.livePhotoVideoId as string,
);
expect(video.checksum).toStrictEqual(checksum); expect(video.checksum).toStrictEqual(checksum);
}); });
} }
@ -731,9 +693,7 @@ describe('/asset', () => {
describe('GET /asset/thumbnail/:id', () => { describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
`/asset/thumbnail/${assetLocation.id}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -775,9 +735,7 @@ describe('/asset', () => {
describe('GET /asset/file/:id', () => { describe('GET /asset/file/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
`/asset/thumbnail/${assetLocation.id}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -792,10 +750,7 @@ describe('/asset', () => {
expect(body).toBeDefined(); expect(body).toBeDefined();
expect(type).toBe('image/jpeg'); expect(type).toBe('image/jpeg');
const asset = await apiUtils.getAssetInfo( const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id);
admin.accessToken,
assetLocation.id,
);
const original = await readFile(locationAssetFilepath); const original = await readFile(locationAssetFilepath);
const originalChecksum = sha1(original); const originalChecksum = sha1(original);

View File

@ -1,9 +1,4 @@
import { import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
deleteAssets,
getAuditFiles,
updateAsset,
type LoginResponseDto,
} from '@immich/sdk';
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils'; import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
@ -20,17 +15,14 @@ describe('/audit', () => {
describe('GET :/file-report', () => { describe('GET :/file-report', () => {
it('excludes assets without issues from report', async () => { it('excludes assets without issues from report', async () => {
const [trashedAsset, archivedAsset, _] = await Promise.all([ const [trashedAsset, archivedAsset] = await Promise.all([
apiUtils.createAsset(admin.accessToken), apiUtils.createAsset(admin.accessToken),
apiUtils.createAsset(admin.accessToken), apiUtils.createAsset(admin.accessToken),
apiUtils.createAsset(admin.accessToken), apiUtils.createAsset(admin.accessToken),
]); ]);
await Promise.all([ await Promise.all([
deleteAssets( deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
{ headers: asBearerAuth(admin.accessToken) },
),
updateAsset( updateAsset(
{ {
id: archivedAsset.id, id: archivedAsset.id,

View File

@ -1,16 +1,6 @@
import { import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
LoginResponseDto,
getAuthDevices,
login,
signUpAdmin,
} from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures'; import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import { import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
deviceDto,
errorDto,
loginResponseDto,
signupResponseDto,
} from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
@ -48,18 +38,14 @@ describe(`/auth/admin-sign-up`, () => {
for (const { should, data } of invalid) { for (const { should, data } of invalid) {
it(`should ${should}`, async () => { it(`should ${should}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
.post('/auth/admin-sign-up')
.send(data);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest()); expect(body).toEqual(errorDto.badRequest());
}); });
} }
it(`should sign up the admin`, async () => { it(`should sign up the admin`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
.post('/auth/admin-sign-up')
.send(signupDto.admin);
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toEqual(signupResponseDto.admin); expect(body).toEqual(signupResponseDto.admin);
}); });
@ -86,9 +72,7 @@ describe(`/auth/admin-sign-up`, () => {
it('should not allow a second admin to sign up', async () => { it('should not allow a second admin to sign up', async () => {
await signUpAdmin({ signUpDto: signupDto.admin }); await signUpAdmin({ signUpDto: signupDto.admin });
const { status, body } = await request(app) const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
.post('/auth/admin-sign-up')
.send(signupDto.admin);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin); expect(body).toEqual(errorDto.alreadyHasAdmin);
@ -107,9 +91,7 @@ describe('/auth/*', () => {
describe(`POST /auth/login`, () => { describe(`POST /auth/login`, () => {
it('should reject an incorrect password', async () => { it('should reject an incorrect password', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
.post('/auth/login')
.send({ email, password: 'incorrect' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.incorrectLogin); expect(body).toEqual(errorDto.incorrectLogin);
}); });
@ -125,9 +107,7 @@ describe('/auth/*', () => {
} }
it('should accept a correct password', async () => { it('should accept a correct password', async () => {
const { status, body, headers } = await request(app) const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
.post('/auth/login')
.send({ email, password });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toEqual(loginResponseDto.admin); expect(body).toEqual(loginResponseDto.admin);
@ -136,15 +116,9 @@ describe('/auth/*', () => {
const cookies = headers['set-cookie']; const cookies = headers['set-cookie'];
expect(cookies).toHaveLength(3); expect(cookies).toHaveLength(3);
expect(cookies[0]).toEqual( expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;` expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
); expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
expect(cookies[1]).toEqual(
'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'
);
expect(cookies[2]).toEqual(
'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'
);
}); });
}); });
@ -176,18 +150,12 @@ describe('/auth/*', () => {
await login({ loginCredentialDto: loginDto.admin }); await login({ loginCredentialDto: loginDto.admin });
} }
await expect( await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
).resolves.toHaveLength(6);
const { status } = await request(app) const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
.delete(`/auth/devices`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
await expect( await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
).resolves.toHaveLength(1);
}); });
it('should throw an error for a non-existent device id', async () => { it('should throw an error for a non-existent device id', async () => {
@ -195,9 +163,7 @@ describe('/auth/*', () => {
.delete(`/auth/devices/${uuidDto.notFound}`) .delete(`/auth/devices/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
errorDto.badRequest('Not found or no authDevice.delete access')
);
}); });
it('should logout a device', async () => { it('should logout a device', async () => {
@ -219,9 +185,7 @@ describe('/auth/*', () => {
describe('POST /auth/validateToken', () => { describe('POST /auth/validateToken', () => {
it('should reject an invalid token', async () => { it('should reject an invalid token', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
.post(`/auth/validateToken`)
.set('Authorization', 'Bearer 123');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidToken); expect(body).toEqual(errorDto.invalidToken);
}); });

View File

@ -42,9 +42,7 @@ describe('/download', () => {
describe('POST /download/asset/:id', () => { describe('POST /download/asset/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post( const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
`/download/asset/${asset1.id}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);

View File

@ -15,16 +15,9 @@ describe(`/oauth`, () => {
describe('POST /oauth/authorize', () => { describe('POST /oauth/authorize', () => {
it(`should throw an error if a redirect uri is not provided`, async () => { it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post('/oauth/authorize').send({});
.post('/oauth/authorize')
.send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
errorDto.badRequest([
'redirectUri must be a string',
'redirectUri should not be empty',
])
);
}); });
}); });
}); });

View File

@ -24,14 +24,8 @@ describe('/partner', () => {
]); ]);
await Promise.all([ await Promise.all([
createPartner( createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
{ id: user2.userId }, createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
{ headers: asBearerAuth(user1.accessToken) }
),
createPartner(
{ id: user1.userId },
{ headers: asBearerAuth(user2.accessToken) }
),
]); ]);
}); });
@ -66,9 +60,7 @@ describe('/partner', () => {
describe('POST /partner/:id', () => { describe('POST /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post( const { status, body } = await request(app).post(`/partner/${user3.userId}`);
`/partner/${user3.userId}`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -89,17 +81,13 @@ describe('/partner', () => {
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
expect.objectContaining({ message: 'Partner already exists' })
);
}); });
}); });
describe('PUT /partner/:id', () => { describe('PUT /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put( const { status, body } = await request(app).put(`/partner/${user2.userId}`);
`/partner/${user2.userId}`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -112,17 +100,13 @@ describe('/partner', () => {
.send({ inTimeline: false }); .send({ inTimeline: false });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
expect.objectContaining({ id: user2.userId, inTimeline: false })
);
}); });
}); });
describe('DELETE /partner/:id', () => { describe('DELETE /partner/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
`/partner/${user3.userId}`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -142,9 +126,7 @@ describe('/partner', () => {
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
expect.objectContaining({ message: 'Partner not found' })
);
}); });
}); });
}); });

View File

@ -65,9 +65,7 @@ describe('/activity', () => {
}); });
it('should return only visible people', async () => { it('should return only visible people', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
.get('/person')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
@ -80,9 +78,7 @@ describe('/activity', () => {
describe('GET /person/:id', () => { describe('GET /person/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
`/person/${uuidDto.notFound}`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -109,9 +105,7 @@ describe('/activity', () => {
describe('PUT /person/:id', () => { describe('PUT /person/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put( const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
`/person/${uuidDto.notFound}`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -139,7 +133,7 @@ describe('/activity', () => {
birthDate: '123567', birthDate: '123567',
response: 'Not found or no person.write access', response: 'Not found or no person.write access',
}, },
{ birthDate: 123567, response: 'Not found or no person.write access' }, { birthDate: 123_567, response: 'Not found or no person.write access' },
]) { ]) {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/person/${uuidDto.notFound}`) .put(`/person/${uuidDto.notFound}`)

View File

@ -97,9 +97,7 @@ describe('/server-info', () => {
describe('GET /server-info/statistics', () => { describe('GET /server-info/statistics', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get('/server-info/statistics');
'/server-info/statistics'
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -145,9 +143,7 @@ describe('/server-info', () => {
describe('GET /server-info/media-types', () => { describe('GET /server-info/media-types', () => {
it('should return accepted media types', async () => { it('should return accepted media types', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get('/server-info/media-types');
'/server-info/media-types'
);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
sidecar: ['.xmp'], sidecar: ['.xmp'],

View File

@ -46,14 +46,8 @@ describe('/shared-link', () => {
]); ]);
[album, deletedAlbum, metadataAlbum] = await Promise.all([ [album, deletedAlbum, metadataAlbum] = await Promise.all([
createAlbum( createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
{ createAlbumDto: { albumName: 'album' } }, createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
{ headers: asBearerAuth(user1.accessToken) },
),
createAlbum(
{ createAlbumDto: { albumName: 'deleted album' } },
{ headers: asBearerAuth(user2.accessToken) },
),
createAlbum( createAlbum(
{ {
createAlbumDto: { createAlbumDto: {
@ -65,47 +59,38 @@ describe('/shared-link', () => {
), ),
]); ]);
[ [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
linkWithDeletedAlbum, await Promise.all([
linkWithAlbum, apiUtils.createSharedLink(user2.accessToken, {
linkWithAssets, type: SharedLinkType.Album,
linkWithPassword, albumId: deletedAlbum.id,
linkWithMetadata, }),
linkWithoutMetadata, apiUtils.createSharedLink(user1.accessToken, {
] = await Promise.all([ type: SharedLinkType.Album,
apiUtils.createSharedLink(user2.accessToken, { albumId: album.id,
type: SharedLinkType.Album, }),
albumId: deletedAlbum.id, apiUtils.createSharedLink(user1.accessToken, {
}), type: SharedLinkType.Individual,
apiUtils.createSharedLink(user1.accessToken, { assetIds: [asset1.id],
type: SharedLinkType.Album, }),
albumId: album.id, apiUtils.createSharedLink(user1.accessToken, {
}), type: SharedLinkType.Album,
apiUtils.createSharedLink(user1.accessToken, { albumId: album.id,
type: SharedLinkType.Individual, password: 'foo',
assetIds: [asset1.id], }),
}), apiUtils.createSharedLink(user1.accessToken, {
apiUtils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album,
type: SharedLinkType.Album, albumId: metadataAlbum.id,
albumId: album.id, showMetadata: true,
password: 'foo', }),
}), apiUtils.createSharedLink(user1.accessToken, {
apiUtils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album,
type: SharedLinkType.Album, albumId: metadataAlbum.id,
albumId: metadataAlbum.id, showMetadata: false,
showMetadata: true, }),
}), ]);
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: false,
}),
]);
await deleteUser( await deleteUser({ id: user2.userId }, { headers: asBearerAuth(admin.accessToken) });
{ id: user2.userId },
{ headers: asBearerAuth(admin.accessToken) },
);
}); });
describe('GET /shared-link', () => { describe('GET /shared-link', () => {
@ -146,17 +131,13 @@ describe('/shared-link', () => {
describe('GET /shared-link/me', () => { describe('GET /shared-link/me', () => {
it('should not require admin authentication', async () => { it('should not require admin authentication', async () => {
const { status } = await request(app) const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
.get('/shared-link/me')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(403); expect(status).toBe(403);
}); });
it('should get data for correct shared link', async () => { it('should get data for correct shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
.get('/shared-link/me')
.query({ key: linkWithAlbum.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
@ -178,18 +159,14 @@ describe('/shared-link', () => {
}); });
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) const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
.get('/shared-link/me')
.query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey); expect(body).toEqual(errorDto.invalidShareKey);
}); });
it('should return unauthorized for password protected link', async () => { it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
.get('/shared-link/me')
.query({ key: linkWithPassword.key });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidSharePassword); expect(body).toEqual(errorDto.invalidSharePassword);
@ -211,9 +188,7 @@ describe('/shared-link', () => {
}); });
it('should return metadata for album shared link', async () => { it('should return metadata for album shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
.get('/shared-link/me')
.query({ key: linkWithMetadata.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toHaveLength(1); expect(body.assets).toHaveLength(1);
@ -229,9 +204,7 @@ describe('/shared-link', () => {
}); });
it('should not return metadata for album shared link without metadata', async () => { it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
.get('/shared-link/me')
.query({ key: linkWithoutMetadata.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toHaveLength(1); expect(body.assets).toHaveLength(1);
@ -247,9 +220,7 @@ describe('/shared-link', () => {
describe('GET /shared-link/:id', () => { describe('GET /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
`/shared-link/${linkWithAlbum.id}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@ -276,9 +247,7 @@ describe('/shared-link', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
expect.objectContaining({ message: 'Shared link not found' }),
);
}); });
}); });
@ -308,9 +277,7 @@ describe('/shared-link', () => {
.send({ type: SharedLinkType.Album }); .send({ type: SharedLinkType.Album });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
expect.objectContaining({ message: 'Invalid albumId' }),
);
}); });
it('should require a valid asset id', async () => { it('should require a valid asset id', async () => {
@ -320,9 +287,7 @@ describe('/shared-link', () => {
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound }); .send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
expect.objectContaining({ message: 'Invalid assetIds' }),
);
}); });
it('should create a shared link', async () => { it('should create a shared link', async () => {
@ -424,9 +389,7 @@ describe('/shared-link', () => {
describe('DELETE /shared-link/:id', () => { describe('DELETE /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
`/shared-link/${linkWithAlbum.id}`,
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);

View File

@ -18,9 +18,7 @@ describe('/system-config', () => {
describe('GET /system-config/map/style.json', () => { describe('GET /system-config/map/style.json', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get('/system-config/map/style.json');
'/system-config/map/style.json'
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -32,11 +30,7 @@ describe('/system-config', () => {
.query({ theme }) .query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
errorDto.badRequest([
'theme must be one of the following values: light, dark',
])
);
} }
}); });

View File

@ -32,24 +32,16 @@ describe('/trash', () => {
const { id: assetId } = await apiUtils.createAsset(admin.accessToken); const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
await apiUtils.deleteAssets(admin.accessToken, [assetId]); await apiUtils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAllAssets( const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
{},
{ headers: asBearerAuth(admin.accessToken) },
);
expect(before.length).toBeGreaterThanOrEqual(1); expect(before.length).toBeGreaterThanOrEqual(1);
const { status } = await request(app) const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
.post('/trash/empty')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
await wsUtils.waitForEvent({ event: 'delete', assetId }); await wsUtils.waitForEvent({ event: 'delete', assetId });
const after = await getAllAssets( const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
{},
{ headers: asBearerAuth(admin.accessToken) },
);
expect(after.length).toBe(0); expect(after.length).toBe(0);
}); });
}); });
@ -69,9 +61,7 @@ describe('/trash', () => {
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true); expect(before.isTrashed).toBe(true);
const { status } = await request(app) const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
.post('/trash/restore')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);

View File

@ -22,10 +22,7 @@ describe('/server-info', () => {
apiUtils.userSetup(admin.accessToken, createUserDto.user3), apiUtils.userSetup(admin.accessToken, createUserDto.user3),
]); ]);
await deleteUser( await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) });
{ id: deletedUser.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
}); });
describe('GET /user', () => { describe('GET /user', () => {
@ -36,9 +33,7 @@ describe('/server-info', () => {
}); });
it('should get users', async () => { it('should get users', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
.get('/user')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
expect(body).toEqual( expect(body).toEqual(
@ -47,7 +42,7 @@ describe('/server-info', () => {
expect.objectContaining({ email: 'user1@immich.cloud' }), expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }),
]) ]),
); );
}); });
@ -63,7 +58,7 @@ describe('/server-info', () => {
expect.objectContaining({ email: 'admin@immich.cloud' }), expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }),
]) ]),
); );
}); });
@ -81,7 +76,7 @@ describe('/server-info', () => {
expect.objectContaining({ email: 'user1@immich.cloud' }), expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }), expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }), expect.objectContaining({ email: 'user3@immich.cloud' }),
]) ]),
); );
}); });
}); });
@ -112,9 +107,7 @@ describe('/server-info', () => {
}); });
it('should get my info', async () => { it('should get my info', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
.get(`/user/me`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
id: admin.userId, id: admin.userId,
@ -125,9 +118,7 @@ describe('/server-info', () => {
describe('POST /user', () => { describe('POST /user', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
.post(`/user`)
.send(createUserDto.user1);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -181,9 +172,7 @@ describe('/server-info', () => {
describe('DELETE /user/:id', () => { describe('DELETE /user/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
`/user/${userToDelete.userId}`
);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@ -241,10 +230,7 @@ describe('/server-info', () => {
}); });
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => { it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
const before = await getUserById( const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
{ id: admin.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/user`)
@ -261,10 +247,7 @@ describe('/server-info', () => {
}); });
it('should update first and last name', async () => { it('should update first and last name', async () => {
const before = await getUserById( const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
{ id: admin.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/user`)
@ -284,10 +267,7 @@ describe('/server-info', () => {
}); });
it('should update memories enabled', async () => { it('should update memories enabled', async () => {
const before = await getUserById( const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
{ id: admin.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/user`)
.send({ .send({

View File

@ -1,6 +1,6 @@
import { stat } from 'node:fs/promises'; import { stat } from 'node:fs/promises';
import { apiUtils, app, dbUtils, immichCli } from 'src/utils'; import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
import { beforeEach, beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`immich login-key`, () => { describe(`immich login-key`, () => {
beforeAll(() => { beforeAll(() => {
@ -24,25 +24,15 @@ describe(`immich login-key`, () => {
}); });
it('should require a valid key', async () => { it('should require a valid key', async () => {
const { stderr, exitCode } = await immichCli([ const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']);
'login-key', expect(stderr).toContain('Failed to connect to server http://127.0.0.1:2283/api: Error: 401');
app,
'immich-is-so-cool',
]);
expect(stderr).toContain(
'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
);
expect(exitCode).toBe(1); expect(exitCode).toBe(1);
}); });
it('should login', async () => { it('should login', async () => {
const admin = await apiUtils.adminSetup(); const admin = await apiUtils.adminSetup();
const key = await apiUtils.createApiKey(admin.accessToken); const key = await apiUtils.createApiKey(admin.accessToken);
const { stdout, stderr, exitCode } = await immichCli([ const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
'login-key',
app,
`${key.secret}`,
]);
expect(stdout.split('\n')).toEqual([ expect(stdout.split('\n')).toEqual([
'Logging in...', 'Logging in...',
'Logged in as admin@immich.cloud', 'Logged in as admin@immich.cloud',

View File

@ -1,13 +1,6 @@
import { getAllAlbums, getAllAssets } from '@immich/sdk'; import { getAllAlbums, getAllAssets } from '@immich/sdk';
import { mkdir, readdir, rm, symlink } from 'fs/promises'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
import { import { apiUtils, asKeyAuth, cliUtils, dbUtils, immichCli, testAssetDir } from 'src/utils';
apiUtils,
asKeyAuth,
cliUtils,
dbUtils,
immichCli,
testAssetDir,
} from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`immich upload`, () => { describe(`immich upload`, () => {
@ -25,16 +18,10 @@ describe(`immich upload`, () => {
describe('immich upload --recursive', () => { describe('immich upload --recursive', () => {
it('should upload a folder recursively', async () => { it('should upload a folder recursively', async () => {
const { stderr, stdout, exitCode } = await immichCli([ const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
]);
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(stdout.split('\n')).toEqual( expect(stdout.split('\n')).toEqual(
expect.arrayContaining([ expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
expect.stringContaining('Successfully uploaded 9 assets'),
]),
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
@ -70,15 +57,9 @@ describe(`immich upload`, () => {
}); });
it('should add existing assets to albums', async () => { it('should add existing assets to albums', async () => {
const response1 = await immichCli([ const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
]);
expect(response1.stdout.split('\n')).toEqual( expect(response1.stdout.split('\n')).toEqual(
expect.arrayContaining([ expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
expect.stringContaining('Successfully uploaded 9 assets'),
]),
); );
expect(response1.stderr).toBe(''); expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0); expect(response1.exitCode).toBe(0);
@ -89,17 +70,10 @@ describe(`immich upload`, () => {
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums1.length).toBe(0); expect(albums1.length).toBe(0);
const response2 = await immichCli([ const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
'--album',
]);
expect(response2.stdout.split('\n')).toEqual( expect(response2.stdout.split('\n')).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.stringContaining( expect.stringContaining('All assets were already uploaded, nothing to do.'),
'All assets were already uploaded, nothing to do.',
),
expect.stringContaining('Successfully updated 9 assets'), expect.stringContaining('Successfully updated 9 assets'),
]), ]),
); );
@ -147,17 +121,10 @@ describe(`immich upload`, () => {
await mkdir(`/tmp/albums/nature`, { recursive: true }); await mkdir(`/tmp/albums/nature`, { recursive: true });
const filesToLink = await readdir(`${testAssetDir}/albums/nature`); const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
for (const file of filesToLink) { for (const file of filesToLink) {
await symlink( await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
`${testAssetDir}/albums/nature/${file}`,
`/tmp/albums/nature/${file}`,
);
} }
const { stderr, stdout, exitCode } = await immichCli([ const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);
'upload',
`/tmp/albums/nature`,
'--delete',
]);
const files = await readdir(`/tmp/albums/nature`); const files = await readdir(`/tmp/albums/nature`);
await rm(`/tmp/albums/nature`, { recursive: true }); await rm(`/tmp/albums/nature`, { recursive: true });

View File

@ -1,4 +1,4 @@
import { spawn, exec } from 'child_process'; import { exec, spawn } from 'node:child_process';
export default async () => { export default async () => {
let _resolve: () => unknown; let _resolve: () => unknown;
@ -19,8 +19,6 @@ export default async () => {
await ready; await ready;
return async () => { return async () => {
await new Promise<void>((resolve) => await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
exec('docker compose down', () => resolve()),
);
}; };
}; };

View File

@ -25,7 +25,6 @@ import { randomBytes } from 'node:crypto';
import { access } from 'node:fs/promises'; import { access } from 'node:fs/promises';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { EventEmitter } from 'node:stream';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import pg from 'pg'; import pg from 'pg';
import { io, type Socket } from 'socket.io-client'; import { io, type Socket } from 'socket.io-client';
@ -70,20 +69,12 @@ let client: pg.Client | null = null;
export const fileUtils = { export const fileUtils = {
reset: async () => { reset: async () => {
await execPromise( await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
);
}, },
}; };
export const dbUtils = { export const dbUtils = {
createFace: async ({ createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
assetId,
personId,
}: {
assetId: string;
personId: string;
}) => {
if (!client) { if (!client) {
return; return;
} }
@ -91,27 +82,23 @@ export const dbUtils = {
const vector = Array.from({ length: 512 }, Math.random); const vector = Array.from({ length: 512 }, Math.random);
const embedding = `[${vector.join(',')}]`; const embedding = `[${vector.join(',')}]`;
await client.query( await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', assetId,
[assetId, personId, embedding], personId,
); embedding,
]);
}, },
setPersonThumbnail: async (personId: string) => { setPersonThumbnail: async (personId: string) => {
if (!client) { if (!client) {
return; return;
} }
await client.query( await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
[personId],
);
}, },
reset: async (tables?: string[]) => { reset: async (tables?: string[]) => {
try { try {
if (!client) { if (!client) {
client = new pg.Client( client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich');
'postgres://postgres:postgres@127.0.0.1:5433/immich',
);
await client.connect(); await client.connect();
} }
@ -223,12 +210,8 @@ export const wsUtils = {
return new Promise<Socket>((resolve) => { return new Promise<Socket>((resolve) => {
websocket websocket
.on('connect', () => resolve(websocket)) .on('connect', () => resolve(websocket))
.on('on_upload_success', (data: AssetResponseDto) => .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id }))
onEvent({ event: 'upload', assetId: data.id }), .on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId }))
)
.on('on_asset_delete', (assetId: string) =>
onEvent({ event: 'delete', assetId }),
)
.connect(); .connect();
}); });
}, },
@ -241,21 +224,14 @@ export const wsUtils = {
set.clear(); set.clear();
} }
}, },
waitForEvent: async ({ waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
event,
assetId,
timeout: ms,
}: WaitOptions): Promise<void> => {
const set = events[event]; const set = events[event];
if (set.has(assetId)) { if (set.has(assetId)) {
return; return;
} }
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const timeout = setTimeout( const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000);
() => reject(new Error(`Timed out waiting for ${event} event`)),
ms || 5000,
);
callbacks[assetId] = () => { callbacks[assetId] = () => {
clearTimeout(timeout); clearTimeout(timeout);
@ -281,31 +257,22 @@ export const apiUtils = {
return response; return response;
}, },
userSetup: async (accessToken: string, dto: CreateUserDto) => { userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUser( await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
{ createUserDto: dto },
{ headers: asBearerAuth(accessToken) },
);
return login({ return login({
loginCredentialDto: { email: dto.email, password: dto.password }, loginCredentialDto: { email: dto.email, password: dto.password },
}); });
}, },
createApiKey: (accessToken: string) => { createApiKey: (accessToken: string) => {
return createApiKey( return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
{ apiKeyCreateDto: { name: 'e2e' } },
{ headers: asBearerAuth(accessToken) },
);
}, },
createAlbum: (accessToken: string, dto: CreateAlbumDto) => createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
createAlbum( createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
{ createAlbumDto: dto },
{ headers: asBearerAuth(accessToken) },
),
createAsset: async ( createAsset: async (
accessToken: string, accessToken: string,
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>, dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
data?: { data?: {
bytes?: Buffer; bytes?: Buffer;
filename?: string; filename: string;
}, },
) => { ) => {
const _dto = { const _dto = {
@ -313,13 +280,13 @@ export const apiUtils = {
deviceId: 'test', deviceId: 'test',
fileCreatedAt: new Date().toISOString(), fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(),
...(dto || {}), ...dto,
}; };
const _assetData = { const _assetData = {
bytes: randomBytes(32), bytes: randomBytes(32),
filename: 'example.jpg', filename: 'example.jpg',
...(data || {}), ...data,
}; };
const builder = request(app) const builder = request(app)
@ -328,39 +295,29 @@ export const apiUtils = {
.set('Authorization', `Bearer ${accessToken}`); .set('Authorization', `Bearer ${accessToken}`);
for (const [key, value] of Object.entries(_dto)) { for (const [key, value] of Object.entries(_dto)) {
builder.field(key, String(value)); void builder.field(key, String(value));
} }
const { body } = await builder; const { body } = await builder;
return body as AssetFileUploadResponseDto; return body as AssetFileUploadResponseDto;
}, },
getAssetInfo: (accessToken: string, id: string) => getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
deleteAssets: (accessToken: string, ids: string[]) => deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets( deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
{ assetBulkDeleteDto: { ids } },
{ headers: asBearerAuth(accessToken) },
),
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => { createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
// TODO fix createPerson to accept a body // TODO fix createPerson to accept a body
let person = await createPerson({ headers: asBearerAuth(accessToken) }); const person = await createPerson({ headers: asBearerAuth(accessToken) });
await dbUtils.setPersonThumbnail(person.id); await dbUtils.setPersonThumbnail(person.id);
if (!dto) { if (!dto) {
return person; return person;
} }
return updatePerson( return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) });
{ id: person.id, personUpdateDto: dto },
{ headers: asBearerAuth(accessToken) },
);
}, },
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
createSharedLink( createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
{ sharedLinkCreateDto: dto },
{ headers: asBearerAuth(accessToken) },
),
}; };
export const cliUtils = { export const cliUtils = {
@ -380,7 +337,7 @@ export const webUtils = {
value: accessToken, value: accessToken,
domain: '127.0.0.1', domain: '127.0.0.1',
path: '/', path: '/',
expires: 1742402728, expires: 1_742_402_728,
httpOnly: true, httpOnly: true,
secure: false, secure: false,
sameSite: 'Lax', sameSite: 'Lax',
@ -390,7 +347,7 @@ export const webUtils = {
value: 'password', value: 'password',
domain: '127.0.0.1', domain: '127.0.0.1',
path: '/', path: '/',
expires: 1742402728, expires: 1_742_402_728,
httpOnly: true, httpOnly: true,
secure: false, secure: false,
sameSite: 'Lax', sameSite: 'Lax',
@ -400,7 +357,7 @@ export const webUtils = {
value: 'true', value: 'true',
domain: '127.0.0.1', domain: '127.0.0.1',
path: '/', path: '/',
expires: 1742402728, expires: 1_742_402_728,
httpOnly: false, httpOnly: false,
secure: false, secure: false,
sameSite: 'Lax', sameSite: 'Lax',

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { apiUtils, dbUtils, webUtils } from 'src/utils'; import { apiUtils, dbUtils, webUtils } from 'src/utils';
test.describe('Registration', () => { test.describe('Registration', () => {
@ -68,7 +68,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
// change password // change password
expect(page.getByRole('heading')).toHaveText('Change Password'); await expect(page.getByRole('heading')).toHaveText('Change Password');
await expect(page).toHaveURL('/auth/change-password'); await expect(page).toHaveURL('/auth/change-password');
await page.getByLabel('New Password').fill('new-password'); await page.getByLabel('New Password').fill('new-password');
await page.getByLabel('Confirm Password').fill('new-password'); await page.getByLabel('Confirm Password').fill('new-password');

View File

@ -28,7 +28,7 @@ test.describe('Shared Links', () => {
assetIds: [asset.id], assetIds: [asset.id],
}, },
}, },
{ headers: asBearerAuth(admin.accessToken) } { headers: asBearerAuth(admin.accessToken) },
); );
sharedLink = await apiUtils.createSharedLink(admin.accessToken, { sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album, type: SharedLinkType.Album,

View File

@ -18,5 +18,6 @@
"rootDirs": ["src"], "rootDirs": ["src"],
"baseUrl": "./" "baseUrl": "./"
}, },
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
} }

View File

@ -4,8 +4,8 @@ import { InfraModule, InfraTestModule, dataSource } from '@app/infra';
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { randomBytes } from 'crypto';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { randomBytes } from 'node:crypto';
import { EntityTarget, ObjectLiteral } from 'typeorm'; import { EntityTarget, ObjectLiteral } from 'typeorm';
import { AppService } from '../../src/microservices/app.service'; import { AppService } from '../../src/microservices/app.service';
import { newJobRepositoryMock, newMetadataRepositoryMock } from '../../test'; import { newJobRepositoryMock, newMetadataRepositoryMock } from '../../test';