diff --git a/.dockerignore b/.dockerignore index 603449cd1bf09..87d63b5942f88 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,5 +24,7 @@ cli/.reverse-geocoding-dump/ cli/upload/ cli/dist/ +e2e/ + open-api/typescript-sdk/node_modules/ open-api/typescript-sdk/build/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99bb024ae598a..1a23bd351dafe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -205,6 +205,34 @@ jobs: run: npm run test:cov if: ${{ !cancelled() }} + web-e2e-tests: + name: Web (e2e) + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./e2e + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run setup typescript-sdk + run: npm ci && npm run build + working-directory: ./open-api/typescript-sdk + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Docker build + run: docker compose -f docker/docker-compose.e2e.yml build + working-directory: ./ + + - name: Run e2e tests + run: npx playwright test + mobile-unit-tests: name: Mobile runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index e66714a20f7ab..e28232bef8961 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,10 @@ server-e2e-jobs: server-e2e-api: npm run e2e:api --prefix server +.PHONY: e2e +e2e: + docker compose -f ./docker/docker-compose.e2e.yml up --build -V --remove-orphans + prod: docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans diff --git a/docker/docker-compose.e2e.yml b/docker/docker-compose.e2e.yml new file mode 100644 index 0000000000000..5584335966214 --- /dev/null +++ b/docker/docker-compose.e2e.yml @@ -0,0 +1,49 @@ +version: "3.8" + +name: immich-e2e + +x-server-build: &server-common + image: immich-server:latest + build: + context: ../ + dockerfile: server/Dockerfile + environment: + - DB_HOSTNAME=database + - DB_USERNAME=postgres + - DB_PASSWORD=postgres + - DB_DATABASE_NAME=immich + - REDIS_HOSTNAME=redis + volumes: + - upload:/usr/src/app/upload + depends_on: + - redis + - database + +services: + immich-server: + command: [ "./start.sh", "immich" ] + <<: *server-common + ports: + - 2283:3001 + + immich-microservices: + command: [ "./start.sh", "microservices" ] + <<: *server-common + + + redis: + image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + restart: always + + database: + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: immich + ports: + - 5432:5432 + +volumes: + model-cache: + upload: diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000000000..68c5d18f00dcb --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 0000000000000..ac7c363350946 --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,383 @@ +{ + "name": "immich-e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "immich-e2e", + "version": "1.0.0", + "license": "GNU Affero General Public License version 3", + "devDependencies": { + "@immich/sdk": "file:../open-api/typescript-sdk", + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.17", + "@types/pg": "^8.11.0", + "pg": "^8.11.3", + "typescript": "^5.3.3" + } + }, + "../open-api/typescript-sdk": { + "name": "@immich/sdk", + "version": "1.92.1", + "dev": true, + "license": "GNU Affero General Public License version 3", + "devDependencies": { + "@types/node": "^20.11.0", + "oazapfts": "^5.1.4", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "axios": "^1.6.7" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + } + } + }, + "node_modules/@immich/sdk": { + "resolved": "../open-api/typescript-sdk", + "link": true + }, + "node_modules/@playwright/test": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "dev": true, + "dependencies": { + "playwright": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/pg": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.0.tgz", + "integrity": "sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "dev": true + }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/playwright": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "dev": true, + "dependencies": { + "playwright-core": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000000000..122dde73e1c20 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,22 @@ +{ + "name": "immich-e2e", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "npx playwright test", + "build": "tsc" + }, + "keywords": [], + "author": "", + "license": "GNU Affero General Public License version 3", + "devDependencies": { + "@immich/sdk": "file:../open-api/typescript-sdk", + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.17", + "@types/pg": "^8.11.0", + "pg": "^8.11.3", + "typescript": "^5.3.3" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000000000..2ff2d92acf4d3 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,61 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './specs/', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + baseURL: 'http://127.0.0.1:2283', + trace: 'on-first-retry', + }, + + testMatch: /.*\.e2e-spec\.ts/, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: + 'docker compose -f ../docker/docker-compose.e2e.yml up --build -V --remove-orphans', + url: 'http://127.0.0.1:2283', + reuseExistingServer: true, + }, +}); diff --git a/e2e/specs/auth.e2e-spec.ts b/e2e/specs/auth.e2e-spec.ts new file mode 100644 index 0000000000000..4c55d67ac1ffc --- /dev/null +++ b/e2e/specs/auth.e2e-spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test'; +import { app } from '../test-utils'; + +test.describe('Registration', () => { + test.beforeAll(async () => { + await app.reset(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('admin registration', async ({ page }) => { + // welcome + await page.goto('/'); + await page.getByRole('button', { name: 'Getting Started' }).click(); + + // register + await expect(page).toHaveTitle(/Admin Registration/); + await page.getByLabel('Admin Email').fill('admin@immich.app'); + await page.getByLabel('Admin Password', { exact: true }).fill('password'); + await page.getByLabel('Confirm Admin Password').fill('password'); + await page.getByLabel('Name').fill('Immich Admin'); + await page.getByRole('button', { name: 'Sign up' }).click(); + + // login + await expect(page).toHaveTitle(/Login/); + await page.goto('/auth/login'); + await page.getByLabel('Email').fill('admin@immich.app'); + await page.getByLabel('Password').fill('password'); + await page.getByRole('button', { name: 'Login' }).click(); + + // onboarding + await expect(page).toHaveURL('/auth/onboarding'); + await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Storage Template' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + + // success + await expect(page).toHaveURL('/photos'); + }); + + test('user registration', async ({ context, page }) => { + await app.adminSetup(context); + + // create user + await page.goto('/admin/user-management'); + await expect(page).toHaveTitle(/User Management/); + await page.getByRole('button', { name: 'Create user' }).click(); + await page.getByLabel('Email').fill('user@immich.cloud'); + await page.getByLabel('Password', { exact: true }).fill('password'); + await page.getByLabel('Confirm Password').fill('password'); + await page.getByLabel('Name').fill('Immich User'); + await page.getByRole('button', { name: 'Create', exact: true }).click(); + + // logout + await context.clearCookies(); + + // login + await page.goto('/auth/login'); + await page.getByLabel('Email').fill('user@immich.cloud'); + await page.getByLabel('Password').fill('password'); + await page.getByRole('button', { name: 'Login' }).click(); + + // change password + 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'); + await page.getByRole('button', { name: 'Change password' }).click(); + + // login with new password + await expect(page).toHaveURL('/auth/login'); + await page.getByLabel('Email').fill('user@immich.cloud'); + await page.getByLabel('Password').fill('new-password'); + await page.getByRole('button', { name: 'Login' }).click(); + + // success + await expect(page).toHaveURL(/\/photos/); + }); +}); diff --git a/e2e/test-utils.ts b/e2e/test-utils.ts new file mode 100644 index 0000000000000..f0d13be816292 --- /dev/null +++ b/e2e/test-utils.ts @@ -0,0 +1,79 @@ +import pg from 'pg'; +import { defaults, login, setAdminOnboarding, signUpAdmin } from '@immich/sdk'; +import { BrowserContext } from '@playwright/test'; + +const client = new pg.Client( + 'postgres://postgres:postgres@localhost:5432/immich' +); +let connected = false; + +const loginCredentialDto = { + email: 'admin@immich.cloud', + password: 'password', +}; +const signUpDto = { ...loginCredentialDto, name: 'Immich Admin' }; + +const setBaseUrl = () => (defaults.baseUrl = 'http://127.0.0.1:2283/api'); +const asAuthHeader = (accessToken: string) => ({ + Authorization: `Bearer ${accessToken}`, +}); + +export const app = { + adminSetup: async (context: BrowserContext) => { + setBaseUrl(); + await signUpAdmin({ signUpDto }); + + const response = await login({ loginCredentialDto }); + + await context.addCookies([ + { + name: 'immich_access_token', + value: response.accessToken, + domain: '127.0.0.1', + path: '/', + expires: 1742402728, + httpOnly: true, + secure: false, + sameSite: 'Lax', + }, + { + name: 'immich_auth_type', + value: 'password', + domain: '127.0.0.1', + path: '/', + expires: 1742402728, + httpOnly: true, + secure: false, + sameSite: 'Lax', + }, + { + name: 'immich_is_authenticated', + value: 'true', + domain: '127.0.0.1', + path: '/', + expires: 1742402728, + httpOnly: false, + secure: false, + sameSite: 'Lax', + }, + ]); + + await setAdminOnboarding({ headers: asAuthHeader(response.accessToken) }); + + return response; + }, + reset: async () => { + if (!connected) { + await client.connect(); + } + + for (const table of ['users', 'system_metadata']) { + await client.query(`DELETE FROM ${table} CASCADE;`); + } + }, + teardown: async () => { + if (connected) { + await client.end(); + } + }, +}; diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000000000..c91b03d9dbe25 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "target": "es2022", + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "esModuleInterop": true, + "rootDirs": ["src"], + "baseUrl": "./", + "types": ["vitest/globals"] + }, + "exclude": ["dist", "node_modules"] +}