Compare commits
34 commits
main
...
feat/e2e-q
Author | SHA1 | Date | |
---|---|---|---|
|
c96e3becc3 | ||
|
067c0e5d41 | ||
|
0b4835ef3c | ||
|
702520af4e | ||
|
bb947b07ce | ||
|
41b20c1079 | ||
|
fe2c5b590b | ||
|
ef5c121417 | ||
|
a0048118e1 | ||
|
a923123f5f | ||
|
1b2fa9255e | ||
|
0c2a104e07 | ||
|
5055be1b5b | ||
|
582816d03a | ||
|
ab154ca2ca | ||
|
81870a9d50 | ||
|
f20e90e544 | ||
|
8f26db9a6b | ||
|
335ee427da | ||
|
04511679fe | ||
|
9d8dda1755 | ||
|
f091c0e17d | ||
|
ae08463a49 | ||
|
a094805c27 | ||
|
6429234bd5 | ||
|
8cd61d3291 | ||
|
6a2eca9b0a | ||
|
3835221cda | ||
|
56b6ecb8a4 | ||
|
b059c95cad | ||
|
8ad2be195d | ||
|
e8a20fd492 | ||
|
e9c12650bb | ||
|
fec116250c |
23 changed files with 800 additions and 22 deletions
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
|
@ -12,21 +12,45 @@ concurrency:
|
||||||
jobs:
|
jobs:
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: Run end-to-end test suites
|
name: Run end-to-end test suites
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
|
|
||||||
|
env:
|
||||||
|
ENV LD_LIBRARY_PATH: /usr/local/lib:$LD_LIBRARY_PATH
|
||||||
|
ENV LD_RUN_PATH: /usr/local/lib:$LD_RUN_PATH
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
|
||||||
- name: Run npm install
|
- name: Run npm install
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "21.0.0-nightly20230921480ab8c3a4"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt-get update && sudo apt-get install -yqq build-essential ninja-build meson pkg-config jq zlib1g autoconf libglib2.0-dev libexpat1-dev librsvg2-dev libexif-dev libwebp-dev liborc-0.4-dev libjpeg-turbo8-dev libgsf-1-dev libheif-dev liblcms2-2 mesa-va-drivers libmimalloc2.0 $(if [ $(arch) = "x86_64" ]; then echo "intel-media-va-driver-non-free"; fi) && sudo FFMPEG_PLATFORM=jammy ./bin/install-ffmpeg.sh && sudo apt-get autoremove && sudo apt-get clean && sudo rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
- name: Copy build files
|
||||||
|
run: mkdir build && cp bin/build-libraw.sh bin/build-imagemagick.sh bin/build-libvips.sh bin/use-camera-wb.patch build-lock.json build
|
||||||
|
|
||||||
|
- name: Install libraw, imagemagick, libvips
|
||||||
|
run: cd build && sudo sudo ./build-libraw.sh && sudo ./build-imagemagick.sh && sudo ./build-libvips.sh
|
||||||
|
|
||||||
|
- name: Checkout test assets
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: etnoy/immich-test-assets
|
||||||
|
path: ./server/test/assets
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
run: npm run test:e2e
|
run: npm run test:e2e -- --forceExit --detectOpenHandles
|
||||||
if: ${{ !cancelled() }}
|
|
||||||
|
|
||||||
doc-tests:
|
doc-tests:
|
||||||
name: Run documentation checks
|
name: Run documentation checks
|
||||||
|
|
146
docker/docker-compose.e2e.yml
Normal file
146
docker/docker-compose.e2e.yml
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
container_name: immich_server-e2e
|
||||||
|
image: immich-server-dev:latest
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: builder
|
||||||
|
command: npm run start:debug immich
|
||||||
|
volumes:
|
||||||
|
- ../server:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
ports:
|
||||||
|
- 3001:3001
|
||||||
|
- 9230:9230
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
- typesense
|
||||||
|
|
||||||
|
immich-machine-learning:
|
||||||
|
container_name: immich_machine_learning-e2e
|
||||||
|
image: immich-machine-learning-dev:latest
|
||||||
|
build:
|
||||||
|
context: ../machine-learning
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- 3003:3003
|
||||||
|
volumes:
|
||||||
|
- ../machine-learning:/usr/src/app
|
||||||
|
- model-cache:/cache
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
immich-microservices:
|
||||||
|
container_name: immich_microservices-e2e
|
||||||
|
image: immich-microservices:latest
|
||||||
|
# extends:
|
||||||
|
# file: hwaccel.yml
|
||||||
|
# service: hwaccel
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: builder
|
||||||
|
command: npm run start:debug microservices
|
||||||
|
volumes:
|
||||||
|
- ../server:/usr/src/app
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 9231:9230
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
- immich-server
|
||||||
|
- typesense
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
container_name: immich_web-e2e
|
||||||
|
image: immich-web-dev:1.9.0
|
||||||
|
build:
|
||||||
|
context: ../web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: dev
|
||||||
|
command: npm run dev --host
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# Rename these values for svelte public interface
|
||||||
|
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
|
||||||
|
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
- 24678:24678
|
||||||
|
volumes:
|
||||||
|
- ../web:/usr/src/app
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- immich-server
|
||||||
|
|
||||||
|
typesense:
|
||||||
|
container_name: immich_typesense-e2e
|
||||||
|
image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
|
||||||
|
environment:
|
||||||
|
- TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
|
||||||
|
- TYPESENSE_DATA_DIR=/data
|
||||||
|
# remove this to get debug messages
|
||||||
|
- GLOG_minloglevel=1
|
||||||
|
volumes:
|
||||||
|
- tsdata:/data
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: immich_redis-e2e
|
||||||
|
image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: immich_postgres-e2e
|
||||||
|
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
|
||||||
|
immich-proxy:
|
||||||
|
container_name: immich_proxy-e2e
|
||||||
|
image: immich-proxy-dev:latest
|
||||||
|
environment:
|
||||||
|
# Make sure these values get passed through from the env file
|
||||||
|
- IMMICH_SERVER_URL
|
||||||
|
- IMMICH_WEB_URL
|
||||||
|
build:
|
||||||
|
context: ../nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- 2285:8080
|
||||||
|
depends_on:
|
||||||
|
- immich-server
|
||||||
|
- immich-web
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
model-cache:
|
||||||
|
tsdata:
|
|
@ -2,17 +2,22 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
: "${FFMPEG_PLATFORM:=bookworm}"
|
||||||
|
|
||||||
|
echo Using platform $FFMPEG_PLATFORM
|
||||||
|
|
||||||
LOCK=$(jq -c '.packages[] | select(.name == "ffmpeg")' build-lock.json)
|
LOCK=$(jq -c '.packages[] | select(.name == "ffmpeg")' build-lock.json)
|
||||||
export TARGETARCH=${TARGETARCH:=$(dpkg --print-architecture)}
|
TARGETARCH=${TARGETARCH:=$(dpkg --print-architecture)}
|
||||||
|
export PLATFORMARCH=${FFMPEG_PLATFORM}-${TARGETARCH}
|
||||||
FFMPEG_VERSION=${FFMPEG_VERSION:=$(echo $LOCK | jq -r '.version')}
|
FFMPEG_VERSION=${FFMPEG_VERSION:=$(echo $LOCK | jq -r '.version')}
|
||||||
FFMPEG_SHA256=${FFMPEG_SHA256:=$(echo $LOCK | jq -r '.sha256[$ENV.TARGETARCH]')}
|
FFMPEG_SHA256=${FFMPEG_SHA256:=$(echo $LOCK | jq -r '.sha256[$ENV.PLATFORMARCH]')}
|
||||||
|
|
||||||
echo "$FFMPEG_SHA256 jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb" > ffmpeg.sha256
|
echo "$FFMPEG_SHA256 jellyfin-ffmpeg6_${FFMPEG_VERSION}-${FFMPEG_PLATFORM}_${TARGETARCH}.deb" > ffmpeg.sha256
|
||||||
|
cat ffmpeg.sha256
|
||||||
wget -nv https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_VERSION}/jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb
|
wget -nv https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_VERSION}/jellyfin-ffmpeg6_${FFMPEG_VERSION}-${FFMPEG_PLATFORM}_${TARGETARCH}.deb
|
||||||
sha256sum -c ffmpeg.sha256
|
sha256sum -c ffmpeg.sha256
|
||||||
apt-get -yqq -f install ./jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb
|
apt-get -y -f install ./jellyfin-ffmpeg6_${FFMPEG_VERSION}-${FFMPEG_PLATFORM}_${TARGETARCH}.deb
|
||||||
rm jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb
|
rm jellyfin-ffmpeg6_${FFMPEG_VERSION}-${FFMPEG_PLATFORM}_${TARGETARCH}.deb
|
||||||
rm ffmpeg.sha256
|
rm ffmpeg.sha256
|
||||||
ldconfig /usr/lib/jellyfin-ffmpeg/lib
|
ldconfig /usr/lib/jellyfin-ffmpeg/lib
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,10 @@
|
||||||
"name": "ffmpeg",
|
"name": "ffmpeg",
|
||||||
"version": "6.0-4",
|
"version": "6.0-4",
|
||||||
"sha256": {
|
"sha256": {
|
||||||
"amd64": "18d98b292b891cde86c2a08e5e989c3430e51a136cdc232bc4162fef3b4f0f44",
|
"bookworm-amd64": "18d98b292b891cde86c2a08e5e989c3430e51a136cdc232bc4162fef3b4f0f44",
|
||||||
"arm64": "67eb1e5a38ac695dd253d9ac290ad0e9fb709e8260449a7445e8460b7db3c516",
|
"bookworm-arm64": "67eb1e5a38ac695dd253d9ac290ad0e9fb709e8260449a7445e8460b7db3c516",
|
||||||
"armhf": "a29605ab0eced3511c8a6623504fab5b8bb174a486d87f94bf5522ed9a5970e6"
|
"bookworm-armhf": "a29605ab0eced3511c8a6623504fab5b8bb174a486d87f94bf5522ed9a5970e6",
|
||||||
|
"jammy-amd64": "f5c04cce7a7e6733049debbff5af7373fe8d71fb37725edb91c49a36025fea8c"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
159
server/package-lock.json
generated
159
server/package-lock.json
generated
|
@ -60,6 +60,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.1.16",
|
"@nestjs/cli": "^10.1.16",
|
||||||
|
"@nestjs/microservices": "^10.2.5",
|
||||||
"@nestjs/schematics": "^10.0.2",
|
"@nestjs/schematics": "^10.0.2",
|
||||||
"@nestjs/testing": "^10.2.2",
|
"@nestjs/testing": "^10.2.2",
|
||||||
"@openapitools/openapi-generator-cli": "2.7.0",
|
"@openapitools/openapi-generator-cli": "2.7.0",
|
||||||
|
@ -2117,6 +2118,64 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/microservices": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-oeBR2Tpg9zT0VL84nyH+bjsXlpDlKreWnYPwASgW1Oe/LqaeGhBKpbmYcV9qrD+QKGREHbz1zAffktxpNcnJ9w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"tslib": "2.6.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/nest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@grpc/grpc-js": "*",
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/websockets": "^10.0.0",
|
||||||
|
"amqp-connection-manager": "*",
|
||||||
|
"amqplib": "*",
|
||||||
|
"cache-manager": "*",
|
||||||
|
"ioredis": "*",
|
||||||
|
"kafkajs": "*",
|
||||||
|
"mqtt": "*",
|
||||||
|
"nats": "*",
|
||||||
|
"reflect-metadata": "^0.1.12",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@grpc/grpc-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@nestjs/websockets": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"amqp-connection-manager": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"amqplib": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"cache-manager": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"ioredis": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kafkajs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nats": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/platform-express": {
|
"node_modules/@nestjs/platform-express": {
|
||||||
"version": "10.2.2",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.2.tgz",
|
||||||
|
@ -2520,6 +2579,74 @@
|
||||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
|
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/microservices": {
|
||||||
|
"version": "9.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.3.tgz",
|
||||||
|
"integrity": "sha512-piMw8d3C4ppc5St5AhQEtecMhyeBK2Q1VYk4AL3NKtG6U0fzz/6KLiETpWdKXmazeI/m7qac2upOvwmRzle0aA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"tslib": "2.5.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/nest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@grpc/grpc-js": "*",
|
||||||
|
"@nestjs/common": "^9.0.0",
|
||||||
|
"@nestjs/core": "^9.0.0",
|
||||||
|
"@nestjs/websockets": "^9.0.0",
|
||||||
|
"amqp-connection-manager": "*",
|
||||||
|
"amqplib": "*",
|
||||||
|
"cache-manager": "*",
|
||||||
|
"ioredis": "*",
|
||||||
|
"kafkajs": "*",
|
||||||
|
"mqtt": "*",
|
||||||
|
"nats": "*",
|
||||||
|
"reflect-metadata": "^0.1.12",
|
||||||
|
"rxjs": "^7.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@grpc/grpc-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@nestjs/websockets": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"amqp-connection-manager": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"amqplib": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"cache-manager": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"ioredis": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kafkajs": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nats": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/microservices/node_modules/tslib": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/platform-express": {
|
"node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/platform-express": {
|
||||||
"version": "9.4.3",
|
"version": "9.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.3.tgz",
|
||||||
|
@ -15674,6 +15801,16 @@
|
||||||
"integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==",
|
"integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"@nestjs/microservices": {
|
||||||
|
"version": "10.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.2.5.tgz",
|
||||||
|
"integrity": "sha512-oeBR2Tpg9zT0VL84nyH+bjsXlpDlKreWnYPwASgW1Oe/LqaeGhBKpbmYcV9qrD+QKGREHbz1zAffktxpNcnJ9w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"requires": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"tslib": "2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nestjs/platform-express": {
|
"@nestjs/platform-express": {
|
||||||
"version": "10.2.2",
|
"version": "10.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.2.tgz",
|
||||||
|
@ -15903,6 +16040,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@nestjs/microservices": {
|
||||||
|
"version": "9.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.3.tgz",
|
||||||
|
"integrity": "sha512-piMw8d3C4ppc5St5AhQEtecMhyeBK2Q1VYk4AL3NKtG6U0fzz/6KLiETpWdKXmazeI/m7qac2upOvwmRzle0aA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"requires": {
|
||||||
|
"iterare": "1.2.1",
|
||||||
|
"tslib": "2.5.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nestjs/platform-express": {
|
"@nestjs/platform-express": {
|
||||||
"version": "9.4.3",
|
"version": "9.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.3.tgz",
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config test/e2e/jest-e2e.json --runInBand",
|
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand",
|
||||||
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
|
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
|
||||||
"typeorm:migrations:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:create",
|
"typeorm:migrations:create": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:create",
|
||||||
"typeorm:migrations:generate": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d ./src/infra/database.config.ts",
|
"typeorm:migrations:generate": "node --require ts-node/register ./node_modules/typeorm/cli.js migration:generate -d ./src/infra/database.config.ts",
|
||||||
|
@ -61,8 +61,8 @@
|
||||||
"exiftool-vendored": "^23.0.0",
|
"exiftool-vendored": "^23.0.0",
|
||||||
"exiftool-vendored.pl": "^12.62.0",
|
"exiftool-vendored.pl": "^12.62.0",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"glob": "^10.3.3",
|
|
||||||
"geo-tz": "^7.0.7",
|
"geo-tz": "^7.0.7",
|
||||||
|
"glob": "^10.3.3",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"i18n-iso-countries": "^7.6.0",
|
"i18n-iso-countries": "^7.6.0",
|
||||||
"immich": "^0.41.0",
|
"immich": "^0.41.0",
|
||||||
|
@ -86,6 +86,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.1.16",
|
"@nestjs/cli": "^10.1.16",
|
||||||
|
"@nestjs/microservices": "^10.2.5",
|
||||||
"@nestjs/schematics": "^10.0.2",
|
"@nestjs/schematics": "^10.0.2",
|
||||||
"@nestjs/testing": "^10.2.2",
|
"@nestjs/testing": "^10.2.2",
|
||||||
"@openapitools/openapi-generator-cli": "2.7.0",
|
"@openapitools/openapi-generator-cli": "2.7.0",
|
||||||
|
|
|
@ -111,4 +111,5 @@ export interface IJobRepository {
|
||||||
empty(name: QueueName): Promise<void>;
|
empty(name: QueueName): Promise<void>;
|
||||||
getQueueStatus(name: QueueName): Promise<QueueStatus>;
|
getQueueStatus(name: QueueName): Promise<QueueStatus>;
|
||||||
getJobCounts(name: QueueName): Promise<JobCounts>;
|
getJobCounts(name: QueueName): Promise<JobCounts>;
|
||||||
|
obliterate(name: QueueName, force: boolean): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,12 @@ export class JobService {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async obliterateAll(force = false): Promise<void> {
|
||||||
|
for (const queueName of Object.values(QueueName)) {
|
||||||
|
await this.jobRepository.obliterate(queueName, force);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async start(name: QueueName, { force }: JobCommandDto): Promise<void> {
|
private async start(name: QueueName, { force }: JobCommandDto): Promise<void> {
|
||||||
const { isActive } = await this.jobRepository.getQueueStatus(name);
|
const { isActive } = await this.jobRepository.getQueueStatus(name);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
|
|
@ -49,6 +49,10 @@ export class JobRepository implements IJobRepository {
|
||||||
return this.getQueue(name).drain();
|
return this.getQueue(name).drain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
obliterate(name: QueueName, force = false) {
|
||||||
|
return this.getQueue(name).obliterate({ force });
|
||||||
|
}
|
||||||
|
|
||||||
getJobCounts(name: QueueName): Promise<JobCounts> {
|
getJobCounts(name: QueueName): Promise<JobCounts> {
|
||||||
return this.getQueue(name).getJobCounts(
|
return this.getQueue(name).getJobCounts(
|
||||||
'active',
|
'active',
|
||||||
|
|
|
@ -42,7 +42,12 @@ export class AppService {
|
||||||
private libraryService: LibraryService,
|
private libraryService: LibraryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init() {
|
async init(clearBeforeStart = false) {
|
||||||
|
if (clearBeforeStart) {
|
||||||
|
// Clear all jobs on application startup, mainly used for e2e testing
|
||||||
|
await this.jobService.obliterateAll(true);
|
||||||
|
}
|
||||||
|
|
||||||
await this.jobService.registerHandlers({
|
await this.jobService.registerHandlers({
|
||||||
[JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data),
|
[JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data),
|
||||||
[JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
|
[JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
|
||||||
|
|
|
@ -14,6 +14,11 @@ export const assetApi = {
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
return body as AssetResponseDto;
|
return body as AssetResponseDto;
|
||||||
},
|
},
|
||||||
|
getAllAssets: async (server: any, accessToken: string) => {
|
||||||
|
const { body, status } = await request(server).get(`/asset/`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as AssetResponseDto[];
|
||||||
|
},
|
||||||
upload: async (server: any, accessToken: string, id: string, dto: UploadDto = {}) => {
|
upload: async (server: any, accessToken: string, id: string, dto: UploadDto = {}) => {
|
||||||
const { content, isFavorite = false, isArchived = false } = dto;
|
const { content, isFavorite = false, isArchived = false } = dto;
|
||||||
const { body, status } = await request(server)
|
const { body, status } = await request(server)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { albumApi } from './album-api';
|
import { albumApi } from './album-api';
|
||||||
import { assetApi } from './asset-api';
|
import { assetApi } from './asset-api';
|
||||||
import { authApi } from './auth-api';
|
import { authApi } from './auth-api';
|
||||||
|
import { jobApi } from './job-api';
|
||||||
import { libraryApi } from './library-api';
|
import { libraryApi } from './library-api';
|
||||||
import { sharedLinkApi } from './shared-link-api';
|
import { sharedLinkApi } from './shared-link-api';
|
||||||
import { userApi } from './user-api';
|
import { userApi } from './user-api';
|
||||||
|
@ -12,4 +13,5 @@ export const api = {
|
||||||
sharedLinkApi,
|
sharedLinkApi,
|
||||||
albumApi,
|
albumApi,
|
||||||
userApi,
|
userApi,
|
||||||
|
jobApi,
|
||||||
};
|
};
|
||||||
|
|
10
server/test/api/job-api.ts
Normal file
10
server/test/api/job-api.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { AllJobStatusResponseDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const jobApi = {
|
||||||
|
getAllJobsStatus: async (server: any, accessToken: string) => {
|
||||||
|
const { body, status } = await request(server).get(`/jobs/`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as AllJobStatusResponseDto;
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { LibraryResponseDto } from '@app/domain';
|
import { CreateLibraryDto, LibraryResponseDto, LibraryStatsResponseDto, ScanLibraryDto } from '@app/domain';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
export const libraryApi = {
|
export const libraryApi = {
|
||||||
|
@ -7,4 +7,38 @@ export const libraryApi = {
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
return body as LibraryResponseDto[];
|
return body as LibraryResponseDto[];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createLibrary: async (server: any, accessToken: string, dto: CreateLibraryDto) => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.post(`/library/`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
return body as LibraryResponseDto;
|
||||||
|
},
|
||||||
|
|
||||||
|
setImportPaths: async (server: any, accessToken: string, id: string, importPaths: string[]) => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.put(`/library/${id}`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send({ importPaths });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as LibraryResponseDto;
|
||||||
|
},
|
||||||
|
|
||||||
|
scanLibrary: async (server: any, accessToken: string, id: string, dto: ScanLibraryDto) => {
|
||||||
|
const { status } = await request(server)
|
||||||
|
.post(`/library/${id}/scan`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
},
|
||||||
|
|
||||||
|
getLibraryStatistics: async (server: any, accessToken: string, id: string): Promise<LibraryStatsResponseDto> => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.get(`/library/${id}/statistics`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
1
server/test/assets
Submodule
1
server/test/assets
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 20e0423a08f7dfc590db2702a9e5232d6b6e4734
|
|
@ -3,6 +3,7 @@ import { dataSource } from '@app/infra';
|
||||||
export const db = {
|
export const db = {
|
||||||
reset: async () => {
|
reset: async () => {
|
||||||
if (!dataSource.isInitialized) {
|
if (!dataSource.isInitialized) {
|
||||||
|
console.log('Initializing test database...');
|
||||||
await dataSource.initialize();
|
await dataSource.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
128
server/test/e2e/formats.e2e-spec.ts
Normal file
128
server/test/e2e/formats.e2e-spec.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import { JobService, LibraryResponseDto, LoginResponseDto } from '@app/domain';
|
||||||
|
import { AppModule } from '@app/immich/app.module';
|
||||||
|
import { RedisIoAdapter } from '@app/infra';
|
||||||
|
import { AssetType, LibraryType } from '@app/infra/entities';
|
||||||
|
import { INestApplication, Logger } from '@nestjs/common';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { api } from '@test/api';
|
||||||
|
import { db } from '@test/db';
|
||||||
|
import { waitForQueues } from '@test/test-utils';
|
||||||
|
import { AppService as MicroAppService } from 'src/microservices/app.service';
|
||||||
|
|
||||||
|
import { MetadataExtractionProcessor } from 'src/microservices/processors/metadata-extraction.processor';
|
||||||
|
|
||||||
|
describe('File format (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let jobService: JobService;
|
||||||
|
let server: any;
|
||||||
|
let moduleFixture: TestingModule;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// We expect https://github.com/etnoy/immich-test-assets to be cloned into the e2e/assets folder
|
||||||
|
|
||||||
|
await db.reset();
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
moduleFixture = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
providers: [MetadataExtractionProcessor, MicroAppService],
|
||||||
|
})
|
||||||
|
// .setLogger(new Logger())
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
|
||||||
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
server = app.getHttpServer();
|
||||||
|
|
||||||
|
jobService = moduleFixture.get(JobService);
|
||||||
|
|
||||||
|
await moduleFixture.get(MicroAppService).init(true);
|
||||||
|
await jobService.obliterateAll(true);
|
||||||
|
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
await api.userApi.update(server, admin.accessToken, { id: admin.userId, externalPath: '/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await jobService.obliterateAll(true);
|
||||||
|
await app.close();
|
||||||
|
await moduleFixture.close();
|
||||||
|
await db.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File format', () => {
|
||||||
|
let library: LibraryResponseDto;
|
||||||
|
beforeEach(async () => {
|
||||||
|
library = await api.libraryApi.createLibrary(server, admin.accessToken, {
|
||||||
|
type: LibraryType.EXTERNAL,
|
||||||
|
name: 'Library',
|
||||||
|
importPaths: [`${__dirname}/../assets/formats/jpg`],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import a jpg file', async () => {
|
||||||
|
library = await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||||
|
`${__dirname}/../assets/formats/jpg`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
console.log(assets);
|
||||||
|
const jpgAsset = assets[0];
|
||||||
|
expect(jpgAsset.type).toBe(AssetType.IMAGE);
|
||||||
|
expect(jpgAsset.originalFileName).toBe('el_torcal_rocks');
|
||||||
|
expect(jpgAsset.exifInfo?.exifImageHeight).toBe(341);
|
||||||
|
expect(jpgAsset.exifInfo?.exifImageWidth).toBe(512);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import a jpeg file', async () => {
|
||||||
|
library = await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||||
|
`${__dirname}/../assets/formats/jpeg`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
console.log(assets);
|
||||||
|
const jpegAsset = assets[0];
|
||||||
|
expect(jpegAsset.type).toBe(AssetType.IMAGE);
|
||||||
|
expect(jpegAsset.originalFileName).toBe('el_torcal_rocks');
|
||||||
|
expect(jpegAsset.exifInfo?.exifImageHeight).toBe(341);
|
||||||
|
expect(jpegAsset.exifInfo?.exifImageWidth).toBe(512);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import a heic file', async () => {
|
||||||
|
library = await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||||
|
`${__dirname}/../assets/formats/heic`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
const heicAsset = assets[0];
|
||||||
|
console.log(heicAsset);
|
||||||
|
expect(heicAsset.type).toBe(AssetType.IMAGE);
|
||||||
|
expect(heicAsset.originalFileName).toBe('IMG_2682');
|
||||||
|
expect(heicAsset.duration).toBe(null);
|
||||||
|
expect(heicAsset.fileCreatedAt).toBe('2029-03-21T11:04:00.000Z');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
101
server/test/e2e/formats2.e2e-spec.ts
Normal file
101
server/test/e2e/formats2.e2e-spec.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { JobService, LibraryResponseDto, LoginResponseDto } from '@app/domain';
|
||||||
|
import { AppModule } from '@app/immich/app.module';
|
||||||
|
import { RedisIoAdapter } from '@app/infra';
|
||||||
|
import { AssetType, LibraryType } from '@app/infra/entities';
|
||||||
|
import { INestApplication, Logger } from '@nestjs/common';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { api } from '@test/api';
|
||||||
|
import { waitForQueues } from '@test/test-utils';
|
||||||
|
import { AppService as MicroAppService } from 'src/microservices/app.service';
|
||||||
|
|
||||||
|
import { MetadataExtractionProcessor } from 'src/microservices/processors/metadata-extraction.processor';
|
||||||
|
import { DockerComposeEnvironment } from 'testcontainers';
|
||||||
|
|
||||||
|
describe('File format (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let jobService: JobService;
|
||||||
|
let server: any;
|
||||||
|
let moduleFixture: TestingModule;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// We expect https://github.com/etnoy/immich-test-assets to be cloned into the e2e/assets folder
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
await jobService.obliterateAll(true);
|
||||||
|
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
await api.userApi.update(server, admin.accessToken, { id: admin.userId, externalPath: '/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File format', () => {
|
||||||
|
let library: LibraryResponseDto;
|
||||||
|
beforeEach(async () => {
|
||||||
|
library = await api.libraryApi.createLibrary(server, admin.accessToken, {
|
||||||
|
type: LibraryType.EXTERNAL,
|
||||||
|
name: 'Library',
|
||||||
|
importPaths: [`${__dirname}/../assets/formats/jpg`],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import a jpg file', async () => {
|
||||||
|
library = await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||||
|
`${__dirname}/../assets/formats/jpg`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
console.log(assets);
|
||||||
|
const jpgAsset = assets[0];
|
||||||
|
expect(jpgAsset.type).toBe(AssetType.IMAGE);
|
||||||
|
expect(jpgAsset.originalFileName).toBe('el_torcal_rocks');
|
||||||
|
expect(jpgAsset.exifInfo?.exifImageHeight).toBe(341);
|
||||||
|
expect(jpgAsset.exifInfo?.exifImageWidth).toBe(512);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import a jpeg file', async () => {
|
||||||
|
library = await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||||
|
`${__dirname}/../assets/formats/jpeg`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
console.log(assets);
|
||||||
|
const jpegAsset = assets[0];
|
||||||
|
expect(jpegAsset.type).toBe(AssetType.IMAGE);
|
||||||
|
expect(jpegAsset.originalFileName).toBe('el_torcal_rocks');
|
||||||
|
expect(jpegAsset.exifInfo?.exifImageHeight).toBe(341);
|
||||||
|
expect(jpegAsset.exifInfo?.exifImageWidth).toBe(512);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should import a heic file', async () => {
|
||||||
|
library = await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||||
|
`${__dirname}/../assets/formats/heic`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
const heicAsset = assets[0];
|
||||||
|
console.log(heicAsset);
|
||||||
|
expect(heicAsset.type).toBe(AssetType.IMAGE);
|
||||||
|
expect(heicAsset.originalFileName).toBe('IMG_2682');
|
||||||
|
expect(heicAsset.duration).toBe(null);
|
||||||
|
expect(heicAsset.fileCreatedAt).toBe('2029-03-21T11:04:00.000Z');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
94
server/test/e2e/library2.e2e-spec.ts
Normal file
94
server/test/e2e/library2.e2e-spec.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { JobService, LoginResponseDto } from '@app/domain';
|
||||||
|
import { AppModule } from '@app/immich/app.module';
|
||||||
|
import { RedisIoAdapter } from '@app/infra';
|
||||||
|
import { LibraryType } from '@app/infra/entities';
|
||||||
|
import { INestApplication, Logger } from '@nestjs/common';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { api } from '@test/api';
|
||||||
|
import { db } from '@test/db';
|
||||||
|
import { waitForQueues } from '@test/test-utils';
|
||||||
|
import { AppService as MicroAppService } from 'src/microservices/app.service';
|
||||||
|
|
||||||
|
import { MetadataExtractionProcessor } from 'src/microservices/processors/metadata-extraction.processor';
|
||||||
|
|
||||||
|
describe('Library queue e2e', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
let jobService: JobService;
|
||||||
|
let server: any;
|
||||||
|
let moduleFixture: TestingModule;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await db.reset();
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
moduleFixture = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
providers: [MetadataExtractionProcessor, MicroAppService],
|
||||||
|
})
|
||||||
|
.setLogger(new Logger())
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
|
||||||
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
|
await app.init();
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
server = app.getHttpServer();
|
||||||
|
|
||||||
|
jobService = moduleFixture.get(JobService);
|
||||||
|
|
||||||
|
await moduleFixture.get(MicroAppService).init(true);
|
||||||
|
|
||||||
|
// We expect https://github.com/etnoy/immich-test-assets to be cloned into the e2e/assets folder
|
||||||
|
|
||||||
|
await jobService.obliterateAll(true);
|
||||||
|
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
await api.userApi.update(server, admin.accessToken, { id: admin.userId, externalPath: '/' });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await jobService.obliterateAll(true);
|
||||||
|
await app.close();
|
||||||
|
await moduleFixture.close();
|
||||||
|
await db.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should scan the whole folder', async () => {
|
||||||
|
const library = await api.libraryApi.createLibrary(server, admin.accessToken, {
|
||||||
|
type: LibraryType.EXTERNAL,
|
||||||
|
name: 'Library',
|
||||||
|
importPaths: [`${__dirname}/../assets/nature`],
|
||||||
|
exclusionPatterns: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
expect(assets).toHaveLength(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scan with exclusions', async () => {
|
||||||
|
const library = await api.libraryApi.createLibrary(server, admin.accessToken, {
|
||||||
|
type: LibraryType.EXTERNAL,
|
||||||
|
name: 'Library',
|
||||||
|
importPaths: [`${__dirname}/../assets/nature/`],
|
||||||
|
exclusionPatterns: ['**/*o*/**', '**/*c*/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, {});
|
||||||
|
|
||||||
|
await waitForQueues(jobService);
|
||||||
|
|
||||||
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||||
|
|
||||||
|
expect(assets).toHaveLength(1);
|
||||||
|
expect(assets[0].originalFileName).toBe('silver_fir');
|
||||||
|
});
|
||||||
|
});
|
|
@ -81,16 +81,16 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
|
||||||
const { status, body } = await request(server).get('/server-info/features');
|
const { status, body } = await request(server).get('/server-info/features');
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
clipEncode: true,
|
clipEncode: false,
|
||||||
configFile: false,
|
configFile: false,
|
||||||
facialRecognition: true,
|
facialRecognition: false,
|
||||||
map: true,
|
map: true,
|
||||||
oauth: false,
|
oauth: false,
|
||||||
oauthAutoLaunch: false,
|
oauthAutoLaunch: false,
|
||||||
passwordLogin: true,
|
passwordLogin: true,
|
||||||
search: false,
|
search: false,
|
||||||
sidecar: true,
|
sidecar: true,
|
||||||
tagImage: true,
|
tagImage: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,28 @@
|
||||||
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
||||||
import { GenericContainer } from 'testcontainers';
|
import { DockerComposeEnvironment, GenericContainer, Wait } from 'testcontainers';
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
const composeFilePath = '../docker/';
|
||||||
|
const composeFile = 'docker-compose.e2e.yml';
|
||||||
|
|
||||||
|
process.env.IMMICH_SERVER_URL = 'asdf';
|
||||||
|
process.env.IMMICH_API_URL_EXTERNAL = 'asdf';
|
||||||
|
process.env.TYPESENSE_ENABLED = 'false';
|
||||||
|
|
||||||
|
process.env.IMMICH_MACHINE_LEARNING_ENABLED = 'false';
|
||||||
|
|
||||||
|
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile)
|
||||||
|
.withBuild()
|
||||||
|
// .withWaitStrategy('immich_server-e2e', Wait.forLogMessage('Immich Server is listening on'))
|
||||||
|
// .withWaitStrategy('immich_microservices-e2e ', Wait.forLogMessage('Immich Microservices is listening on'))
|
||||||
|
//.withBuild()
|
||||||
|
.up();
|
||||||
|
console.log(environment);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function waitForQueues() {
|
||||||
process.env.NODE_ENV = 'development';
|
process.env.NODE_ENV = 'development';
|
||||||
process.env.TYPESENSE_ENABLED = 'false';
|
process.env.TYPESENSE_ENABLED = 'false';
|
||||||
|
process.env.IMMICH_MACHINE_LEARNING_ENABLED = 'false';
|
||||||
|
|
||||||
const pg = await new PostgreSqlContainer('postgres')
|
const pg = await new PostgreSqlContainer('postgres')
|
||||||
.withExposedPorts(5432)
|
.withExposedPorts(5432)
|
||||||
|
@ -18,4 +38,4 @@ export default async () => {
|
||||||
|
|
||||||
process.env.REDIS_PORT = String(redis.getMappedPort(6379));
|
process.env.REDIS_PORT = String(redis.getMappedPort(6379));
|
||||||
process.env.REDIS_HOSTNAME = redis.getHost();
|
process.env.REDIS_HOSTNAME = redis.getHost();
|
||||||
};
|
}
|
||||||
|
|
|
@ -10,5 +10,6 @@ export const newJobRepositoryMock = (): jest.Mocked<IJobRepository> => {
|
||||||
queue: jest.fn().mockImplementation(() => Promise.resolve()),
|
queue: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
getQueueStatus: jest.fn(),
|
getQueueStatus: jest.fn(),
|
||||||
getJobCounts: jest.fn(),
|
getJobCounts: jest.fn(),
|
||||||
|
obliterate: jest.fn(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
AuthDeviceResponseDto,
|
AuthDeviceResponseDto,
|
||||||
AuthUserDto,
|
AuthUserDto,
|
||||||
CreateUserDto,
|
CreateUserDto,
|
||||||
|
JobService,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginCredentialDto,
|
LoginCredentialDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
|
@ -49,6 +50,34 @@ export function getAuthUser(): AuthUserDto {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForQueues(jobService: JobService) {
|
||||||
|
let isFinished = false;
|
||||||
|
// TODO: this shouldn't be a while loop
|
||||||
|
while (!isFinished) {
|
||||||
|
const jobStatus = await jobService.getAllJobsStatus();
|
||||||
|
|
||||||
|
let jobsActive = false;
|
||||||
|
Object.values(jobStatus).forEach((job) => {
|
||||||
|
if (job.queueStatus.isActive) {
|
||||||
|
jobsActive = true;
|
||||||
|
}
|
||||||
|
if (job.queueStatus.active > 0 || job.queueStatus.waiting > 0) {
|
||||||
|
jobsActive = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!jobsActive) {
|
||||||
|
isFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
adminSignUp: async (server: any) => {
|
adminSignUp: async (server: any) => {
|
||||||
const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub);
|
const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub);
|
||||||
|
|
Loading…
Reference in a new issue