You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	chore: linting (#7532)
* chore: linting * fix: broken tests * fix: formatting
This commit is contained in:
		| @@ -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'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user