wip
This commit is contained in:
parent
67827e0929
commit
11c4051bd8
14 changed files with 2659 additions and 9 deletions
2307
cli/package-lock.json
generated
2307
cli/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -17,6 +17,7 @@
|
||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testcontainers/postgresql": "^10.2.2",
|
||||||
"@types/byte-size": "^8.1.0",
|
"@types/byte-size": "^8.1.0",
|
||||||
"@types/chai": "^4.3.5",
|
"@types/chai": "^4.3.5",
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
|
@ -33,12 +34,14 @@
|
||||||
"eslint-plugin-jest": "^27.2.2",
|
"eslint-plugin-jest": "^27.2.2",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-unicorn": "^47.0.0",
|
"eslint-plugin-unicorn": "^47.0.0",
|
||||||
|
"immich": "file:../server",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-extended": "^4.0.0",
|
"jest-extended": "^4.0.0",
|
||||||
"jest-message-util": "^29.5.0",
|
"jest-message-util": "^29.5.0",
|
||||||
"jest-mock-axios": "^4.7.2",
|
"jest-mock-axios": "^4.7.2",
|
||||||
"jest-when": "^3.5.2",
|
"jest-when": "^3.5.2",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tslib": "^2.5.3",
|
"tslib": "^2.5.3",
|
||||||
|
@ -52,7 +55,8 @@
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"format": "prettier --check .",
|
"format": "prettier --check .",
|
||||||
"format:fix": "prettier --write .",
|
"format:fix": "prettier --write .",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit",
|
||||||
|
"test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"clearMocks": true,
|
"clearMocks": true,
|
||||||
|
@ -70,7 +74,10 @@
|
||||||
"<rootDir>/src/**/*.(t|j)s"
|
"<rootDir>/src/**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^@api(|/.*)$": "<rootDir>/src/api/$1"
|
"^@api(|/.*)$": "<rootDir>/src/api/$1",
|
||||||
|
"^@app/immich(|/.*)$": "<rootDir>../server/src/immich/$1",
|
||||||
|
"^@app/infra(|/.*)$": "<rootDir>../server/src/infra/$1",
|
||||||
|
"^@app/domain(|/.*)$": "<rootDir>../server/src/domain/$1"
|
||||||
},
|
},
|
||||||
"coverageDirectory": "./coverage",
|
"coverageDirectory": "./coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
|
|
14
cli/test/api/activity-api.ts
Normal file
14
cli/test/api/activity-api.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { ActivityCreateDto, ActivityResponseDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const activityApi = {
|
||||||
|
create: async (server: any, accessToken: string, dto: ActivityCreateDto) => {
|
||||||
|
const res = await request(server).post('/activity').set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||||
|
expect(res.status === 200 || res.status === 201).toBe(true);
|
||||||
|
return res.body as ActivityResponseDto;
|
||||||
|
},
|
||||||
|
delete: async (server: any, accessToken: string, id: string) => {
|
||||||
|
const res = await request(server).delete(`/activity/${id}`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(res.status).toEqual(204);
|
||||||
|
},
|
||||||
|
};
|
23
cli/test/api/album-api.ts
Normal file
23
cli/test/api/album-api.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { AddUsersDto, AlbumResponseDto, BulkIdResponseDto, BulkIdsDto, CreateAlbumDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const albumApi = {
|
||||||
|
create: async (server: any, accessToken: string, dto: CreateAlbumDto) => {
|
||||||
|
const res = await request(server).post('/album').set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||||
|
expect(res.status).toEqual(201);
|
||||||
|
return res.body as AlbumResponseDto;
|
||||||
|
},
|
||||||
|
addAssets: async (server: any, accessToken: string, id: string, dto: BulkIdsDto) => {
|
||||||
|
const res = await request(server)
|
||||||
|
.put(`/album/${id}/assets`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
return res.body as BulkIdResponseDto[];
|
||||||
|
},
|
||||||
|
addUsers: async (server: any, accessToken: string, id: string, dto: AddUsersDto) => {
|
||||||
|
const res = await request(server).put(`/album/${id}/users`).set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
return res.body as AlbumResponseDto;
|
||||||
|
},
|
||||||
|
};
|
65
cli/test/api/asset-api.ts
Normal file
65
cli/test/api/asset-api.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { AssetResponseDto } from '@app/domain';
|
||||||
|
import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto';
|
||||||
|
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
type UploadDto = Partial<CreateAssetDto> & { content?: Buffer };
|
||||||
|
|
||||||
|
const asset = {
|
||||||
|
deviceAssetId: 'test-1',
|
||||||
|
deviceId: 'test',
|
||||||
|
fileCreatedAt: new Date(),
|
||||||
|
fileModifiedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assetApi = {
|
||||||
|
create: async (
|
||||||
|
server: any,
|
||||||
|
accessToken: string,
|
||||||
|
dto?: Omit<CreateAssetDto, 'assetData'>,
|
||||||
|
): Promise<AssetResponseDto> => {
|
||||||
|
dto = dto || asset;
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.post(`/asset/upload`)
|
||||||
|
.field('deviceAssetId', dto.deviceAssetId)
|
||||||
|
.field('deviceId', dto.deviceId)
|
||||||
|
.field('fileCreatedAt', dto.fileCreatedAt.toISOString())
|
||||||
|
.field('fileModifiedAt', dto.fileModifiedAt.toISOString())
|
||||||
|
.attach('assetData', randomBytes(32), 'example.jpg')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
|
expect([200, 201].includes(status)).toBe(true);
|
||||||
|
|
||||||
|
return body as AssetResponseDto;
|
||||||
|
},
|
||||||
|
get: async (server: any, accessToken: string, id: string): Promise<AssetResponseDto> => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.get(`/asset/assetById/${id}`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as AssetResponseDto;
|
||||||
|
},
|
||||||
|
getAllAssets: async (server: any, accessToken: string) => {
|
||||||
|
const { body, status } = await request(server).get(`/asset/`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as AssetResponseDto[];
|
||||||
|
},
|
||||||
|
upload: async (server: any, accessToken: string, id: string, dto: UploadDto = {}) => {
|
||||||
|
const { content, isFavorite = false, isArchived = false } = dto;
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.post('/asset/upload')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.field('deviceAssetId', id)
|
||||||
|
.field('deviceId', 'TEST')
|
||||||
|
.field('fileCreatedAt', new Date().toISOString())
|
||||||
|
.field('fileModifiedAt', new Date().toISOString())
|
||||||
|
.field('isFavorite', isFavorite)
|
||||||
|
.field('isArchived', isArchived)
|
||||||
|
.field('duration', '0:00:00.000000')
|
||||||
|
.attach('assetData', content || randomBytes(32), 'example.jpg');
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
return body as AssetFileUploadResponseDto;
|
||||||
|
},
|
||||||
|
};
|
45
cli/test/api/auth-api.ts
Normal file
45
cli/test/api/auth-api.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { AuthDeviceResponseDto, LoginCredentialDto, LoginResponseDto, UserResponseDto } from '@app/domain';
|
||||||
|
import { adminSignupStub, loginResponseStub, loginStub } from '@test';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const authApi = {
|
||||||
|
adminSignUp: async (server: any) => {
|
||||||
|
const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub);
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
return body as UserResponseDto;
|
||||||
|
},
|
||||||
|
adminLogin: async (server: any) => {
|
||||||
|
const { status, body } = await request(server).post('/auth/login').send(loginStub.admin);
|
||||||
|
|
||||||
|
expect(body).toEqual(loginResponseStub.admin.response);
|
||||||
|
expect(body).toMatchObject({ accessToken: expect.any(String) });
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
return body as LoginResponseDto;
|
||||||
|
},
|
||||||
|
login: async (server: any, dto: LoginCredentialDto) => {
|
||||||
|
const { status, body } = await request(server).post('/auth/login').send(dto);
|
||||||
|
|
||||||
|
expect(status).toEqual(201);
|
||||||
|
expect(body).toMatchObject({ accessToken: expect.any(String) });
|
||||||
|
|
||||||
|
return body as LoginResponseDto;
|
||||||
|
},
|
||||||
|
getAuthDevices: async (server: any, accessToken: string) => {
|
||||||
|
const { status, body } = await request(server).get('/auth/devices').set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
|
expect(body).toEqual(expect.any(Array));
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
return body as AuthDeviceResponseDto[];
|
||||||
|
},
|
||||||
|
validateToken: async (server: any, accessToken: string) => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.post('/auth/validateToken')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(body).toEqual({ authStatus: true });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
},
|
||||||
|
};
|
19
cli/test/api/index.ts
Normal file
19
cli/test/api/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { activityApi } from './activity-api';
|
||||||
|
import { albumApi } from './album-api';
|
||||||
|
import { assetApi } from './asset-api';
|
||||||
|
import { authApi } from './auth-api';
|
||||||
|
import { libraryApi } from './library-api';
|
||||||
|
import { partnerApi } from './partner-api';
|
||||||
|
import { sharedLinkApi } from './shared-link-api';
|
||||||
|
import { userApi } from './user-api';
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
activityApi,
|
||||||
|
authApi,
|
||||||
|
assetApi,
|
||||||
|
libraryApi,
|
||||||
|
sharedLinkApi,
|
||||||
|
albumApi,
|
||||||
|
userApi,
|
||||||
|
partnerApi,
|
||||||
|
};
|
47
cli/test/api/library-api.ts
Normal file
47
cli/test/api/library-api.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { CreateLibraryDto, LibraryResponseDto, LibraryStatsResponseDto, ScanLibraryDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const libraryApi = {
|
||||||
|
getAll: async (server: any, accessToken: string) => {
|
||||||
|
const { body, status } = await request(server).get(`/library/`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as LibraryResponseDto[];
|
||||||
|
},
|
||||||
|
create: async (server: any, accessToken: string, dto: CreateLibraryDto) => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.post(`/library/`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
return body as LibraryResponseDto;
|
||||||
|
},
|
||||||
|
setImportPaths: async (server: any, accessToken: string, id: string, importPaths: string[]) => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.put(`/library/${id}`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send({ importPaths });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as LibraryResponseDto;
|
||||||
|
},
|
||||||
|
scanLibrary: async (server: any, accessToken: string, id: string, dto: ScanLibraryDto = {}) => {
|
||||||
|
const { status } = await request(server)
|
||||||
|
.post(`/library/${id}/scan`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
},
|
||||||
|
removeOfflineFiles: async (server: any, accessToken: string, id: string) => {
|
||||||
|
const { status } = await request(server)
|
||||||
|
.post(`/library/${id}/removeOffline`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send();
|
||||||
|
expect(status).toBe(201);
|
||||||
|
},
|
||||||
|
getLibraryStatistics: async (server: any, accessToken: string, id: string): Promise<LibraryStatsResponseDto> => {
|
||||||
|
const { body, status } = await request(server)
|
||||||
|
.get(`/library/${id}/statistics`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
};
|
10
cli/test/api/partner-api.ts
Normal file
10
cli/test/api/partner-api.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { PartnerResponseDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const partnerApi = {
|
||||||
|
create: async (server: any, accessToken: string, id: string) => {
|
||||||
|
const { status, body } = await request(server).post(`/partner/${id}`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
return body as PartnerResponseDto;
|
||||||
|
},
|
||||||
|
};
|
20
cli/test/api/shared-link-api.ts
Normal file
20
cli/test/api/shared-link-api.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { SharedLinkCreateDto, SharedLinkResponseDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const sharedLinkApi = {
|
||||||
|
create: async (server: any, accessToken: string, dto: SharedLinkCreateDto) => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.post('/shared-link')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
return body as SharedLinkResponseDto;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMySharedLink: async (server: any, key: string) => {
|
||||||
|
const { status, body } = await request(server).get('/shared-link/me').query({ key });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
return body as SharedLinkResponseDto;
|
||||||
|
},
|
||||||
|
};
|
50
cli/test/api/user-api.ts
Normal file
50
cli/test/api/user-api.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { CreateUserDto, UpdateUserDto, UserResponseDto } from '@app/domain';
|
||||||
|
import request from 'supertest';
|
||||||
|
|
||||||
|
export const userApi = {
|
||||||
|
create: async (server: any, accessToken: string, dto: CreateUserDto) => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.post('/user')
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
email: dto.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
return body as UserResponseDto;
|
||||||
|
},
|
||||||
|
get: async (server: any, accessToken: string, id: string) => {
|
||||||
|
const { status, body } = await request(server)
|
||||||
|
.get(`/user/info/${id}`)
|
||||||
|
.set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id });
|
||||||
|
|
||||||
|
return body as UserResponseDto;
|
||||||
|
},
|
||||||
|
update: async (server: any, accessToken: string, dto: UpdateUserDto) => {
|
||||||
|
const { status, body } = await request(server).put('/user').set('Authorization', `Bearer ${accessToken}`).send(dto);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id: dto.id });
|
||||||
|
|
||||||
|
return body as UserResponseDto;
|
||||||
|
},
|
||||||
|
setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => {
|
||||||
|
return await userApi.update(server, accessToken, { id, externalPath });
|
||||||
|
},
|
||||||
|
delete: async (server: any, accessToken: string, id: string) => {
|
||||||
|
const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ id, deletedAt: expect.any(String) });
|
||||||
|
|
||||||
|
return body as UserResponseDto;
|
||||||
|
},
|
||||||
|
};
|
30
cli/test/e2e/cli.e2e-spec.ts
Normal file
30
cli/test/e2e/cli.e2e-spec.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { api } from '@test/api';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import {
|
||||||
|
IMMICH_TEST_ASSET_PATH,
|
||||||
|
IMMICH_TEST_ASSET_TEMP_PATH,
|
||||||
|
restoreTempFolder,
|
||||||
|
testApp,
|
||||||
|
} from 'immich/test/test-utils';
|
||||||
|
import { LoginResponseDto } from 'src/api/open-api';
|
||||||
|
|
||||||
|
describe(`CLI (e2e)`, () => {
|
||||||
|
let server: any;
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
[server] = await testApp.create({ jobs: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testApp.teardown();
|
||||||
|
await restoreTempFolder();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testApp.reset();
|
||||||
|
await restoreTempFolder();
|
||||||
|
await api.authApi.adminSignUp(server);
|
||||||
|
admin = await api.authApi.adminLogin(server);
|
||||||
|
});
|
||||||
|
});
|
24
cli/test/e2e/jest-e2e.json
Normal file
24
cli/test/e2e/jest-e2e.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"modulePaths": ["<rootDir>"],
|
||||||
|
"rootDir": "../..",
|
||||||
|
"globalSetup": "<rootDir>/../server/test/e2e/setup.ts",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"testTimeout": 60000,
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"<rootDir>/src/**/*.(t|j)s",
|
||||||
|
"!<rootDir>/src/**/*.spec.(t|s)s",
|
||||||
|
"!<rootDir>/src/infra/migrations/**"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "./coverage",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@test(|/.*)$": "<rootDir>/test/$1",
|
||||||
|
"^@app/immich(|/.*)$": "<rootDir>/src/immich/$1",
|
||||||
|
"^@app/infra(|/.*)$": "<rootDir>/src/infra/$1",
|
||||||
|
"^@app/domain(|/.*)$": "<rootDir>/src/domain/$1"
|
||||||
|
}
|
||||||
|
}
|
3
cli/test/global-setup.js
Normal file
3
cli/test/global-setup.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = async () => {
|
||||||
|
process.env.TZ = 'UTC';
|
||||||
|
};
|
Loading…
Reference in a new issue