mirror of
				https://github.com/immich-app/immich
				synced 2025-10-17 18:19:27 +00:00 
			
		
		
		
	chore: linting (#7532)
* chore: linting * fix: broken tests * fix: formatting
This commit is contained in:
		
							parent
							
								
									09a7291527
								
							
						
					
					
						commit
						af0de1a768
					
				
					 33 changed files with 2480 additions and 548 deletions
				
			
		| 
						 | 
				
			
			@ -16,4 +16,4 @@ max_line_length = off
 | 
			
		|||
trim_trailing_whitespace = false
 | 
			
		||||
 | 
			
		||||
[*.{yml,yaml}]
 | 
			
		||||
quote_type = double
 | 
			
		||||
quote_type = single
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								.github/workflows/test.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/test.yml
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -35,7 +35,7 @@ jobs:
 | 
			
		|||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: "recursive"
 | 
			
		||||
          submodules: 'recursive'
 | 
			
		||||
 | 
			
		||||
      - name: Run e2e tests
 | 
			
		||||
        run: make server-e2e-jobs
 | 
			
		||||
| 
						 | 
				
			
			@ -184,7 +184,7 @@ jobs:
 | 
			
		|||
      - name: Checkout code
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: "recursive"
 | 
			
		||||
          submodules: 'recursive'
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4
 | 
			
		||||
| 
						 | 
				
			
			@ -194,25 +194,40 @@ jobs:
 | 
			
		|||
      - name: Run setup typescript-sdk
 | 
			
		||||
        run: npm ci && npm run build
 | 
			
		||||
        working-directory: ./open-api/typescript-sdk
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
 | 
			
		||||
      - name: Run setup cli
 | 
			
		||||
        run: npm ci && npm run build
 | 
			
		||||
        working-directory: ./cli
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        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
 | 
			
		||||
        run: npx playwright install --with-deps chromium
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
 | 
			
		||||
      - name: Docker build
 | 
			
		||||
        run: docker compose build
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
 | 
			
		||||
      - name: Run e2e tests (api & cli)
 | 
			
		||||
        run: npm run test
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
 | 
			
		||||
      - name: Run e2e tests (web)
 | 
			
		||||
        run: npx playwright test
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
 | 
			
		||||
  mobile-unit-tests:
 | 
			
		||||
    name: Mobile
 | 
			
		||||
| 
						 | 
				
			
			@ -222,8 +237,8 @@ jobs:
 | 
			
		|||
      - name: Setup Flutter SDK
 | 
			
		||||
        uses: subosito/flutter-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          channel: "stable"
 | 
			
		||||
          flutter-version: "3.16.9"
 | 
			
		||||
          channel: 'stable'
 | 
			
		||||
          flutter-version: '3.16.9'
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        working-directory: ./mobile
 | 
			
		||||
        run: flutter test -j 1
 | 
			
		||||
| 
						 | 
				
			
			@ -241,7 +256,7 @@ jobs:
 | 
			
		|||
      - uses: actions/setup-python@v5
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: 3.11
 | 
			
		||||
          cache: "poetry"
 | 
			
		||||
          cache: 'poetry'
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          poetry install --with dev --with cpu
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
# - https://immich.app/docs/developer/setup
 | 
			
		||||
# - https://immich.app/docs/developer/troubleshooting
 | 
			
		||||
 | 
			
		||||
version: "3.8"
 | 
			
		||||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
name: immich-dev
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ x-server-build: &server-common
 | 
			
		|||
services:
 | 
			
		||||
  immich-server:
 | 
			
		||||
    container_name: immich_server
 | 
			
		||||
    command: [ "/usr/src/app/bin/immich-dev", "immich" ]
 | 
			
		||||
    command: ['/usr/src/app/bin/immich-dev', 'immich']
 | 
			
		||||
    <<: *server-common
 | 
			
		||||
    ports:
 | 
			
		||||
      - 3001:3001
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +41,7 @@ services:
 | 
			
		|||
 | 
			
		||||
  immich-microservices:
 | 
			
		||||
    container_name: immich_microservices
 | 
			
		||||
    command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
 | 
			
		||||
    command: ['/usr/src/app/bin/immich-dev', 'microservices']
 | 
			
		||||
    <<: *server-common
 | 
			
		||||
    # extends:
 | 
			
		||||
    #   file: hwaccel.transcoding.yml
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +57,7 @@ services:
 | 
			
		|||
    image: immich-web-dev:latest
 | 
			
		||||
    build:
 | 
			
		||||
      context: ../web
 | 
			
		||||
    command: [ "/usr/src/app/bin/immich-web" ]
 | 
			
		||||
    command: ['/usr/src/app/bin/immich-web']
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
    ports:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
version: "3.8"
 | 
			
		||||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
name: immich-prod
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ x-server-build: &server-common
 | 
			
		|||
services:
 | 
			
		||||
  immich-server:
 | 
			
		||||
    container_name: immich_server
 | 
			
		||||
    command: [ "start.sh", "immich" ]
 | 
			
		||||
    command: ['start.sh', 'immich']
 | 
			
		||||
    <<: *server-common
 | 
			
		||||
    ports:
 | 
			
		||||
      - 2283:3001
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +27,7 @@ services:
 | 
			
		|||
 | 
			
		||||
  immich-microservices:
 | 
			
		||||
    container_name: immich_microservices
 | 
			
		||||
    command: [ "start.sh", "microservices" ]
 | 
			
		||||
    command: ['start.sh', 'microservices']
 | 
			
		||||
    <<: *server-common
 | 
			
		||||
    # extends:
 | 
			
		||||
    #   file: hwaccel.transcoding.yml
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
version: "3.8"
 | 
			
		||||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ services:
 | 
			
		|||
  immich-server:
 | 
			
		||||
    container_name: immich_server
 | 
			
		||||
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
 | 
			
		||||
    command: [ "start.sh", "immich" ]
 | 
			
		||||
    command: ['start.sh', 'immich']
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
 | 
			
		||||
      - /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
 | 
			
		||||
    #   file: hwaccel.transcoding.yml
 | 
			
		||||
    #   service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
 | 
			
		||||
    command: [ "start.sh", "microservices" ]
 | 
			
		||||
    command: ['start.sh', 'microservices']
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
 | 
			
		||||
      - /etc/localtime:/etc/localtime:ro
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										31
									
								
								e2e/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								e2e/.eslintrc.cjs
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										16
									
								
								e2e/.prettierignore
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										8
									
								
								e2e/.prettierrc
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
{
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "trailingComma": "all",
 | 
			
		||||
  "printWidth": 120,
 | 
			
		||||
  "semi": true,
 | 
			
		||||
  "organizeImportsSkipDestructiveCodeActions": true,
 | 
			
		||||
  "plugins": ["prettier-plugin-organize-imports"]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
version: "3.8"
 | 
			
		||||
version: '3.8'
 | 
			
		||||
 | 
			
		||||
name: immich-e2e
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -23,14 +23,14 @@ x-server-build: &server-common
 | 
			
		|||
services:
 | 
			
		||||
  immich-server:
 | 
			
		||||
    container_name: immich-e2e-server
 | 
			
		||||
    command: [ "./start.sh", "immich" ]
 | 
			
		||||
    command: ['./start.sh', 'immich']
 | 
			
		||||
    <<: *server-common
 | 
			
		||||
    ports:
 | 
			
		||||
      - 2283:3001
 | 
			
		||||
 | 
			
		||||
  immich-microservices:
 | 
			
		||||
    container_name: immich-e2e-microservices
 | 
			
		||||
    command: [ "./start.sh", "microservices" ]
 | 
			
		||||
    command: ['./start.sh', 'microservices']
 | 
			
		||||
    <<: *server-common
 | 
			
		||||
 | 
			
		||||
  redis:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2185
									
								
								e2e/package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2185
									
								
								e2e/package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
				
			
			@ -7,7 +7,11 @@
 | 
			
		|||
  "scripts": {
 | 
			
		||||
    "test": "vitest --config vitest.config.ts",
 | 
			
		||||
    "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": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
| 
						 | 
				
			
			@ -20,10 +24,18 @@
 | 
			
		|||
    "@types/node": "^20.11.17",
 | 
			
		||||
    "@types/pg": "^8.11.0",
 | 
			
		||||
    "@types/supertest": "^6.0.2",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.1.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.1.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",
 | 
			
		||||
    "luxon": "^3.4.4",
 | 
			
		||||
    "pg": "^8.11.3",
 | 
			
		||||
    "prettier": "^3.2.5",
 | 
			
		||||
    "prettier-plugin-organize-imports": "^3.2.4",
 | 
			
		||||
    "socket.io-client": "^4.7.4",
 | 
			
		||||
    "supertest": "^6.3.4",
 | 
			
		||||
    "typescript": "^5.3.3",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,10 +20,7 @@ describe('/activity', () => {
 | 
			
		|||
  let album: AlbumResponseDto;
 | 
			
		||||
 | 
			
		||||
  const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
 | 
			
		||||
    create(
 | 
			
		||||
      { activityCreateDto: dto },
 | 
			
		||||
      { headers: asBearerAuth(accessToken || admin.accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) });
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    apiUtils.setup();
 | 
			
		||||
| 
						 | 
				
			
			@ -56,13 +53,9 @@ describe('/activity', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require an albumId', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should reject an invalid albumId', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -71,9 +64,7 @@ describe('/activity', () => {
 | 
			
		|||
        .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'])),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should reject an invalid assetId', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -82,9 +73,7 @@ describe('/activity', () => {
 | 
			
		|||
        .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'])),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should start off empty', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -160,9 +149,7 @@ describe('/activity', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should filter by userId', async () => {
 | 
			
		||||
      const [reaction] = await Promise.all([
 | 
			
		||||
        createActivity({ albumId: album.id, type: ReactionType.Like }),
 | 
			
		||||
      ]);
 | 
			
		||||
      const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
 | 
			
		||||
 | 
			
		||||
      const response1 = await request(app)
 | 
			
		||||
        .get('/activity')
 | 
			
		||||
| 
						 | 
				
			
			@ -215,9 +202,7 @@ describe('/activity', () => {
 | 
			
		|||
        .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'])),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require a comment when type is comment', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -226,12 +211,7 @@ describe('/activity', () => {
 | 
			
		|||
        .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',
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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 () => {
 | 
			
		||||
      const [reaction] = await Promise.all([
 | 
			
		||||
        createActivity({ albumId: album.id, type: ReactionType.Like }),
 | 
			
		||||
      ]);
 | 
			
		||||
      const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/activity')
 | 
			
		||||
| 
						 | 
				
			
			@ -356,9 +334,7 @@ describe('/activity', () => {
 | 
			
		|||
 | 
			
		||||
  describe('DELETE /activity/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).delete(
 | 
			
		||||
        `/activity/${uuidDto.notFound}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -420,9 +396,7 @@ describe('/activity', () => {
 | 
			
		|||
        .set('Authorization', `Bearer ${nonOwner.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest('Not found or no activity.delete access'),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest('Not found or no activity.delete access'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should let a non-owner remove their own comment', async () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,10 +93,7 @@ describe('/album', () => {
 | 
			
		|||
      }),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    await deleteUser(
 | 
			
		||||
      { id: user3.userId },
 | 
			
		||||
      { headers: asBearerAuth(admin.accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    await deleteUser({ id: user3.userId }, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('GET /album', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,9 +108,7 @@ describe('/album', () => {
 | 
			
		|||
        .get('/album?shared=invalid')
 | 
			
		||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest(['shared must be a boolean value']),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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 () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/album')
 | 
			
		||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
      const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toHaveLength(3);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
| 
						 | 
				
			
			@ -250,9 +243,7 @@ describe('/album', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /album/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/album/${user1Albums[0].id}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -326,9 +317,7 @@ describe('/album', () => {
 | 
			
		|||
 | 
			
		||||
  describe('POST /album', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/album')
 | 
			
		||||
        .send({ albumName: 'New album' });
 | 
			
		||||
      const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -360,9 +349,7 @@ describe('/album', () => {
 | 
			
		|||
 | 
			
		||||
  describe('PUT /album/:id/assets', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).put(
 | 
			
		||||
        `/album/${user1Albums[0].id}/assets`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -375,9 +362,7 @@ describe('/album', () => {
 | 
			
		|||
        .send({ ids: [asset.id] });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual([
 | 
			
		||||
        expect.objectContaining({ id: asset.id, success: true }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should be able to add own asset to shared album', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -388,9 +373,7 @@ describe('/album', () => {
 | 
			
		|||
        .send({ ids: [asset.id] });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual([
 | 
			
		||||
        expect.objectContaining({ id: asset.id, success: true }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -473,9 +456,7 @@ describe('/album', () => {
 | 
			
		|||
        .send({ ids: [user1Asset1.id] });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual([
 | 
			
		||||
        expect.objectContaining({ id: user1Asset1.id, success: true }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should be able to remove own asset from shared album', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -485,9 +466,7 @@ describe('/album', () => {
 | 
			
		|||
        .send({ ids: [user1Asset1.id] });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual([
 | 
			
		||||
        expect.objectContaining({ id: user1Asset1.id, success: true }),
 | 
			
		||||
      ]);
 | 
			
		||||
      expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -501,9 +480,7 @@ describe('/album', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .put(`/album/${user1Albums[0].id}/users`)
 | 
			
		||||
        .send({ sharedUserIds: [] });
 | 
			
		||||
      const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,21 +13,15 @@ import { basename, join } from 'node:path';
 | 
			
		|||
import { Socket } from 'socket.io-client';
 | 
			
		||||
import { createUserDto, uuidDto } from 'src/fixtures';
 | 
			
		||||
import { errorDto } from 'src/responses';
 | 
			
		||||
import {
 | 
			
		||||
  apiUtils,
 | 
			
		||||
  app,
 | 
			
		||||
  dbUtils,
 | 
			
		||||
  tempDir,
 | 
			
		||||
  testAssetDir,
 | 
			
		||||
  wsUtils,
 | 
			
		||||
} from 'src/utils';
 | 
			
		||||
import { apiUtils, app, dbUtils, tempDir, testAssetDir, wsUtils } from 'src/utils';
 | 
			
		||||
import request from 'supertest';
 | 
			
		||||
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 sha1 = (bytes: Buffer) =>
 | 
			
		||||
  createHash('sha1').update(bytes).digest('base64');
 | 
			
		||||
const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64');
 | 
			
		||||
 | 
			
		||||
const readTags = async (bytes: Buffer, filename: string) => {
 | 
			
		||||
  const filepath = join(tempDir, filename);
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +77,6 @@ describe('/asset', () => {
 | 
			
		|||
        user1.accessToken,
 | 
			
		||||
        {
 | 
			
		||||
          isFavorite: true,
 | 
			
		||||
          isExternal: true,
 | 
			
		||||
          isReadOnly: true,
 | 
			
		||||
          fileCreatedAt: yesterday.toISO(),
 | 
			
		||||
          fileModifiedAt: yesterday.toISO(),
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +89,10 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
    user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
 | 
			
		||||
 | 
			
		||||
    for (const asset of [...user1Assets, ...user2Assets]) {
 | 
			
		||||
      expect(asset.duplicate).toBe(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
      // stats
 | 
			
		||||
      apiUtils.createAsset(userStats.accessToken),
 | 
			
		||||
| 
						 | 
				
			
			@ -126,9 +123,7 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /asset/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/asset/${uuidDto.notFound}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/asset/${uuidDto.notFound}`);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -163,9 +158,7 @@ describe('/asset', () => {
 | 
			
		|||
        assetIds: [user1Assets[0].id],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toMatchObject({ id: user1Assets[0].id });
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -195,9 +188,7 @@ describe('/asset', () => {
 | 
			
		|||
        assetIds: [user1Assets[0].id],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const data = await request(app).get(
 | 
			
		||||
        `/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
 | 
			
		||||
      );
 | 
			
		||||
      const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
 | 
			
		||||
      expect(data.status).toBe(200);
 | 
			
		||||
      expect(data.body).toMatchObject({ people: [] });
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -280,7 +271,7 @@ describe('/asset', () => {
 | 
			
		|||
      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)
 | 
			
		||||
        .get('/asset/random')
 | 
			
		||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -290,14 +281,9 @@ describe('/asset', () => {
 | 
			
		|||
      const assets: AssetResponseDto[] = body;
 | 
			
		||||
      expect(assets.length).toBe(1);
 | 
			
		||||
      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)
 | 
			
		||||
        .get('/asset/random?count=2')
 | 
			
		||||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
| 
						 | 
				
			
			@ -309,24 +295,18 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
      for (const asset of assets) {
 | 
			
		||||
        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',
 | 
			
		||||
      async () => {
 | 
			
		||||
        const { status, body } = await request(app)
 | 
			
		||||
          .get('/[]asset/random')
 | 
			
		||||
          .get('/asset/random')
 | 
			
		||||
          .set('Authorization', `Bearer ${user2.accessToken}`);
 | 
			
		||||
 | 
			
		||||
        expect(status).toBe(200);
 | 
			
		||||
        expect(body).toEqual([
 | 
			
		||||
          expect.objectContaining({ id: user2Assets[0].id }),
 | 
			
		||||
        ]);
 | 
			
		||||
        expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -341,9 +321,7 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
  describe('PUT /asset/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).put(
 | 
			
		||||
        `/asset/:${uuidDto.notFound}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).put(`/asset/:${uuidDto.notFound}`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -365,10 +343,7 @@ describe('/asset', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should favorite an asset', async () => {
 | 
			
		||||
      const before = await apiUtils.getAssetInfo(
 | 
			
		||||
        user1.accessToken,
 | 
			
		||||
        user1Assets[0].id,
 | 
			
		||||
      );
 | 
			
		||||
      const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
 | 
			
		||||
      expect(before.isFavorite).toBe(false);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
| 
						 | 
				
			
			@ -380,10 +355,7 @@ describe('/asset', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should archive an asset', async () => {
 | 
			
		||||
      const before = await apiUtils.getAssetInfo(
 | 
			
		||||
        user1.accessToken,
 | 
			
		||||
        user1Assets[0].id,
 | 
			
		||||
      );
 | 
			
		||||
      const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id);
 | 
			
		||||
      expect(before.isArchived).toBe(false);
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
| 
						 | 
				
			
			@ -497,9 +469,7 @@ describe('/asset', () => {
 | 
			
		|||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest(['each value in ids must be a UUID']),
 | 
			
		||||
      );
 | 
			
		||||
      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 () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -509,9 +479,7 @@ describe('/asset', () => {
 | 
			
		|||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest('Not found or no asset.delete access'),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest('Not found or no asset.delete access'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should move an asset to the trash', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -714,16 +682,10 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
        expect(response.duplicate).toBe(false);
 | 
			
		||||
 | 
			
		||||
        const asset = await apiUtils.getAssetInfo(
 | 
			
		||||
          admin.accessToken,
 | 
			
		||||
          response.id,
 | 
			
		||||
        );
 | 
			
		||||
        const asset = await apiUtils.getAssetInfo(admin.accessToken, response.id);
 | 
			
		||||
        expect(asset.livePhotoVideoId).toBeDefined();
 | 
			
		||||
 | 
			
		||||
        const video = await apiUtils.getAssetInfo(
 | 
			
		||||
          admin.accessToken,
 | 
			
		||||
          asset.livePhotoVideoId as string,
 | 
			
		||||
        );
 | 
			
		||||
        const video = await apiUtils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
 | 
			
		||||
        expect(video.checksum).toStrictEqual(checksum);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -731,9 +693,7 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /asset/thumbnail/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/asset/thumbnail/${assetLocation.id}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -775,9 +735,7 @@ describe('/asset', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /asset/file/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/asset/thumbnail/${assetLocation.id}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -792,10 +750,7 @@ describe('/asset', () => {
 | 
			
		|||
      expect(body).toBeDefined();
 | 
			
		||||
      expect(type).toBe('image/jpeg');
 | 
			
		||||
 | 
			
		||||
      const asset = await apiUtils.getAssetInfo(
 | 
			
		||||
        admin.accessToken,
 | 
			
		||||
        assetLocation.id,
 | 
			
		||||
      );
 | 
			
		||||
      const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id);
 | 
			
		||||
 | 
			
		||||
      const original = await readFile(locationAssetFilepath);
 | 
			
		||||
      const originalChecksum = sha1(original);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,4 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteAssets,
 | 
			
		||||
  getAuditFiles,
 | 
			
		||||
  updateAsset,
 | 
			
		||||
  type LoginResponseDto,
 | 
			
		||||
} from '@immich/sdk';
 | 
			
		||||
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
 | 
			
		||||
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
 | 
			
		||||
import { beforeAll, describe, expect, it } from 'vitest';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,17 +15,14 @@ describe('/audit', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET :/file-report', () => {
 | 
			
		||||
    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),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
      await Promise.all([
 | 
			
		||||
        deleteAssets(
 | 
			
		||||
          { assetBulkDeleteDto: { ids: [trashedAsset.id] } },
 | 
			
		||||
          { headers: asBearerAuth(admin.accessToken) },
 | 
			
		||||
        ),
 | 
			
		||||
        deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
 | 
			
		||||
        updateAsset(
 | 
			
		||||
          {
 | 
			
		||||
            id: archivedAsset.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,6 @@
 | 
			
		|||
import {
 | 
			
		||||
  LoginResponseDto,
 | 
			
		||||
  getAuthDevices,
 | 
			
		||||
  login,
 | 
			
		||||
  signUpAdmin,
 | 
			
		||||
} from '@immich/sdk';
 | 
			
		||||
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
 | 
			
		||||
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
 | 
			
		||||
import {
 | 
			
		||||
  deviceDto,
 | 
			
		||||
  errorDto,
 | 
			
		||||
  loginResponseDto,
 | 
			
		||||
  signupResponseDto,
 | 
			
		||||
} from 'src/responses';
 | 
			
		||||
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
 | 
			
		||||
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
 | 
			
		||||
import request from 'supertest';
 | 
			
		||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 | 
			
		||||
| 
						 | 
				
			
			@ -48,18 +38,14 @@ describe(`/auth/admin-sign-up`, () => {
 | 
			
		|||
 | 
			
		||||
    for (const { should, data } of invalid) {
 | 
			
		||||
      it(`should ${should}`, async () => {
 | 
			
		||||
        const { status, body } = await request(app)
 | 
			
		||||
          .post('/auth/admin-sign-up')
 | 
			
		||||
          .send(data);
 | 
			
		||||
        const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
 | 
			
		||||
        expect(status).toEqual(400);
 | 
			
		||||
        expect(body).toEqual(errorDto.badRequest());
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    it(`should sign up the admin`, async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/auth/admin-sign-up')
 | 
			
		||||
        .send(signupDto.admin);
 | 
			
		||||
      const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
 | 
			
		||||
      expect(status).toBe(201);
 | 
			
		||||
      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 () => {
 | 
			
		||||
      await signUpAdmin({ signUpDto: signupDto.admin });
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/auth/admin-sign-up')
 | 
			
		||||
        .send(signupDto.admin);
 | 
			
		||||
      const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(errorDto.alreadyHasAdmin);
 | 
			
		||||
| 
						 | 
				
			
			@ -107,9 +91,7 @@ describe('/auth/*', () => {
 | 
			
		|||
 | 
			
		||||
  describe(`POST /auth/login`, () => {
 | 
			
		||||
    it('should reject an incorrect password', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/auth/login')
 | 
			
		||||
        .send({ email, password: 'incorrect' });
 | 
			
		||||
      const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.incorrectLogin);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -125,9 +107,7 @@ describe('/auth/*', () => {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    it('should accept a correct password', async () => {
 | 
			
		||||
      const { status, body, headers } = await request(app)
 | 
			
		||||
        .post('/auth/login')
 | 
			
		||||
        .send({ email, password });
 | 
			
		||||
      const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
 | 
			
		||||
      expect(status).toBe(201);
 | 
			
		||||
      expect(body).toEqual(loginResponseDto.admin);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -136,15 +116,9 @@ describe('/auth/*', () => {
 | 
			
		|||
 | 
			
		||||
      const cookies = headers['set-cookie'];
 | 
			
		||||
      expect(cookies).toHaveLength(3);
 | 
			
		||||
      expect(cookies[0]).toEqual(
 | 
			
		||||
        `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[0]).toEqual(`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;');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -176,18 +150,12 @@ describe('/auth/*', () => {
 | 
			
		|||
        await login({ loginCredentialDto: loginDto.admin });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await expect(
 | 
			
		||||
        getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
 | 
			
		||||
      ).resolves.toHaveLength(6);
 | 
			
		||||
      await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
 | 
			
		||||
 | 
			
		||||
      const { status } = await request(app)
 | 
			
		||||
        .delete(`/auth/devices`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toBe(204);
 | 
			
		||||
 | 
			
		||||
      await expect(
 | 
			
		||||
        getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
 | 
			
		||||
      ).resolves.toHaveLength(1);
 | 
			
		||||
      await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error for a non-existent device id', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -195,9 +163,7 @@ describe('/auth/*', () => {
 | 
			
		|||
        .delete(`/auth/devices/${uuidDto.notFound}`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest('Not found or no authDevice.delete access')
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should logout a device', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -219,9 +185,7 @@ describe('/auth/*', () => {
 | 
			
		|||
 | 
			
		||||
  describe('POST /auth/validateToken', () => {
 | 
			
		||||
    it('should reject an invalid token', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post(`/auth/validateToken`)
 | 
			
		||||
        .set('Authorization', 'Bearer 123');
 | 
			
		||||
      const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.invalidToken);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -42,9 +42,7 @@ describe('/download', () => {
 | 
			
		|||
 | 
			
		||||
  describe('POST /download/asset/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).post(
 | 
			
		||||
        `/download/asset/${asset1.id}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,16 +15,9 @@ describe(`/oauth`, () => {
 | 
			
		|||
 | 
			
		||||
  describe('POST /oauth/authorize', () => {
 | 
			
		||||
    it(`should throw an error if a redirect uri is not provided`, async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post('/oauth/authorize')
 | 
			
		||||
        .send({});
 | 
			
		||||
      const { status, body } = await request(app).post('/oauth/authorize').send({});
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        errorDto.badRequest([
 | 
			
		||||
          'redirectUri must be a string',
 | 
			
		||||
          'redirectUri should not be empty',
 | 
			
		||||
        ])
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,14 +24,8 @@ describe('/partner', () => {
 | 
			
		|||
    ]);
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
      createPartner(
 | 
			
		||||
        { id: user2.userId },
 | 
			
		||||
        { headers: asBearerAuth(user1.accessToken) }
 | 
			
		||||
      ),
 | 
			
		||||
      createPartner(
 | 
			
		||||
        { id: user1.userId },
 | 
			
		||||
        { headers: asBearerAuth(user2.accessToken) }
 | 
			
		||||
      ),
 | 
			
		||||
      createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
 | 
			
		||||
      createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
 | 
			
		||||
    ]);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -66,9 +60,7 @@ describe('/partner', () => {
 | 
			
		|||
 | 
			
		||||
  describe('POST /partner/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).post(
 | 
			
		||||
        `/partner/${user3.userId}`
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).post(`/partner/${user3.userId}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -89,17 +81,13 @@ describe('/partner', () => {
 | 
			
		|||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        expect.objectContaining({ message: 'Partner already exists' })
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('PUT /partner/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).put(
 | 
			
		||||
        `/partner/${user2.userId}`
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).put(`/partner/${user2.userId}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -112,17 +100,13 @@ describe('/partner', () => {
 | 
			
		|||
        .send({ inTimeline: false });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        expect.objectContaining({ id: user2.userId, inTimeline: false })
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('DELETE /partner/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).delete(
 | 
			
		||||
        `/partner/${user3.userId}`
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -142,9 +126,7 @@ describe('/partner', () => {
 | 
			
		|||
        .set('Authorization', `Bearer ${user1.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        expect.objectContaining({ message: 'Partner not found' })
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,9 +65,7 @@ describe('/activity', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return only visible people', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/person')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual({
 | 
			
		||||
| 
						 | 
				
			
			@ -80,9 +78,7 @@ describe('/activity', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /person/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/person/${uuidDto.notFound}`
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -109,9 +105,7 @@ describe('/activity', () => {
 | 
			
		|||
 | 
			
		||||
  describe('PUT /person/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).put(
 | 
			
		||||
        `/person/${uuidDto.notFound}`
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +133,7 @@ describe('/activity', () => {
 | 
			
		|||
          birthDate: '123567',
 | 
			
		||||
          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)
 | 
			
		||||
          .put(`/person/${uuidDto.notFound}`)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -97,9 +97,7 @@ describe('/server-info', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /server-info/statistics', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        '/server-info/statistics'
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get('/server-info/statistics');
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -145,9 +143,7 @@ describe('/server-info', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /server-info/media-types', () => {
 | 
			
		||||
    it('should return accepted media types', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        '/server-info/media-types'
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get('/server-info/media-types');
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual({
 | 
			
		||||
        sidecar: ['.xmp'],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,14 +46,8 @@ describe('/shared-link', () => {
 | 
			
		|||
    ]);
 | 
			
		||||
 | 
			
		||||
    [album, deletedAlbum, metadataAlbum] = await Promise.all([
 | 
			
		||||
      createAlbum(
 | 
			
		||||
        { createAlbumDto: { albumName: 'album' } },
 | 
			
		||||
        { headers: asBearerAuth(user1.accessToken) },
 | 
			
		||||
      ),
 | 
			
		||||
      createAlbum(
 | 
			
		||||
        { createAlbumDto: { albumName: 'deleted album' } },
 | 
			
		||||
        { headers: asBearerAuth(user2.accessToken) },
 | 
			
		||||
      ),
 | 
			
		||||
      createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
 | 
			
		||||
      createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
 | 
			
		||||
      createAlbum(
 | 
			
		||||
        {
 | 
			
		||||
          createAlbumDto: {
 | 
			
		||||
| 
						 | 
				
			
			@ -65,47 +59,38 @@ describe('/shared-link', () => {
 | 
			
		|||
      ),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    [
 | 
			
		||||
      linkWithDeletedAlbum,
 | 
			
		||||
      linkWithAlbum,
 | 
			
		||||
      linkWithAssets,
 | 
			
		||||
      linkWithPassword,
 | 
			
		||||
      linkWithMetadata,
 | 
			
		||||
      linkWithoutMetadata,
 | 
			
		||||
    ] = await Promise.all([
 | 
			
		||||
      apiUtils.createSharedLink(user2.accessToken, {
 | 
			
		||||
        type: SharedLinkType.Album,
 | 
			
		||||
        albumId: deletedAlbum.id,
 | 
			
		||||
      }),
 | 
			
		||||
      apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
        type: SharedLinkType.Album,
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
      }),
 | 
			
		||||
      apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
        type: SharedLinkType.Individual,
 | 
			
		||||
        assetIds: [asset1.id],
 | 
			
		||||
      }),
 | 
			
		||||
      apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
        type: SharedLinkType.Album,
 | 
			
		||||
        albumId: album.id,
 | 
			
		||||
        password: 'foo',
 | 
			
		||||
      }),
 | 
			
		||||
      apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
        type: SharedLinkType.Album,
 | 
			
		||||
        albumId: metadataAlbum.id,
 | 
			
		||||
        showMetadata: true,
 | 
			
		||||
      }),
 | 
			
		||||
      apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
        type: SharedLinkType.Album,
 | 
			
		||||
        albumId: metadataAlbum.id,
 | 
			
		||||
        showMetadata: false,
 | 
			
		||||
      }),
 | 
			
		||||
    ]);
 | 
			
		||||
    [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
 | 
			
		||||
      await Promise.all([
 | 
			
		||||
        apiUtils.createSharedLink(user2.accessToken, {
 | 
			
		||||
          type: SharedLinkType.Album,
 | 
			
		||||
          albumId: deletedAlbum.id,
 | 
			
		||||
        }),
 | 
			
		||||
        apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
          type: SharedLinkType.Album,
 | 
			
		||||
          albumId: album.id,
 | 
			
		||||
        }),
 | 
			
		||||
        apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
          type: SharedLinkType.Individual,
 | 
			
		||||
          assetIds: [asset1.id],
 | 
			
		||||
        }),
 | 
			
		||||
        apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
          type: SharedLinkType.Album,
 | 
			
		||||
          albumId: album.id,
 | 
			
		||||
          password: 'foo',
 | 
			
		||||
        }),
 | 
			
		||||
        apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
          type: SharedLinkType.Album,
 | 
			
		||||
          albumId: metadataAlbum.id,
 | 
			
		||||
          showMetadata: true,
 | 
			
		||||
        }),
 | 
			
		||||
        apiUtils.createSharedLink(user1.accessToken, {
 | 
			
		||||
          type: SharedLinkType.Album,
 | 
			
		||||
          albumId: metadataAlbum.id,
 | 
			
		||||
          showMetadata: false,
 | 
			
		||||
        }),
 | 
			
		||||
      ]);
 | 
			
		||||
 | 
			
		||||
    await deleteUser(
 | 
			
		||||
      { id: user2.userId },
 | 
			
		||||
      { headers: asBearerAuth(admin.accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    await deleteUser({ id: user2.userId }, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('GET /shared-link', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -146,17 +131,13 @@ describe('/shared-link', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /shared-link/me', () => {
 | 
			
		||||
    it('should not require admin authentication', async () => {
 | 
			
		||||
      const { status } = await request(app)
 | 
			
		||||
        .get('/shared-link/me')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(403);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should get data for correct shared link', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/shared-link/me')
 | 
			
		||||
        .query({ key: linkWithAlbum.key });
 | 
			
		||||
      const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
| 
						 | 
				
			
			@ -178,18 +159,14 @@ describe('/shared-link', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return unauthorized if target has been soft deleted', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/shared-link/me')
 | 
			
		||||
        .query({ key: linkWithDeletedAlbum.key });
 | 
			
		||||
      const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.invalidShareKey);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return unauthorized for password protected link', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/shared-link/me')
 | 
			
		||||
        .query({ key: linkWithPassword.key });
 | 
			
		||||
      const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.invalidSharePassword);
 | 
			
		||||
| 
						 | 
				
			
			@ -211,9 +188,7 @@ describe('/shared-link', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return metadata for album shared link', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/shared-link/me')
 | 
			
		||||
        .query({ key: linkWithMetadata.key });
 | 
			
		||||
      const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body.assets).toHaveLength(1);
 | 
			
		||||
| 
						 | 
				
			
			@ -229,9 +204,7 @@ describe('/shared-link', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not return metadata for album shared link without metadata', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/shared-link/me')
 | 
			
		||||
        .query({ key: linkWithoutMetadata.key });
 | 
			
		||||
      const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body.assets).toHaveLength(1);
 | 
			
		||||
| 
						 | 
				
			
			@ -247,9 +220,7 @@ describe('/shared-link', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /shared-link/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        `/shared-link/${linkWithAlbum.id}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			@ -276,9 +247,7 @@ describe('/shared-link', () => {
 | 
			
		|||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        expect.objectContaining({ message: 'Shared link not found' }),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -308,9 +277,7 @@ describe('/shared-link', () => {
 | 
			
		|||
        .send({ type: SharedLinkType.Album });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        expect.objectContaining({ message: 'Invalid albumId' }),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should require a valid asset id', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -320,9 +287,7 @@ describe('/shared-link', () => {
 | 
			
		|||
        .send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(400);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
        expect.objectContaining({ message: 'Invalid assetIds' }),
 | 
			
		||||
      );
 | 
			
		||||
      expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create a shared link', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -424,9 +389,7 @@ describe('/shared-link', () => {
 | 
			
		|||
 | 
			
		||||
  describe('DELETE /shared-link/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).delete(
 | 
			
		||||
        `/shared-link/${linkWithAlbum.id}`,
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
 | 
			
		||||
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,9 +18,7 @@ describe('/system-config', () => {
 | 
			
		|||
 | 
			
		||||
  describe('GET /system-config/map/style.json', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).get(
 | 
			
		||||
        '/system-config/map/style.json'
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).get('/system-config/map/style.json');
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -32,11 +30,7 @@ describe('/system-config', () => {
 | 
			
		|||
          .query({ theme })
 | 
			
		||||
          .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
        expect(status).toBe(400);
 | 
			
		||||
        expect(body).toEqual(
 | 
			
		||||
          errorDto.badRequest([
 | 
			
		||||
            'theme must be one of the following values: light, dark',
 | 
			
		||||
          ])
 | 
			
		||||
        );
 | 
			
		||||
        expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,24 +32,16 @@ describe('/trash', () => {
 | 
			
		|||
      const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
 | 
			
		||||
      await apiUtils.deleteAssets(admin.accessToken, [assetId]);
 | 
			
		||||
 | 
			
		||||
      const before = await getAllAssets(
 | 
			
		||||
        {},
 | 
			
		||||
        { headers: asBearerAuth(admin.accessToken) },
 | 
			
		||||
      );
 | 
			
		||||
      const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
 | 
			
		||||
      expect(before.length).toBeGreaterThanOrEqual(1);
 | 
			
		||||
 | 
			
		||||
      const { status } = await request(app)
 | 
			
		||||
        .post('/trash/empty')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toBe(204);
 | 
			
		||||
 | 
			
		||||
      await wsUtils.waitForEvent({ event: 'delete', assetId });
 | 
			
		||||
 | 
			
		||||
      const after = await getAllAssets(
 | 
			
		||||
        {},
 | 
			
		||||
        { headers: asBearerAuth(admin.accessToken) },
 | 
			
		||||
      );
 | 
			
		||||
      const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
      expect(after.length).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -69,9 +61,7 @@ describe('/trash', () => {
 | 
			
		|||
      const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
 | 
			
		||||
      expect(before.isTrashed).toBe(true);
 | 
			
		||||
 | 
			
		||||
      const { status } = await request(app)
 | 
			
		||||
        .post('/trash/restore')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toBe(204);
 | 
			
		||||
 | 
			
		||||
      const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,10 +22,7 @@ describe('/server-info', () => {
 | 
			
		|||
      apiUtils.userSetup(admin.accessToken, createUserDto.user3),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    await deleteUser(
 | 
			
		||||
      { id: deletedUser.userId },
 | 
			
		||||
      { headers: asBearerAuth(admin.accessToken) }
 | 
			
		||||
    );
 | 
			
		||||
    await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('GET /user', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,9 +33,7 @@ describe('/server-info', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should get users', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get('/user')
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toEqual(200);
 | 
			
		||||
      expect(body).toHaveLength(4);
 | 
			
		||||
      expect(body).toEqual(
 | 
			
		||||
| 
						 | 
				
			
			@ -47,7 +42,7 @@ describe('/server-info', () => {
 | 
			
		|||
          expect.objectContaining({ email: 'user1@immich.cloud' }),
 | 
			
		||||
          expect.objectContaining({ email: 'user2@immich.cloud' }),
 | 
			
		||||
          expect.objectContaining({ email: 'user3@immich.cloud' }),
 | 
			
		||||
        ])
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +58,7 @@ describe('/server-info', () => {
 | 
			
		|||
          expect.objectContaining({ email: 'admin@immich.cloud' }),
 | 
			
		||||
          expect.objectContaining({ email: 'user2@immich.cloud' }),
 | 
			
		||||
          expect.objectContaining({ email: 'user3@immich.cloud' }),
 | 
			
		||||
        ])
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -81,7 +76,7 @@ describe('/server-info', () => {
 | 
			
		|||
          expect.objectContaining({ email: 'user1@immich.cloud' }),
 | 
			
		||||
          expect.objectContaining({ email: 'user2@immich.cloud' }),
 | 
			
		||||
          expect.objectContaining({ email: 'user3@immich.cloud' }),
 | 
			
		||||
        ])
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -112,9 +107,7 @@ describe('/server-info', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should get my info', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .get(`/user/me`)
 | 
			
		||||
        .set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
 | 
			
		||||
      expect(status).toBe(200);
 | 
			
		||||
      expect(body).toMatchObject({
 | 
			
		||||
        id: admin.userId,
 | 
			
		||||
| 
						 | 
				
			
			@ -125,9 +118,7 @@ describe('/server-info', () => {
 | 
			
		|||
 | 
			
		||||
  describe('POST /user', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .post(`/user`)
 | 
			
		||||
        .send(createUserDto.user1);
 | 
			
		||||
      const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -181,9 +172,7 @@ describe('/server-info', () => {
 | 
			
		|||
 | 
			
		||||
  describe('DELETE /user/:id', () => {
 | 
			
		||||
    it('should require authentication', async () => {
 | 
			
		||||
      const { status, body } = await request(app).delete(
 | 
			
		||||
        `/user/${userToDelete.userId}`
 | 
			
		||||
      );
 | 
			
		||||
      const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
 | 
			
		||||
      expect(status).toBe(401);
 | 
			
		||||
      expect(body).toEqual(errorDto.unauthorized);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -241,10 +230,7 @@ describe('/server-info', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
 | 
			
		||||
      const before = await getUserById(
 | 
			
		||||
        { id: admin.userId },
 | 
			
		||||
        { headers: asBearerAuth(admin.accessToken) }
 | 
			
		||||
      );
 | 
			
		||||
      const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .put(`/user`)
 | 
			
		||||
| 
						 | 
				
			
			@ -261,10 +247,7 @@ describe('/server-info', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should update first and last name', async () => {
 | 
			
		||||
      const before = await getUserById(
 | 
			
		||||
        { id: admin.userId },
 | 
			
		||||
        { headers: asBearerAuth(admin.accessToken) }
 | 
			
		||||
      );
 | 
			
		||||
      const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .put(`/user`)
 | 
			
		||||
| 
						 | 
				
			
			@ -284,10 +267,7 @@ describe('/server-info', () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should update memories enabled', async () => {
 | 
			
		||||
      const before = await getUserById(
 | 
			
		||||
        { id: admin.userId },
 | 
			
		||||
        { headers: asBearerAuth(admin.accessToken) }
 | 
			
		||||
      );
 | 
			
		||||
      const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
 | 
			
		||||
      const { status, body } = await request(app)
 | 
			
		||||
        .put(`/user`)
 | 
			
		||||
        .send({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import { stat } from 'node:fs/promises';
 | 
			
		||||
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`, () => {
 | 
			
		||||
  beforeAll(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -24,25 +24,15 @@ describe(`immich login-key`, () => {
 | 
			
		|||
  });
 | 
			
		||||
 | 
			
		||||
  it('should require a valid key', async () => {
 | 
			
		||||
    const { stderr, exitCode } = await immichCli([
 | 
			
		||||
      'login-key',
 | 
			
		||||
      app,
 | 
			
		||||
      'immich-is-so-cool',
 | 
			
		||||
    ]);
 | 
			
		||||
    expect(stderr).toContain(
 | 
			
		||||
      'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
 | 
			
		||||
    );
 | 
			
		||||
    const { stderr, exitCode } = await immichCli(['login-key', 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);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should login', async () => {
 | 
			
		||||
    const admin = await apiUtils.adminSetup();
 | 
			
		||||
    const key = await apiUtils.createApiKey(admin.accessToken);
 | 
			
		||||
    const { stdout, stderr, exitCode } = await immichCli([
 | 
			
		||||
      'login-key',
 | 
			
		||||
      app,
 | 
			
		||||
      `${key.secret}`,
 | 
			
		||||
    ]);
 | 
			
		||||
    const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
 | 
			
		||||
    expect(stdout.split('\n')).toEqual([
 | 
			
		||||
      'Logging in...',
 | 
			
		||||
      'Logged in as admin@immich.cloud',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,6 @@
 | 
			
		|||
import { getAllAlbums, getAllAssets } from '@immich/sdk';
 | 
			
		||||
import { mkdir, readdir, rm, symlink } from 'fs/promises';
 | 
			
		||||
import {
 | 
			
		||||
  apiUtils,
 | 
			
		||||
  asKeyAuth,
 | 
			
		||||
  cliUtils,
 | 
			
		||||
  dbUtils,
 | 
			
		||||
  immichCli,
 | 
			
		||||
  testAssetDir,
 | 
			
		||||
} from 'src/utils';
 | 
			
		||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
 | 
			
		||||
import { apiUtils, asKeyAuth, cliUtils, dbUtils, immichCli, testAssetDir } from 'src/utils';
 | 
			
		||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
 | 
			
		||||
 | 
			
		||||
describe(`immich upload`, () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -25,16 +18,10 @@ describe(`immich upload`, () => {
 | 
			
		|||
 | 
			
		||||
  describe('immich upload --recursive', () => {
 | 
			
		||||
    it('should upload a folder recursively', async () => {
 | 
			
		||||
      const { stderr, stdout, exitCode } = await immichCli([
 | 
			
		||||
        'upload',
 | 
			
		||||
        `${testAssetDir}/albums/nature/`,
 | 
			
		||||
        '--recursive',
 | 
			
		||||
      ]);
 | 
			
		||||
      const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
 | 
			
		||||
      expect(stderr).toBe('');
 | 
			
		||||
      expect(stdout.split('\n')).toEqual(
 | 
			
		||||
        expect.arrayContaining([
 | 
			
		||||
          expect.stringContaining('Successfully uploaded 9 assets'),
 | 
			
		||||
        ]),
 | 
			
		||||
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
 | 
			
		||||
      );
 | 
			
		||||
      expect(exitCode).toBe(0);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -70,15 +57,9 @@ describe(`immich upload`, () => {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    it('should add existing assets to albums', async () => {
 | 
			
		||||
      const response1 = await immichCli([
 | 
			
		||||
        'upload',
 | 
			
		||||
        `${testAssetDir}/albums/nature/`,
 | 
			
		||||
        '--recursive',
 | 
			
		||||
      ]);
 | 
			
		||||
      const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
 | 
			
		||||
      expect(response1.stdout.split('\n')).toEqual(
 | 
			
		||||
        expect.arrayContaining([
 | 
			
		||||
          expect.stringContaining('Successfully uploaded 9 assets'),
 | 
			
		||||
        ]),
 | 
			
		||||
        expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
 | 
			
		||||
      );
 | 
			
		||||
      expect(response1.stderr).toBe('');
 | 
			
		||||
      expect(response1.exitCode).toBe(0);
 | 
			
		||||
| 
						 | 
				
			
			@ -89,17 +70,10 @@ describe(`immich upload`, () => {
 | 
			
		|||
      const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
 | 
			
		||||
      expect(albums1.length).toBe(0);
 | 
			
		||||
 | 
			
		||||
      const response2 = await immichCli([
 | 
			
		||||
        'upload',
 | 
			
		||||
        `${testAssetDir}/albums/nature/`,
 | 
			
		||||
        '--recursive',
 | 
			
		||||
        '--album',
 | 
			
		||||
      ]);
 | 
			
		||||
      const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
 | 
			
		||||
      expect(response2.stdout.split('\n')).toEqual(
 | 
			
		||||
        expect.arrayContaining([
 | 
			
		||||
          expect.stringContaining(
 | 
			
		||||
            'All assets were already uploaded, nothing to do.',
 | 
			
		||||
          ),
 | 
			
		||||
          expect.stringContaining('All assets were already uploaded, nothing to do.'),
 | 
			
		||||
          expect.stringContaining('Successfully updated 9 assets'),
 | 
			
		||||
        ]),
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			@ -147,17 +121,10 @@ describe(`immich upload`, () => {
 | 
			
		|||
      await mkdir(`/tmp/albums/nature`, { recursive: true });
 | 
			
		||||
      const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
 | 
			
		||||
      for (const file of filesToLink) {
 | 
			
		||||
        await symlink(
 | 
			
		||||
          `${testAssetDir}/albums/nature/${file}`,
 | 
			
		||||
          `/tmp/albums/nature/${file}`,
 | 
			
		||||
        );
 | 
			
		||||
        await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const { stderr, stdout, exitCode } = await immichCli([
 | 
			
		||||
        'upload',
 | 
			
		||||
        `/tmp/albums/nature`,
 | 
			
		||||
        '--delete',
 | 
			
		||||
      ]);
 | 
			
		||||
      const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);
 | 
			
		||||
 | 
			
		||||
      const files = await readdir(`/tmp/albums/nature`);
 | 
			
		||||
      await rm(`/tmp/albums/nature`, { recursive: true });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { spawn, exec } from 'child_process';
 | 
			
		||||
import { exec, spawn } from 'node:child_process';
 | 
			
		||||
 | 
			
		||||
export default async () => {
 | 
			
		||||
  let _resolve: () => unknown;
 | 
			
		||||
| 
						 | 
				
			
			@ -19,8 +19,6 @@ export default async () => {
 | 
			
		|||
  await ready;
 | 
			
		||||
 | 
			
		||||
  return async () => {
 | 
			
		||||
    await new Promise<void>((resolve) =>
 | 
			
		||||
      exec('docker compose down', () => resolve()),
 | 
			
		||||
    );
 | 
			
		||||
    await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,6 @@ import { randomBytes } from 'node:crypto';
 | 
			
		|||
import { access } from 'node:fs/promises';
 | 
			
		||||
import { tmpdir } from 'node:os';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import { EventEmitter } from 'node:stream';
 | 
			
		||||
import { promisify } from 'node:util';
 | 
			
		||||
import pg from 'pg';
 | 
			
		||||
import { io, type Socket } from 'socket.io-client';
 | 
			
		||||
| 
						 | 
				
			
			@ -70,20 +69,12 @@ let client: pg.Client | null = null;
 | 
			
		|||
 | 
			
		||||
export const fileUtils = {
 | 
			
		||||
  reset: async () => {
 | 
			
		||||
    await execPromise(
 | 
			
		||||
      `docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
 | 
			
		||||
    );
 | 
			
		||||
    await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const dbUtils = {
 | 
			
		||||
  createFace: async ({
 | 
			
		||||
    assetId,
 | 
			
		||||
    personId,
 | 
			
		||||
  }: {
 | 
			
		||||
    assetId: string;
 | 
			
		||||
    personId: string;
 | 
			
		||||
  }) => {
 | 
			
		||||
  createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
 | 
			
		||||
    if (!client) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -91,27 +82,23 @@ export const dbUtils = {
 | 
			
		|||
    const vector = Array.from({ length: 512 }, Math.random);
 | 
			
		||||
    const embedding = `[${vector.join(',')}]`;
 | 
			
		||||
 | 
			
		||||
    await client.query(
 | 
			
		||||
      'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
 | 
			
		||||
      [assetId, personId, embedding],
 | 
			
		||||
    );
 | 
			
		||||
    await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
 | 
			
		||||
      assetId,
 | 
			
		||||
      personId,
 | 
			
		||||
      embedding,
 | 
			
		||||
    ]);
 | 
			
		||||
  },
 | 
			
		||||
  setPersonThumbnail: async (personId: string) => {
 | 
			
		||||
    if (!client) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await client.query(
 | 
			
		||||
      `UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
 | 
			
		||||
      [personId],
 | 
			
		||||
    );
 | 
			
		||||
    await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
 | 
			
		||||
  },
 | 
			
		||||
  reset: async (tables?: string[]) => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!client) {
 | 
			
		||||
        client = new pg.Client(
 | 
			
		||||
          'postgres://postgres:postgres@127.0.0.1:5433/immich',
 | 
			
		||||
        );
 | 
			
		||||
        client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich');
 | 
			
		||||
        await client.connect();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -223,12 +210,8 @@ export const wsUtils = {
 | 
			
		|||
    return new Promise<Socket>((resolve) => {
 | 
			
		||||
      websocket
 | 
			
		||||
        .on('connect', () => resolve(websocket))
 | 
			
		||||
        .on('on_upload_success', (data: AssetResponseDto) =>
 | 
			
		||||
          onEvent({ event: 'upload', assetId: data.id }),
 | 
			
		||||
        )
 | 
			
		||||
        .on('on_asset_delete', (assetId: string) =>
 | 
			
		||||
          onEvent({ event: 'delete', assetId }),
 | 
			
		||||
        )
 | 
			
		||||
        .on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id }))
 | 
			
		||||
        .on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId }))
 | 
			
		||||
        .connect();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			@ -241,21 +224,14 @@ export const wsUtils = {
 | 
			
		|||
      set.clear();
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  waitForEvent: async ({
 | 
			
		||||
    event,
 | 
			
		||||
    assetId,
 | 
			
		||||
    timeout: ms,
 | 
			
		||||
  }: WaitOptions): Promise<void> => {
 | 
			
		||||
  waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
 | 
			
		||||
    const set = events[event];
 | 
			
		||||
    if (set.has(assetId)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return new Promise<void>((resolve, reject) => {
 | 
			
		||||
      const timeout = setTimeout(
 | 
			
		||||
        () => reject(new Error(`Timed out waiting for ${event} event`)),
 | 
			
		||||
        ms || 5000,
 | 
			
		||||
      );
 | 
			
		||||
      const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000);
 | 
			
		||||
 | 
			
		||||
      callbacks[assetId] = () => {
 | 
			
		||||
        clearTimeout(timeout);
 | 
			
		||||
| 
						 | 
				
			
			@ -281,31 +257,22 @@ export const apiUtils = {
 | 
			
		|||
    return response;
 | 
			
		||||
  },
 | 
			
		||||
  userSetup: async (accessToken: string, dto: CreateUserDto) => {
 | 
			
		||||
    await createUser(
 | 
			
		||||
      { createUserDto: dto },
 | 
			
		||||
      { headers: asBearerAuth(accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
 | 
			
		||||
    return login({
 | 
			
		||||
      loginCredentialDto: { email: dto.email, password: dto.password },
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
  createApiKey: (accessToken: string) => {
 | 
			
		||||
    return createApiKey(
 | 
			
		||||
      { apiKeyCreateDto: { name: 'e2e' } },
 | 
			
		||||
      { headers: asBearerAuth(accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
 | 
			
		||||
  },
 | 
			
		||||
  createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
 | 
			
		||||
    createAlbum(
 | 
			
		||||
      { createAlbumDto: dto },
 | 
			
		||||
      { headers: asBearerAuth(accessToken) },
 | 
			
		||||
    ),
 | 
			
		||||
    createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
 | 
			
		||||
  createAsset: async (
 | 
			
		||||
    accessToken: string,
 | 
			
		||||
    dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
 | 
			
		||||
    data?: {
 | 
			
		||||
      bytes?: Buffer;
 | 
			
		||||
      filename?: string;
 | 
			
		||||
      filename: string;
 | 
			
		||||
    },
 | 
			
		||||
  ) => {
 | 
			
		||||
    const _dto = {
 | 
			
		||||
| 
						 | 
				
			
			@ -313,13 +280,13 @@ export const apiUtils = {
 | 
			
		|||
      deviceId: 'test',
 | 
			
		||||
      fileCreatedAt: new Date().toISOString(),
 | 
			
		||||
      fileModifiedAt: new Date().toISOString(),
 | 
			
		||||
      ...(dto || {}),
 | 
			
		||||
      ...dto,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const _assetData = {
 | 
			
		||||
      bytes: randomBytes(32),
 | 
			
		||||
      filename: 'example.jpg',
 | 
			
		||||
      ...(data || {}),
 | 
			
		||||
      ...data,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const builder = request(app)
 | 
			
		||||
| 
						 | 
				
			
			@ -328,39 +295,29 @@ export const apiUtils = {
 | 
			
		|||
      .set('Authorization', `Bearer ${accessToken}`);
 | 
			
		||||
 | 
			
		||||
    for (const [key, value] of Object.entries(_dto)) {
 | 
			
		||||
      builder.field(key, String(value));
 | 
			
		||||
      void builder.field(key, String(value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { body } = await builder;
 | 
			
		||||
 | 
			
		||||
    return body as AssetFileUploadResponseDto;
 | 
			
		||||
  },
 | 
			
		||||
  getAssetInfo: (accessToken: string, id: string) =>
 | 
			
		||||
    getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
 | 
			
		||||
  getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
 | 
			
		||||
  deleteAssets: (accessToken: string, ids: string[]) =>
 | 
			
		||||
    deleteAssets(
 | 
			
		||||
      { assetBulkDeleteDto: { ids } },
 | 
			
		||||
      { headers: asBearerAuth(accessToken) },
 | 
			
		||||
    ),
 | 
			
		||||
    deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
 | 
			
		||||
  createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
 | 
			
		||||
    // 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);
 | 
			
		||||
 | 
			
		||||
    if (!dto) {
 | 
			
		||||
      return person;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return updatePerson(
 | 
			
		||||
      { id: person.id, personUpdateDto: dto },
 | 
			
		||||
      { headers: asBearerAuth(accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) });
 | 
			
		||||
  },
 | 
			
		||||
  createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
 | 
			
		||||
    createSharedLink(
 | 
			
		||||
      { sharedLinkCreateDto: dto },
 | 
			
		||||
      { headers: asBearerAuth(accessToken) },
 | 
			
		||||
    ),
 | 
			
		||||
    createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cliUtils = {
 | 
			
		||||
| 
						 | 
				
			
			@ -380,7 +337,7 @@ export const webUtils = {
 | 
			
		|||
        value: accessToken,
 | 
			
		||||
        domain: '127.0.0.1',
 | 
			
		||||
        path: '/',
 | 
			
		||||
        expires: 1742402728,
 | 
			
		||||
        expires: 1_742_402_728,
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        secure: false,
 | 
			
		||||
        sameSite: 'Lax',
 | 
			
		||||
| 
						 | 
				
			
			@ -390,7 +347,7 @@ export const webUtils = {
 | 
			
		|||
        value: 'password',
 | 
			
		||||
        domain: '127.0.0.1',
 | 
			
		||||
        path: '/',
 | 
			
		||||
        expires: 1742402728,
 | 
			
		||||
        expires: 1_742_402_728,
 | 
			
		||||
        httpOnly: true,
 | 
			
		||||
        secure: false,
 | 
			
		||||
        sameSite: 'Lax',
 | 
			
		||||
| 
						 | 
				
			
			@ -400,7 +357,7 @@ export const webUtils = {
 | 
			
		|||
        value: 'true',
 | 
			
		||||
        domain: '127.0.0.1',
 | 
			
		||||
        path: '/',
 | 
			
		||||
        expires: 1742402728,
 | 
			
		||||
        expires: 1_742_402_728,
 | 
			
		||||
        httpOnly: false,
 | 
			
		||||
        secure: false,
 | 
			
		||||
        sameSite: 'Lax',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { test, expect } from '@playwright/test';
 | 
			
		||||
import { expect, test } from '@playwright/test';
 | 
			
		||||
import { apiUtils, dbUtils, webUtils } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
test.describe('Registration', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ test.describe('Registration', () => {
 | 
			
		|||
    await page.getByRole('button', { name: 'Login' }).click();
 | 
			
		||||
 | 
			
		||||
    // 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 page.getByLabel('New Password').fill('new-password');
 | 
			
		||||
    await page.getByLabel('Confirm Password').fill('new-password');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ test.describe('Shared Links', () => {
 | 
			
		|||
          assetIds: [asset.id],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      { headers: asBearerAuth(admin.accessToken) }
 | 
			
		||||
      { headers: asBearerAuth(admin.accessToken) },
 | 
			
		||||
    );
 | 
			
		||||
    sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
 | 
			
		||||
      type: SharedLinkType.Album,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,5 +18,6 @@
 | 
			
		|||
    "rootDirs": ["src"],
 | 
			
		||||
    "baseUrl": "./"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*.ts"],
 | 
			
		||||
  "exclude": ["dist", "node_modules"]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,8 +4,8 @@ import { InfraModule, InfraTestModule, dataSource } from '@app/infra';
 | 
			
		|||
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
 | 
			
		||||
import { INestApplication } from '@nestjs/common';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
import { randomBytes } from 'crypto';
 | 
			
		||||
import { DateTime } from 'luxon';
 | 
			
		||||
import { randomBytes } from 'node:crypto';
 | 
			
		||||
import { EntityTarget, ObjectLiteral } from 'typeorm';
 | 
			
		||||
import { AppService } from '../../src/microservices/app.service';
 | 
			
		||||
import { newJobRepositoryMock, newMetadataRepositoryMock } from '../../test';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue