Compare commits

...

34 commits

Author SHA1 Message Date
Jonathan Jogenfors
c96e3becc3 fix 2023-09-25 11:25:13 +02:00
Jonathan Jogenfors
067c0e5d41 fix 2023-09-23 23:10:02 +02:00
Jonathan Jogenfors
0b4835ef3c Merge branch 'main' of https://github.com/immich-app/immich into feat/e2e-queues 2023-09-23 22:47:46 +02:00
Jonathan Jogenfors
702520af4e only reset db if needed 2023-09-23 22:47:38 +02:00
Jonathan Jogenfors
bb947b07ce fix 2023-09-23 01:05:22 +02:00
Jonathan Jogenfors
41b20c1079 fix 2023-09-23 01:03:23 +02:00
Jonathan Jogenfors
fe2c5b590b fix 2023-09-23 00:59:28 +02:00
Jonathan Jogenfors
ef5c121417 fix 2023-09-23 00:48:45 +02:00
Jonathan Jogenfors
a0048118e1 add autoupdate 2023-09-22 15:57:34 +02:00
Jonathan Jogenfors
a923123f5f fix sha256 2023-09-22 15:05:20 +02:00
Jonathan Jogenfors
1b2fa9255e fix 2023-09-22 15:00:47 +02:00
Jonathan Jogenfors
0c2a104e07 fix 2023-09-22 14:57:30 +02:00
Jonathan Jogenfors
5055be1b5b add jammy checksum 2023-09-22 14:54:12 +02:00
Jonathan Jogenfors
582816d03a ignore checksum 2023-09-22 14:48:34 +02:00
Jonathan Jogenfors
ab154ca2ca set right env var 2023-09-22 14:45:30 +02:00
Jonathan Jogenfors
81870a9d50 sudo in the right place 2023-09-22 14:43:09 +02:00
Jonathan Jogenfors
f20e90e544 set env var 2023-09-22 14:35:56 +02:00
Jonathan Jogenfors
8f26db9a6b ffmpeg for jammy 2023-09-22 14:27:50 +02:00
Jonathan Jogenfors
335ee427da verbose ffmpeg 2023-09-22 14:07:43 +02:00
Jonathan Jogenfors
04511679fe sudo 2023-09-22 14:01:36 +02:00
Jonathan Jogenfors
9d8dda1755 install ffmpeg 2023-09-22 13:56:46 +02:00
Jonathan Jogenfors
f091c0e17d add ubuntu pkgs 2023-09-22 13:40:06 +02:00
Jonathan Jogenfors
ae08463a49 run apt as sudo 2023-09-22 13:31:22 +02:00
Jonathan Jogenfors
a094805c27 install docker dependencies 2023-09-22 13:28:10 +02:00
Jonathan Jogenfors
6429234bd5 clear job queue on app startup 2023-09-22 09:51:52 +02:00
Jonathan Jogenfors
8cd61d3291 close workers 2023-09-22 09:04:12 +02:00
Jonathan Jogenfors
6a2eca9b0a add more logging 2023-09-22 08:40:47 +02:00
Jonathan Jogenfors
3835221cda add e2e debugs 2023-09-22 08:29:32 +02:00
Jonathan Jogenfors
56b6ecb8a4 Set env 2023-09-22 00:12:25 +02:00
Jonathan Jogenfors
b059c95cad cleanup 2023-09-21 23:30:33 +02:00
Jonathan Jogenfors
8ad2be195d fix tests 2023-09-21 23:11:45 +02:00
Jonathan Jogenfors
e8a20fd492 fix: use correct node version for ci 2023-09-21 14:36:40 +02:00
Jonathan Jogenfors
e9c12650bb fix: get nightly after ci 2023-09-21 14:33:47 +02:00
Jonathan Jogenfors
fec116250c feat: play around with e2e tests 2023-09-21 14:24:56 +02:00
23 changed files with 800 additions and 22 deletions

View file

@ -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

View 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:

View file

@ -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

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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>;
} }

View file

@ -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) {

View file

@ -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',

View file

@ -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(),

View file

@ -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)

View file

@ -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,
}; };

View 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;
},
};

View file

@ -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

@ -0,0 +1 @@
Subproject commit 20e0423a08f7dfc590db2702a9e5232d6b6e4734

View file

@ -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();
} }

View 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');
});
});
});

View 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');
});
});
});

View 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');
});
});

View file

@ -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,
}); });
}); });
}); });

View file

@ -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();
}; }

View file

@ -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(),
}; };
}; };

View file

@ -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);