can do e2e tests in the cli

This commit is contained in:
Jonathan Jogenfors 2023-11-17 11:16:48 +01:00
parent 11adf2e906
commit 4c3a1ca168
13 changed files with 214 additions and 32 deletions

3
cli/.gitignore vendored
View file

@ -11,4 +11,5 @@ oclif.manifest.json
.vscode
.idea
/coverage/
.reverse-geocoding-dump/
.reverse-geocoding-dump/
upload/

126
cli/package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

6
cli/test/e2e/setup.ts Normal file
View file

@ -0,0 +1,6 @@
import setup from '../../../server/test/e2e/setup';
export default async () => {
// Call server e2e setup
await setup();
};

View file

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

View file

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

View file

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

View file

@ -11,3 +11,7 @@ export const keyStub = {
user: userStub.admin,
} as APIKeyEntity),
};
export const apiKeyCreateStub = {
name: 'API Key',
};

View file

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