refactor: cli e2e (#7211)

This commit is contained in:
Jason Rasmussen
2024-02-19 17:25:57 -05:00
committed by GitHub
parent 870d517ce3
commit 947bcf2d68
26 changed files with 442 additions and 500 deletions
+7 -7
View File
@@ -11,15 +11,15 @@ import {
loginResponseDto,
signupResponseDto,
} from 'src/responses';
import { app, asAuthHeader, dbUtils } from 'src/utils';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const { name, email, password } = signupDto.admin;
describe(`Registration`, () => {
describe(`/auth/admin-sign-up`, () => {
beforeAll(() => {
dbUtils.setup();
apiUtils.setup();
});
beforeEach(async () => {
@@ -96,7 +96,7 @@ describe(`Registration`, () => {
});
});
describe('Auth', () => {
describe('/auth/*', () => {
let admin: LoginResponseDto;
beforeEach(async () => {
@@ -177,7 +177,7 @@ describe('Auth', () => {
}
await expect(
getAuthDevices({ headers: asAuthHeader(admin.accessToken) })
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
).resolves.toHaveLength(6);
const { status } = await request(app)
@@ -186,7 +186,7 @@ describe('Auth', () => {
expect(status).toBe(204);
await expect(
getAuthDevices({ headers: asAuthHeader(admin.accessToken) })
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
).resolves.toHaveLength(1);
});
@@ -202,7 +202,7 @@ describe('Auth', () => {
it('should logout a device', async () => {
const [device] = await getAuthDevices({
headers: asAuthHeader(admin.accessToken),
headers: asBearerAuth(admin.accessToken),
});
const { status } = await request(app)
.delete(`/auth/devices/${device.id}`)
+58
View File
@@ -0,0 +1,58 @@
import { stat } from 'node:fs/promises';
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
import { beforeEach, beforeAll, describe, expect, it } from 'vitest';
describe(`immich login-key`, () => {
beforeAll(() => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
});
it('should require a url', async () => {
const { stderr, exitCode } = await immichCli(['login-key']);
expect(stderr).toBe("error: missing required argument 'url'");
expect(exitCode).toBe(1);
});
it('should require a key', async () => {
const { stderr, exitCode } = await immichCli(['login-key', app]);
expect(stderr).toBe("error: missing required argument 'key'");
expect(exitCode).toBe(1);
});
it('should require a valid key', async () => {
const { stderr, exitCode } = await immichCli([
'login-key',
app,
'immich-is-so-cool',
]);
expect(stderr).toContain(
'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
);
expect(exitCode).toBe(1);
});
it('should login', async () => {
const admin = await apiUtils.adminSetup();
const key = await apiUtils.createApiKey(admin.accessToken);
const { stdout, stderr, exitCode } = await immichCli([
'login-key',
app,
`${key.secret}`,
]);
expect(stdout.split('\n')).toEqual([
'Logging in...',
'Logged in as admin@immich.cloud',
'Wrote auth info to /tmp/immich/auth.yml',
]);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const stats = await stat('/tmp/immich/auth.yml');
const mode = (stats.mode & 0o777).toString(8);
expect(mode).toEqual('600');
});
});
+28
View File
@@ -0,0 +1,28 @@
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`immich server-info`, () => {
beforeAll(() => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
await cliUtils.login();
});
it('should return the server info', async () => {
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
expect(stdout.split('\n')).toEqual([
expect.stringContaining('Server Version:'),
expect.stringContaining('Image Types:'),
expect.stringContaining('Video Types:'),
'Statistics:',
' Images: 0',
' Videos: 0',
' Total: 0',
]);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
});
});
+127
View File
@@ -0,0 +1,127 @@
import { getAllAlbums, getAllAssets } from '@immich/sdk';
import {
apiUtils,
asKeyAuth,
cliUtils,
dbUtils,
immichCli,
testAssetDir,
} from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`immich upload`, () => {
let key: string;
beforeAll(() => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
key = await cliUtils.login();
});
describe('immich upload --recursive', () => {
it('should upload a folder recursively', async () => {
const { stderr, stdout, exitCode } = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
]);
expect(stderr).toBe('');
expect(stdout.split('\n')).toEqual([
expect.stringContaining('Successfully uploaded 9 assets'),
]);
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
});
});
describe('immich upload --recursive --album', () => {
it('should create albums from folder names', async () => {
const { stderr, stdout, exitCode } = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
'--album',
]);
expect(stdout.split('\n')).toEqual([
expect.stringContaining('Successfully uploaded 9 assets'),
]);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1);
expect(albums[0].albumName).toBe('nature');
});
it('should add existing assets to albums', async () => {
const response1 = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
]);
expect(response1.stdout.split('\n')).toEqual([
expect.stringContaining('Successfully uploaded 9 assets'),
]);
expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0);
const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets1.length).toBe(9);
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums1.length).toBe(0);
const response2 = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
'--album',
]);
expect(response2.stdout.split('\n')).toEqual([
expect.stringContaining(
'All assets were already uploaded, nothing to do.'
),
]);
expect(response2.stderr).toBe('');
expect(response2.exitCode).toBe(0);
const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets2.length).toBe(9);
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums2.length).toBe(1);
expect(albums2[0].albumName).toBe('nature');
});
});
describe('immich upload --recursive --album-name=e2e', () => {
it('should create a named album', async () => {
const { stderr, stdout, exitCode } = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
'--album-name=e2e',
]);
expect(stdout.split('\n')).toEqual([
expect.stringContaining('Successfully uploaded 9 assets'),
]);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1);
expect(albums[0].albumName).toBe('e2e');
});
});
});
+29
View File
@@ -0,0 +1,29 @@
import { readFileSync } from 'node:fs';
import { apiUtils, immichCli } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
describe(`immich --version`, () => {
beforeAll(() => {
apiUtils.setup();
});
describe('immich --version', () => {
it('should print the cli version', async () => {
const { stdout, stderr, exitCode } = await immichCli(['--version']);
expect(stdout).toEqual(pkg.version);
expect(stderr).toEqual('');
expect(exitCode).toBe(0);
});
});
describe('immich -V', () => {
it('should print the cli version', async () => {
const { stdout, stderr, exitCode } = await immichCli(['-V']);
expect(stdout).toEqual(pkg.version);
expect(stderr).toEqual('');
expect(exitCode).toBe(0);
});
});
});
+1 -1
View File
@@ -2,7 +2,7 @@ import { spawn, exec } from 'child_process';
export default async () => {
let _resolve: () => unknown;
const promise = new Promise<void>((resolve, reject) => (_resolve = resolve));
const promise = new Promise<void>((resolve) => (_resolve = resolve));
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
+76 -7
View File
@@ -1,29 +1,44 @@
import {
LoginResponseDto,
createApiKey,
defaults,
login,
setAdminOnboarding,
signUpAdmin,
} from '@immich/sdk';
import { BrowserContext } from '@playwright/test';
import { spawn } from 'child_process';
import { access } from 'node:fs/promises';
import path from 'node:path';
import pg from 'pg';
import { loginDto, signupDto } from 'src/fixtures';
export const app = 'http://127.0.0.1:2283/api';
defaults.baseUrl = app;
const directoryExists = (directory: string) =>
access(directory)
.then(() => true)
.catch(() => false);
// TODO move test assets into e2e/assets
export const testAssetDir = path.resolve(`./../server/test/assets/`);
if (!(await directoryExists(`${testAssetDir}/albums`))) {
throw new Error(
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`
);
}
const setBaseUrl = () => (defaults.baseUrl = app);
export const asAuthHeader = (accessToken: string) => ({
export const asBearerAuth = (accessToken: string) => ({
Authorization: `Bearer ${accessToken}`,
});
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
let client: pg.Client | null = null;
export const dbUtils = {
setup: () => {
setBaseUrl();
},
reset: async () => {
try {
if (!client) {
@@ -33,7 +48,14 @@ export const dbUtils = {
await client.connect();
}
for (const table of ['user_token', 'users', 'system_metadata']) {
for (const table of [
'albums',
'assets',
'api_keys',
'user_token',
'users',
'system_metadata',
]) {
await client.query(`DELETE FROM ${table} CASCADE;`);
}
} catch (error) {
@@ -53,14 +75,61 @@ export const dbUtils = {
}
},
};
export interface CliResponse {
stdout: string;
stderr: string;
exitCode: number | null;
}
export const immichCli = async (args: string[]) => {
let _resolve: (value: CliResponse) => void;
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args];
const child = spawn('node', _args, {
stdio: 'pipe',
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => (stdout += data.toString()));
child.stderr.on('data', (data) => (stderr += data.toString()));
child.on('exit', (exitCode) => {
_resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode,
});
});
return deferred;
};
export const apiUtils = {
setup: () => {
setBaseUrl();
},
adminSetup: async () => {
await signUpAdmin({ signUpDto: signupDto.admin });
const response = await login({ loginCredentialDto: loginDto.admin });
await setAdminOnboarding({ headers: asAuthHeader(response.accessToken) });
await setAdminOnboarding({ headers: asBearerAuth(response.accessToken) });
return response;
},
createApiKey: (accessToken: string) => {
return createApiKey(
{ apiKeyCreateDto: { name: 'e2e' } },
{ headers: asBearerAuth(accessToken) }
);
},
};
export const cliUtils = {
login: async () => {
const admin = await apiUtils.adminSetup();
const key = await apiUtils.createApiKey(admin.accessToken);
await immichCli(['login-key', app, `${key.secret}`]);
return key.secret;
},
};
export const webUtils = {
+4
View File
@@ -2,6 +2,10 @@ import { test, expect } from '@playwright/test';
import { apiUtils, dbUtils, webUtils } from 'src/utils';
test.describe('Registration', () => {
test.beforeAll(() => {
apiUtils.setup();
});
test.beforeEach(async () => {
await dbUtils.reset();
});