Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions src/test/api.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import login from '@metacall/protocol/login';
import { Plans } from '@metacall/protocol/plan';
import { waitFor } from '@metacall/protocol/protocol';
import { notStrictEqual, ok, strictEqual } from 'assert';
import { writeFile } from 'fs/promises';
import { join } from 'path';
import args from '../cli/args';
import { InspectFormat } from '../cli/args';
import { inspect } from '../cli/inspect';
import { deleteBySelection } from '../delete';
import { deployFromRepository, deployPackage } from '../deploy';
import { createTmpDirectory } from './cli';
import {
AuthContext,
ProcessExitMock,
SelectionMock,
StartupMock,
clearCache,
getAPI
} from './api';

describe('Integration API (Deploy)', function () {
this.timeout(2000000);

const url = 'https://github.com/metacall/examples';
const addRepoSuffix = 'metacall-examples';

const workDirSuffix = 'time-app-web';
const filePath = join(
process.cwd(),
'src',
'test',
'resources',
'integration',
'time-app-web'
);

let auth: AuthContext;

const awaitReady = (suffix: string) =>
waitFor(async cancel => {
const deploy = await auth.api.inspectByName(suffix);

if (deploy.status === 'create') {
throw new Error('Not ready yet');
} else if (deploy.status === 'fail') {
cancel('Deploy failed');
}

return deploy;
});

const awaitDeleted = (suffix: string) =>
waitFor(async () => {
const deployments = await auth.api.inspect();
const found = deployments.find(d => d.suffix === suffix);

if (found) {
throw new Error('Still exists');
}

return true;
});

before(async function () {
SelectionMock.install();
auth = await getAPI();
StartupMock.install(auth.config);
});

after(function () {
StartupMock.restore();
SelectionMock.restore();
});

// --email & --password
it('Should be able to login using --email & --password flag', async function () {
const email = process.env.METACALL_AUTH_EMAIL;
const password = process.env.METACALL_AUTH_PASSWORD;

if (!email || !password) {
return this.skip();
}

const token = await login(email, password, auth.config.baseURL);
notStrictEqual(token, '');
});

// --token
it('Should be able to login using --token flag', async function () {
await clearCache();

const freshAuth = await getAPI();
notStrictEqual(freshAuth.config.token, '');
});

// --confDir
it('Should be able to login using --confDir flag', async function () {
const confDir = await createTmpDirectory();
const configPath = join(confDir, 'config.ini');
await writeFile(configPath, `token=${auth.config.token}`, 'utf8');

const confDirAuth = await getAPI(confDir);
notStrictEqual(confDirAuth.config.token, '');
});

// --inspect with invalid parameter
it('Should fail --inspect command with proper output', async function () {
ProcessExitMock.install();
try {
await inspect(InspectFormat.Invalid, auth.config, auth.api);
} catch (err) {
ok(
String(err).includes('process.exit') ||
String(err).includes('Invalid format')
);
} finally {
ProcessExitMock.restore();
}
});

// --inspect without parameter
it('Should pass --inspect command with valid output', async function () {
await inspect(InspectFormat.Raw, auth.config, auth.api);
});

// --addrepo
it('Should be able to deploy repository using --addrepo flag', async function () {
await deployFromRepository(auth.api, Plans.Essential, url);

const deploy = await awaitReady(addRepoSuffix);
strictEqual(deploy.status, 'ready');
});

// --delete
it('Should be able to delete deployed repository using --delete flag', async function () {
await awaitReady(addRepoSuffix);
await deleteBySelection(auth.api);

strictEqual(await awaitDeleted(addRepoSuffix), true);
});

// --workdir & --projectName
it('Should be able to deploy repository using --workdir & --projectName flag', async function () {
args.projectName = workDirSuffix;

await deployPackage(filePath, auth.api, Plans.Essential);

const deploy = await awaitReady(workDirSuffix);
strictEqual(deploy.status, 'ready');
});

// --delete
it('Should be able to delete deployed repository using --delete flag', async function () {
await awaitReady(workDirSuffix);
await deleteBySelection(auth.api);

strictEqual(await awaitDeleted(workDirSuffix), true);
});

// --addrepo with env vars
it('Should be able to deploy repository using --addrepo flag with environment vars', async function () {
SelectionMock.install({ listChoice: 'first', consent: true });

await deployFromRepository(auth.api, Plans.Essential, url);

const deploy = await awaitReady(addRepoSuffix);
strictEqual(deploy.status, 'ready');

SelectionMock.install();
});

// --delete
it('Should be able to delete deployed repository using --delete flag', async function () {
await awaitReady(addRepoSuffix);
await deleteBySelection(auth.api);

strictEqual(await awaitDeleted(addRepoSuffix), true);
});

// --workdir with .env file
it('Should be able to deploy repository using --workdir & getting the .env file', async function () {
const projectPath = join(
process.cwd(),
'src',
'test',
'resources',
'integration',
'env'
);
args.projectName = 'env';

await deployPackage(projectPath, auth.api, Plans.Essential);

const deploy = await awaitReady('env');
strictEqual(deploy.status, 'ready');
});

// --delete
it('Should be able to delete deployed repository using --delete flag', async function () {
await awaitReady('env');
await deleteBySelection(auth.api);

strictEqual(await awaitDeleted('env'), true);
});

// --workdir & --projectName & --plan
it('Should be able to deploy repository using --workdir & --plan flag', async function () {
args.projectName = workDirSuffix;

await deployPackage(filePath, auth.api, Plans.Essential);

const deploy = await awaitReady(workDirSuffix);
strictEqual(deploy.status, 'ready');
});

// --delete
it('Should be able to delete deployed repository using --delete flag', async function () {
await awaitReady(workDirSuffix);
await deleteBySelection(auth.api);

strictEqual(await awaitDeleted(workDirSuffix), true);
});
});
134 changes: 134 additions & 0 deletions src/test/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import login from '@metacall/protocol/login';
import API, { API as APIInterface } from '@metacall/protocol/protocol';
import { ok } from 'assert';
import * as dotenv from 'dotenv';
import { promises as fs } from 'fs';
import args from '../cli/args';
import * as selectionModule from '../cli/selection';
import { Config, configFilePath, defaultPath, load } from '../config';
import * as logsModule from '../logs';
import * as startupModule from '../startup';
import { exists } from '../utils';

dotenv.config();

type Writable<T> = { -readonly [K in keyof T]: T[K] };

export type AuthContext = {
config: Config & { token: string };
api: APIInterface;
};

export const getAPI = async (confDir?: string): Promise<AuthContext> => {
const config = await load(confDir ?? args['confDir'] ?? defaultPath);

let token = process.env['METACALL_API_KEY'] || config.token || '';

if (!token) {
const email = process.env['METACALL_AUTH_EMAIL'] || '';
const password = process.env['METACALL_AUTH_PASSWORD'] || '';

ok(
email && password,
'No token found and no METACALL_AUTH_EMAIL/PASSWORD set in .env'
);

token = await login(email, password, config.baseURL);
}

ok(token, 'Failed to obtain auth token');

return {
config: { ...config, token } as Config & { token: string },
api: API(token, config.baseURL)
};
};

const originals = {
startup: startupModule.startup,
logs: logsModule.logs,
listSelection: selectionModule.listSelection,
consentSelection: selectionModule.consentSelection,
processExit: process.exit.bind(process)
};

export const StartupMock = {
install(config: Config & { token: string }): void {
const mutableStartup = startupModule as Writable<typeof startupModule>;
const mutableLogs = logsModule as Writable<typeof logsModule>;

mutableStartup.startup = () => Promise.resolve(config);
mutableLogs.logs = () => Promise.resolve();
},

restore(): void {
const mutableStartup = startupModule as Writable<typeof startupModule>;
const mutableLogs = logsModule as Writable<typeof logsModule>;

mutableStartup.startup = originals.startup;
mutableLogs.logs = originals.logs;
}
};

export type SelectionMockOptions = {
listChoice: 'first' | 'last' | number;
consent: boolean;
};

const selectionDefaults: SelectionMockOptions = {
listChoice: 'first',
consent: false
};

export const SelectionMock = {
install(opts: Partial<SelectionMockOptions> = {}): void {
const { listChoice, consent } = { ...selectionDefaults, ...opts };
const mutable = selectionModule as Writable<typeof selectionModule>;

mutable.listSelection = (
choices: Array<string | { name: string; value: string }>
): Promise<string> => {
const idx =
listChoice === 'first'
? 0
: listChoice === 'last'
? choices.length - 1
: listChoice;

const choice =
choices[Math.min(idx, choices.length - 1)] ?? choices[0];
return Promise.resolve(
typeof choice === 'string' ? choice : choice.value
);
};

mutable.consentSelection = (): Promise<boolean> =>
Promise.resolve(consent);
},

restore(): void {
const mutable = selectionModule as Writable<typeof selectionModule>;

mutable.listSelection = originals.listSelection;
mutable.consentSelection = originals.consentSelection;
}
};

export const ProcessExitMock = {
install(): void {
process.exit = ((code?: number): never => {
throw new Error(`process.exit(${code ?? 0})`);
}) as typeof process.exit;
},

restore(): void {
process.exit = originals.processExit;
}
};

export const clearCache = async (): Promise<void> => {
const path = configFilePath();
if (await exists(path)) {
await fs.unlink(path);
}
};