mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-23 15:32:22 +00:00
Merge pull request #1572 from balena-io/add-tests-push-build-deploy
Add test cases for the push, build and deploy commands
This commit is contained in:
commit
be1a260af6
@ -18,7 +18,7 @@
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile, spawn } from 'child_process';
|
||||
import { execFile } from 'child_process';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as filehound from 'filehound';
|
||||
import * as fs from 'fs-extra';
|
||||
@ -27,15 +27,17 @@ import * as path from 'path';
|
||||
import { exec as execPkg } from 'pkg';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
import * as util from 'util';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
// Note: the following 'tslint disable' line was only required to
|
||||
// satisfy ts-node under Appveyor's MSYS2 on Windows -- oddly specific.
|
||||
// Maybe something to do with '/' vs '\' in paths in some tslint file.
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
export const packageJSON = require(path.join(ROOT, 'package.json'));
|
||||
import {
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
MSYS2_BASH,
|
||||
ROOT,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
|
||||
@ -69,34 +71,6 @@ export const finalReleaseAssets: { [platform: string]: string[] } = {
|
||||
linux: [standaloneZips['linux']],
|
||||
};
|
||||
|
||||
const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', code => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the 'pkg' module to create a single large executable file with
|
||||
* the contents of 'node_modules' and the CLI's javascript code.
|
||||
@ -183,9 +157,7 @@ async function testPkg() {
|
||||
}
|
||||
if (semver.major(process.version) !== pkgNodeMajorVersion) {
|
||||
throw new Error(
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${
|
||||
process.version
|
||||
}"`,
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||
);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
@ -315,62 +287,24 @@ export async function buildOclifInstaller() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
* Wrapper around the npm `catch-uncommitted` package in order to run it
|
||||
* conditionally, only when:
|
||||
* - A CI env var is set (CI=true), and
|
||||
* - The OS is not Windows. (`catch-uncommitted` fails on Windows)
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
line => !line.startsWith('[debug]'),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
export async function catchUncommitted(): Promise<void> {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] CI=${process.env.CI} platform=${process.platform}`);
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
['true', 'yes', '1'].includes(process.env.CI.toLowerCase()) &&
|
||||
process.platform !== 'win32'
|
||||
) {
|
||||
await whichSpawn('npx', [
|
||||
'catch-uncommitted',
|
||||
'--catch-no-git',
|
||||
'--skip-node-versionbot-changes',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -127,9 +127,7 @@ export class MarkdownFileParser {
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Markdown section not found: title="${title}" file="${
|
||||
this.mdFilePath
|
||||
}"`,
|
||||
`Markdown section not found: title="${title}" file="${this.mdFilePath}"`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -74,9 +74,7 @@ function getOctokit(): any {
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
console.warn(
|
||||
`Request quota exhausted for request ${options.method} ${
|
||||
options.url
|
||||
}`,
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
// retries 3 times
|
||||
if (options.request.retryCount < 3) {
|
||||
@ -174,9 +172,7 @@ async function updateGitHubReleaseDescriptions(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${
|
||||
cliRelease.id
|
||||
})`;
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
||||
if (cliRelease.draft === true) {
|
||||
console.info(`${skipMsg}: draft release`);
|
||||
continue;
|
||||
@ -201,9 +197,7 @@ async function updateGitHubReleaseDescriptions(
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${
|
||||
cliRelease.id
|
||||
}) old body="${oldBodyPreview}"`,
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
||||
);
|
||||
try {
|
||||
await octokit.repos.updateRelease(updatedRelease);
|
||||
|
@ -20,14 +20,13 @@ import * as _ from 'lodash';
|
||||
import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
fixPathForMsys,
|
||||
ROOT,
|
||||
runUnderMsys,
|
||||
catchUncommitted,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
@ -54,9 +53,10 @@ export async function run(args?: string[]) {
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void } = {
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
|
174
automation/utils.ts
Normal file
174
automation/utils.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
|
||||
export const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', code => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
export async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
line => !line.startsWith('[debug]'),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling wrapper around the npm `which` package:
|
||||
* "Like the unix which utility. Finds the first instance of a specified
|
||||
* executable in the PATH environment variable. Does not cache the results,
|
||||
* so hash -r is not needed when the PATH changes."
|
||||
*
|
||||
* @param program Basename of a program, for example 'ssh'
|
||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||
*/
|
||||
export async function which(program: string): Promise<string> {
|
||||
const whichMod = await import('which');
|
||||
let programPath: string;
|
||||
try {
|
||||
programPath = await whichMod(program);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new Error(`'${program}' program not found. Is it installed?`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return programPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call which(programName) and spawn() with the given arguments. Throw an error
|
||||
* if the process exit code is not zero.
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
let exitCode: number | undefined;
|
||||
try {
|
||||
exitCode = await new Promise<number>((resolve, reject) => {
|
||||
try {
|
||||
spawn(program, args, { stdio: 'inherit' })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error || exitCode) {
|
||||
const msg = [
|
||||
`${programName} failed with exit code ${exitCode}:`,
|
||||
`"${program}" [${args}]`,
|
||||
];
|
||||
if (error) {
|
||||
msg.push(`${error}`);
|
||||
}
|
||||
throw new Error(msg.join('\n'));
|
||||
}
|
||||
}
|
@ -80,9 +80,9 @@ export default class DevicesSupportedCmd extends Command {
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
const sdk = SDK.fromSharedOptions();
|
||||
let deviceTypes: Array<
|
||||
Partial<DeviceTypeWithAliases>
|
||||
> = await sdk.models.config.getDeviceTypes();
|
||||
let deviceTypes: Array<Partial<
|
||||
DeviceTypeWithAliases
|
||||
>> = await sdk.models.config.getDeviceTypes();
|
||||
if (!options.discontinued) {
|
||||
deviceTypes = deviceTypes.filter(dt => dt.state !== 'DISCONTINUED');
|
||||
}
|
||||
|
@ -299,9 +299,7 @@ async function getDeviceVars(
|
||||
deviceVars.push(...deviceConfigVars);
|
||||
} else {
|
||||
if (options.service || options.all) {
|
||||
const pineOpts: SDK.PineOptionsFor<
|
||||
SDK.DeviceServiceEnvironmentVariable
|
||||
> = {
|
||||
const pineOpts: SDK.PineOptionsFor<SDK.DeviceServiceEnvironmentVariable> = {
|
||||
$expand: {
|
||||
service_install: {
|
||||
$expand: 'installs__service',
|
||||
|
@ -306,9 +306,7 @@ async function checkDeviceTypeCompatibility(
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
||||
throw new ExpectedError(
|
||||
`Device type ${
|
||||
options['device-type']
|
||||
} is incompatible with application ${options.application}`,
|
||||
`Device type ${options['device-type']} is incompatible with application ${options.application}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -88,9 +88,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
});
|
||||
|
||||
const selected = await selectFromList(
|
||||
`${
|
||||
entries.length
|
||||
} applications found with that name, please select the application you would like to push to`,
|
||||
`${entries.length} applications found with that name, please select the application you would like to push to`,
|
||||
entries,
|
||||
);
|
||||
|
||||
|
@ -58,17 +58,13 @@ async function getContainerId(
|
||||
});
|
||||
if (request.status !== 200) {
|
||||
throw new Error(
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${
|
||||
request.status
|
||||
}.`,
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`,
|
||||
);
|
||||
}
|
||||
const body = request.body;
|
||||
if (body.status !== 'success') {
|
||||
throw new Error(
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${
|
||||
body.message
|
||||
}`,
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${body.message}`,
|
||||
);
|
||||
}
|
||||
containerId = body.services[serviceName];
|
||||
|
@ -206,9 +206,7 @@ export const tunnel: CommandDefinition<Args, Options> = {
|
||||
)
|
||||
.then(() => {
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}`,
|
||||
` - tunnelling ${localAddress}:${localPort} to ${device.uuid}:${remotePort}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
|
@ -45,9 +45,7 @@ function checkNodeVersion() {
|
||||
const { stripIndent } = require('common-tags');
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${
|
||||
process.version
|
||||
}" does not match required versions "${validNodeVersions}".
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
|
@ -30,9 +30,7 @@ export interface AppOptions {
|
||||
export async function routeCliFramework(argv: string[], options: AppOptions) {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(
|
||||
`[debug] original argv0="${process.argv0}" argv=[${argv}] length=${
|
||||
argv.length
|
||||
}`,
|
||||
`[debug] original argv0="${process.argv0}" argv=[${argv}] length=${argv.length}`,
|
||||
);
|
||||
}
|
||||
const cmdSlice = argv.slice(2);
|
||||
|
@ -157,9 +157,7 @@ async function parseRegistrySecrets(
|
||||
return registrySecrets;
|
||||
} catch (error) {
|
||||
return exitWithExpectedError(
|
||||
`Error validating registry secrets file "${secretsFilename}":\n${
|
||||
error.message
|
||||
}`,
|
||||
`Error validating registry secrets file "${secretsFilename}":\n${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -252,9 +250,7 @@ async function performResolution(
|
||||
buildTask.buildStream = clonedStream;
|
||||
if (!buildTask.external && !buildTask.resolved) {
|
||||
throw new Error(
|
||||
`Project type for service "${
|
||||
buildTask.serviceName
|
||||
}" could not be determined. Missing a Dockerfile?`,
|
||||
`Project type for service "${buildTask.serviceName}" could not be determined. Missing a Dockerfile?`,
|
||||
);
|
||||
}
|
||||
return buildTask;
|
||||
|
@ -95,9 +95,7 @@ async function environmentFromInput(
|
||||
// exists
|
||||
if (!(match[1] in ret)) {
|
||||
logger.logDebug(
|
||||
`Warning: Cannot find a service with name ${
|
||||
match[1]
|
||||
}. Treating the string as part of the environment variable name.`,
|
||||
`Warning: Cannot find a service with name ${match[1]}. Treating the string as part of the environment variable name.`,
|
||||
);
|
||||
match[2] = `${match[1]}:${match[2]}`;
|
||||
} else {
|
||||
@ -135,9 +133,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
|
||||
await api.ping();
|
||||
} catch (e) {
|
||||
exitWithExpectedError(
|
||||
`Could not communicate with local mode device at address ${
|
||||
opts.deviceHost
|
||||
}`,
|
||||
`Could not communicate with local mode device at address ${opts.deviceHost}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -45,13 +45,9 @@ export function stateToString(state: OperationState) {
|
||||
|
||||
switch (state.operation.command) {
|
||||
case 'copy':
|
||||
return `${result} ${state.operation.from.path} -> ${
|
||||
state.operation.to.path
|
||||
}`;
|
||||
return `${result} ${state.operation.from.path} -> ${state.operation.to.path}`;
|
||||
case 'replace':
|
||||
return `${result} ${state.operation.file.path}, ${
|
||||
state.operation.copy
|
||||
} -> ${state.operation.replace}`;
|
||||
return `${result} ${state.operation.file.path}, ${state.operation.copy} -> ${state.operation.replace}`;
|
||||
case 'run-script':
|
||||
return `${result} ${state.operation.script}`;
|
||||
default:
|
||||
|
@ -116,7 +116,10 @@ export class FileIgnorer {
|
||||
}
|
||||
|
||||
// Don't ignore Dockerfile (with or without extension) or docker-compose.yml
|
||||
if (/^Dockerfile$|^Dockerfile\.\S+/.test(path.basename(relFile)) || path.basename(relFile) === 'docker-compose.yml') {
|
||||
if (
|
||||
/^Dockerfile$|^Dockerfile\.\S+/.test(path.basename(relFile)) ||
|
||||
path.basename(relFile) === 'docker-compose.yml'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -366,10 +366,12 @@ export async function getOnlineTargetUuid(
|
||||
logger.logDebug(
|
||||
`Fetching device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
||||
);
|
||||
return (await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid'],
|
||||
$filter: { is_online: true },
|
||||
})).uuid;
|
||||
return (
|
||||
await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid'],
|
||||
$filter: { is_online: true },
|
||||
})
|
||||
).uuid;
|
||||
}
|
||||
|
||||
// otherwise, it may be a device OR an application...
|
||||
@ -409,10 +411,12 @@ export async function getOnlineTargetUuid(
|
||||
logger.logDebug(
|
||||
`Fetching device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
||||
);
|
||||
return (await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid'],
|
||||
$filter: { is_online: true },
|
||||
})).uuid;
|
||||
return (
|
||||
await sdk.models.device.get(applicationOrDevice, {
|
||||
$select: ['uuid'],
|
||||
$filter: { is_online: true },
|
||||
})
|
||||
).uuid;
|
||||
}
|
||||
|
||||
export function selectFromList<T>(
|
||||
|
@ -332,9 +332,7 @@ function createRemoteBuildRequest(
|
||||
if (response.statusCode >= 100 && response.statusCode < 400) {
|
||||
if (DEBUG_MODE) {
|
||||
console.error(
|
||||
`[debug] received HTTP ${response.statusCode} ${
|
||||
response.statusMessage
|
||||
}`,
|
||||
`[debug] received HTTP ${response.statusCode} ${response.statusMessage}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
104
npm-shrinkwrap.json
generated
104
npm-shrinkwrap.json
generated
@ -707,9 +707,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "1.16.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.16.4.tgz",
|
||||
"integrity": "sha512-MG7ExKBo7AQ5UrL1awyYLNinNM/kyXgE4iP4Ul9fB+T7n768Z5Xem8IZeP6Bna0xze8gkDly49Rgge2HOEw4xA==",
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.0.tgz",
|
||||
"integrity": "sha512-gDE8JJEygpay7IjA/u3JiIURvwZW08f0cZSZLAzFoX/ZmeqvS0Sqv+97aKuHpNsalAMMhwPe+iAS6fQbfmbt7A==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prettyjson": {
|
||||
@ -2702,9 +2702,9 @@
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"catch-uncommitted": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/catch-uncommitted/-/catch-uncommitted-1.3.0.tgz",
|
||||
"integrity": "sha512-JJrlxvOX8mLEmQ7zk/w+su70FQeuTkRH9OYqWg8df3YLjz+rEkHKlWx0+C3/jjWZxRSrB1JBVhS5MhXJ3VhU1A==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/catch-uncommitted/-/catch-uncommitted-1.4.0.tgz",
|
||||
"integrity": "sha512-xrLMj7iYrMc3TXSLsRO9tTxfcWEUICGCDZm+WI40WznxLp/+mVE8v4RxipC/ufL5TDfAYAe1ppu5VURBN990SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"chai": {
|
||||
@ -3033,6 +3033,12 @@
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"coffee-script": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz",
|
||||
"integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==",
|
||||
"dev": true
|
||||
},
|
||||
"coffeelint": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/coffeelint/-/coffeelint-1.16.2.tgz",
|
||||
@ -3760,17 +3766,6 @@
|
||||
"lru-cache": "^4.0.1",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
@ -3905,6 +3900,15 @@
|
||||
"read-pkg": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
@ -3949,9 +3953,9 @@
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"deprecate": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/deprecate/-/deprecate-1.1.0.tgz",
|
||||
"integrity": "sha512-b5dDNQYdy2vW9WXUD8+RQlfoxvqztLLhDE+T7Gd37I5E8My7nJkKu6FmhdDeRWJ8B+yjZKuwjCta8pgi8kgSqA==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecate/-/deprecate-1.1.1.tgz",
|
||||
"integrity": "sha512-ZGDXefq1xknT292LnorMY5s8UVU08/WKdzDZCUT6t9JzsiMSP4uzUhgpqugffNVcT5WC6wMBiSQ+LFjlv3v7iQ==",
|
||||
"dev": true
|
||||
},
|
||||
"deprecation": {
|
||||
@ -13873,9 +13877,9 @@
|
||||
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.0.tgz",
|
||||
"integrity": "sha512-sXe5lSt2WQlCbydGETgfm1YBShgOX4HxQkFPvbxkcwgDvGDeqVau8h+12+lmSVlP3rHPz0oavfddSZg/q+Szjw==",
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-bytes": {
|
||||
@ -15478,9 +15482,9 @@
|
||||
}
|
||||
},
|
||||
"resin-lint": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/resin-lint/-/resin-lint-3.0.4.tgz",
|
||||
"integrity": "sha512-TVxY7SaJqQRZcLubJn5yO49db/M4eRXRr7FbA4xwqSYxQSqujNql8ThMoNMoRrx+1F7NrfSdhIsLEaMqCea4VA==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/resin-lint/-/resin-lint-3.1.1.tgz",
|
||||
"integrity": "sha512-BgIsrj9fvWcELoqfiu0dGflqkysByn7m/XVgbv19YdnnVToEtyQkFzfF9oY+h6nnr45pRYkorE6NAFYaVaYhLQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/bluebird": "^3.5.26",
|
||||
@ -15488,7 +15492,7 @@
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/node": "^8.10.45",
|
||||
"@types/optimist": "0.0.29",
|
||||
"@types/prettier": "^1.16.1",
|
||||
"@types/prettier": "^1.18.3",
|
||||
"bluebird": "^3.5.4",
|
||||
"coffee-script": "^1.10.0",
|
||||
"coffeelint": "^1.15.0",
|
||||
@ -15497,7 +15501,7 @@
|
||||
"glob": "^7.0.3",
|
||||
"merge": "^1.2.0",
|
||||
"optimist": "^0.6.1",
|
||||
"prettier": "^1.16.4",
|
||||
"prettier": "^1.19.1",
|
||||
"tslint": "^5.15.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-no-unused-expression-chai": "^0.1.4",
|
||||
@ -15505,9 +15509,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/bluebird": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.27.tgz",
|
||||
"integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==",
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz",
|
||||
"integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/glob": {
|
||||
@ -15522,15 +15526,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "8.10.49",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.49.tgz",
|
||||
"integrity": "sha512-YX30JVx0PvSmJ3Eqr74fYLGeBxD+C7vIL20ek+GGGLJeUbVYRUW3EzyAXpIRA0K8c8o0UWqR/GwEFYiFoz1T8w==",
|
||||
"dev": true
|
||||
},
|
||||
"coffee-script": {
|
||||
"version": "1.12.7",
|
||||
"resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz",
|
||||
"integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==",
|
||||
"version": "8.10.59",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz",
|
||||
"integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -17141,16 +17139,16 @@
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||
},
|
||||
"tslint": {
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.18.0.tgz",
|
||||
"integrity": "sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==",
|
||||
"version": "5.20.1",
|
||||
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
|
||||
"integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"builtin-modules": "^1.1.1",
|
||||
"chalk": "^2.3.0",
|
||||
"commander": "^2.12.1",
|
||||
"diff": "^3.2.0",
|
||||
"diff": "^4.0.1",
|
||||
"glob": "^7.1.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"minimatch": "^3.0.4",
|
||||
@ -17162,15 +17160,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
|
||||
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
|
||||
"dev": true
|
||||
},
|
||||
"diff": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
@ -17191,9 +17183,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"tsutils": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz",
|
||||
"integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==",
|
||||
"version": "3.17.1",
|
||||
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
|
||||
"integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.8.1"
|
||||
|
@ -41,7 +41,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"prebuild": "rimraf build/ build-bin/",
|
||||
"build": "npm run build:src",
|
||||
"build": "npm run build:src && npm run catch-uncommitted",
|
||||
"build:src": "npm run prettify && npm run lint && npm run build:fast && npm run build:doc",
|
||||
"build:fast": "gulp build && tsc",
|
||||
"build:doc": "mkdirp doc/ && ts-node --type-check -P automation/tsconfig.json automation/capitanodoc/index.ts > doc/cli.markdown",
|
||||
@ -52,6 +52,7 @@
|
||||
"pretest": "npm run build",
|
||||
"test": "mocha --timeout 6000 -r ts-node/register \"tests/**/*.spec.ts\"",
|
||||
"test:fast": "npm run build:fast && npm run test",
|
||||
"catch-uncommitted": "ts-node --type-check -P automation/tsconfig.json automation/run.ts catch-uncommitted",
|
||||
"ci": "npm run test && catch-uncommitted",
|
||||
"watch": "gulp watch",
|
||||
"prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.[tj]s\" --config ./node_modules/resin-lint/config/.prettierrc",
|
||||
@ -124,7 +125,7 @@
|
||||
"@types/tar-stream": "1.6.0",
|
||||
"@types/through2": "2.0.33",
|
||||
"@types/which": "^1.3.2",
|
||||
"catch-uncommitted": "^1.3.0",
|
||||
"catch-uncommitted": "^1.4.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"ent": "^2.2.0",
|
||||
@ -140,9 +141,9 @@
|
||||
"nock": "^11.0.7",
|
||||
"parse-link-header": "~1.0.1",
|
||||
"pkg": "^4.4.0",
|
||||
"prettier": "1.17.0",
|
||||
"prettier": "^1.19.1",
|
||||
"publish-release": "^1.6.0",
|
||||
"resin-lint": "^3.0.1",
|
||||
"resin-lint": "^3.1.1",
|
||||
"rewire": "^3.0.2",
|
||||
"sinon": "^7.4.1",
|
||||
"ts-node": "^8.1.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,59 +16,111 @@
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
|
||||
export class BalenaAPIMock {
|
||||
public static basePathPattern = /api\.balena-cloud\.com/;
|
||||
public readonly scope: nock.Scope;
|
||||
// Expose `scope` as `expect` to allow for better semantics in tests
|
||||
public readonly expect = this.scope;
|
||||
import { NockMock, ScopeOpts } from './nock-mock';
|
||||
|
||||
// For debugging tests
|
||||
get unfulfilledCallCount(): number {
|
||||
return this.scope.pendingMocks().length;
|
||||
}
|
||||
const apiResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'api-response'),
|
||||
);
|
||||
|
||||
const jHeader = { 'Content-Type': 'application/json' };
|
||||
|
||||
export class BalenaAPIMock extends NockMock {
|
||||
constructor() {
|
||||
nock.cleanAll();
|
||||
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
|
||||
this.scope = nock(BalenaAPIMock.basePathPattern);
|
||||
|
||||
nock.emitter.on('no match', this.handleUnexpectedRequest);
|
||||
super('https://api.balena-cloud.com');
|
||||
}
|
||||
|
||||
public done() {
|
||||
// scope.done() will throw an error if there are expected api calls that have not happened.
|
||||
// So ensures that all expected calls have been made.
|
||||
this.scope.done();
|
||||
// Remove 'no match' handler, for tests using nock without this module
|
||||
nock.emitter.removeListener('no match', this.handleUnexpectedRequest);
|
||||
// Restore unmocked behavior
|
||||
nock.cleanAll();
|
||||
nock.restore();
|
||||
public expectGetApplication(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v5\/application($|[(?])/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'application-GET-v5-expanded-app-type.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectTestApp() {
|
||||
this.scope
|
||||
.get(/^\/v\d+\/application($|\?)/)
|
||||
.reply(200, { d: [{ id: 1234567 }] });
|
||||
public expectGetMyApplication(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v5\/my_application($|[(?])/, opts).reply(
|
||||
200,
|
||||
JSON.parse(`{"d": [{
|
||||
"user": [{ "username": "bob", "__metadata": {} }],
|
||||
"id": 1301645,
|
||||
"__metadata": { "uri": "/resin/my_application(@id)?@id=1301645" }}]}
|
||||
`),
|
||||
);
|
||||
}
|
||||
|
||||
public expectTestDevice(
|
||||
fullUUID = 'f63fd7d7812c34c4c14ae023fdff05f5',
|
||||
inaccessibleApp = false,
|
||||
) {
|
||||
public expectGetAuth(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/auth\/v1\//, opts).reply(200, {
|
||||
// "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJZVFk6TlE3WDpKSDVCOlFFWFk6RkU2TjpLTlVVOklWNTI6TFFRQTo3UjRWOjJVUFI6Qk9ISjpDNklPIn0.eyJqdGkiOiI3ZTNlN2RmMS1iYjljLTQxZTMtOTlkMi00NjVlMjE4YzFmOWQiLCJuYmYiOjE1NzkxOTQ1MjgsImFjY2VzcyI6W3sibmFtZSI6InYyL2MwODljNDIxZmIyMzM2ZDA0NzUxNjZmYmYzZDBmOWZhIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsibmFtZSI6InYyLzljMDBjOTQxMzk0MmNkMTVjZmM5MTg5YzVkYWMzNTlkIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19XSwiaWF0IjoxNTc5MTk0NTM4LCJleHAiOjE1NzkyMDg5MzgsImF1ZCI6InJlZ2lzdHJ5Mi5iYWxlbmEtY2xvdWQuY29tIiwiaXNzIjoiYXBpLmJhbGVuYS1jbG91ZC5jb20iLCJzdWIiOiJnaF9wYXVsb19jYXN0cm8ifQ.bRw5_lg-nT-c1V4RxIJjujfPuVewZTs0BRNENEw2-sk_6zepLs-sLl9DOSEHYBdi87EtyCiUB3Wqee6fvz2HyQ"
|
||||
token: 'test',
|
||||
});
|
||||
}
|
||||
|
||||
public expectGetRelease(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v5\/release($|[(?])/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'release-GET-v5.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectPatchRelease(opts: ScopeOpts = {}) {
|
||||
this.optPatch(/^\/v5\/release($|[(?])/, opts).reply(200, 'OK');
|
||||
}
|
||||
|
||||
public expectPostRelease(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/v5\/release($|[(?])/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'release-POST-v5.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectPatchImage(opts: ScopeOpts = {}) {
|
||||
this.optPatch(/^\/v5\/image($|[(?])/, opts).reply(200, 'OK');
|
||||
}
|
||||
|
||||
public expectPostImage(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/v5\/image($|[(?])/, opts).replyWithFile(
|
||||
201,
|
||||
path.join(apiResponsePath, 'image-POST-v5.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectPostImageLabel(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/v5\/image_label($|[(?])/, opts).replyWithFile(
|
||||
201,
|
||||
path.join(apiResponsePath, 'image-label-POST-v5.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectPostImageIsPartOfRelease(opts: ScopeOpts = {}) {
|
||||
this.optPost(
|
||||
/^\/v5\/image__is_part_of__release($|[(?])/,
|
||||
opts,
|
||||
).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'image-is-part-of-release-POST-v5.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectGetDevice(opts: {
|
||||
fullUUID: string;
|
||||
inaccessibleApp?: boolean;
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
}) {
|
||||
const id = 7654321;
|
||||
this.scope.get(/^\/v\d+\/device($|\?)/).reply(200, {
|
||||
this.optGet(/^\/v\d+\/device($|\?)/, opts).reply(200, {
|
||||
d: [
|
||||
{
|
||||
id,
|
||||
uuid: fullUUID,
|
||||
belongs_to__application: inaccessibleApp
|
||||
uuid: opts.fullUUID,
|
||||
belongs_to__application: opts.inaccessibleApp
|
||||
? []
|
||||
: [{ app_name: 'test' }],
|
||||
},
|
||||
@ -76,10 +128,10 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectAppEnvVars() {
|
||||
this.scope
|
||||
.get(/^\/v\d+\/application_environment_variable($|\?)/)
|
||||
.reply(200, {
|
||||
public expectGetAppEnvVars(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v\d+\/application_environment_variable($|\?)/, opts).reply(
|
||||
200,
|
||||
{
|
||||
d: [
|
||||
{
|
||||
id: 120101,
|
||||
@ -92,11 +144,12 @@ export class BalenaAPIMock {
|
||||
value: '22',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public expectAppConfigVars() {
|
||||
this.scope.get(/^\/v\d+\/application_config_variable($|\?)/).reply(200, {
|
||||
public expectGetAppConfigVars(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v\d+\/application_config_variable($|\?)/, opts).reply(200, {
|
||||
d: [
|
||||
{
|
||||
id: 120300,
|
||||
@ -107,10 +160,9 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectAppServiceVars() {
|
||||
this.scope
|
||||
.get(/^\/v\d+\/service_environment_variable($|\?)/)
|
||||
.reply(function(uri, _requestBody) {
|
||||
public expectGetAppServiceVars(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
|
||||
function(uri, _requestBody) {
|
||||
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
|
||||
const serviceName = (match && match[1]) || undefined;
|
||||
let varArray: any[];
|
||||
@ -121,11 +173,12 @@ export class BalenaAPIMock {
|
||||
varArray = _.map(appServiceVarsByService, value => value);
|
||||
}
|
||||
return [200, { d: varArray }];
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public expectDeviceEnvVars() {
|
||||
this.scope.get(/^\/v\d+\/device_environment_variable($|\?)/).reply(200, {
|
||||
public expectGetDeviceEnvVars(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v\d+\/device_environment_variable($|\?)/, opts).reply(200, {
|
||||
d: [
|
||||
{
|
||||
id: 120203,
|
||||
@ -141,8 +194,8 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectDeviceConfigVars() {
|
||||
this.scope.get(/^\/v\d+\/device_config_variable($|\?)/).reply(200, {
|
||||
public expectGetDeviceConfigVars(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v\d+\/device_config_variable($|\?)/, opts).reply(200, {
|
||||
d: [
|
||||
{
|
||||
id: 120400,
|
||||
@ -153,25 +206,34 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectDeviceServiceVars() {
|
||||
this.scope
|
||||
.get(/^\/v\d+\/device_service_environment_variable($|\?)/)
|
||||
.reply(function(uri, _requestBody) {
|
||||
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
|
||||
const serviceName = (match && match[1]) || undefined;
|
||||
let varArray: any[];
|
||||
if (serviceName) {
|
||||
const varObj = deviceServiceVarsByService[serviceName];
|
||||
varArray = varObj ? [varObj] : [];
|
||||
} else {
|
||||
varArray = _.map(deviceServiceVarsByService, value => value);
|
||||
}
|
||||
return [200, { d: varArray }];
|
||||
});
|
||||
public expectGetDeviceServiceVars(opts: ScopeOpts = {}) {
|
||||
this.optGet(
|
||||
/^\/v\d+\/device_service_environment_variable($|\?)/,
|
||||
opts,
|
||||
).reply(function(uri, _requestBody) {
|
||||
const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
|
||||
const serviceName = (match && match[1]) || undefined;
|
||||
let varArray: any[];
|
||||
if (serviceName) {
|
||||
const varObj = deviceServiceVarsByService[serviceName];
|
||||
varArray = varObj ? [varObj] : [];
|
||||
} else {
|
||||
varArray = _.map(deviceServiceVarsByService, value => value);
|
||||
}
|
||||
return [200, { d: varArray }];
|
||||
});
|
||||
}
|
||||
|
||||
public expectConfigVars() {
|
||||
this.scope.get('/config/vars').reply(200, {
|
||||
public expectGetDeviceTypes(opts: ScopeOpts = {}) {
|
||||
this.optGet('/device-types/v1', opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'device-types-GET-v1.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
public expectGetConfigVars(opts: ScopeOpts = {}) {
|
||||
this.optGet('/config/vars', opts).reply(200, {
|
||||
reservedNames: [],
|
||||
reservedNamespaces: [],
|
||||
invalidRegex: '/^d|W/',
|
||||
@ -182,52 +244,53 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectService(serviceName: string, serviceId = 243768) {
|
||||
this.scope.get(/^\/v\d+\/service($|\?)/).reply(200, {
|
||||
d: [{ id: serviceId, service_name: serviceName }],
|
||||
public expectGetService(opts: {
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
serviceId?: number;
|
||||
serviceName: string;
|
||||
}) {
|
||||
const serviceId = opts.serviceId || 243768;
|
||||
this.optGet(/^\/v\d+\/service($|\?)/, opts).reply(200, {
|
||||
d: [{ id: serviceId, service_name: opts.serviceName }],
|
||||
});
|
||||
}
|
||||
|
||||
public expectPostService404(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/v\d+\/service$/, opts).reply(
|
||||
404,
|
||||
'Unique key constraint violated',
|
||||
);
|
||||
}
|
||||
|
||||
public expectGetUser(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v5\/user/, opts).reply(200, {
|
||||
d: [
|
||||
{
|
||||
id: 99999,
|
||||
actor: 1234567,
|
||||
username: 'gh_user',
|
||||
created_at: '2018-08-19T13:55:04.485Z',
|
||||
__metadata: {
|
||||
uri: '/resin/user(@id)?@id=43699',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// User details are cached in the SDK
|
||||
// so often we don't know if we can expect the whoami request
|
||||
public expectWhoAmI(persist = false, optional = true) {
|
||||
const get = (persist ? this.scope.persist() : this.scope).get(
|
||||
'/user/v1/whoami',
|
||||
);
|
||||
(optional ? get.optionally() : get).reply(200, {
|
||||
public expectGetWhoAmI(opts: ScopeOpts = {}) {
|
||||
this.optGet('/user/v1/whoami', opts).reply(200, {
|
||||
id: 99999,
|
||||
username: 'testuser',
|
||||
username: 'gh_user',
|
||||
email: 'testuser@test.com',
|
||||
});
|
||||
}
|
||||
|
||||
public expectMixpanel(optional = true) {
|
||||
const get = this.scope.get(/^\/mixpanel\/track/);
|
||||
(optional ? get.optionally() : get).reply(200, {});
|
||||
}
|
||||
|
||||
protected handleUnexpectedRequest(req: any) {
|
||||
console.error(`Unexpected http request!: ${req.path}`);
|
||||
// Errors thrown here are not causing the tests to fail for some reason.
|
||||
// Possibly due to CLI global error handlers? (error.js)
|
||||
// (Also, nock should automatically throw an error, but also not happening)
|
||||
// For now, the console.error is sufficient (will fail the test)
|
||||
}
|
||||
|
||||
public debug() {
|
||||
const scope = this.scope;
|
||||
let mocks = scope.pendingMocks();
|
||||
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
||||
|
||||
this.scope.on('request', function(_req, _interceptor, _body) {
|
||||
console.log(`>> REQUEST:` + _req.path);
|
||||
mocks = scope.pendingMocks();
|
||||
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
||||
});
|
||||
|
||||
this.scope.on('replied', function(_req) {
|
||||
console.log(`<< REPLIED:` + _req.path);
|
||||
});
|
||||
public expectGetMixpanel(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/mixpanel\/track/, opts).reply(200, {});
|
||||
}
|
||||
}
|
||||
|
||||
|
60
tests/builder-mock.ts
Normal file
60
tests/builder-mock.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Bluebird = require('bluebird');
|
||||
import * as _ from 'lodash';
|
||||
import * as zlib from 'zlib';
|
||||
|
||||
import { NockMock } from './nock-mock';
|
||||
|
||||
export class BuilderMock extends NockMock {
|
||||
constructor() {
|
||||
super('https://builder.balena-cloud.com');
|
||||
}
|
||||
|
||||
public expectPostBuild(opts: {
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
responseBody: any;
|
||||
responseCode: number;
|
||||
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
|
||||
}) {
|
||||
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(async function(
|
||||
_uri,
|
||||
requestBody,
|
||||
callback,
|
||||
) {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
if (typeof requestBody === 'string') {
|
||||
const gzipped = Buffer.from(requestBody, 'hex');
|
||||
const gunzipped = await Bluebird.fromCallback<Buffer>(cb => {
|
||||
zlib.gunzip(gzipped, cb);
|
||||
});
|
||||
await opts.checkBuildRequestBody(gunzipped);
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected requestBody type "${typeof requestBody}"`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
callback(error, [opts.responseCode, opts.responseBody]);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,3 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
@ -37,8 +54,8 @@ describe('balena app create', function() {
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('app create -h');
|
||||
|
||||
|
110
tests/commands/build.spec.ts
Normal file
110
tests/commands/build.spec.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { configureBluebird } from '../../build/app-common';
|
||||
|
||||
configureBluebird();
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { DockerMock } from '../docker-mock';
|
||||
import {
|
||||
cleanOutput,
|
||||
inspectTarStream,
|
||||
runCommand,
|
||||
TarStreamFiles,
|
||||
} from '../helpers';
|
||||
|
||||
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
||||
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
|
||||
|
||||
describe('balena build', function() {
|
||||
let api: BalenaAPIMock;
|
||||
let docker: DockerMock;
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
docker = new DockerMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
docker.expectGetPing();
|
||||
docker.expectGetInfo();
|
||||
docker.expectGetVersion();
|
||||
docker.expectGetImages();
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
docker.done();
|
||||
});
|
||||
|
||||
it('should create the expected tar stream', async () => {
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const expectedFiles: TarStreamFiles = {
|
||||
'src/start.sh': { fileSize: 89, type: 'file' },
|
||||
Dockerfile: { fileSize: 85, type: 'file' },
|
||||
};
|
||||
const responseBody = stripIndent`
|
||||
{"stream":"Step 1/4 : FROM busybox"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e 64f5d945efcc\\n"}
|
||||
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e Using cache\\n"}
|
||||
{"stream":" ---\\u003e 97098fc9d757\\n"}
|
||||
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e Using cache\\n"}
|
||||
{"stream":" ---\\u003e 33728e2e3f7e\\n"}
|
||||
{"stream":"Step 4/4 : CMD [\\"/start.sh\\"]"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e Using cache\\n"}
|
||||
{"stream":" ---\\u003e 2590e3b11eaf\\n"}
|
||||
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
|
||||
{"stream":"Successfully built 2590e3b11eaf\\n"}
|
||||
{"stream":"Successfully tagged basic_main:latest\\n"}`;
|
||||
|
||||
docker.expectPostBuild({
|
||||
tag: 'basic_main',
|
||||
responseCode: 200,
|
||||
responseBody,
|
||||
checkBuildRequestBody: (buildRequestBody: string) =>
|
||||
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
|
||||
});
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`build ${projectPath} --deviceType nuc --arch amd64`,
|
||||
);
|
||||
|
||||
expect(err).to.have.members([]);
|
||||
expect(
|
||||
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members([
|
||||
`[Info] Creating default composition with source: ${projectPath}`,
|
||||
'[Info] Building for amd64/nuc',
|
||||
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
|
||||
'[Info] Docker itself will determine and enable architecture emulation if required,',
|
||||
'[Info] without balena-cli intervention and regardless of the --emulated option.',
|
||||
'[Build] main Image size: 1.14 MB',
|
||||
'[Success] Build succeeded!',
|
||||
]);
|
||||
});
|
||||
});
|
131
tests/commands/deploy.spec.ts
Normal file
131
tests/commands/deploy.spec.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { configureBluebird } from '../../build/app-common';
|
||||
|
||||
configureBluebird();
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { DockerMock } from '../docker-mock';
|
||||
import {
|
||||
cleanOutput,
|
||||
inspectTarStream,
|
||||
runCommand,
|
||||
TarStreamFiles,
|
||||
} from '../helpers';
|
||||
|
||||
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
||||
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
|
||||
|
||||
describe('balena deploy', function() {
|
||||
let api: BalenaAPIMock;
|
||||
let docker: DockerMock;
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
docker = new DockerMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetDeviceTypes();
|
||||
api.expectGetApplication();
|
||||
api.expectPatchRelease();
|
||||
api.expectPostRelease();
|
||||
api.expectGetRelease();
|
||||
api.expectGetUser();
|
||||
api.expectGetService({ serviceName: 'main' });
|
||||
api.expectPostService404();
|
||||
api.expectGetAuth();
|
||||
api.expectPostImage();
|
||||
api.expectPostImageIsPartOfRelease();
|
||||
api.expectPostImageLabel();
|
||||
api.expectPatchImage();
|
||||
|
||||
docker.expectGetPing();
|
||||
docker.expectGetInfo();
|
||||
docker.expectGetVersion();
|
||||
docker.expectGetImages({ persist: true });
|
||||
docker.expectPostImagesTag();
|
||||
docker.expectPostImagesPush();
|
||||
docker.expectDeleteImages();
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
docker.done();
|
||||
});
|
||||
|
||||
it('should create the expected --build tar stream', async () => {
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const expectedFiles: TarStreamFiles = {
|
||||
'src/start.sh': { fileSize: 89, type: 'file' },
|
||||
Dockerfile: { fileSize: 85, type: 'file' },
|
||||
};
|
||||
const responseBody = stripIndent`
|
||||
{"stream":"Step 1/4 : FROM busybox"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e 64f5d945efcc\\n"}
|
||||
{"stream":"Step 2/4 : COPY ./src/start.sh /start.sh"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e Using cache\\n"}
|
||||
{"stream":" ---\\u003e 97098fc9d757\\n"}
|
||||
{"stream":"Step 3/4 : RUN chmod a+x /start.sh"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e Using cache\\n"}
|
||||
{"stream":" ---\\u003e 33728e2e3f7e\\n"}
|
||||
{"stream":"Step 4/4 : CMD [\\"/start.sh\\"]"}
|
||||
{"stream":"\\n"}
|
||||
{"stream":" ---\\u003e Using cache\\n"}
|
||||
{"stream":" ---\\u003e 2590e3b11eaf\\n"}
|
||||
{"aux":{"ID":"sha256:2590e3b11eaf739491235016b53fec5d209c81837160abdd267c8fe5005ff1bd"}}
|
||||
{"stream":"Successfully built 2590e3b11eaf\\n"}
|
||||
{"stream":"Successfully tagged basic_main:latest\\n"}`;
|
||||
|
||||
docker.expectPostBuild({
|
||||
tag: 'basic_main',
|
||||
responseCode: 200,
|
||||
responseBody,
|
||||
checkBuildRequestBody: (buildRequestBody: string) =>
|
||||
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
|
||||
});
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`deploy testApp --build --source ${projectPath}`,
|
||||
);
|
||||
|
||||
expect(err).to.have.members([]);
|
||||
expect(
|
||||
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members([
|
||||
`[Info] Creating default composition with source: ${projectPath}`,
|
||||
'[Info] Building for armv7hf/raspberrypi3',
|
||||
'[Info] Docker Desktop detected (daemon architecture: "x86_64")',
|
||||
'[Info] Docker itself will determine and enable architecture emulation if required,',
|
||||
'[Info] without balena-cli intervention and regardless of the --emulated option.',
|
||||
'[Build] main Image size: 1.14 MB',
|
||||
'[Info] Creating release...',
|
||||
'[Info] Pushing images to registry...',
|
||||
'[Info] Saving release...',
|
||||
'[Success] Deploy succeeded!',
|
||||
'[Success] Release: 09f7c3e1fdec609be818002299edfc2a',
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,3 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
@ -32,8 +49,8 @@ describe('balena device move', function() {
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('device move -h');
|
||||
|
||||
@ -45,8 +62,8 @@ describe('balena device move', function() {
|
||||
it.skip('should error if uuid not provided', async () => {
|
||||
// TODO: Figure out how to test for expected errors with current setup
|
||||
// including exit codes if possible.
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('device move');
|
||||
const errLines = cleanOutput(err);
|
||||
|
@ -1,4 +1,23 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
@ -12,6 +31,10 @@ Examples:
|
||||
\t$ balena device 7cf02a6
|
||||
`;
|
||||
|
||||
const apiResponsePath = path.normalize(
|
||||
path.join(__dirname, '..', '..', 'test-data', 'api-response'),
|
||||
);
|
||||
|
||||
describe('balena device', function() {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -25,8 +48,8 @@ describe('balena device', function() {
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('device -h');
|
||||
|
||||
@ -38,8 +61,8 @@ describe('balena device', function() {
|
||||
it.skip('should error if uuid not provided', async () => {
|
||||
// TODO: Figure out how to test for expected errors with current setup
|
||||
// including exit codes if possible.
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('device');
|
||||
const errLines = cleanOutput(err);
|
||||
@ -49,12 +72,12 @@ describe('balena device', function() {
|
||||
});
|
||||
|
||||
it('should list device details for provided uuid', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
api.scope
|
||||
.get(/^\/v5\/device/)
|
||||
.replyWithFile(200, __dirname + '/device.api-response.json', {
|
||||
.replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
@ -72,14 +95,18 @@ describe('balena device', function() {
|
||||
it('correctly handles devices with missing application', async () => {
|
||||
// Devices with missing applications will have application name set to `N/a`.
|
||||
// e.g. When user has a device associated with app that user is no longer a collaborator of.
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
api.scope
|
||||
.get(/^\/v5\/device/)
|
||||
.replyWithFile(200, __dirname + '/device.api-response.missing-app.json', {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
.replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'device-missing-app.json'),
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
const { out, err } = await runCommand('device 27fda508c');
|
||||
|
||||
|
@ -1,4 +1,23 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
@ -21,6 +40,10 @@ Options:
|
||||
--application, -a, --app <application> application name
|
||||
`;
|
||||
|
||||
const apiResponsePath = path.normalize(
|
||||
path.join(__dirname, '..', '..', 'test-data', 'api-response'),
|
||||
);
|
||||
|
||||
describe('balena devices', function() {
|
||||
let api: BalenaAPIMock;
|
||||
|
||||
@ -34,8 +57,8 @@ describe('balena devices', function() {
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('devices -h');
|
||||
|
||||
@ -45,14 +68,14 @@ describe('balena devices', function() {
|
||||
});
|
||||
|
||||
it('should list devices from own and collaborator apps', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
api.scope
|
||||
.get(
|
||||
'/v5/device?$orderby=device_name%20asc&$expand=belongs_to__application($select=app_name)',
|
||||
)
|
||||
.replyWithFile(200, __dirname + '/devices.api-response.json', {
|
||||
.replyWithFile(200, path.join(apiResponsePath, 'devices.json'), {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { BalenaAPIMock } from '../../balena-api-mock';
|
||||
import { cleanOutput, runCommand } from '../../helpers';
|
||||
|
||||
@ -15,8 +33,8 @@ describe('balena devices supported', function() {
|
||||
});
|
||||
|
||||
it('should print help text with the -h flag', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
const { out, err } = await runCommand('devices supported -h');
|
||||
|
||||
@ -26,15 +44,9 @@ describe('balena devices supported', function() {
|
||||
});
|
||||
|
||||
it('should list currently supported devices, with correct filtering', async () => {
|
||||
api.expectWhoAmI();
|
||||
api.expectMixpanel();
|
||||
|
||||
// TODO: Using the alias api.expect here causes route /config/vars to be called unexpectedly - why?
|
||||
api.scope
|
||||
.get('/device-types/v1')
|
||||
.replyWithFile(200, __dirname + '/device-types.api-response.json', {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetDeviceTypes();
|
||||
|
||||
const { out, err } = await runCommand('devices supported');
|
||||
|
||||
|
14
tests/commands/env/add.spec.ts
vendored
14
tests/commands/env/add.spec.ts
vendored
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -25,8 +25,8 @@ describe('balena env add', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectWhoAmI(true);
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -35,14 +35,14 @@ describe('balena env add', function() {
|
||||
});
|
||||
|
||||
it('should successfully add an environment variable', async () => {
|
||||
const deviceId = 'f63fd7d7812c34c4c14ae023fdff05f5';
|
||||
api.expectTestDevice();
|
||||
api.expectConfigVars();
|
||||
const fullUUID = 'f63fd7d7812c34c4c14ae023fdff05f5';
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetConfigVars();
|
||||
api.scope
|
||||
.post(/^\/v\d+\/device_environment_variable($|\?)/)
|
||||
.reply(200, 'OK');
|
||||
|
||||
const { out, err } = await runCommand(`env add TEST 1 -d ${deviceId}`);
|
||||
const { out, err } = await runCommand(`env add TEST 1 -d ${fullUUID}`);
|
||||
|
||||
expect(out.join('')).to.equal('');
|
||||
expect(err.join('')).to.equal('');
|
||||
|
118
tests/commands/env/envs.spec.ts
vendored
118
tests/commands/env/envs.spec.ts
vendored
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -29,8 +29,8 @@ describe('balena envs', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectWhoAmI(true);
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
// Random device UUID used to frustrate _.memoize() in utils/cloud.ts
|
||||
fullUUID = require('crypto')
|
||||
.randomBytes(16)
|
||||
@ -44,8 +44,8 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list env vars for a test app', async () => {
|
||||
api.expectTestApp();
|
||||
api.expectAppEnvVars();
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName}`);
|
||||
|
||||
@ -60,8 +60,8 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list config vars for a test app', async () => {
|
||||
api.expectTestApp();
|
||||
api.expectAppConfigVars();
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppConfigVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName} --config`);
|
||||
|
||||
@ -75,8 +75,8 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list config vars for a test app (JSON output)', async () => {
|
||||
api.expectTestApp();
|
||||
api.expectAppConfigVars();
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppConfigVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -cja ${appName}`);
|
||||
|
||||
@ -92,9 +92,9 @@ describe('balena envs', function() {
|
||||
|
||||
it('should successfully list service variables for a test app (-s flag)', async () => {
|
||||
const serviceName = 'service2';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectAppServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName}`,
|
||||
@ -111,9 +111,9 @@ describe('balena envs', function() {
|
||||
|
||||
it('should produce an empty JSON array when no app service variables exist', async () => {
|
||||
const serviceName = 'nono';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectAppServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} -s ${serviceName} -j`,
|
||||
@ -124,9 +124,9 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list env and service vars for a test app (--all flag)', async () => {
|
||||
api.expectTestApp();
|
||||
api.expectAppEnvVars();
|
||||
api.expectAppServiceVars();
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -a ${appName} --all`);
|
||||
|
||||
@ -144,10 +144,10 @@ describe('balena envs', function() {
|
||||
|
||||
it('should successfully list env and service vars for a test app (--all -s flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectAppEnvVars();
|
||||
api.expectAppServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -a ${appName} --all -s ${serviceName}`,
|
||||
@ -165,8 +165,8 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list env variables for a test device', async () => {
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceEnvVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -d ${shortUUID}`);
|
||||
|
||||
@ -181,8 +181,8 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list env variables for a test device (JSON output)', async () => {
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceEnvVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -jd ${shortUUID}`);
|
||||
|
||||
@ -202,8 +202,8 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list config variables for a test device', async () => {
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceConfigVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceConfigVars();
|
||||
|
||||
const { out, err } = await runCommand(`envs -d ${shortUUID} --config`);
|
||||
|
||||
@ -218,10 +218,10 @@ describe('balena envs', function() {
|
||||
|
||||
it('should successfully list service variables for a test device (-s flag)', async () => {
|
||||
const serviceName = 'service2';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} -s ${serviceName}`,
|
||||
@ -238,10 +238,10 @@ describe('balena envs', function() {
|
||||
|
||||
it('should produce an empty JSON array when no device service variables exist', async () => {
|
||||
const serviceName = 'nono';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} -s ${serviceName} -j`,
|
||||
@ -252,12 +252,12 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list env and service variables for a test device (--all flag)', async () => {
|
||||
api.expectTestApp();
|
||||
api.expectAppEnvVars();
|
||||
api.expectAppServiceVars();
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceEnvVars();
|
||||
api.expectDeviceServiceVars();
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} --all`);
|
||||
@ -279,9 +279,9 @@ describe('balena envs', function() {
|
||||
});
|
||||
|
||||
it('should successfully list env and service variables for a test device (unknown app)', async () => {
|
||||
api.expectTestDevice(fullUUID, true);
|
||||
api.expectDeviceEnvVars();
|
||||
api.expectDeviceServiceVars();
|
||||
api.expectGetDevice({ fullUUID, inaccessibleApp: true });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(`envs -d ${uuid} --all`);
|
||||
@ -300,13 +300,13 @@ describe('balena envs', function() {
|
||||
|
||||
it('should successfully list env and service vars for a test device (--all -s flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectAppEnvVars();
|
||||
api.expectAppServiceVars();
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceEnvVars();
|
||||
api.expectDeviceServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const uuid = shortUUID;
|
||||
const { out, err } = await runCommand(
|
||||
@ -329,13 +329,13 @@ describe('balena envs', function() {
|
||||
|
||||
it('should successfully list env and service vars for a test device (--all -js flags)', async () => {
|
||||
const serviceName = 'service1';
|
||||
api.expectService(serviceName);
|
||||
api.expectTestApp();
|
||||
api.expectAppEnvVars();
|
||||
api.expectAppServiceVars();
|
||||
api.expectTestDevice(fullUUID);
|
||||
api.expectDeviceEnvVars();
|
||||
api.expectDeviceServiceVars();
|
||||
api.expectGetService({ serviceName });
|
||||
api.expectGetApplication();
|
||||
api.expectGetAppEnvVars();
|
||||
api.expectGetAppServiceVars();
|
||||
api.expectGetDevice({ fullUUID });
|
||||
api.expectGetDeviceEnvVars();
|
||||
api.expectGetDeviceServiceVars();
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`envs -d ${shortUUID} --all -js ${serviceName}`,
|
||||
|
6
tests/commands/env/rename.spec.ts
vendored
6
tests/commands/env/rename.spec.ts
vendored
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -25,8 +25,8 @@ describe('balena env rename', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectWhoAmI(true);
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
6
tests/commands/env/rm.spec.ts
vendored
6
tests/commands/env/rm.spec.ts
vendored
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -25,8 +25,8 @@ describe('balena env rm', function() {
|
||||
|
||||
beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectWhoAmI(true);
|
||||
api.expectMixpanel();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
121
tests/commands/push.spec.ts
Normal file
121
tests/commands/push.spec.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { configureBluebird } from '../../build/app-common';
|
||||
|
||||
configureBluebird();
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { fs } from 'mz';
|
||||
import * as path from 'path';
|
||||
|
||||
import { BalenaAPIMock } from '../balena-api-mock';
|
||||
import { BuilderMock } from '../builder-mock';
|
||||
// import { DockerMock } from '../docker-mock';
|
||||
import {
|
||||
cleanOutput,
|
||||
inspectTarStream,
|
||||
runCommand,
|
||||
TarStreamFiles,
|
||||
} from '../helpers';
|
||||
|
||||
const repoPath = path.normalize(path.join(__dirname, '..', '..'));
|
||||
const projectsPath = path.join(repoPath, 'tests', 'test-data', 'projects');
|
||||
const builderResponsePath = path.normalize(
|
||||
path.join(__dirname, '..', 'test-data', 'builder-response'),
|
||||
);
|
||||
|
||||
describe('balena push', function() {
|
||||
let api: BalenaAPIMock;
|
||||
let builder: BuilderMock;
|
||||
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
builder = new BuilderMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
api.expectGetMyApplication();
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
// Check all expected api calls have been made and clean up.
|
||||
api.done();
|
||||
builder.done();
|
||||
});
|
||||
|
||||
it('should create the expected tar stream', async () => {
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const expectedFiles: TarStreamFiles = {
|
||||
'src/start.sh': { fileSize: 89, type: 'file' },
|
||||
Dockerfile: { fileSize: 85, type: 'file' },
|
||||
};
|
||||
const responseBody = await fs.readFile(
|
||||
path.join(builderResponsePath, 'build-POST-v3.json'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
builder.expectPostBuild({
|
||||
responseCode: 200,
|
||||
responseBody,
|
||||
checkBuildRequestBody: (buildRequestBody: string | Buffer) =>
|
||||
inspectTarStream(buildRequestBody, expectedFiles, projectPath, expect),
|
||||
});
|
||||
|
||||
const { out, err } = await runCommand(
|
||||
`push testApp --source ${projectPath}`,
|
||||
);
|
||||
|
||||
expect(err).to.have.members([]);
|
||||
expect(
|
||||
cleanOutput(out).map(line =>
|
||||
line
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.replace(/in \d+? seconds/, 'in 20 seconds'),
|
||||
),
|
||||
).to.include.members([
|
||||
'[Info] Starting build for testApp, user gh_user',
|
||||
'[Info] Dashboard link: https://dashboard.balena-cloud.com/apps/1301645/devices',
|
||||
'[Info] Building on arm01',
|
||||
'[Info] Pulling previous images for caching purposes...',
|
||||
'[Success] Successfully pulled cache images',
|
||||
'[main] Step 1/4 : FROM busybox',
|
||||
'[main] ---> 76aea0766768',
|
||||
'[main] Step 2/4 : COPY ./src/start.sh /start.sh',
|
||||
'[main] ---> b563ad6a0801',
|
||||
'[main] Step 3/4 : RUN chmod a+x /start.sh',
|
||||
'[main] ---> Running in 10d4ddc40bfc',
|
||||
'[main] Removing intermediate container 10d4ddc40bfc',
|
||||
'[main] ---> 82e98871a32c',
|
||||
'[main] Step 4/4 : CMD ["/start.sh"]',
|
||||
'[main] ---> Running in 0682894e13eb',
|
||||
'[main] Removing intermediate container 0682894e13eb',
|
||||
'[main] ---> 889ccb6afc7c',
|
||||
'[main] Successfully built 889ccb6afc7c',
|
||||
'[Info] Uploading images',
|
||||
'[Success] Successfully uploaded images',
|
||||
'[Info] Built on arm01',
|
||||
'[Success] Release successfully created!',
|
||||
'[Info] Release: 05a24b5b034c9f95f25d4d74f0593bea (id: 1220245)',
|
||||
'[Info] ┌─────────┬────────────┬────────────┐',
|
||||
'[Info] │ Service │ Image Size │ Build Time │',
|
||||
'[Info] ├─────────┼────────────┼────────────┤',
|
||||
'[Info] │ main │ 1.32 MB │ 11 seconds │',
|
||||
'[Info] └─────────┴────────────┴────────────┘',
|
||||
'[Info] Build finished in 20 seconds',
|
||||
]);
|
||||
});
|
||||
});
|
130
tests/docker-mock.ts
Normal file
130
tests/docker-mock.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import { NockMock, ScopeOpts } from './nock-mock';
|
||||
|
||||
const dockerResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'docker-response'),
|
||||
);
|
||||
|
||||
export class DockerMock extends NockMock {
|
||||
constructor() {
|
||||
super('http://localhost');
|
||||
}
|
||||
|
||||
public expectGetPing(opts: ScopeOpts = {}) {
|
||||
this.optGet('/_ping', opts).reply(200, 'OK');
|
||||
}
|
||||
|
||||
public expectGetInfo(opts: ScopeOpts = {}) {
|
||||
// this body is a partial copy from Docker for Mac v18.06.1-ce-mac73
|
||||
const body = {
|
||||
KernelVersion: '4.9.93-linuxkit-aufs',
|
||||
OperatingSystem: 'Docker for Mac',
|
||||
OSType: 'linux',
|
||||
Architecture: 'x86_64',
|
||||
};
|
||||
this.optGet('/info', opts).reply(200, body);
|
||||
}
|
||||
|
||||
public expectGetVersion(opts: ScopeOpts = {}) {
|
||||
// this body is partial copy from Docker for Mac v18.06.1-ce-mac73
|
||||
const body = {
|
||||
Platform: {
|
||||
Name: '',
|
||||
},
|
||||
Version: '18.06.1-ce',
|
||||
ApiVersion: '1.38',
|
||||
MinAPIVersion: '1.12',
|
||||
GitCommit: 'e68fc7a',
|
||||
GoVersion: 'go1.10.3',
|
||||
Os: 'linux',
|
||||
Arch: 'amd64',
|
||||
KernelVersion: '4.9.93-linuxkit-aufs',
|
||||
Experimental: true,
|
||||
BuildTime: '2018-08-21T17:29:02.000000000+00:00',
|
||||
};
|
||||
this.optGet('/version', opts).reply(200, body);
|
||||
}
|
||||
|
||||
public expectPostBuild(opts: {
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
responseBody: any;
|
||||
responseCode: number;
|
||||
tag: string;
|
||||
checkBuildRequestBody: (requestBody: string) => Promise<void>;
|
||||
}) {
|
||||
this.optPost(
|
||||
new RegExp(`^/build\\?t=${_.escapeRegExp(opts.tag)}&`),
|
||||
opts,
|
||||
).reply(async function(_uri, requestBody, cb) {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
if (typeof requestBody === 'string') {
|
||||
await opts.checkBuildRequestBody(requestBody);
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected requestBody type "${typeof requestBody}"`,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
cb(error, [opts.responseCode, opts.responseBody]);
|
||||
});
|
||||
}
|
||||
|
||||
public expectGetImages(opts: ScopeOpts = {}) {
|
||||
// this body is partial copy from Docker for Mac v18.06.1-ce-mac73
|
||||
const body = {
|
||||
Size: 1199596,
|
||||
};
|
||||
this.optGet(/^\/images\//, opts).reply(200, body);
|
||||
}
|
||||
|
||||
public expectDeleteImages(opts: ScopeOpts = {}) {
|
||||
this.optDelete(/^\/images\//, opts).reply(200, [
|
||||
{
|
||||
Untagged:
|
||||
'registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa:latest',
|
||||
},
|
||||
{
|
||||
Untagged:
|
||||
'registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa@sha256:444a5e0c57eed51f5e752b908cb95188c25a0476fc6e5f43e5113edfc4d07199',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public expectPostImagesTag(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/images\/.+?\/tag\?/, opts).reply(201);
|
||||
}
|
||||
|
||||
public expectPostImagesPush(opts: ScopeOpts = {}) {
|
||||
this.optPost(/^\/images\/.+?\/push/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(dockerResponsePath, 'images-push-POST.json'),
|
||||
{
|
||||
'api-version': '1.38',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -17,8 +17,13 @@
|
||||
|
||||
import intercept = require('intercept-stdout');
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
import { PathUtils } from 'resin-multibuild';
|
||||
import { Readable } from 'stream';
|
||||
import * as tar from 'tar-stream';
|
||||
import { streamToBuffer } from 'tar-utils';
|
||||
|
||||
import * as balenaCLI from '../build/app';
|
||||
import { configureBluebird, setMaxListeners } from '../build/app-common';
|
||||
@ -44,7 +49,7 @@ export const runCommand = async (cmd: string) => {
|
||||
// Skip over debug messages
|
||||
if (
|
||||
typeof log === 'string' &&
|
||||
!log.startsWith('[debug]') &&
|
||||
!log.match(/\[debug\]/i) &&
|
||||
// TODO stop this warning message from appearing when running
|
||||
// sdk.setSharedOptions multiple times in the same process
|
||||
!log.startsWith('Shared SDK options') &&
|
||||
@ -87,14 +92,96 @@ export const balenaAPIMock = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export function cleanOutput(output: string[] | string) {
|
||||
export function cleanOutput(output: string[] | string): string[] {
|
||||
return _(_.castArray(output))
|
||||
.map(log => {
|
||||
.map((log: string) => {
|
||||
return log.split('\n').map(line => {
|
||||
return line.trim();
|
||||
return monochrome(line.trim());
|
||||
});
|
||||
})
|
||||
.flatten()
|
||||
.compact()
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove text colors (ASCII escape sequences). Example:
|
||||
* Input: '\u001b[2K\r\u001b[34m[Build]\u001b[39m \u001b[1mmain\u001b[22m Image size: 1.14 MB'
|
||||
* Output: '[Build] main Image size: 1.14 MB'
|
||||
*
|
||||
* TODO: check this function against a spec (ASCII escape sequences). It was
|
||||
* coded from observation of a few samples only, and may not cover all cases.
|
||||
*/
|
||||
export function monochrome(text: string): string {
|
||||
return text.replace(/\u001b\[\??\d+?[a-zA-Z]\r?/g, '');
|
||||
}
|
||||
|
||||
export interface TarStreamFiles {
|
||||
[filePath: string]: {
|
||||
fileSize: number;
|
||||
type: tar.Headers['type'];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a few chai.expect() test assertions on a tar stream/buffer produced by
|
||||
* the balena push, build and deploy commands, intercepted at HTTP level on
|
||||
* their way from the CLI to the Docker daemon or balenaCloud builders.
|
||||
*
|
||||
* @param tarRequestBody Intercepted buffer of tar stream to be sent to builders/Docker
|
||||
* @param expectedFiles Details of files expected to be found in the buffer
|
||||
* @param projectPath Path of test project that was tarred, to compare file contents
|
||||
* @param expect chai.expect function
|
||||
*/
|
||||
export async function inspectTarStream(
|
||||
tarRequestBody: string | Buffer,
|
||||
expectedFiles: TarStreamFiles,
|
||||
projectPath: string,
|
||||
expect: Chai.ExpectStatic,
|
||||
): Promise<void> {
|
||||
// string to stream: https://stackoverflow.com/a/22085851
|
||||
const sourceTarStream = new Readable();
|
||||
sourceTarStream._read = () => undefined;
|
||||
sourceTarStream.push(tarRequestBody);
|
||||
sourceTarStream.push(null);
|
||||
|
||||
const found: TarStreamFiles = await new Promise((resolve, reject) => {
|
||||
const foundFiles: TarStreamFiles = {};
|
||||
const extract = tar.extract();
|
||||
extract.on('error', reject);
|
||||
extract.on(
|
||||
'entry',
|
||||
async (header: tar.Headers, stream: Readable, next: tar.Callback) => {
|
||||
try {
|
||||
// TODO: test the .balena folder instead of ignoring it
|
||||
if (header.name.startsWith('.balena/')) {
|
||||
stream.resume();
|
||||
} else {
|
||||
expect(foundFiles).to.not.have.property(header.name);
|
||||
foundFiles[header.name] = {
|
||||
fileSize: header.size || 0,
|
||||
type: header.type,
|
||||
};
|
||||
const [buf, buf2] = await Promise.all([
|
||||
streamToBuffer(stream),
|
||||
fs.readFile(
|
||||
path.join(projectPath, PathUtils.toNativePath(header.name)),
|
||||
),
|
||||
]);
|
||||
expect(buf.equals(buf2)).to.be.true;
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
next();
|
||||
},
|
||||
);
|
||||
extract.once('finish', () => {
|
||||
resolve(foundFiles);
|
||||
});
|
||||
sourceTarStream.on('error', reject);
|
||||
sourceTarStream.pipe(extract);
|
||||
});
|
||||
|
||||
expect(found).to.deep.equal(expectedFiles);
|
||||
}
|
||||
|
150
tests/nock-mock.ts
Normal file
150
tests/nock-mock.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { configureBluebird } from '../build/app-common';
|
||||
|
||||
configureBluebird();
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as nock from 'nock';
|
||||
|
||||
export interface ScopeOpts {
|
||||
optional?: boolean;
|
||||
persist?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for tests using nock to intercept HTTP requests.
|
||||
* Subclasses include BalenaAPIMock, DockerMock and BuilderMock.
|
||||
*/
|
||||
export class NockMock {
|
||||
public readonly scope: nock.Scope;
|
||||
// Expose `scope` as `expect` to allow for better semantics in tests
|
||||
public readonly expect = this.scope;
|
||||
protected static instanceCount = 0;
|
||||
|
||||
constructor(public basePathPattern: string | RegExp) {
|
||||
if (NockMock.instanceCount === 0) {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
nock.emitter.on('no match', this.handleUnexpectedRequest);
|
||||
} else if (process.env.DEBUG) {
|
||||
console.error(
|
||||
`[debug] NockMock.constructor() instance count is ${NockMock.instanceCount}`,
|
||||
);
|
||||
}
|
||||
NockMock.instanceCount += 1;
|
||||
this.scope = nock(this.basePathPattern);
|
||||
}
|
||||
|
||||
public optGet(
|
||||
uri: string | RegExp | ((uri: string) => boolean),
|
||||
opts: ScopeOpts,
|
||||
): nock.Interceptor {
|
||||
opts = _.assign({ optional: false, persist: false }, opts);
|
||||
const get = (opts.persist ? this.scope.persist() : this.scope).get(uri);
|
||||
return opts.optional ? get.optionally() : get;
|
||||
}
|
||||
|
||||
public optDelete(
|
||||
uri: string | RegExp | ((uri: string) => boolean),
|
||||
opts: ScopeOpts,
|
||||
) {
|
||||
opts = _.assign({ optional: false, persist: false }, opts);
|
||||
const del = (opts.persist ? this.scope.persist() : this.scope).delete(uri);
|
||||
return opts.optional ? del.optionally() : del;
|
||||
}
|
||||
|
||||
public optPatch(
|
||||
uri: string | RegExp | ((uri: string) => boolean),
|
||||
opts: ScopeOpts,
|
||||
) {
|
||||
opts = _.assign({ optional: false, persist: false }, opts);
|
||||
const patch = (opts.persist ? this.scope.persist() : this.scope).patch(uri);
|
||||
return opts.optional ? patch.optionally() : patch;
|
||||
}
|
||||
|
||||
public optPost(
|
||||
uri: string | RegExp | ((uri: string) => boolean),
|
||||
opts: ScopeOpts,
|
||||
) {
|
||||
opts = _.assign({ optional: false, persist: false }, opts);
|
||||
const post = (opts.persist ? this.scope.persist() : this.scope).post(uri);
|
||||
return opts.optional ? post.optionally() : post;
|
||||
}
|
||||
|
||||
public done() {
|
||||
try {
|
||||
// scope.done() will throw an error if there are expected api calls that have not happened.
|
||||
// So ensure that all expected calls have been made.
|
||||
this.scope.done();
|
||||
} finally {
|
||||
const count = NockMock.instanceCount - 1;
|
||||
if (count < 0 && process.env.DEBUG) {
|
||||
console.error(
|
||||
`[debug] Warning: NockMock.instanceCount is negative (${count})`,
|
||||
);
|
||||
}
|
||||
NockMock.instanceCount = Math.max(0, count);
|
||||
if (NockMock.instanceCount === 0) {
|
||||
// Remove 'no match' handler, for tests using nock without this module
|
||||
nock.emitter.removeAllListeners('no match');
|
||||
nock.cleanAll();
|
||||
nock.restore();
|
||||
} else if (process.env.DEBUG) {
|
||||
console.error(
|
||||
`[debug] NockMock.done() instance count is ${NockMock.instanceCount}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected handleUnexpectedRequest(req: any) {
|
||||
const o = req.options || {};
|
||||
const u = o.uri || {};
|
||||
console.error(
|
||||
`Unexpected http request!: ${req.method} ${o.proto ||
|
||||
u.protocol}//${o.host || u.host}${req.path || o.path || u.path}`,
|
||||
);
|
||||
// Errors thrown here are not causing the tests to fail for some reason.
|
||||
// Possibly due to CLI global error handlers? (error.js)
|
||||
// (Also, nock should automatically throw an error, but also not happening)
|
||||
// For now, the console.error is sufficient (will fail the test)
|
||||
}
|
||||
|
||||
// For debugging tests
|
||||
get unfulfilledCallCount(): number {
|
||||
return this.scope.pendingMocks().length;
|
||||
}
|
||||
|
||||
public debug() {
|
||||
const scope = this.scope;
|
||||
let mocks = scope.pendingMocks();
|
||||
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
||||
|
||||
this.scope.on('request', function(_req, _interceptor, _body) {
|
||||
console.log(`>> REQUEST:` + _req.path);
|
||||
mocks = scope.pendingMocks();
|
||||
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
||||
});
|
||||
|
||||
this.scope.on('replied', function(_req) {
|
||||
console.log(`<< REPLIED:` + _req.path);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"application_type": [
|
||||
{
|
||||
"name": "Starter",
|
||||
"slug": "microservices-starter",
|
||||
"supports_multicontainer": true,
|
||||
"is_legacy": false,
|
||||
"__metadata": {}
|
||||
}
|
||||
],
|
||||
"id": 1301645,
|
||||
"user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(43699)"
|
||||
},
|
||||
"__id": 43699
|
||||
},
|
||||
"depends_on__application": null,
|
||||
"actor": 3423895,
|
||||
"app_name": "testApp",
|
||||
"slug": "gh_user/testApp",
|
||||
"commit": "96eec431d57e6976d3a756df33fde7e2",
|
||||
"device_type": "raspberrypi3",
|
||||
"should_track_latest_release": true,
|
||||
"is_accessible_by_support_until__date": null,
|
||||
"is_public": false,
|
||||
"is_host": false,
|
||||
"__metadata": {
|
||||
"uri": "/resin/application(@id)?@id=1301645"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
25
tests/test-data/api-response/image-POST-v5.json
Normal file
25
tests/test-data/api-response/image-POST-v5.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"created_at": "2020-01-16T17:08:56.652Z",
|
||||
"id": 1859016,
|
||||
"start_timestamp": "2020-01-16T17:08:56.219Z",
|
||||
"end_timestamp": null,
|
||||
"dockerfile": null,
|
||||
"is_a_build_of__service": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service(233455)"
|
||||
},
|
||||
"__id": 233455
|
||||
},
|
||||
"image_size": null,
|
||||
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa",
|
||||
"project_type": null,
|
||||
"error_message": null,
|
||||
"build_log": null,
|
||||
"push_timestamp": null,
|
||||
"status": "running",
|
||||
"content_hash": null,
|
||||
"contract": null,
|
||||
"__metadata": {
|
||||
"uri": "/resin/image(@id)?@id=1859016"
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": 1774668,
|
||||
"created_at": "2020-01-16T17:08:57.043Z",
|
||||
"image": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/image(1859016)"
|
||||
},
|
||||
"__id": 1859016
|
||||
},
|
||||
"is_part_of__release": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/release(1218643)"
|
||||
},
|
||||
"__id": 1218643
|
||||
},
|
||||
"__metadata": {
|
||||
"uri": "/resin/image__is_part_of__release(@id)?@id=1774668"
|
||||
}
|
||||
}
|
15
tests/test-data/api-response/image-label-POST-v5.json
Normal file
15
tests/test-data/api-response/image-label-POST-v5.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": 99699617,
|
||||
"created_at": "2020-01-16T17:08:57.443Z",
|
||||
"release_image": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/image__is_part_of__release(1774668)"
|
||||
},
|
||||
"__id": 1774668
|
||||
},
|
||||
"label_name": "io.resin.features.firmware",
|
||||
"value": "1",
|
||||
"__metadata": {
|
||||
"uri": "/resin/image_label(@id)?@id=99699617"
|
||||
}
|
||||
}
|
52
tests/test-data/api-response/release-GET-v5.json
Normal file
52
tests/test-data/api-response/release-GET-v5.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"d": [
|
||||
{
|
||||
"contains__image": [
|
||||
{
|
||||
"image": [
|
||||
{
|
||||
"id": 1820810,
|
||||
"created_at": "2020-01-04T01:13:08.805Z",
|
||||
"start_timestamp": "2020-01-04T01:13:08.583Z",
|
||||
"end_timestamp": "2020-01-04T01:13:11.920Z",
|
||||
"dockerfile": "# FROM busybox\n# FROM arm32v7/busybox\n# FROM arm32v7/alpine\n# FROM eu.gcr.io/buoyant-idea-226013/arm32v7/busybox\n# FROM eu.gcr.io/buoyant-idea-226013/amd64/busybox\n# FROM balenalib/raspberrypi3-debian:jessie-build\nFROM balenalib/raspberrypi3:stretch\nENV UDEV=1\n\n# FROM sander85/rpi-busybox # armv6\n# FROM balenalib/raspberrypi3-alpine\n\n# COPY start.sh /\n# COPY /src/start.sh /src/start.sh\n# COPY /src/hello.txt /\n# COPY src/hi.txt /\n\n# RUN cat /hello.txt\n# RUN cat /hi.txt\n# RUN cat /run/secrets/my-secret.txt\n# EXPOSE 80\nRUN uname -a\n\n# FROM alpine\n# RUN apk update && apk add bash\n# SHELL [\"/bin/bash\", \"-c\"]\n# CMD for ((i=1; i > 0; i++)); do echo \"(Plain Dockerfile 34-$i) $(uname -a)\"; sleep ${INTERVAL=5}; done\n\n# CMD i=1; while :; do echo \"Plain Dockerfile 36 ($i) $(uname -a)\"; sleep 10; i=$((i+1)); done\n# ENTRYPOINT [\"/usr/bin/entry.sh\"]\nCMD [\"/bin/bash\"]\n",
|
||||
"is_a_build_of__service": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/service(233455)"
|
||||
},
|
||||
"__id": 233455
|
||||
},
|
||||
"image_size": 134320410,
|
||||
"is_stored_at__image_location": "registry2.balena-cloud.com/v2/9c00c9413942cd15cfc9189c5dac359d",
|
||||
"project_type": "Standard Dockerfile",
|
||||
"error_message": null,
|
||||
"build_log": "Step 1/4 : FROM balenalib/raspberrypi3:stretch\n ---> 8a75ea61d9c0\nStep 2/4 : ENV UDEV=1\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 159206067c8a\nStep 3/4 : RUN uname -a\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> dd1b3d9c334b\nStep 4/4 : CMD [\"/bin/bash\"]\n\u001b[42m\u001b[30mUsing cache\u001b[39m\u001b[49m\n ---> 5211b6f4bb72\nSuccessfully built 5211b6f4bb72\n",
|
||||
"push_timestamp": "2020-01-04T01:13:14.415Z",
|
||||
"status": "success",
|
||||
"content_hash": "sha256:6b5471aae43ae81e8f69e10d1a516cb412569a6d5020a57eae311f8fa16d688a",
|
||||
"contract": null,
|
||||
"__metadata": {
|
||||
"uri": "/resin/image(@id)?@id=1820810"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 1738663,
|
||||
"created_at": "2020-01-04T01:13:14.646Z",
|
||||
"is_part_of__release": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/release(1203844)"
|
||||
},
|
||||
"__id": 1203844
|
||||
},
|
||||
"__metadata": {
|
||||
"uri": "/resin/image__is_part_of__release(@id)?@id=1738663"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 1203844,
|
||||
"__metadata": {
|
||||
"uri": "/resin/release(@id)?@id=1203844"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
54
tests/test-data/api-response/release-POST-v5.json
Normal file
54
tests/test-data/api-response/release-POST-v5.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": 1218643,
|
||||
"created_at": "2020-01-16T17:08:53.016Z",
|
||||
"belongs_to__application": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/application(1301645)"
|
||||
},
|
||||
"__id": 1301645
|
||||
},
|
||||
"is_created_by__user": {
|
||||
"__deferred": {
|
||||
"uri": "/resin/user(43699)"
|
||||
},
|
||||
"__id": 43699
|
||||
},
|
||||
"commit": "09f7c3e1fdec609be818002299edfc2a",
|
||||
"composition": {
|
||||
"version": "2.1",
|
||||
"networks": {},
|
||||
"volumes": {
|
||||
"resin-data": {}
|
||||
},
|
||||
"services": {
|
||||
"main": {
|
||||
"build": {
|
||||
"context": "."
|
||||
},
|
||||
"privileged": true,
|
||||
"tty": true,
|
||||
"restart": "always",
|
||||
"network_mode": "host",
|
||||
"volumes": [
|
||||
"resin-data:/data"
|
||||
],
|
||||
"labels": {
|
||||
"io.resin.features.kernel-modules": "1",
|
||||
"io.resin.features.firmware": "1",
|
||||
"io.resin.features.dbus": "1",
|
||||
"io.resin.features.supervisor-api": "1",
|
||||
"io.resin.features.resin-api": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": "running",
|
||||
"source": "local",
|
||||
"build_log": null,
|
||||
"start_timestamp": "2020-01-16T17:08:52.710Z",
|
||||
"end_timestamp": null,
|
||||
"update_timestamp": "2020-01-16T17:08:53.017Z",
|
||||
"__metadata": {
|
||||
"uri": "/resin/release(@id)?@id=1218643"
|
||||
}
|
||||
}
|
99
tests/test-data/builder-response/build-POST-v3.json
Normal file
99
tests/test-data/builder-response/build-POST-v3.json
Normal file
@ -0,0 +1,99 @@
|
||||
[
|
||||
{"type":"metadata","resource":"buildLogId","value":1220245}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Starting build for testApp, user gh_user"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Dashboard link: https://dashboard.balena-cloud.com/apps/1301645/devices"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Building on arm01"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Pulling previous images for caching purposes..."}
|
||||
,
|
||||
{"message":"[=> ] 2%","replace":true}
|
||||
,
|
||||
{"message":"[===> ] 6%","replace":true}
|
||||
,
|
||||
{"message":"[======> ] 13%","replace":true}
|
||||
,
|
||||
{"message":"[=================================================> ] 98%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"type":"metadata","resource":"cursor","value":"erase"}
|
||||
,
|
||||
{"message":"\u001b[32m[Success]\u001b[39m Successfully pulled cache images"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Step 1/4 : FROM busybox"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m ---> 76aea0766768"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Step 2/4 : COPY ./src/start.sh /start.sh"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m ---> b563ad6a0801"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Step 3/4 : RUN chmod a+x /start.sh"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m ---> Running in 10d4ddc40bfc"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Removing intermediate container 10d4ddc40bfc"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m ---> 82e98871a32c"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Step 4/4 : CMD [\"/start.sh\"]"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m ---> Running in 0682894e13eb"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Removing intermediate container 0682894e13eb"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m ---> 889ccb6afc7c"}
|
||||
,
|
||||
{"message":"\u001b[34m[main]\u001b[39m Successfully built 889ccb6afc7c"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Uploading images"}
|
||||
,
|
||||
{"message":"[================> ] 33%","replace":true}
|
||||
,
|
||||
{"message":"[=========================> ] 50%","replace":true}
|
||||
,
|
||||
{"message":"[=================================> ] 67%","replace":true}
|
||||
,
|
||||
{"message":"[=================================> ] 67%","replace":true}
|
||||
,
|
||||
{"message":"[==========================================> ] 84%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"message":"[==================================================>] 100%","replace":true}
|
||||
,
|
||||
{"type":"metadata","resource":"cursor","value":"erase"}
|
||||
,
|
||||
{"message":"\u001b[32m[Success]\u001b[39m Successfully uploaded images"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Built on arm01"}
|
||||
,
|
||||
{"message":"\u001b[32m[Success]\u001b[39m Release successfully created!"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Release: \u001b[34m05a24b5b034c9f95f25d4d74f0593bea\u001b[39m (id: \u001b[32m1220245\u001b[39m)"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m┌─────────\u001b[39m\u001b[90m┬────────────\u001b[39m\u001b[90m┬────────────┐\u001b[39m"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m│\u001b[39m \u001b[1mService\u001b[22m \u001b[90m│\u001b[39m \u001b[1mImage Size\u001b[22m \u001b[90m│\u001b[39m \u001b[1mBuild Time\u001b[22m \u001b[90m│\u001b[39m"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m├─────────\u001b[39m\u001b[90m┼────────────\u001b[39m\u001b[90m┼────────────┤\u001b[39m"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m│\u001b[39m main \u001b[90m│\u001b[39m 1.32 MB \u001b[90m│\u001b[39m 11 seconds \u001b[90m│\u001b[39m"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m \u001b[90m└─────────\u001b[39m\u001b[90m┴────────────\u001b[39m\u001b[90m┴────────────┘\u001b[39m"}
|
||||
,
|
||||
{"message":"\u001b[36m[Info]\u001b[39m Build finished in 26 seconds"}
|
||||
,
|
||||
{"message":"\u001b[1m\u001b[34m\t\t\t \\\n\t\t\t \\\n\t\t\t \\\\\n\t\t\t \\\\\n\t\t\t >\\/7\n\t\t\t _.-(6' \\\n\t\t\t (=___._/` \\\n\t\t\t ) \\ |\n\t\t\t / / |\n\t\t\t / > /\n\t\t\t j < _\\\n\t\t\t _.-' : ``.\n\t\t\t \\ r=._\\ `.\n\t\t\t<`\\\\_ \\ .`-.\n\t\t\t \\ r-7 `-. ._ ' . `\\\n\t\t\t \\`, `-.`7 7) )\n\t\t\t \\/ \\| \\' / `-._\n\t\t\t || .'\n\t\t\t \\\\ (\n\t\t\t >\\ >\n\t\t\t ,.-' >.'\n\t\t\t <.'_.''\n\t\t\t <'\u001b[39m\u001b[22m","isSuccess":true}
|
||||
]
|
19
tests/test-data/docker-response/images-push-POST.json
Normal file
19
tests/test-data/docker-response/images-push-POST.json
Normal file
@ -0,0 +1,19 @@
|
||||
{"status":"The push refers to repository [registry2.balena-cloud.com/v2/c089c421fb2336d0475166fbf3d0f9fa]"}
|
||||
{"status":"Preparing","progressDetail":{},"id":"a5b1f6c006f8"}
|
||||
{"status":"Preparing","progressDetail":{},"id":"2b74be40c29e"}
|
||||
{"status":"Preparing","progressDetail":{},"id":"d1156b98822d"}
|
||||
{"status":"Pushing","progressDetail":{"current":512,"total":89},"progress":"[==================================================\u003e] 512B","id":"a5b1f6c006f8"}
|
||||
{"status":"Pushing","progressDetail":{"current":2048,"total":89},"progress":"[==================================================\u003e] 2.048kB","id":"a5b1f6c006f8"}
|
||||
{"status":"Pushing","progressDetail":{"current":512,"total":89},"progress":"[==================================================\u003e] 512B","id":"2b74be40c29e"}
|
||||
{"status":"Pushing","progressDetail":{"current":33792,"total":1199418},"progress":"[=\u003e ] 33.79kB/1.199MB","id":"d1156b98822d"}
|
||||
{"status":"Pushing","progressDetail":{"current":2048,"total":89},"progress":"[==================================================\u003e] 2.048kB","id":"2b74be40c29e"}
|
||||
{"status":"Pushing","progressDetail":{"current":99328,"total":1199418},"progress":"[====\u003e ] 99.33kB/1.199MB","id":"d1156b98822d"}
|
||||
{"status":"Pushing","progressDetail":{"current":787456,"total":1199418},"progress":"[================================\u003e ] 787.5kB/1.199MB","id":"d1156b98822d"}
|
||||
{"status":"Pushing","progressDetail":{"current":852992,"total":1199418},"progress":"[===================================\u003e ] 853kB/1.199MB","id":"d1156b98822d"}
|
||||
{"status":"Pushing","progressDetail":{"current":951296,"total":1199418},"progress":"[=======================================\u003e ] 951.3kB/1.199MB","id":"d1156b98822d"}
|
||||
{"status":"Pushing","progressDetail":{"current":1415680,"total":1199418},"progress":"[==================================================\u003e] 1.416MB","id":"d1156b98822d"}
|
||||
{"status":"Pushed","progressDetail":{},"id":"a5b1f6c006f8"}
|
||||
{"status":"Pushed","progressDetail":{},"id":"2b74be40c29e"}
|
||||
{"status":"Pushed","progressDetail":{},"id":"d1156b98822d"}
|
||||
{"status":"latest: digest: sha256:444a5e0c57eed51f5e752b908cb95188c25a0476fc6e5f43e5113edfc4d07199 size: 941"}
|
||||
{"progressDetail":{},"aux":{"Tag":"latest","Digest":"sha256:444a5e0c57eed51f5e752b908cb95188c25a0476fc6e5f43e5113edfc4d07199","Size":941}}
|
@ -0,0 +1,4 @@
|
||||
FROM busybox
|
||||
COPY ./src/start.sh /start.sh
|
||||
RUN chmod a+x /start.sh
|
||||
CMD ["/start.sh"]
|
2
tests/test-data/projects/no-docker-compose/basic/src/start.sh
Executable file
2
tests/test-data/projects/no-docker-compose/basic/src/start.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
i=1; while :; do echo "basic test ($i) $(uname -a)"; sleep 5; i=$((i+1)); done
|
Loading…
Reference in New Issue
Block a user