can do e2e tests in the cli
This commit is contained in:
parent
11adf2e906
commit
4c3a1ca168
13 changed files with 214 additions and 32 deletions
3
cli/.gitignore
vendored
3
cli/.gitignore
vendored
|
@ -11,4 +11,5 @@ oclif.manifest.json
|
|||
.vscode
|
||||
.idea
|
||||
/coverage/
|
||||
.reverse-geocoding-dump/
|
||||
.reverse-geocoding-dump/
|
||||
upload/
|
126
cli/package-lock.json
generated
126
cli/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
6
cli/test/e2e/setup.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import setup from '../../../server/test/e2e/setup';
|
||||
|
||||
export default async () => {
|
||||
// Call server e2e setup
|
||||
await setup();
|
||||
};
|
22
server/test/api/api-key-api.ts
Normal file
22
server/test/api/api-key-api.ts
Normal 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;
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
4
server/test/fixtures/api-key.stub.ts
vendored
4
server/test/fixtures/api-key.stub.ts
vendored
|
@ -11,3 +11,7 @@ export const keyStub = {
|
|||
user: userStub.admin,
|
||||
} as APIKeyEntity),
|
||||
};
|
||||
|
||||
export const apiKeyCreateStub = {
|
||||
name: 'API Key',
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue