mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-20 06:07:55 +00:00
Add tests for push, deploy and build commands
Change-type: patch Signed-off-by: Paulo Castro <paulo@balena.io>
This commit is contained in:
parent
bbea58a9c8
commit
cc5fe60a15
@ -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();
|
||||
super('https://api.balena-cloud.com');
|
||||
}
|
||||
|
||||
this.scope = nock(BalenaAPIMock.basePathPattern);
|
||||
|
||||
nock.emitter.on('no match', this.handleUnexpectedRequest);
|
||||
public expectGetApplication(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/v5\/application($|[(?])/, opts).replyWithFile(
|
||||
200,
|
||||
path.join(apiResponsePath, 'application-GET-v5-expanded-app-type.json'),
|
||||
jHeader,
|
||||
);
|
||||
}
|
||||
|
||||
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 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 expectTestApp() {
|
||||
this.scope
|
||||
.get(/^\/v\d+\/application($|\?)/)
|
||||
.reply(200, { d: [{ id: 1234567 }] });
|
||||
public expectGetAuth(opts: ScopeOpts = {}) {
|
||||
this.optGet(/^\/auth\/v1\//, opts).reply(200, {
|
||||
// "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJZVFk6TlE3WDpKSDVCOlFFWFk6RkU2TjpLTlVVOklWNTI6TFFRQTo3UjRWOjJVUFI6Qk9ISjpDNklPIn0.eyJqdGkiOiI3ZTNlN2RmMS1iYjljLTQxZTMtOTlkMi00NjVlMjE4YzFmOWQiLCJuYmYiOjE1NzkxOTQ1MjgsImFjY2VzcyI6W3sibmFtZSI6InYyL2MwODljNDIxZmIyMzM2ZDA0NzUxNjZmYmYzZDBmOWZhIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19LHsibmFtZSI6InYyLzljMDBjOTQxMzk0MmNkMTVjZmM5MTg5YzVkYWMzNTlkIiwidHlwZSI6InJlcG9zaXRvcnkiLCJhY3Rpb25zIjpbInB1bGwiLCJwdXNoIl19XSwiaWF0IjoxNTc5MTk0NTM4LCJleHAiOjE1NzkyMDg5MzgsImF1ZCI6InJlZ2lzdHJ5Mi5iYWxlbmEtY2xvdWQuY29tIiwiaXNzIjoiYXBpLmJhbGVuYS1jbG91ZC5jb20iLCJzdWIiOiJnaF9wYXVsb19jYXN0cm8ifQ.bRw5_lg-nT-c1V4RxIJjujfPuVewZTs0BRNENEw2-sk_6zepLs-sLl9DOSEHYBdi87EtyCiUB3Wqee6fvz2HyQ"
|
||||
token: 'test',
|
||||
});
|
||||
}
|
||||
|
||||
public expectTestDevice(
|
||||
fullUUID = 'f63fd7d7812c34c4c14ae023fdff05f5',
|
||||
inaccessibleApp = false,
|
||||
) {
|
||||
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,10 +206,11 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
public expectDeviceServiceVars() {
|
||||
this.scope
|
||||
.get(/^\/v\d+\/device_service_environment_variable($|\?)/)
|
||||
.reply(function(uri, _requestBody) {
|
||||
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[];
|
||||
@ -170,8 +224,16 @@ export class BalenaAPIMock {
|
||||
});
|
||||
}
|
||||
|
||||
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', {
|
||||
.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);
|
||||
}
|
||||
|
152
tests/nock-mock.ts
Normal file
152
tests/nock-mock.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @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