/** * @license * Copyright 2019-2021 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'; export const apiResponsePath = path.normalize( path.join(__dirname, '..', 'test-data', 'api-response'), ); const jHeader = { 'Content-Type': 'application/json' }; export class BalenaAPIMock extends NockMock { constructor() { super(/api\.balena-cloud\.com/); } public expectGetApplication({ notFound = false, optional = false, persist = false, expandArchitecture = false, } = {}) { const interceptor = this.optGet(/^\/v6\/application($|[(?])/, { optional, persist, }); if (notFound) { interceptor.reply(200, { d: [] }); } else { interceptor.replyWithFile( 200, path.join( apiResponsePath, !expandArchitecture ? 'application-GET-v6-expanded-app-type.json' : 'application-GET-v6-expanded-app-type-cpu-arch.json', ), jHeader, ); } } public expectDownloadConfig(opts: ScopeOpts = {}) { this.optPost('/download-config', opts).reply( 200, JSON.parse(`{ "applicationId":1301645, "deviceType":"raspberrypi3", "userId":43699, "appUpdatePollInterval":600000, "listenPort":48484, "vpnPort":443, "apiEndpoint":"https://api.balena-cloud.com", "vpnEndpoint":"vpn.balena-cloud.com", "registryEndpoint":"registry2.balena-cloud.com", "deltaEndpoint":"https://delta.balena-cloud.com", "mixpanelToken":"", "apiKey":"nothingtoseehere" }`), ); } public expectApplicationProvisioning(opts: ScopeOpts = {}) { // The endpoint changed in balena-sdk v15.45.0: // before: '/api-key/application/${applicationId}/provisioning' // after: '/api-key/v1/' this.optPost(/^\/api-key\/v[0-9]\/?$/, opts).reply(200, 'dummykey'); } public expectGetMyApplication(opts: ScopeOpts = {}) { this.optGet(/^\/v6\/my_application($|[(?])/, opts).reply( 200, JSON.parse(`{"d": [{ "organization": [{ "handle": "bob", "__metadata": {} }], "id": 1301645, "__metadata": { "uri": "/resin/my_application(@id)?@id=1301645" }}]} `), ); } public expectGetAuth(opts: ScopeOpts = {}) { this.optGet(/^\/auth\/v1\//, opts).reply(200, { token: 'test', }); } public expectGetRelease({ notFound = false, optional = false, persist = false, } = {}) { const interceptor = this.optGet(/^\/v6\/release($|[(?])/, { persist, optional, }); if (notFound) { interceptor.reply(200, { d: [] }); } else { this.optGet(/^\/v6\/release($|[(?])/, { persist, optional, }).replyWithFile( 200, path.join(apiResponsePath, 'release-GET-v6.json'), jHeader, ); } } /** * Mocks balena-release call */ public expectPatchRelease({ replyBody = 'OK', statusCode = 200, inspectRequest = this.inspectNoOp, optional = false, persist = false, }) { this.optPatch(/^\/v6\/release($|[(?])/, { optional, persist }).reply( statusCode, this.getInspectedReplyBodyFunction(inspectRequest, replyBody), ); } /** * Mocks balena-release call */ public expectPostRelease({ statusCode = 200, inspectRequest = this.inspectNoOp, optional = false, persist = false, }) { this.optPost(/^\/v6\/release($|[(?])/, { optional, persist }).reply( statusCode, this.getInspectedReplyFileFunction( inspectRequest, path.join(apiResponsePath, 'release-POST-v6.json'), ), ); } /** * Mocks balena-release call */ public expectPatchImage({ replyBody = 'OK', statusCode = 200, inspectRequest = this.inspectNoOp, optional = false, persist = false, }) { this.optPatch(/^\/v6\/image($|[(?])/, { optional, persist }).reply( statusCode, this.getInspectedReplyBodyFunction(inspectRequest, replyBody), ); } /** * Mocks balena-release call */ public expectPostImage(opts: ScopeOpts = {}) { this.optPost(/^\/v6\/image($|[(?])/, opts).replyWithFile( 201, path.join(apiResponsePath, 'image-POST-v6.json'), jHeader, ); } /** * Mocks balena-release call */ public expectPostImageLabel(opts: ScopeOpts = {}) { this.optPost(/^\/v6\/image_label($|[(?])/, opts).replyWithFile( 201, path.join(apiResponsePath, 'image-label-POST-v6.json'), jHeader, ); } /** * Mocks balena-release call */ public expectPostImageIsPartOfRelease(opts: ScopeOpts = {}) { this.optPost( /^\/v6\/image__is_part_of__release($|[(?])/, opts, ).replyWithFile( 200, path.join(apiResponsePath, 'image-is-part-of-release-POST-v6.json'), jHeader, ); } public expectGetDevice(opts: { fullUUID: string; shortUUID?: string; inaccessibleApp?: boolean; isOnline?: boolean; optional?: boolean; persist?: boolean; }) { const id = 7654321; const providedUuid = opts.shortUUID ?? opts.fullUUID; this.optGet( providedUuid.length !== 32 ? /^\/v\d+\/device($|\?)/ : /^\/v\d+\/device\(uuid=%27[0-9a-f]{32}%27\)/, opts, ).reply(200, { d: [ { id, uuid: opts.fullUUID, is_online: opts.isOnline, belongs_to__application: opts.inaccessibleApp ? [] : [{ app_name: 'test', slug: 'org/test' }], }, ], }); } public expectGetDeviceStatus(opts: ScopeOpts = {}) { this.optGet( /^\/v\d+\/device\?.+&\$select=overall_status$/, opts, ).replyWithFile( 200, path.join(apiResponsePath, 'device-status.json'), jHeader, ); } public expectGetAppEnvVars(opts: ScopeOpts = {}) { this.optGet(/^\/v\d+\/application_environment_variable($|\?)/, opts).reply( 200, { d: [ { id: 120101, name: 'var1', value: 'var1-val', }, { id: 120102, name: 'var2', value: '22', }, ], }, ); } public expectGetAppConfigVars(opts: ScopeOpts = {}) { this.optGet(/^\/v\d+\/application_config_variable($|\?)/, opts).reply(200, { d: [ { id: 120300, name: 'RESIN_SUPERVISOR_NATIVE_LOGGER', value: 'false', }, ], }); } 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[]; if (serviceName) { const varObj = appServiceVarsByService[serviceName]; varArray = varObj ? [varObj] : []; } else { varArray = _.map(appServiceVarsByService, (value) => value); } return [200, { d: varArray }]; }, ); } public expectGetDeviceEnvVars(opts: ScopeOpts = {}) { this.optGet(/^\/v\d+\/device_environment_variable($|\?)/, opts).reply(200, { d: [ { id: 120203, name: 'var3', value: 'var3-val', }, { id: 120204, name: 'var4', value: '44', }, ], }); } public expectGetDeviceConfigVars(opts: ScopeOpts = {}) { this.optGet(/^\/v\d+\/device_config_variable($|\?)/, opts).reply(200, { d: [ { id: 120400, name: 'RESIN_SUPERVISOR_POLL_INTERVAL', value: '900900', }, ], }); } 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 expectGetConfigDeviceTypes(opts: ScopeOpts = {}) { this.optGet('/device-types/v1', opts).replyWithFile( 200, path.join(apiResponsePath, 'device-types-GET-v1.json'), jHeader, ); } public expectGetDeviceTypes(opts: ScopeOpts = {}) { this.optGet(/^\/v\d+\/device_type($|\?)/, opts).replyWithFile( 200, path.join(apiResponsePath, 'device-type-GET-v6.json'), jHeader, ); } public expectGetConfigVars(opts: ScopeOpts = {}) { this.optGet('/config/vars', opts).reply(200, { reservedNames: [], reservedNamespaces: [], invalidRegex: '/^d|W/', whiteListedNames: [], whiteListedNamespaces: [], blackListedNames: [], configVarSchema: [], }); } 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 expectGetServiceFromApp(opts: { optional?: boolean; persist?: boolean; serviceId?: number; serviceName: string; }) { const serviceId = opts.serviceId || 243768; this.optGet(/^\/v6\/application($|\?).*\$expand=service.*/, opts).reply( 200, { d: [{ service: [{ id: serviceId, service_name: opts.serviceName }] }], }, ); } public expectPostService409(opts: ScopeOpts = {}) { this.optPost(/^\/v\d+\/service$/, opts).reply( 409, 'Unique key constraint violated', ); } /** * Mocks balena-release call */ public expectGetUser(opts: ScopeOpts = {}) { this.optGet(/^\/v6\/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 expectGetWhoAmI(opts: ScopeOpts = { optional: true }) { this.optGet('/actor/v1/whoami', opts).reply(200, { id: 1234, actorType: 'user', actorTypeId: 99999, username: 'gh_user', email: 'testuser@test.com', }); } public expectDeviceWhoAmI(opts: ScopeOpts = { optional: true }) { this.optGet('/actor/v1/whoami', opts).reply(200, { id: 1235, actorType: 'device', actorTypeId: 88888, uuid: 'a11dc1acd31b623a0e4e084a6cf13aaa', }); } public expectApplicationWhoAmI(opts: ScopeOpts = { optional: true }) { this.optGet('/actor/v1/whoami', opts).reply(200, { id: 1236, actorType: 'application', actorTypeId: 77777, slug: 'mytestorf/mytestfleet', }); } public expectWhoAmIFail(opts: ScopeOpts = { optional: true }) { this.optGet('/actor/v1/whoami', opts).reply(401); } public expectGetMixpanel(opts: ScopeOpts = {}) { this.optGet(/^\/mixpanel\/track/, opts).reply(200, {}); } } const appServiceVarsByService: { [key: string]: any } = { service1: { id: 120110, name: 'svar1', value: 'svar1-value', service: [ { id: 210110, service_name: 'service1', }, ], }, service2: { id: 120111, name: 'svar2', value: 'svar2-value', service: [ { id: 210111, service_name: 'service2', }, ], }, }; const deviceServiceVarsByService: { [key: string]: any } = { service1: { id: 120120, name: 'svar3', value: 'svar3-value', service: [ { id: 210110, service_name: 'service1', }, ], }, service2: { id: 120121, name: 'svar4', value: 'svar4-value', service: [ { id: 210111, service_name: 'service2', }, ], }, };