test(cli): e2e testing (#5101)

* Allow building and installing cli

* feat: add format fix

* docs: remove cli folder

* feat: use immich scoped package

* feat: rewrite cli readme

* docs: add info on running without building

* cleanup

* chore: remove import functionality from cli

* feat: add logout to cli

* docs: add todo for file format from server

* docs: add compilation step to cli

* fix: success message spacing

* feat: can create albums

* fix: add check step to cli

* fix: typos

* feat: pull file formats from server

* chore: use crawl service from server

* chore: fix lint

* docs: add cli documentation

* chore: rename ignore pattern

* chore: add version number to cli

* feat: use sdk

* fix: cleanup

* feat: album name on windows

* chore: remove skipped asset field

* feat: add more info to server-info command

* chore: cleanup

* wip

* chore: remove unneeded packages

* e2e test can start

* git ignore for geocode in cli

* add cli e2e to github actions

* can do e2e tests in the cli

* simplify e2e test

* cleanup

* set matrix strategy in workflow

* run npm ci in server

* choose different working directory

* check out submodules too

* increase test timeout

* set node version

* cli docker e2e tests

* fix cli docker file

* run cli e2e in correct folder

* set docker context

* correct docker build

* remove cli from dockerignore

* chore: fix docs links

* feat: add cli v2 milestone

* fix: set correct cli date

* remove submodule

* chore: add npmignore

* chore(cli): push to npm

* fix: server e2e

* run npm ci in server

* remove state from e2e

* run npm ci in server

* reshuffle docker compose files

* use new e2e composes in makefile

* increase test timeout to 10 minutes

* make github actions run makefile e2e tests

* cleanup github test names

* assert on server version

* chore: split cli e2e tests into one file per command

* chore: set cli release working dir

* chore: add repo url to npmjs

* chore: bump node setup to v4

* chore: normalize the github url

* check e2e code in lint

* fix lint

* test key login flow

* feat: allow configurable config dir

* fix session service tests

* create missing dir

* cleanup

* bump cli version to 2.0.4

* remove form-data

* feat: allow single files as argument

* add version option

* bump dependencies

* fix lint

* wip use axios as upload

* version bump

* cApiTALiZaTiON

* don't touch package lock

* wip: don't use job queues

* don't use make for cli e2e

* fix server e2e

* chore: remove old gha step

* add npm ci to server

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Jonathan Jogenfors
2023-12-19 03:29:26 +01:00
committed by GitHub
parent baed16dab6
commit 4e9b96ff1a
50 changed files with 2486 additions and 149 deletions

View File

@@ -20,4 +20,9 @@ export const albumApi = {
expect(res.status).toEqual(200);
return res.body as AlbumResponseDto;
},
getAllAlbums: async (server: any, accessToken: string) => {
const res = await request(server).get(`/album/`).set('Authorization', `Bearer ${accessToken}`).send();
expect(res.status).toEqual(200);
return res.body as AlbumResponseDto[];
},
};

View File

@@ -0,0 +1,16 @@
import { APIKeyCreateResponseDto } from '@app/domain';
import { apiKeyCreateStub } 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

@@ -0,0 +1,32 @@
version: '3.8'
name: 'immich-test-e2e'
services:
immich-server:
image: immich-server-dev:latest
build:
context: ../../
dockerfile: server/Dockerfile
target: dev
entrypoint: ['/usr/local/bin/npm', 'run']
command: test:e2e
volumes:
- /usr/src/app/node_modules
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=e2e_test
- IMMICH_RUN_ALL_TESTS=true
depends_on:
- database
database:
image: tensorchord/pgvecto-rs:pg14-v0.1.11@sha256:0335a1a22f8c5dd1b697f14f079934f5152eaaa216c09b61e293be285491f8ee
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
POSTGRES_DB: e2e_test
logging:
driver: none

View File

@@ -15,7 +15,7 @@ describe(`${ActivityController.name} (e2e)`, () => {
let nonOwner: LoginResponseDto;
beforeAll(async () => {
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
await testApp.reset();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);

View File

@@ -24,7 +24,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
let user2Albums: AlbumResponseDto[];
beforeAll(async () => {
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
});
afterAll(async () => {

View File

@@ -63,7 +63,8 @@ describe(`${AssetController.name} (e2e)`, () => {
};
beforeAll(async () => {
[server, app] = await testApp.create();
app = await testApp.create();
server = app.getHttpServer();
assetRepository = app.get<IAssetRepository>(IAssetRepository);
await testApp.reset();

View File

@@ -39,8 +39,7 @@ describe(`${AuthController.name} (e2e)`, () => {
let accessToken: string;
beforeAll(async () => {
await testApp.reset();
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
});
afterAll(async () => {

View File

@@ -90,10 +90,7 @@ describe(`Supported file formats (e2e)`, () => {
iso: 20,
focalLength: 3.99,
fNumber: 1.8,
state: 'Douglas County, Nebraska',
timeZone: 'America/Chicago',
city: 'Ralston',
country: 'United States of America',
},
},
{
@@ -168,7 +165,7 @@ describe(`Supported file formats (e2e)`, () => {
const testsToRun = formatTests.filter((formatTest) => formatTest.runTest);
beforeAll(async () => {
[server] = await testApp.create({ jobs: true });
server = (await testApp.create({ jobs: true })).getHttpServer();
});
afterAll(async () => {

View File

@@ -0,0 +1,11 @@
{
"reverseGeocoding": {
"enabled": false
},
"machineLearning": {
"enabled": false
},
"logging": {
"enabled": false
}
}

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 })).getHttpServer();
});
afterAll(async () => {

View File

@@ -8,7 +8,7 @@ describe(`${OAuthController.name} (e2e)`, () => {
let server: any;
beforeAll(async () => {
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
});
afterAll(async () => {

View File

@@ -12,7 +12,7 @@ describe(`${PartnerController.name} (e2e)`, () => {
let user3: LoginResponseDto;
beforeAll(async () => {
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
await testApp.reset();
await api.authApi.adminSignUp(server);

View File

@@ -17,7 +17,8 @@ describe(`${PersonController.name}`, () => {
let hiddenPerson: PersonEntity;
beforeAll(async () => {
[server, app] = await testApp.create();
app = await testApp.create();
server = app.getHttpServer();
personRepository = app.get<IPersonRepository>(IPersonRepository);
});

View File

@@ -24,7 +24,8 @@ describe(`${SearchController.name}`, () => {
let asset1: AssetResponseDto;
beforeAll(async () => {
[server, app] = await testApp.create();
app = await testApp.create();
server = app.getHttpServer();
assetRepository = app.get<IAssetRepository>(IAssetRepository);
smartInfoRepository = app.get<ISmartInfoRepository>(ISmartInfoRepository);
});

View File

@@ -11,7 +11,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
await testApp.reset();
await api.authApi.adminSignUp(server);
@@ -74,10 +74,10 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
expect(status).toBe(200);
expect(body).toEqual({
clipEncode: false,
configFile: false,
configFile: true,
facialRecognition: false,
map: true,
reverseGeocoding: true,
reverseGeocoding: false,
oauth: false,
oauthAutoLaunch: false,
passwordLogin: true,

View File

@@ -8,8 +8,8 @@ export default async () => {
if (!allTests) {
console.warn(
`\n\n
*** Not running all e2e tests. Run 'make test-e2e' to run all tests inside Docker (recommended)\n
*** or set 'IMMICH_RUN_ALL_TESTS=true' to run all tests(requires dependencies to be installed)\n`,
*** Not running all server e2e tests. Run 'make test-e2e' to run all tests inside Docker (recommended)\n
*** or set 'IMMICH_RUN_ALL_TESTS=true' to run all tests (requires dependencies to be installed)\n`,
);
}
@@ -47,7 +47,7 @@ export default async () => {
}
process.env.NODE_ENV = 'development';
process.env.IMMICH_MACHINE_LEARNING_ENABLED = 'false';
process.env.IMMICH_TEST_ENV = 'true';
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/immich-e2e-config.json`);
process.env.TZ = 'Z';
};

View File

@@ -33,7 +33,8 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
let app: INestApplication<any>;
beforeAll(async () => {
[server, app] = await testApp.create();
app = await testApp.create();
server = app.getHttpServer();
const assetRepository = app.get<IAssetRepository>(IAssetRepository);
await testApp.reset();

View File

@@ -11,7 +11,7 @@ describe(`${SystemConfigController.name} (e2e)`, () => {
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
[server] = await testApp.create();
server = (await testApp.create()).getHttpServer();
await testApp.reset();
await api.authApi.adminSignUp(server);

View File

@@ -18,7 +18,8 @@ describe(`${UserController.name}`, () => {
let userRepository: Repository<UserEntity>;
beforeAll(async () => {
[server, app] = await testApp.create();
app = await testApp.create();
server = app.getHttpServer();
userRepository = app.select(AppModule).get(getRepositoryToken(UserEntity));
});

View File

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

View File

@@ -4,10 +4,12 @@ import { dataSource, databaseChecks } from '@app/infra';
import { AssetEntity, AssetType, LibraryType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { randomBytes } from 'crypto';
import * as fs from 'fs';
import { DateTime } from 'luxon';
import path from 'path';
import { Server } from 'tls';
import { EntityTarget, ObjectLiteral } from 'typeorm';
import { AppService } from '../src/microservices/app.service';
@@ -61,7 +63,7 @@ interface TestAppOptions {
let app: INestApplication;
export const testApp = {
create: async (options?: TestAppOptions): Promise<[any, INestApplication]> => {
create: async (options?: TestAppOptions): Promise<INestApplication> => {
const { jobs } = options || { jobs: false };
const moduleFixture = await Test.createTestingModule({ imports: [AppModule], providers: [AppService] })
@@ -84,20 +86,27 @@ export const testApp = {
.compile();
app = await moduleFixture.createNestApplication().init();
await app.listen(0);
if (jobs) {
await app.get(AppService).init();
}
return [app.getHttpServer(), app];
const port = app.getHttpServer().address().port;
const protocol = app instanceof Server ? 'https' : 'http';
process.env.IMMICH_INSTANCE_URL = protocol + '://127.0.0.1:' + port;
return app;
},
reset: async (options?: ResetOptions) => {
await db.reset(options);
},
teardown: async () => {
await app.get(AppService).teardown();
if (app) {
await app.get(AppService).teardown();
await app.close();
}
await db.disconnect();
await app.close();
},
};