From 4c3a1ca16809a61d53c1e51bc81887f98fe314c9 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Fri, 17 Nov 2023 11:16:48 +0100 Subject: [PATCH] can do e2e tests in the cli --- cli/.gitignore | 3 +- cli/package-lock.json | 126 +++++++++++++++++++++++++++ cli/package.json | 1 + cli/src/cli/base-command.ts | 9 +- cli/src/cores/errors/login-error.ts | 2 - cli/src/services/session.service.ts | 52 +++++++---- cli/test/e2e/cli.e2e-spec.ts | 8 +- cli/test/e2e/setup.ts | 6 ++ server/test/api/api-key-api.ts | 22 +++++ server/test/api/index.ts | 2 + server/test/e2e/library.e2e-spec.ts | 2 +- server/test/fixtures/api-key.stub.ts | 4 + server/test/test-utils.ts | 9 +- 13 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 cli/test/e2e/setup.ts create mode 100644 server/test/api/api-key-api.ts diff --git a/cli/.gitignore b/cli/.gitignore index 56083ed26..a26b03fe6 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -11,4 +11,5 @@ oclif.manifest.json .vscode .idea /coverage/ -.reverse-geocoding-dump/ \ No newline at end of file +.reverse-geocoding-dump/ +upload/ \ No newline at end of file diff --git a/cli/package-lock.json b/cli/package-lock.json index b5e0ce5da..f7acd16e2 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -20,6 +20,7 @@ "immich": "dist/index.js" }, "devDependencies": { + "@nestjs/common": "^10.2.8", "@testcontainers/postgresql": "^10.2.2", "@types/byte-size": "^8.1.0", "@types/chai": "^4.3.5", @@ -1468,6 +1469,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.8.tgz", + "integrity": "sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==", + "dev": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4326,6 +4365,15 @@ "node": ">=8" } }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -6116,6 +6164,13 @@ "node": ">=10" } }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true, + "peer": true + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -6288,6 +6343,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -7040,6 +7105,18 @@ "node": ">=4.2.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dev": true, + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -8415,6 +8492,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true + }, + "@nestjs/common": { + "version": "10.2.8", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.8.tgz", + "integrity": "sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==", + "dev": true, + "requires": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10614,6 +10708,12 @@ "istanbul-lib-report": "^3.0.0" } }, + "iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "dev": true + }, "jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -11970,6 +12070,13 @@ } } }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true, + "peer": true + }, "regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -12085,6 +12192,16 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "peer": true, + "requires": { + "tslib": "^2.1.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12646,6 +12763,15 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true }, + "uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dev": true, + "requires": { + "@lukeed/csprng": "^1.0.0" + } + }, "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/cli/package.json b/cli/package.json index c52e5303d..dffa8b742 100644 --- a/cli/package.json +++ b/cli/package.json @@ -15,6 +15,7 @@ "yaml": "^2.3.1" }, "devDependencies": { + "@nestjs/common": "^10.2.8", "@testcontainers/postgresql": "^10.2.2", "@types/byte-size": "^8.1.0", "@types/chai": "^4.3.5", diff --git a/cli/src/cli/base-command.ts b/cli/src/cli/base-command.ts index d47f973ac..f3229f14b 100644 --- a/cli/src/cli/base-command.ts +++ b/cli/src/cli/base-command.ts @@ -3,7 +3,6 @@ import path from 'node:path'; import { SessionService } from '../services/session.service'; import { LoginError } from '../cores/errors/login-error'; import { exit } from 'node:process'; -import os from 'os'; import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api'; export abstract class BaseCommand { @@ -12,14 +11,8 @@ export abstract class BaseCommand { protected user!: UserResponseDto; protected serverVersion!: ServerVersionResponseDto; - protected configDir; - protected authPath; - constructor() { - const userHomeDir = os.homedir(); - this.configDir = path.join(userHomeDir, '.config/immich/'); - this.sessionService = new SessionService(this.configDir); - this.authPath = path.join(this.configDir, 'auth.yml'); + this.sessionService = new SessionService(); } public async connect(): Promise { diff --git a/cli/src/cores/errors/login-error.ts b/cli/src/cores/errors/login-error.ts index da60bca0c..e00b997f1 100644 --- a/cli/src/cores/errors/login-error.ts +++ b/cli/src/cores/errors/login-error.ts @@ -2,10 +2,8 @@ export class LoginError extends Error { constructor(message: string) { super(message); - // assign the error class name in your custom error (as a shortcut) this.name = this.constructor.name; - // capturing the stack trace keeps the reference to your error class Error.captureStackTrace(this, this.constructor); } } diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts index d1c9d789c..1fd7d974c 100644 --- a/cli/src/services/session.service.ts +++ b/cli/src/services/session.service.ts @@ -3,35 +3,51 @@ import yaml from 'yaml'; import path from 'node:path'; import { ImmichApi } from '../api/client'; import { LoginError } from '../cores/errors/login-error'; +import { config } from 'node:process'; +import os from 'os'; export class SessionService { - readonly configDir: string; + readonly configDir!: string; readonly authPath!: string; private api!: ImmichApi; - constructor(configDir: string) { - this.configDir = configDir; + constructor() { + const configDir = process.env.IMMICH_CONFIG_DIR; + + if (!configDir) { + const userHomeDir = os.homedir(); + this.configDir = path.join(userHomeDir, '.config/immich/'); + } else { + this.configDir = configDir; + } + this.authPath = path.join(this.configDir, 'auth.yml'); } public async connect(): Promise { - await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => { - if (error.code === 'ENOENT') { - throw new LoginError('No auth file exist. Please login first'); + let instanceUrl = process.env.IMMICH_INSTANCE_URL; + let apiKey = process.env.IMMICH_API_KEY; + + if (!instanceUrl || !apiKey) { + await fs.promises.access(this.authPath, fs.constants.F_OK).catch((error) => { + if (error.code === 'ENOENT') { + throw new LoginError('No auth file exist. Please login first'); + } + }); + + const data: string = await fs.promises.readFile(this.authPath, 'utf8'); + const parsedConfig = yaml.parse(data); + + instanceUrl = parsedConfig.instanceUrl; + apiKey = parsedConfig.apiKey; + + if (!instanceUrl) { + throw new LoginError('Instance URL missing in auth config file ' + this.authPath); } - }); - const data: string = await fs.promises.readFile(this.authPath, 'utf8'); - const parsedConfig = yaml.parse(data); - const instanceUrl: string = parsedConfig.instanceUrl; - const apiKey: string = parsedConfig.apiKey; - - if (!instanceUrl) { - throw new LoginError('Instance URL missing in auth config file ' + this.authPath); - } - - if (!apiKey) { - throw new LoginError('API key missing in auth config file ' + this.authPath); + if (!apiKey) { + throw new LoginError('API key missing in auth config file ' + this.authPath); + } } this.api = new ImmichApi(instanceUrl, apiKey); diff --git a/cli/test/e2e/cli.e2e-spec.ts b/cli/test/e2e/cli.e2e-spec.ts index f003bb0de..61f5c7347 100644 --- a/cli/test/e2e/cli.e2e-spec.ts +++ b/cli/test/e2e/cli.e2e-spec.ts @@ -9,10 +9,14 @@ import { import { LoginResponseDto } from 'src/api/open-api'; import ServerInfo from 'src/commands/server-info'; import Upload from 'src/commands/upload'; +import { INestApplication } from '@nestjs/common'; +import { Http2SecureServer } from 'http2'; +import { APIKeyCreateResponseDto } from '@app/domain'; describe(`CLI (e2e)`, () => { let server: any; let admin: LoginResponseDto; + let apiKey: APIKeyCreateResponseDto; beforeAll(async () => { [server] = await testApp.create({ jobs: true }); @@ -28,6 +32,8 @@ describe(`CLI (e2e)`, () => { await restoreTempFolder(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); + apiKey = await api.apiKeyApi.createApiKey(server, admin.accessToken); + process.env.IMMICH_API_KEY = apiKey.secret; }); describe('server-info', () => { @@ -44,7 +50,7 @@ describe(`CLI (e2e)`, () => { await new Upload().run([`${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`], {}); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - console.log(assets); + expect(assets.length).toBeGreaterThan(4); }); }); }); diff --git a/cli/test/e2e/setup.ts b/cli/test/e2e/setup.ts new file mode 100644 index 000000000..66d47c4a8 --- /dev/null +++ b/cli/test/e2e/setup.ts @@ -0,0 +1,6 @@ +import setup from '../../../server/test/e2e/setup'; + +export default async () => { + // Call server e2e setup + await setup(); +}; diff --git a/server/test/api/api-key-api.ts b/server/test/api/api-key-api.ts new file mode 100644 index 000000000..0e89937ba --- /dev/null +++ b/server/test/api/api-key-api.ts @@ -0,0 +1,22 @@ +import { + APIKeyCreateResponseDto, + AuthDeviceResponseDto, + LoginCredentialDto, + LoginResponseDto, + UserResponseDto, +} from '@app/domain'; +import { adminSignupStub, apiKeyCreateStub, loginResponseStub, loginStub } from '@test'; +import request from 'supertest'; + +export const apiKeyApi = { + createApiKey: async (server: any, accessToken: string) => { + const { status, body } = await request(server) + .post('/api-key') + .set('Authorization', `Bearer ${accessToken}`) + .send(apiKeyCreateStub); + + expect(status).toBe(201); + + return body as APIKeyCreateResponseDto; + }, +}; diff --git a/server/test/api/index.ts b/server/test/api/index.ts index 55cf2526d..21987c500 100644 --- a/server/test/api/index.ts +++ b/server/test/api/index.ts @@ -1,5 +1,6 @@ import { activityApi } from './activity-api'; import { albumApi } from './album-api'; +import { apiKeyApi } from './api-key-api'; import { assetApi } from './asset-api'; import { authApi } from './auth-api'; import { libraryApi } from './library-api'; @@ -10,6 +11,7 @@ import { userApi } from './user-api'; export const api = { activityApi, authApi, + apiKeyApi, assetApi, libraryApi, sharedLinkApi, diff --git a/server/test/e2e/library.e2e-spec.ts b/server/test/e2e/library.e2e-spec.ts index 92c604e00..6fcaf98fc 100644 --- a/server/test/e2e/library.e2e-spec.ts +++ b/server/test/e2e/library.e2e-spec.ts @@ -13,7 +13,7 @@ describe(`${LibraryController.name} (e2e)`, () => { let admin: LoginResponseDto; beforeAll(async () => { - [server] = await testApp.create({ jobs: true }); + server = await testApp.create({ jobs: true, listen: false }); }); afterAll(async () => { diff --git a/server/test/fixtures/api-key.stub.ts b/server/test/fixtures/api-key.stub.ts index 36554ef68..b907eccf8 100644 --- a/server/test/fixtures/api-key.stub.ts +++ b/server/test/fixtures/api-key.stub.ts @@ -11,3 +11,7 @@ export const keyStub = { user: userStub.admin, } as APIKeyEntity), }; + +export const apiKeyCreateStub = { + name: 'API Key', +}; diff --git a/server/test/test-utils.ts b/server/test/test-utils.ts index 2cbd4f19a..b030b9afc 100644 --- a/server/test/test-utils.ts +++ b/server/test/test-utils.ts @@ -5,6 +5,7 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as fs from 'fs'; import path from 'path'; +import { Server } from 'tls'; import { EntityTarget, ObjectLiteral } from 'typeorm'; import { AppService } from '../src/microservices/app.service'; @@ -78,12 +79,18 @@ export const testApp = { .compile(); app = await moduleFixture.createNestApplication().init(); + app.listen(0); if (jobs) { await app.get(AppService).init(); } - return [app.getHttpServer(), app]; + const httpServer = app.getHttpServer(); + const port = httpServer.address().port; + const protocol = app instanceof Server ? 'https' : 'http'; + process.env.IMMICH_INSTANCE_URL = protocol + '://127.0.0.1:' + port; + + return [httpServer, app]; }, reset: async (options?: ResetOptions) => { await db.reset(options);