mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-21 03:55:23 +00:00
Add some more unit tests to the multicontainer supervisor
We add a bunch of additional unit tests, and also a coverage report using istanbul. The tests are not meant to cover everything, but they're a first attempt at having *some* unit testing on the supervisor. There's much to improve but hopefully it helps catch obvious errors. Change-Type: patch Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
parent
4e5f8cabb8
commit
652b596c80
15
.gitignore
vendored
15
.gitignore
vendored
@ -4,17 +4,14 @@
|
||||
/meta-resin/
|
||||
*.swp
|
||||
/data/
|
||||
bin/gosuper
|
||||
gosuper/bin/
|
||||
|
||||
base-image/build/bitbake.lock
|
||||
base-image/build/cache
|
||||
base-image/build/downloads
|
||||
base-image/build/sstate-cache
|
||||
base-image/build/tmp-glibc
|
||||
|
||||
/build/
|
||||
/dist/
|
||||
tools/dind/config/
|
||||
tools/dind/config.json*
|
||||
tools/dind/apps.json
|
||||
test/data/config.json
|
||||
test/data/config-apibinder.json
|
||||
test/data/*.sqlite
|
||||
test/data/led_file
|
||||
/coverage/
|
||||
report.xml
|
||||
|
@ -11,8 +11,9 @@
|
||||
"start": "./entry.sh",
|
||||
"build": "webpack",
|
||||
"lint": "resin-lint src/ test/",
|
||||
"test": "npm run lint && JUNIT_REPORT_PATH=report.xml mocha --exit -r ts-node/register -r coffee-script/register -r register-coffee-coverage test/*.{js,coffee} && npm run coverage",
|
||||
"versionist": "versionist",
|
||||
"test": "npm run lint && mocha -r ts-node/register -r coffee-script/register test/**/*"
|
||||
"coverage": "istanbul report text && istanbul report html"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/lodash": "^4.14.108",
|
||||
@ -28,7 +29,7 @@
|
||||
"bluebird": "^3.5.0",
|
||||
"body-parser": "^1.12.0",
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"chai": "^4.1.2",
|
||||
"chai-events": "0.0.1",
|
||||
"coffee-loader": "^0.7.3",
|
||||
"coffee-script": "~1.11.0",
|
||||
"copy-webpack-plugin": "^4.2.3",
|
||||
@ -39,6 +40,7 @@
|
||||
"duration-js": "^4.0.0",
|
||||
"event-stream": "^3.0.20",
|
||||
"express": "^4.0.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"json-mask": "^0.3.8",
|
||||
"knex": "~0.12.3",
|
||||
"lockfile": "^1.0.1",
|
||||
@ -47,13 +49,14 @@
|
||||
"memoizee": "^0.4.1",
|
||||
"mixpanel": "0.0.20",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^5.0.5",
|
||||
"mocha": "^5.1.1",
|
||||
"mochainon": "^2.0.0",
|
||||
"network-checker": "~0.0.5",
|
||||
"node-loader": "^0.6.0",
|
||||
"null-loader": "^0.1.1",
|
||||
"pinejs-client": "^2.4.0",
|
||||
"pubnub": "^3.7.13",
|
||||
"register-coffee-coverage": "0.0.1",
|
||||
"request": "^2.51.0",
|
||||
"resin-lint": "^1.3.1",
|
||||
"resin-register-device": "^3.0.0",
|
||||
|
@ -165,6 +165,10 @@ module.exports = class Logger
|
||||
@opts = opts
|
||||
@_startBackend()
|
||||
|
||||
stop: =>
|
||||
if @backend?
|
||||
@backend.stop()
|
||||
|
||||
_startBackend: =>
|
||||
if checkTruthy(@opts.nativeLogger)
|
||||
@backend = new NativeLoggerBackend()
|
||||
|
19
test/00-init.coffee
Normal file
19
test/00-init.coffee
Normal file
@ -0,0 +1,19 @@
|
||||
process.env.ROOT_MOUNTPOINT = './test/data'
|
||||
process.env.BOOT_MOUNTPOINT = '/mnt/boot'
|
||||
process.env.CONFIG_JSON_PATH = '/config.json'
|
||||
process.env.DATABASE_PATH = './test/data/database.sqlite'
|
||||
process.env.DATABASE_PATH_2 = './test/data/database2.sqlite'
|
||||
process.env.DATABASE_PATH_3 = './test/data/database3.sqlite'
|
||||
process.env.LED_FILE = './test/data/led_file'
|
||||
|
||||
m = require 'mochainon'
|
||||
|
||||
{ stub } = m.sinon
|
||||
|
||||
dbus = require 'dbus-native'
|
||||
|
||||
stub(dbus, 'systemBus').returns({
|
||||
invoke: (obj, cb) ->
|
||||
console.log(obj)
|
||||
cb()
|
||||
})
|
12
test/01-constants.spec.coffee
Normal file
12
test/01-constants.spec.coffee
Normal file
@ -0,0 +1,12 @@
|
||||
prepare = require './lib/prepare'
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
constants = require '../src/lib/constants'
|
||||
|
||||
describe 'constants', ->
|
||||
before ->
|
||||
prepare()
|
||||
it 'has the correct configJsonPathOnHost', ->
|
||||
expect(constants.configJsonPathOnHost).to.equal('/config.json')
|
||||
it 'has the correct rootMountPoint', ->
|
||||
expect(constants.rootMountPoint).to.equal('./test/data')
|
83
test/02-db.spec.coffee
Normal file
83
test/02-db.spec.coffee
Normal file
@ -0,0 +1,83 @@
|
||||
prepare = require './lib/prepare'
|
||||
Promise = require 'bluebird'
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
Knex = require('knex')
|
||||
DB = require('../src/db')
|
||||
|
||||
createOldDatabase = (path) ->
|
||||
knex = new Knex(
|
||||
client: 'sqlite3'
|
||||
connection:
|
||||
filename: path
|
||||
useNullAsDefault: true
|
||||
)
|
||||
createEmptyTable = (name, fn) ->
|
||||
knex.schema.createTable name, (t) ->
|
||||
fn(t) if fn?
|
||||
createEmptyTable 'app', (t) ->
|
||||
t.increments('id').primary()
|
||||
t.boolean('privileged')
|
||||
t.string('containerId')
|
||||
.then ->
|
||||
createEmptyTable 'config', (t) ->
|
||||
t.string('key')
|
||||
t.string('value')
|
||||
.then ->
|
||||
createEmptyTable 'dependentApp', (t) ->
|
||||
t.increments('id').primary()
|
||||
.then ->
|
||||
createEmptyTable 'dependentDevice', (t) ->
|
||||
t.increments('id').primary()
|
||||
.then ->
|
||||
return knex
|
||||
|
||||
|
||||
describe 'DB', ->
|
||||
before ->
|
||||
prepare()
|
||||
@db = new DB()
|
||||
|
||||
it 'initializes correctly, running the migrations', ->
|
||||
expect(@db.init()).to.be.fulfilled
|
||||
|
||||
it 'creates a database at the path from an env var', ->
|
||||
promise = fs.statAsync(process.env.DATABASE_PATH)
|
||||
expect(promise).to.be.fulfilled
|
||||
|
||||
it 'creates a database at the path passed on creation', ->
|
||||
db2 = new DB({ databasePath: process.env.DATABASE_PATH_2 })
|
||||
promise = db2.init().then( -> fs.statAsync(process.env.DATABASE_PATH_2))
|
||||
expect(promise).to.be.fulfilled
|
||||
|
||||
it 'adds new fields and removes old ones in an old database', ->
|
||||
databasePath = process.env.DATABASE_PATH_3
|
||||
createOldDatabase(databasePath)
|
||||
.then (knexForDB) ->
|
||||
db = new DB({ databasePath })
|
||||
db.init()
|
||||
.then ->
|
||||
Promise.all([
|
||||
expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true
|
||||
expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be.true
|
||||
expect(knexForDB.schema.hasColumn('app', 'config')).to.eventually.be.false
|
||||
expect(knexForDB.schema.hasColumn('app', 'privileged')).to.eventually.be.false
|
||||
expect(knexForDB.schema.hasColumn('app', 'containerId')).to.eventually.be.false
|
||||
expect(knexForDB.schema.hasColumn('dependentApp', 'environment')).to.eventually.be.true
|
||||
expect(knexForDB.schema.hasColumn('dependentDevice', 'markedForDeletion')).to.eventually.be.true
|
||||
expect(knexForDB.schema.hasColumn('dependentDevice', 'localId')).to.eventually.be.true
|
||||
expect(knexForDB.schema.hasColumn('dependentDevice', 'is_managed_by')).to.eventually.be.true
|
||||
expect(knexForDB.schema.hasColumn('dependentDevice', 'lock_expiry_date')).to.eventually.be.true
|
||||
])
|
||||
|
||||
it 'creates a deviceConfig table with a single default value', ->
|
||||
promise = @db.models('deviceConfig').select()
|
||||
Promise.all([
|
||||
expect(promise).to.eventually.have.lengthOf(1)
|
||||
expect(promise).to.eventually.deep.equal([ { targetValues: '{}' } ])
|
||||
])
|
||||
|
||||
it 'allows performing transactions', ->
|
||||
@db.transaction (trx) ->
|
||||
expect(trx.commit()).to.be.fulfilled
|
116
test/03-config.spec.coffee
Normal file
116
test/03-config.spec.coffee
Normal file
@ -0,0 +1,116 @@
|
||||
prepare = require './lib/prepare'
|
||||
Promise = require 'bluebird'
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
m.chai.use(require('chai-events'))
|
||||
|
||||
DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
constants = require('../src/lib/constants')
|
||||
|
||||
describe 'Config', ->
|
||||
before ->
|
||||
prepare()
|
||||
@db = new DB()
|
||||
@conf = new Config({ @db })
|
||||
@initialization = @db.init().then =>
|
||||
@conf.init()
|
||||
|
||||
it 'uses the correct config.json path', ->
|
||||
expect(@conf.configJsonPath()).to.eventually.equal('test/data/config.json')
|
||||
|
||||
it 'uses the correct config.json path from the root mount when passed as argument to the constructor', ->
|
||||
conf2 = new Config({ @db, configPath: '/foo.json' })
|
||||
expect(conf2.configJsonPath()).to.eventually.equal('test/data/foo.json')
|
||||
|
||||
it 'initializes correctly', ->
|
||||
expect(@initialization).to.be.fulfilled
|
||||
|
||||
it 'reads and exposes values from the config.json', ->
|
||||
promise = @conf.get('applicationId')
|
||||
expect(promise).to.eventually.equal(78373)
|
||||
|
||||
it 'allows reading several values in one getMany call', ->
|
||||
promise = @conf.getMany([ 'applicationId', 'apiEndpoint' ])
|
||||
expect(promise).to.eventually.deep.equal({ applicationId: 78373, apiEndpoint: 'https://api.resin.io' })
|
||||
|
||||
it 'provides the correct pubnub config', ->
|
||||
promise = @conf.get('pubnub')
|
||||
expect(promise).to.eventually.deep.equal({ subscribe_key: 'foo', publish_key: 'bar', ssl: true })
|
||||
|
||||
it 'generates a uuid and stores it in config.json', ->
|
||||
promise = @conf.get('uuid')
|
||||
promise2 = fs.readFileAsync('./test/data/config.json').then(JSON.parse).get('uuid')
|
||||
Promise.all([
|
||||
expect(promise).to.be.fulfilled
|
||||
expect(promise2).to.be.fulfilled
|
||||
]).then ([uuid1, uuid2]) ->
|
||||
expect(uuid1).to.be.a('string')
|
||||
expect(uuid1).to.have.lengthOf(62)
|
||||
expect(uuid1).to.equal(uuid2)
|
||||
|
||||
it 'does not allow setting an immutable field', ->
|
||||
promise = @conf.set({ username: 'somebody else' })
|
||||
# We catch it to avoid the unhandled error log
|
||||
promise.catch(->)
|
||||
expect(promise).to.be.rejected
|
||||
|
||||
it 'allows setting both config.json and database fields transparently', ->
|
||||
promise = @conf.set({ appUpdatePollInterval: 30000, name: 'a new device name' }).then =>
|
||||
@conf.getMany([ 'appUpdatePollInterval', 'name' ])
|
||||
expect(promise).to.eventually.deep.equal({ appUpdatePollInterval: 30000, name: 'a new device name' })
|
||||
|
||||
it 'allows removing a db key', ->
|
||||
promise = @conf.remove('name').then =>
|
||||
@conf.get('name')
|
||||
expect(promise).to.be.fulfilled
|
||||
expect(promise).to.eventually.be.undefined
|
||||
|
||||
it 'allows deleting a config.json key and returns a default value if none is set', ->
|
||||
promise = @conf.remove('appUpdatePollInterval').then =>
|
||||
@conf.get('appUpdatePollInterval')
|
||||
expect(promise).to.be.fulfilled
|
||||
expect(promise).to.eventually.equal(60000)
|
||||
|
||||
it 'allows deleting a config.json key if it is null', ->
|
||||
promise = @conf.set('apiKey': null).then =>
|
||||
@conf.get('apiKey')
|
||||
expect(promise).to.be.fulfilled
|
||||
expect(promise).to.eventually.be.undefined
|
||||
.then ->
|
||||
fs.readFileAsync('./test/data/config.json')
|
||||
.then(JSON.parse)
|
||||
.then (confFromFile) ->
|
||||
expect(confFromFile).to.not.have.property('apiKey')
|
||||
|
||||
it 'does not allow modifying or removing a function value', ->
|
||||
promise1 = @conf.remove('version')
|
||||
promise1.catch(->)
|
||||
promise2 = @conf.set(version: '2.0')
|
||||
promise2.catch(->)
|
||||
Promise.all([
|
||||
expect(promise1).to.be.rejected
|
||||
expect(promise2).to.be.rejected
|
||||
])
|
||||
|
||||
it 'throws when asked for an unknown key', ->
|
||||
promise = @conf.get('unknownInvalidValue')
|
||||
promise.catch(->)
|
||||
expect(promise).to.be.rejected
|
||||
|
||||
it 'emits a change event when values are set', (done) ->
|
||||
@conf.on 'change', (val) ->
|
||||
expect(val).to.deep.equal({ name: 'someValue' })
|
||||
done()
|
||||
@conf.set({ name: 'someValue' })
|
||||
expect(@conf).to.emit('change')
|
||||
return
|
||||
|
||||
it "returns an undefined OS variant if it doesn't exist", ->
|
||||
oldPath = constants.hostOSVersionPath
|
||||
constants.hostOSVersionPath = 'test/data/etc/os-release-novariant'
|
||||
@conf.get('osVariant')
|
||||
.then (osVariant) ->
|
||||
constants.hostOSVersionPath = oldPath
|
||||
expect(osVariant).to.be.undefined
|
@ -1,5 +1,6 @@
|
||||
{ expect } = require 'chai'
|
||||
Service = require '../../src/compose/service'
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
Service = require '../src/compose/service'
|
||||
|
||||
describe 'compose/service.cofee', ->
|
||||
|
277
test/05-device-state.spec.coffee
Normal file
277
test/05-device-state.spec.coffee
Normal file
@ -0,0 +1,277 @@
|
||||
Promise = require 'bluebird'
|
||||
_ = require 'lodash'
|
||||
m = require 'mochainon'
|
||||
{ stub } = m.sinon
|
||||
m.chai.use(require('chai-events'))
|
||||
{ expect } = m.chai
|
||||
|
||||
prepare = require './lib/prepare'
|
||||
DeviceState = require '../src/device-state'
|
||||
DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
|
||||
Service = require '../src/compose/service'
|
||||
|
||||
mockedInitialConfig = {
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||
'RESIN_SUPERVISOR_DELTA': 'false'
|
||||
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': ''
|
||||
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
|
||||
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||
}
|
||||
|
||||
testTarget1 = {
|
||||
local: {
|
||||
name: 'aDevice'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '256'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '0'
|
||||
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||
'RESIN_SUPERVISOR_DELTA': 'false'
|
||||
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': ''
|
||||
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
|
||||
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||
'RESIN_SUPERVISOR_NATIVE_LOGGER': 'true'
|
||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||
}
|
||||
apps: {
|
||||
'1234': {
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'abcdef'
|
||||
releaseId: 1
|
||||
services: [
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
imageId: 12345
|
||||
serviceName: 'someservice'
|
||||
releaseId: 1
|
||||
image: 'registry2.resin.io/superapp/abcdef:latest'
|
||||
labels: {
|
||||
'io.resin.something': 'bar'
|
||||
}
|
||||
}
|
||||
]
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
testTarget2 = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: {
|
||||
'1234': {
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc'
|
||||
environment: {
|
||||
'FOO': 'bar'
|
||||
}
|
||||
labels: {}
|
||||
},
|
||||
'24': {
|
||||
serviceName: 'anotherService'
|
||||
imageId: 12346
|
||||
image: 'registry2.resin.io/superapp/afaff'
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
}
|
||||
labels: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
testTargetWithDefaults2 = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
'RESIN_SUPERVISOR_CONNECTIVITY_CHECK': 'true'
|
||||
'RESIN_SUPERVISOR_DELTA': 'false'
|
||||
'RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT': ''
|
||||
'RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT': '30000'
|
||||
'RESIN_SUPERVISOR_DELTA_RETRY_COUNT': '30'
|
||||
'RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL': '10000'
|
||||
'RESIN_SUPERVISOR_LOCAL_MODE': 'false'
|
||||
'RESIN_SUPERVISOR_LOG_CONTROL': 'true'
|
||||
'RESIN_SUPERVISOR_NATIVE_LOGGER': 'true'
|
||||
'RESIN_SUPERVISOR_OVERRIDE_LOCK': 'false'
|
||||
'RESIN_SUPERVISOR_POLL_INTERVAL': '60000'
|
||||
'RESIN_SUPERVISOR_VPN_CONTROL': 'true'
|
||||
}
|
||||
apps: {
|
||||
'1234': {
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: [
|
||||
_.merge({ appId: 1234, serviceId: 23, releaseId: 2 }, _.clone(testTarget2.local.apps['1234'].services['23'])),
|
||||
_.merge({ appId: 1234, serviceId: 24, releaseId: 2 }, _.clone(testTarget2.local.apps['1234'].services['24']))
|
||||
]
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
testTargetInvalid = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: '1234'
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: '2'
|
||||
config: {}
|
||||
services: [
|
||||
{
|
||||
serviceId: '23'
|
||||
serviceName: 'aservice'
|
||||
imageId: '12345'
|
||||
image: 'registry2.resin.io/superapp/edfabc'
|
||||
config: {}
|
||||
environment: {
|
||||
' FOO': 'bar'
|
||||
}
|
||||
labels: {}
|
||||
},
|
||||
{
|
||||
serviceId: '24'
|
||||
serviceName: 'anotherService'
|
||||
imageId: '12346'
|
||||
image: 'registry2.resin.io/superapp/afaff'
|
||||
config: {}
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
}
|
||||
labels: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
describe 'deviceState', ->
|
||||
before ->
|
||||
@timeout(5000)
|
||||
prepare()
|
||||
@db = new DB()
|
||||
@config = new Config({ @db })
|
||||
eventTracker = {
|
||||
track: console.log
|
||||
}
|
||||
stub(Service.prototype, 'extendEnvVars').callsFake (env) ->
|
||||
@environment['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||
return @environment
|
||||
@deviceState = new DeviceState({ @db, @config, eventTracker })
|
||||
stub(@deviceState.applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
|
||||
stub(@deviceState.applications.images, 'inspectByName').callsFake ->
|
||||
Promise.try ->
|
||||
err = new Error()
|
||||
err.statusCode = 404
|
||||
throw err
|
||||
@db.init()
|
||||
.then =>
|
||||
@config.init()
|
||||
|
||||
after ->
|
||||
Service.prototype.extendEnvVars.restore()
|
||||
@deviceState.applications.docker.getNetworkGateway.restore()
|
||||
@deviceState.applications.images.inspectByName.restore()
|
||||
|
||||
it 'loads a target state from an apps.json file and saves it as target state, then returns it', ->
|
||||
stub(@deviceState.applications.images, 'save').returns(Promise.resolve())
|
||||
stub(@deviceState.deviceConfig, 'getCurrent').returns(Promise.resolve(mockedInitialConfig))
|
||||
@deviceState.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json')
|
||||
.then =>
|
||||
@deviceState.getTarget()
|
||||
.then (targetState) =>
|
||||
testTarget = _.cloneDeep(testTarget1)
|
||||
testTarget.local.apps['1234'].services = _.map testTarget.local.apps['1234'].services, (s) ->
|
||||
s.imageName = s.image
|
||||
return new Service(s)
|
||||
# We serialize and parse JSON to avoid checking fields that are functions or undefined
|
||||
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))
|
||||
@deviceState.applications.images.save.restore()
|
||||
@deviceState.deviceConfig.getCurrent.restore()
|
||||
|
||||
it 'emits a change event when a new state is reported', ->
|
||||
@deviceState.reportCurrentState({ someStateDiff: 'someValue' })
|
||||
expect(@deviceState).to.emit('change')
|
||||
|
||||
it 'returns the current state'
|
||||
|
||||
it 'writes the target state to the db with some extra defaults', ->
|
||||
testTarget = _.cloneDeep(testTargetWithDefaults2)
|
||||
Promise.map testTarget.local.apps['1234'].services, (s) =>
|
||||
@deviceState.applications.images.normalise(s.image)
|
||||
.then (imageName) ->
|
||||
s.image = imageName
|
||||
s.imageName = imageName
|
||||
new Service(s)
|
||||
.then (services) =>
|
||||
testTarget.local.apps['1234'].services = services
|
||||
@deviceState.setTarget(testTarget2)
|
||||
.then =>
|
||||
@deviceState.getTarget()
|
||||
.then (target) ->
|
||||
expect(JSON.parse(JSON.stringify(target))).to.deep.equal(JSON.parse(JSON.stringify(testTarget)))
|
||||
|
||||
it 'does not allow setting an invalid target state', ->
|
||||
promise = @deviceState.setTarget(testTargetInvalid)
|
||||
promise.catch(->)
|
||||
expect(promise).to.be.rejected
|
||||
|
||||
it 'allows triggering applying the target state', (done) ->
|
||||
stub(@deviceState, 'applyTarget').returns(Promise.resolve())
|
||||
@deviceState.triggerApplyTarget({ force: true })
|
||||
expect(@deviceState.applyTarget).to.not.be.called
|
||||
setTimeout =>
|
||||
expect(@deviceState.applyTarget).to.be.calledWith({ force: true, initial: false })
|
||||
@deviceState.applyTarget.restore()
|
||||
done()
|
||||
, 5
|
||||
|
||||
it 'applies the target state for device config'
|
||||
|
||||
it 'applies the target state for applications'
|
43
test/06-iptables.spec.coffee
Normal file
43
test/06-iptables.spec.coffee
Normal file
@ -0,0 +1,43 @@
|
||||
Promise = require 'bluebird'
|
||||
iptables = require '../src/lib/iptables'
|
||||
|
||||
childProcess = require('child_process')
|
||||
|
||||
m = require 'mochainon'
|
||||
{ stub } = m.sinon
|
||||
{ expect } = m.chai
|
||||
|
||||
describe 'iptables', ->
|
||||
it 'calls iptables to delete and recreate rules to block a port', ->
|
||||
stub(childProcess, 'execAsync').returns(Promise.resolve())
|
||||
iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42)
|
||||
.then ->
|
||||
expect(childProcess.execAsync.callCount).to.equal(6)
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j REJECT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j REJECT')
|
||||
.then ->
|
||||
childProcess.execAsync.restore()
|
||||
|
||||
it "falls back to blocking the port with DROP if there's no REJECT support", ->
|
||||
stub(childProcess, 'execAsync').callsFake (cmd) ->
|
||||
if /REJECT$/.test(cmd)
|
||||
Promise.reject()
|
||||
else
|
||||
Promise.resolve()
|
||||
iptables.rejectOnAllInterfacesExcept(['foo', 'bar'], 42)
|
||||
.then ->
|
||||
expect(childProcess.execAsync.callCount).to.equal(8)
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i foo -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -I INPUT -p tcp --dport 42 -i bar -j ACCEPT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j REJECT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j REJECT')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -D INPUT -p tcp --dport 42 -j DROP')
|
||||
expect(childProcess.execAsync).to.be.calledWith('iptables -A INPUT -p tcp --dport 42 -j DROP')
|
||||
.then ->
|
||||
childProcess.execAsync.restore()
|
@ -2,7 +2,7 @@ _ = require 'lodash'
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
|
||||
validation = require '../../src/lib/validation'
|
||||
validation = require '../src/lib/validation'
|
||||
|
||||
almostTooLongText = _.map([0...255], -> 'a').join('')
|
||||
|
||||
@ -216,4 +216,4 @@ describe 'validation', ->
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false)
|
||||
expect(validation.isValidDependentDevicesObject(devices)).to.equal(false)
|
21
test/08-blink.spec.coffee
Normal file
21
test/08-blink.spec.coffee
Normal file
@ -0,0 +1,21 @@
|
||||
Promise = require 'bluebird'
|
||||
constants = require '../src/lib/constants'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
blink = require('../src/lib/blink')
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
|
||||
describe 'blink', ->
|
||||
it 'is a blink function', ->
|
||||
expect(blink).to.be.a('function')
|
||||
|
||||
it 'has a pattern property with start and stop functions', ->
|
||||
expect(blink.pattern.start).to.be.a('function')
|
||||
expect(blink.pattern.stop).to.be.a('function')
|
||||
|
||||
it 'writes to a file that represents the LED, and writes a 0 at the end to turn the LED off', ->
|
||||
blink(1)
|
||||
.then ->
|
||||
fs.readFileAsync(constants.ledFile)
|
||||
.then (contents) ->
|
||||
expect(contents.toString()).to.equal('0')
|
79
test/09-event-tracker.spec.coffee
Normal file
79
test/09-event-tracker.spec.coffee
Normal file
@ -0,0 +1,79 @@
|
||||
mixpanel = require 'mixpanel'
|
||||
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ stub } = m.sinon
|
||||
|
||||
EventTracker = require '../src/event-tracker'
|
||||
describe 'EventTracker', ->
|
||||
before ->
|
||||
stub(mixpanel, 'init').callsFake (token) ->
|
||||
return {
|
||||
token: token
|
||||
track: stub().returns()
|
||||
}
|
||||
|
||||
@eventTrackerOffline = new EventTracker()
|
||||
@eventTracker = new EventTracker()
|
||||
stub(EventTracker.prototype, '_logEvent')
|
||||
|
||||
after ->
|
||||
EventTracker.prototype._logEvent.restore()
|
||||
mixpanel.init.restore()
|
||||
|
||||
it 'initializes in offline mode', ->
|
||||
promise = @eventTrackerOffline.init({
|
||||
offlineMode: true
|
||||
uuid: 'foobar'
|
||||
})
|
||||
expect(promise).to.be.fulfilled
|
||||
.then =>
|
||||
expect(@eventTrackerOffline._client).to.be.null
|
||||
|
||||
it 'logs events in offline mode, with the correct properties', ->
|
||||
@eventTrackerOffline.track('Test event', { appId: 'someValue' })
|
||||
expect(@eventTrackerOffline._logEvent).to.be.calledWith('Event:', 'Test event', JSON.stringify({ appId: 'someValue' }))
|
||||
|
||||
it 'initializes a mixpanel client when not in offline mode', ->
|
||||
promise = @eventTracker.init({
|
||||
mixpanelToken: 'someToken'
|
||||
uuid: 'barbaz'
|
||||
})
|
||||
expect(promise).to.be.fulfilled
|
||||
.then =>
|
||||
expect(mixpanel.init).to.have.been.calledWith('someToken')
|
||||
expect(@eventTracker._client.token).to.equal('someToken')
|
||||
expect(@eventTracker._client.track).to.be.a('function')
|
||||
|
||||
it 'calls the mixpanel client track function with the event, properties and uuid as distinct_id', ->
|
||||
@eventTracker.track('Test event 2', { appId: 'someOtherValue' })
|
||||
expect(@eventTracker._logEvent).to.be.calledWith('Event:', 'Test event 2', JSON.stringify({ appId: 'someOtherValue' }))
|
||||
expect(@eventTracker._client.track).to.be.calledWith('Test event 2', { appId: 'someOtherValue', uuid: 'barbaz', distinct_id: 'barbaz' })
|
||||
|
||||
it 'can be passed an Error and it is added to the event properties', ->
|
||||
theError = new Error('something went wrong')
|
||||
@eventTracker.track('Error event', theError)
|
||||
expect(@eventTracker._client.track).to.be.calledWith('Error event', {
|
||||
error:
|
||||
message: theError.message
|
||||
stack: theError.stack
|
||||
uuid: 'barbaz'
|
||||
distinct_id: 'barbaz'
|
||||
})
|
||||
|
||||
it 'hides service environment variables, to avoid logging keys or secrets', ->
|
||||
props = {
|
||||
service:
|
||||
appId: '1'
|
||||
environment: {
|
||||
RESIN_API_KEY: 'foo'
|
||||
RESIN_SUPERVISOR_API_KEY: 'bar'
|
||||
OTHER_VAR: 'hi'
|
||||
}
|
||||
}
|
||||
@eventTracker.track('Some app event', props)
|
||||
expect(@eventTracker._client.track).to.be.calledWith('Some app event', {
|
||||
service: { appId: '1' }
|
||||
uuid: 'barbaz'
|
||||
distinct_id: 'barbaz'
|
||||
})
|
72
test/10-network.spec.coffee
Normal file
72
test/10-network.spec.coffee
Normal file
@ -0,0 +1,72 @@
|
||||
os = require 'os'
|
||||
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ stub } = m.sinon
|
||||
|
||||
network = require '../src/network'
|
||||
describe 'network', ->
|
||||
describe 'getIPAddresses', ->
|
||||
before ->
|
||||
stub(os, 'networkInterfaces').returns({
|
||||
lo:
|
||||
[{
|
||||
address: '127.0.0.1',
|
||||
netmask: '255.0.0.0',
|
||||
family: 'IPv4',
|
||||
mac: '00:00:00:00:00:00',
|
||||
internal: true
|
||||
},
|
||||
{
|
||||
address: '::1',
|
||||
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
|
||||
family: 'IPv6',
|
||||
mac: '00:00:00:00:00:00',
|
||||
scopeid: 0,
|
||||
internal: true
|
||||
}]
|
||||
docker0:
|
||||
[{
|
||||
address: '172.17.0.1',
|
||||
netmask: '255.255.0.0',
|
||||
family: 'IPv4',
|
||||
mac: '02:42:0f:33:06:ad',
|
||||
internal: false
|
||||
},
|
||||
{
|
||||
address: 'fe80::42:fff:fe33:6ad',
|
||||
netmask: 'ffff:ffff:ffff:ffff::',
|
||||
family: 'IPv6',
|
||||
mac: '02:42:0f:33:06:ad',
|
||||
scopeid: 3,
|
||||
internal: false
|
||||
}]
|
||||
wlan0:
|
||||
[{
|
||||
address: '192.168.1.137',
|
||||
netmask: '255.255.255.0',
|
||||
family: 'IPv4',
|
||||
mac: '60:6d:c7:c6:44:3d',
|
||||
internal: false
|
||||
},
|
||||
{
|
||||
address: '2605:9080:1103:3011:2dbe:35e3:1b5a:b99',
|
||||
netmask: 'ffff:ffff:ffff:ffff::',
|
||||
family: 'IPv6',
|
||||
mac: '60:6d:c7:c6:44:3d',
|
||||
scopeid: 0,
|
||||
internal: false
|
||||
}]
|
||||
'resin-vpn':
|
||||
[{
|
||||
address: '10.10.2.14',
|
||||
netmask: '255.255.0.0',
|
||||
family: 'IPv4',
|
||||
mac: '01:43:1f:32:05:bd',
|
||||
internal: false
|
||||
}]
|
||||
})
|
||||
after ->
|
||||
os.networkInterfaces.restore()
|
||||
it 'returns only the relevant IP addresses', ->
|
||||
expect(network.getIPAddresses()).to.deep.equal([ '192.168.1.137' ])
|
137
test/11-api-binder.spec.coffee
Normal file
137
test/11-api-binder.spec.coffee
Normal file
@ -0,0 +1,137 @@
|
||||
prepare = require './lib/prepare'
|
||||
Promise = require 'bluebird'
|
||||
resinAPI = require './lib/mocked-resin-api'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ stub, spy } = m.sinon
|
||||
|
||||
DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
DeviceState = require('../src/device-state')
|
||||
APIBinder = require('../src/api-binder')
|
||||
|
||||
initModels = ->
|
||||
@timeout(5000)
|
||||
prepare()
|
||||
@db = new DB()
|
||||
@config = new Config({ @db, configPath: '/config-apibinder.json' })
|
||||
@eventTracker = {
|
||||
track: stub().callsFake (ev, props) ->
|
||||
console.log(ev, props)
|
||||
}
|
||||
@deviceState = new DeviceState({ @db, @config, @eventTracker })
|
||||
@apiBinder = new APIBinder({ @db, @config, @eventTracker, @deviceState })
|
||||
@db.init()
|
||||
.then =>
|
||||
@config.init()
|
||||
.then =>
|
||||
@apiBinder.initClient() # Initializes the clients but doesn't trigger provisioning
|
||||
|
||||
mockProvisioningOpts = {
|
||||
apiEndpoint: 'http://0.0.0.0:3000'
|
||||
uuid: 'abcd'
|
||||
deviceApiKey: 'averyvalidkey'
|
||||
provisioningApiKey: 'anotherveryvalidkey'
|
||||
apiTimeout: 30000
|
||||
}
|
||||
|
||||
describe 'APIBinder', ->
|
||||
before ->
|
||||
spy(resinAPI.resinBackend, 'registerHandler')
|
||||
@server = resinAPI.listen(3000)
|
||||
after ->
|
||||
resinAPI.resinBackend.registerHandler.restore()
|
||||
try
|
||||
@server.close()
|
||||
|
||||
# We do not support older OS versions anymore, so we only test this case
|
||||
describe 'on an OS with deviceApiKey support', ->
|
||||
before ->
|
||||
initModels.call(this)
|
||||
|
||||
it 'provisions a device', ->
|
||||
promise = @apiBinder.provisionDevice()
|
||||
expect(promise).to.be.fulfilled
|
||||
.then =>
|
||||
expect(resinAPI.resinBackend.registerHandler).to.be.calledOnce
|
||||
resinAPI.resinBackend.registerHandler.reset()
|
||||
expect(@eventTracker.track).to.be.calledWith('Device bootstrap success')
|
||||
|
||||
it 'deletes the provisioning key', ->
|
||||
expect(@config.get('apiKey')).to.eventually.be.undefined
|
||||
|
||||
it 'sends the correct parameters when provisioning', ->
|
||||
fs.readFileAsync('./test/data/config-apibinder.json')
|
||||
.then(JSON.parse)
|
||||
.then (conf) ->
|
||||
expect(resinAPI.resinBackend.devices).to.deep.equal({
|
||||
'1': {
|
||||
id: 1
|
||||
user: conf.userId
|
||||
application: conf.applicationId
|
||||
uuid: conf.uuid
|
||||
device_type: conf.deviceType
|
||||
api_key: conf.deviceApiKey
|
||||
}
|
||||
})
|
||||
|
||||
describe 'fetchDevice', ->
|
||||
before ->
|
||||
initModels.call(this)
|
||||
|
||||
it 'gets a device by its uuid from the Resin API', ->
|
||||
# Manually add a device to the mocked API
|
||||
resinAPI.resinBackend.devices[3] = {
|
||||
id: 3
|
||||
user: 'foo'
|
||||
application: 1337
|
||||
uuid: 'abcd'
|
||||
device_type: 'intel-nuc'
|
||||
api_key: 'verysecure'
|
||||
}
|
||||
@apiBinder.fetchDevice('abcd', 'someApiKey', 30000)
|
||||
.then (theDevice) ->
|
||||
expect(theDevice).to.deep.equal(resinAPI.resinBackend.devices[3])
|
||||
|
||||
describe '_exchangeKeyAndGetDevice', ->
|
||||
before ->
|
||||
initModels.call(this)
|
||||
|
||||
it 'returns the device if it can fetch it with the deviceApiKey', ->
|
||||
spy(resinAPI.resinBackend, 'deviceKeyHandler')
|
||||
fetchDeviceStub = stub(@apiBinder, 'fetchDevice')
|
||||
fetchDeviceStub.onCall(0).resolves({ id: 1 })
|
||||
@apiBinder._exchangeKeyAndGetDevice(mockProvisioningOpts)
|
||||
.then (device) =>
|
||||
expect(resinAPI.resinBackend.deviceKeyHandler).to.not.be.called
|
||||
expect(device).to.deep.equal({ id: 1 })
|
||||
expect(@apiBinder.fetchDevice).to.be.calledOnce
|
||||
@apiBinder.fetchDevice.restore()
|
||||
resinAPI.resinBackend.deviceKeyHandler.restore()
|
||||
|
||||
it 'throws if it cannot get the device with any of the keys', ->
|
||||
spy(resinAPI.resinBackend, 'deviceKeyHandler')
|
||||
stub(@apiBinder, 'fetchDevice').returns(Promise.resolve(null))
|
||||
promise = @apiBinder._exchangeKeyAndGetDevice(mockProvisioningOpts)
|
||||
promise.catch(->)
|
||||
expect(promise).to.be.rejected
|
||||
.then =>
|
||||
expect(resinAPI.resinBackend.deviceKeyHandler).to.not.be.called
|
||||
expect(@apiBinder.fetchDevice).to.be.calledTwice
|
||||
@apiBinder.fetchDevice.restore()
|
||||
resinAPI.resinBackend.deviceKeyHandler.restore()
|
||||
|
||||
it 'exchanges the key and returns the device if the provisioning key is valid', ->
|
||||
spy(resinAPI.resinBackend, 'deviceKeyHandler')
|
||||
fetchDeviceStub = stub(@apiBinder, 'fetchDevice')
|
||||
fetchDeviceStub.onCall(0).returns(Promise.resolve(null))
|
||||
fetchDeviceStub.onCall(1).returns(Promise.resolve({ id: 1 }))
|
||||
@apiBinder._exchangeKeyAndGetDevice(mockProvisioningOpts)
|
||||
.then (device) =>
|
||||
expect(resinAPI.resinBackend.deviceKeyHandler).to.be.calledOnce
|
||||
expect(device).to.deep.equal({ id: 1 })
|
||||
expect(@apiBinder.fetchDevice).to.be.calledTwice
|
||||
@apiBinder.fetchDevice.restore()
|
||||
resinAPI.resinBackend.deviceKeyHandler.restore()
|
39
test/12-logger.spec.coffee
Normal file
39
test/12-logger.spec.coffee
Normal file
@ -0,0 +1,39 @@
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ spy, useFakeTimers } = m.sinon
|
||||
|
||||
Logger = require '../src/logger'
|
||||
|
||||
describe 'Logger', ->
|
||||
before ->
|
||||
@fakeBinder = {
|
||||
logBatch: spy()
|
||||
}
|
||||
@fakeEventTracker = {
|
||||
track: spy()
|
||||
}
|
||||
@logger = new Logger({ eventTracker: @fakeEventTracker })
|
||||
@logger.init({ pubnub: {}, channel: 'foo', offlineMode: 'false', enable: 'true', nativeLogger: 'true', apiBinder: @fakeBinder })
|
||||
|
||||
after ->
|
||||
@logger.stop()
|
||||
|
||||
it 'publishes logs to the resin API by default', (done) ->
|
||||
theTime = Date.now()
|
||||
@logger.log(message: 'Hello!', timestamp: theTime)
|
||||
setTimeout( =>
|
||||
expect(@fakeBinder.logBatch).to.be.calledWith([ { message: 'Hello!', timestamp: theTime } ])
|
||||
@fakeBinder.logBatch.reset()
|
||||
done()
|
||||
, 1020)
|
||||
|
||||
it 'allows logging system messages which are also reported to the eventTracker', (done) ->
|
||||
clock = useFakeTimers()
|
||||
clock.tick(10)
|
||||
@logger.logSystemMessage('Hello there!', { someProp: 'someVal' }, 'Some event name')
|
||||
clock.restore()
|
||||
setTimeout( =>
|
||||
expect(@fakeBinder.logBatch).to.be.calledWith([ { message: 'Hello there!', timestamp: 10, isSystem: true } ])
|
||||
expect(@fakeEventTracker.track).to.be.calledWith('Some event name', { someProp: 'someVal' })
|
||||
done()
|
||||
, 1020)
|
152
test/13-device-config.spec.coffee
Normal file
152
test/13-device-config.spec.coffee
Normal file
@ -0,0 +1,152 @@
|
||||
Promise = require 'bluebird'
|
||||
prepare = require './lib/prepare'
|
||||
|
||||
m = require 'mochainon'
|
||||
{ expect } = m.chai
|
||||
{ stub, spy } = m.sinon
|
||||
|
||||
fsUtils = require '../src/lib/fs-utils'
|
||||
|
||||
DeviceConfig = require '../src/device-config'
|
||||
|
||||
childProcess = require 'child_process'
|
||||
|
||||
describe 'DeviceConfig', ->
|
||||
before ->
|
||||
@timeout(5000)
|
||||
prepare()
|
||||
@fakeDB = {}
|
||||
@fakeConfig = {}
|
||||
@fakeLogger = {
|
||||
logSystemMessage: spy()
|
||||
}
|
||||
@deviceConfig = new DeviceConfig({ logger: @fakeLogger, db: @fakeDB, config: @fakeConfig })
|
||||
|
||||
|
||||
# Test that the format for special values like initramfs and array variables is parsed correctly
|
||||
it 'allows getting boot config with getBootConfig', ->
|
||||
stub(@deviceConfig, 'readBootConfig').resolves('\
|
||||
initramfs initramf.gz 0x00800000\n\
|
||||
dtparam=i2c=on\n\
|
||||
dtparam=audio=on\n\
|
||||
dtoverlay=ads7846\n\
|
||||
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
||||
foobar=baz\n\
|
||||
')
|
||||
@deviceConfig.getBootConfig('raspberry-pi')
|
||||
.then (conf) =>
|
||||
@deviceConfig.readBootConfig.restore()
|
||||
expect(conf).to.deep.equal({
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
})
|
||||
|
||||
it 'properly reads a real config.txt file', ->
|
||||
@deviceConfig.getBootConfig('raspberrypi3')
|
||||
.then (conf) ->
|
||||
expect(conf).to.deep.equal({
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c_arm=on","spi=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_enable_uart: '1'
|
||||
RESIN_HOST_CONFIG_disable_splash: '1'
|
||||
RESIN_HOST_CONFIG_avoid_warnings: '1'
|
||||
RESIN_HOST_CONFIG_gpu_mem: '16'
|
||||
})
|
||||
|
||||
it 'correctly transforms environments to boot config objects', ->
|
||||
bootConfig = @deviceConfig.envToBootConfig({
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
})
|
||||
expect(bootConfig).to.deep.equal({
|
||||
initramfs: 'initramf.gz 0x00800000'
|
||||
dtparam: [ 'i2c=on', 'audio=on' ]
|
||||
dtoverlay: [ 'ads7846', 'lirc-rpi,gpio_out_pin=17,gpio_in_pin=13' ]
|
||||
foobar: 'baz'
|
||||
})
|
||||
# Test that the format for special values like initramfs and array variables is preserved
|
||||
it 'does not allow setting forbidden keys', ->
|
||||
current = {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
target = {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00810000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||
expect(promise).to.be.rejected
|
||||
promise.catch (err) =>
|
||||
expect(@fakeLogger.logSystemMessage).to.be.calledOnce
|
||||
expect(@fakeLogger.logSystemMessage).to.be.calledWith('Attempt to change blacklisted config value initramfs', {
|
||||
error: 'Attempt to change blacklisted config value initramfs'
|
||||
}, 'Apply boot config error')
|
||||
@fakeLogger.logSystemMessage.reset()
|
||||
|
||||
it 'does not try to change config.txt if it should not change', ->
|
||||
current = {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
target = {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||
expect(promise).to.eventually.equal(false)
|
||||
promise.then =>
|
||||
expect(@fakeLogger.logSystemMessage).to.not.be.called
|
||||
@fakeLogger.logSystemMessage.reset()
|
||||
|
||||
it 'writes the target config.txt', ->
|
||||
stub(fsUtils, 'writeFileAtomic').resolves()
|
||||
stub(childProcess, 'execAsync').resolves()
|
||||
current = {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=on"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'baz'
|
||||
}
|
||||
target = {
|
||||
RESIN_HOST_CONFIG_initramfs: 'initramf.gz 0x00800000'
|
||||
RESIN_HOST_CONFIG_dtparam: '"i2c=on","audio=off"'
|
||||
RESIN_HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"'
|
||||
RESIN_HOST_CONFIG_foobar: 'bat'
|
||||
RESIN_HOST_CONFIG_foobaz: 'bar'
|
||||
}
|
||||
promise = Promise.try =>
|
||||
@deviceConfig.bootConfigChangeRequired('raspberry-pi', current, target)
|
||||
expect(promise).to.eventually.equal(true)
|
||||
promise.then =>
|
||||
@deviceConfig.setBootConfig('raspberry-pi', target)
|
||||
.then =>
|
||||
expect(childProcess.execAsync).to.be.calledOnce
|
||||
expect(@fakeLogger.logSystemMessage).to.be.calledTwice
|
||||
expect(@fakeLogger.logSystemMessage.getCall(1).args[2]).to.equal('Apply boot config success')
|
||||
expect(fsUtils.writeFileAtomic).to.be.calledWith('./test/data/mnt/boot/config.txt', '\
|
||||
initramfs initramf.gz 0x00800000\n\
|
||||
dtparam=i2c=on\n\
|
||||
dtparam=audio=off\n\
|
||||
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13\n\
|
||||
foobar=bat\n\
|
||||
foobaz=bar\n\
|
||||
')
|
||||
fsUtils.writeFileAtomic.restore()
|
||||
childProcess.execAsync.restore()
|
||||
@fakeLogger.logSystemMessage.reset()
|
||||
|
||||
# This will require stubbing device.reboot, gosuper.post, config.get/set
|
||||
it 'applies the target state'
|
370
test/14-application-manager.spec.coffee
Normal file
370
test/14-application-manager.spec.coffee
Normal file
@ -0,0 +1,370 @@
|
||||
Promise = require 'bluebird'
|
||||
_ = require 'lodash'
|
||||
|
||||
m = require 'mochainon'
|
||||
{ stub } = m.sinon
|
||||
m.chai.use(require('chai-events'))
|
||||
{ expect } = m.chai
|
||||
|
||||
prepare = require './lib/prepare'
|
||||
DeviceState = require '../src/device-state'
|
||||
DB = require('../src/db')
|
||||
Config = require('../src/config')
|
||||
Service = require '../src/compose/service'
|
||||
|
||||
{ currentState, targetState, availableImages } = require './lib/application-manager-test-states'
|
||||
|
||||
appDBFormatNormalised = {
|
||||
appId: 1234
|
||||
commit: 'bar'
|
||||
releaseId: 2
|
||||
name: 'app'
|
||||
services: JSON.stringify([
|
||||
{
|
||||
appId: 1234
|
||||
serviceName: 'serv'
|
||||
imageId: 12345
|
||||
environment: { FOO: 'var2' }
|
||||
labels: {}
|
||||
image: 'foo/bar:latest'
|
||||
releaseId: 2
|
||||
serviceId: 4
|
||||
commit: 'bar'
|
||||
}
|
||||
])
|
||||
networks: '{}'
|
||||
volumes: '{}'
|
||||
}
|
||||
|
||||
appStateFormat = {
|
||||
appId: 1234
|
||||
commit: 'bar'
|
||||
releaseId: 2
|
||||
name: 'app'
|
||||
services: {
|
||||
'4': {
|
||||
appId: 1234
|
||||
serviceName: 'serv'
|
||||
imageId: 12345
|
||||
environment: { FOO: 'var2' }
|
||||
labels: {}
|
||||
image: 'foo/bar:latest'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appStateFormatNeedsServiceCreate = {
|
||||
appId: 1234
|
||||
commit: 'bar'
|
||||
releaseId: 2
|
||||
name: 'app'
|
||||
services: [
|
||||
{
|
||||
appId: 1234
|
||||
environment: {
|
||||
FOO: 'var2'
|
||||
}
|
||||
imageId: 12345
|
||||
serviceId: 4
|
||||
releaseId: 2
|
||||
serviceName: 'serv'
|
||||
image: 'foo/bar:latest'
|
||||
}
|
||||
]
|
||||
networks: {}
|
||||
volumes: {}
|
||||
}
|
||||
|
||||
dependentStateFormat = {
|
||||
appId: 1234
|
||||
image: 'foo/bar'
|
||||
commit: 'bar'
|
||||
releaseId: 3
|
||||
name: 'app'
|
||||
config: { RESIN_FOO: 'var' }
|
||||
environment: { FOO: 'var2' }
|
||||
parentApp: 256
|
||||
imageId: 45
|
||||
}
|
||||
|
||||
dependentStateFormatNormalised = {
|
||||
appId: 1234
|
||||
image: 'foo/bar:latest'
|
||||
commit: 'bar'
|
||||
releaseId: 3
|
||||
name: 'app'
|
||||
config: { RESIN_FOO: 'var' }
|
||||
environment: { FOO: 'var2' }
|
||||
parentApp: 256
|
||||
imageId: 45
|
||||
}
|
||||
|
||||
dependentDBFormat = {
|
||||
appId: 1234
|
||||
image: 'foo/bar:latest'
|
||||
commit: 'bar'
|
||||
releaseId: 3
|
||||
name: 'app'
|
||||
config: JSON.stringify({ RESIN_FOO: 'var' })
|
||||
environment: JSON.stringify({ FOO: 'var2' })
|
||||
parentApp: 256
|
||||
imageId: 45
|
||||
}
|
||||
|
||||
describe 'ApplicationManager', ->
|
||||
before ->
|
||||
@timeout(5000)
|
||||
prepare()
|
||||
@db = new DB()
|
||||
@config = new Config({ @db })
|
||||
eventTracker = {
|
||||
track: console.log
|
||||
}
|
||||
@deviceState = new DeviceState({ @db, @config, eventTracker })
|
||||
@applications = @deviceState.applications
|
||||
stub(@applications.images, 'inspectByName').callsFake (imageName) ->
|
||||
Promise.resolve({
|
||||
Config: {
|
||||
Cmd: [ 'someCommand' ]
|
||||
Entrypoint: [ 'theEntrypoint' ]
|
||||
Env: []
|
||||
Labels: {}
|
||||
Volumes: []
|
||||
}
|
||||
})
|
||||
stub(@applications.docker, 'getNetworkGateway').returns(Promise.resolve('172.17.0.1'))
|
||||
stub(Service.prototype, 'extendEnvVars').callsFake (env) ->
|
||||
@environment['ADDITIONAL_ENV_VAR'] = 'foo'
|
||||
return @environment
|
||||
@normaliseCurrent = (current) ->
|
||||
Promise.map current.local.apps, (app) ->
|
||||
Promise.map app.services, (service) ->
|
||||
new Service(service)
|
||||
.then (normalisedServices) ->
|
||||
appCloned = _.clone(app)
|
||||
appCloned.services = normalisedServices
|
||||
return appCloned
|
||||
.then (normalisedApps) ->
|
||||
currentCloned = _.clone(current)
|
||||
currentCloned.local.apps = normalisedApps
|
||||
return currentCloned
|
||||
|
||||
@normaliseTarget = (target, available) =>
|
||||
Promise.map target.local.apps, (app) =>
|
||||
@applications.normaliseAppForDB(app)
|
||||
.then (normalisedApp) =>
|
||||
@applications.normaliseAndExtendAppFromDB(normalisedApp)
|
||||
.then (apps) ->
|
||||
targetCloned = _.cloneDeep(target)
|
||||
# We mock what createTargetService does when an image is available
|
||||
targetCloned.local.apps = _.map apps, (app) ->
|
||||
app.services = _.map app.services, (service) ->
|
||||
img = _.find(available, (i) -> i.name == service.image)
|
||||
if img?
|
||||
service.image = img.dockerImageId
|
||||
return service
|
||||
return app
|
||||
return targetCloned
|
||||
@db.init()
|
||||
.then =>
|
||||
@config.init()
|
||||
|
||||
after ->
|
||||
@applications.images.inspectByName.restore()
|
||||
@applications.docker.getNetworkGateway.restore()
|
||||
Service.prototype.extendEnvVars.restore()
|
||||
|
||||
it 'infers a start step when all that changes is a running state', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[0])
|
||||
@normaliseTarget(targetState[0], availableImages[0])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.deep.equal([{
|
||||
action: 'start'
|
||||
current: current.local.apps[0].services[1]
|
||||
target: target.local.apps[0].services[1]
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
options: {}
|
||||
}])
|
||||
)
|
||||
|
||||
it 'infers a kill step when a service has to be removed', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[0])
|
||||
@normaliseTarget(targetState[1], availableImages[0])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.deep.equal([{
|
||||
action: 'kill'
|
||||
current: current.local.apps[0].services[1]
|
||||
target: null
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
options: {}
|
||||
}])
|
||||
)
|
||||
|
||||
it 'infers a fetch step when a service has to be updated', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[0])
|
||||
@normaliseTarget(targetState[2], availableImages[0])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.deep.equal([{
|
||||
action: 'fetch'
|
||||
image: @applications.imageForService(target.local.apps[0].services[1])
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
}])
|
||||
)
|
||||
|
||||
it 'does not infer a fetch step when the download is already in progress', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[0])
|
||||
@normaliseTarget(targetState[2], availableImages[0])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[0], [ target.local.apps[0].services[1].imageId ], true, current, target, false, {})
|
||||
expect(steps).to.eventually.deep.equal([{ action: 'noop', appId: 1234 }])
|
||||
)
|
||||
|
||||
it 'infers a kill step when a service has to be updated but the strategy is kill-then-download', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[0])
|
||||
@normaliseTarget(targetState[3], availableImages[0])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[0], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.deep.equal([{
|
||||
action: 'kill'
|
||||
current: current.local.apps[0].services[1]
|
||||
target: target.local.apps[0].services[1]
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
options: {}
|
||||
}])
|
||||
)
|
||||
|
||||
it 'does not infer to kill a service with default strategy if a dependency is not downloaded', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[4])
|
||||
@normaliseTarget(targetState[4], availableImages[2])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[2], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.deep.equal([{
|
||||
action: 'fetch'
|
||||
image: @applications.imageForService(target.local.apps[0].services[0])
|
||||
serviceId: 23
|
||||
appId: 1234
|
||||
}])
|
||||
)
|
||||
|
||||
it 'infers to kill several services as long as there is no unmet dependency', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[0])
|
||||
@normaliseTarget(targetState[5], availableImages[1])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.have.deep.members([
|
||||
{
|
||||
action: 'kill'
|
||||
current: current.local.apps[0].services[0]
|
||||
target: target.local.apps[0].services[0]
|
||||
serviceId: 23
|
||||
appId: 1234
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
action: 'kill'
|
||||
current: current.local.apps[0].services[1]
|
||||
target: target.local.apps[0].services[1]
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
options: {}
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
it 'infers to start the dependency first', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[1])
|
||||
@normaliseTarget(targetState[4], availableImages[1])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.have.deep.members([
|
||||
{
|
||||
action: 'start'
|
||||
current: null
|
||||
target: target.local.apps[0].services[0]
|
||||
serviceId: 23
|
||||
appId: 1234
|
||||
options: {}
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
it 'infers to start a service once its dependency has been met', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[2])
|
||||
@normaliseTarget(targetState[4], availableImages[1])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.have.deep.members([
|
||||
{
|
||||
action: 'start'
|
||||
current: null
|
||||
target: target.local.apps[0].services[1]
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
options: {}
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
it 'infers to remove spurious containers', ->
|
||||
Promise.join(
|
||||
@normaliseCurrent(currentState[3])
|
||||
@normaliseTarget(targetState[4], availableImages[1])
|
||||
(current, target) =>
|
||||
steps = @applications._inferNextSteps(false, availableImages[1], [], true, current, target, false, {})
|
||||
expect(steps).to.eventually.have.deep.members([
|
||||
{
|
||||
action: 'kill'
|
||||
current: current.local.apps[0].services[0]
|
||||
target: null
|
||||
serviceId: 23
|
||||
appId: 1234
|
||||
options: {}
|
||||
},
|
||||
{
|
||||
action: 'start'
|
||||
current: null
|
||||
target: target.local.apps[0].services[1]
|
||||
serviceId: 24
|
||||
appId: 1234
|
||||
options: {}
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
it 'converts an app from a state format to a db format, adding missing networks and volumes and normalising the image name', ->
|
||||
app = @applications.normaliseAppForDB(appStateFormat)
|
||||
expect(app).to.eventually.deep.equal(appDBFormatNormalised)
|
||||
|
||||
it 'converts a dependent app from a state format to a db format, normalising the image name', ->
|
||||
app = @applications.proxyvisor.normaliseDependentAppForDB(dependentStateFormat)
|
||||
expect(app).to.eventually.deep.equal(dependentDBFormat)
|
||||
|
||||
it 'converts an app in DB format into state format, adding default and missing fields', ->
|
||||
@applications.normaliseAndExtendAppFromDB(appDBFormatNormalised)
|
||||
.then (app) ->
|
||||
appStateFormatWithDefaults = _.cloneDeep(appStateFormatNeedsServiceCreate)
|
||||
opts = { imageInfo: { Config: { Cmd: [ 'someCommand' ], Entrypoint: [ 'theEntrypoint' ] } } }
|
||||
appStateFormatWithDefaults.services = _.map appStateFormatWithDefaults.services, (service) ->
|
||||
service.imageName = service.image
|
||||
return new Service(service, opts)
|
||||
expect(JSON.parse(JSON.stringify(app))).to.deep.equal(JSON.parse(JSON.stringify(appStateFormatWithDefaults)))
|
||||
|
||||
it 'converts a dependent app in DB format into state format', ->
|
||||
app = @applications.proxyvisor.normaliseDependentAppFromDB(dependentDBFormat)
|
||||
expect(app).to.eventually.deep.equal(dependentStateFormatNormalised)
|
25
test/data/apps.json
Normal file
25
test/data/apps.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "aDevice",
|
||||
"config": {
|
||||
"RESIN_HOST_CONFIG_gpu_mem": "256",
|
||||
"RESIN_HOST_LOG_TO_DISPLAY": "0"
|
||||
},
|
||||
"apps": {
|
||||
"1234": {
|
||||
"name": "superapp",
|
||||
"commit": "abcdef",
|
||||
"releaseId": 1,
|
||||
"services": {
|
||||
"23": {
|
||||
"imageId": 12345,
|
||||
"serviceName": "someservice",
|
||||
"image": "registry2.resin.io/superapp/abcdef",
|
||||
"labels": {
|
||||
"io.resin.something": "bar"
|
||||
},
|
||||
"environment": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
test/data/etc/hostname
Normal file
1
test/data/etc/hostname
Normal file
@ -0,0 +1 @@
|
||||
foobardevice
|
2
test/data/etc/os-release
Normal file
2
test/data/etc/os-release
Normal file
@ -0,0 +1,2 @@
|
||||
PRETTY_NAME="Resin OS 2.0.6 (fake)"
|
||||
VARIANT_ID="dev"
|
2
test/data/etc/os-release-1x
Normal file
2
test/data/etc/os-release-1x
Normal file
@ -0,0 +1,2 @@
|
||||
PRETTY_NAME="Resin OS 1.27.0 (fake)"
|
||||
VARIANT_ID="dev"
|
1
test/data/etc/os-release-novariant
Normal file
1
test/data/etc/os-release-novariant
Normal file
@ -0,0 +1 @@
|
||||
PRETTY_NAME="Resin OS 2.0.6 (fake)"
|
1194
test/data/mnt/boot/config.txt
Normal file
1194
test/data/mnt/boot/config.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
test/data/resin-data/.gitkeep
Normal file
1
test/data/resin-data/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
keep me in the repo
|
1
test/data/testconfig-apibinder.json
Normal file
1
test/data/testconfig-apibinder.json
Normal file
@ -0,0 +1 @@
|
||||
{"applicationName":"supertestrpi3","applicationId":78373,"deviceType":"raspberrypi3","userId":1001,"username":"someone","appUpdatePollInterval":3000,"listenPort":2345,"vpnPort":443,"apiEndpoint":"http://0.0.0.0:3000","vpnEndpoint":"vpn.resin.io","registryEndpoint":"registry2.resin.io","deltaEndpoint":"https://delta.resin.io","pubnubSubscribeKey":"foo","pubnubPublishKey":"bar","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod"}
|
1
test/data/testconfig.json
Normal file
1
test/data/testconfig.json
Normal file
@ -0,0 +1 @@
|
||||
{"applicationName":"supertestrpi3","applicationId":78373,"deviceType":"raspberrypi3","userId":1001,"username":"someone","appUpdatePollInterval":3000,"listenPort":2345,"vpnPort":443,"apiEndpoint":"https://api.resin.io","vpnEndpoint":"vpn.resin.io","registryEndpoint":"registry2.resin.io","deltaEndpoint":"https://delta.resin.io","pubnubSubscribeKey":"foo","pubnubPublishKey":"bar","mixpanelToken":"baz","apiKey":"boo","version":"2.0.6+rev3.prod"}
|
685
test/lib/application-manager-test-states.coffee
Normal file
685
test/lib/application-manager-test-states.coffee
Normal file
@ -0,0 +1,685 @@
|
||||
|
||||
exports.targetState = targetState = []
|
||||
targetState[0] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
appId: 1234
|
||||
serviceName: 'aservice'
|
||||
commit: 'afafafa'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
environment: {
|
||||
'FOO': 'bar'
|
||||
}
|
||||
privileged: false
|
||||
volumes: []
|
||||
labels: {}
|
||||
running: true
|
||||
},
|
||||
'24': {
|
||||
appId: 1234
|
||||
serviceName: 'anotherService'
|
||||
commit: 'afafafa'
|
||||
imageId: 12346
|
||||
image: 'registry2.resin.io/superapp/afaff:latest'
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
}
|
||||
volumes: []
|
||||
privileged: false
|
||||
labels: {}
|
||||
running: true
|
||||
}
|
||||
}
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
targetState[1] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
appId: 1234
|
||||
serviceName: 'aservice'
|
||||
commit: 'afafafa'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
environment: {
|
||||
'FOO': 'bar'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
volumes: []
|
||||
labels: {}
|
||||
running: true
|
||||
}
|
||||
}
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
targetState[2] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
appId: 1234
|
||||
serviceName: 'aservice'
|
||||
commit: 'afafafa'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
environment: {
|
||||
'FOO': 'bar'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
volumes: []
|
||||
labels: {}
|
||||
running: true
|
||||
},
|
||||
'24': {
|
||||
appId: 1234
|
||||
serviceName: 'anotherService'
|
||||
commit: 'afafafa'
|
||||
imageId: 12347
|
||||
image: 'registry2.resin.io/superapp/foooo:latest'
|
||||
depends_on: [ 'aservice' ]
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
volumes: []
|
||||
privileged: false
|
||||
labels: {}
|
||||
running: true
|
||||
}
|
||||
}
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
targetState[3] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
appId: 1234
|
||||
serviceName: 'aservice'
|
||||
commit: 'afafafa'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
environment: {
|
||||
'FOO': 'bar'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
volumes: []
|
||||
labels: {}
|
||||
running: true
|
||||
},
|
||||
'24': {
|
||||
appId: 1234
|
||||
serviceName: 'anotherService'
|
||||
commit: 'afafafa'
|
||||
imageId: 12347
|
||||
image: 'registry2.resin.io/superapp/foooo:latest'
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
volumes: []
|
||||
privileged: false
|
||||
labels: {
|
||||
'io.resin.update.strategy': 'kill-then-download'
|
||||
}
|
||||
running: true
|
||||
}
|
||||
}
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
targetState[4] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
appId: 1234
|
||||
serviceName: 'aservice'
|
||||
commit: 'afafafa'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
environment: {
|
||||
'FOO': 'THIS VALUE CHANGED'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
volumes: []
|
||||
labels: {}
|
||||
running: true
|
||||
},
|
||||
'24': {
|
||||
appId: 1234
|
||||
serviceName: 'anotherService'
|
||||
commit: 'afafafa'
|
||||
imageId: 12347
|
||||
image: 'registry2.resin.io/superapp/foooo:latest'
|
||||
depends_on: [ 'aservice' ]
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
volumes: []
|
||||
privileged: false
|
||||
labels: {}
|
||||
running: true
|
||||
}
|
||||
}
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
targetState[5] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: {
|
||||
'23': {
|
||||
appId: 1234
|
||||
serviceName: 'aservice'
|
||||
commit: 'afafafa'
|
||||
imageId: 12345
|
||||
image: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
environment: {
|
||||
'FOO': 'THIS VALUE CHANGED'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
volumes: []
|
||||
labels: {}
|
||||
running: true
|
||||
},
|
||||
'24': {
|
||||
appId: 1234
|
||||
serviceName: 'anotherService'
|
||||
commit: 'afafafa'
|
||||
imageId: 12347
|
||||
image: 'registry2.resin.io/superapp/foooo:latest'
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
volumes: []
|
||||
privileged: false
|
||||
labels: {}
|
||||
running: true
|
||||
}
|
||||
}
|
||||
volumes: {}
|
||||
networks: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
exports.currentState = currentState = []
|
||||
currentState[0] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: [
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
releaseId: 2
|
||||
commit: 'afafafa'
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
image: 'id1'
|
||||
environment: {
|
||||
'FOO': 'bar'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '23'
|
||||
'io.resin.supervised': 'true'
|
||||
'io.resin.service-name': 'aservice'
|
||||
}
|
||||
running: true
|
||||
createdAt: new Date()
|
||||
containerId: '1'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
},
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 24
|
||||
releaseId: 2
|
||||
commit: 'afafafa'
|
||||
serviceName: 'anotherService'
|
||||
imageId: 12346
|
||||
image: 'id0'
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/anotherService:/tmp/resin'
|
||||
]
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '24'
|
||||
'io.resin.supervised': 'true'
|
||||
'io.resin.service-name': 'anotherService'
|
||||
}
|
||||
running: false
|
||||
createdAt: new Date()
|
||||
containerId: '2'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
]
|
||||
volumes: {}
|
||||
networks: { default: {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
currentState[1] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: []
|
||||
volumes: {}
|
||||
networks: { default: {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
currentState[2] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: [
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
releaseId: 2
|
||||
commit: 'afafafa'
|
||||
expose: []
|
||||
ports: []
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
image: 'id1'
|
||||
environment: {
|
||||
'FOO': 'THIS VALUE CHANGED'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '23'
|
||||
'io.resin.supervised': 'true'
|
||||
'io.resin.service-name': 'aservice'
|
||||
}
|
||||
running: true
|
||||
createdAt: new Date()
|
||||
containerId: '1'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
]
|
||||
volumes: {}
|
||||
networks: { default: {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
currentState[3] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: [
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
releaseId: 2
|
||||
commit: 'afafafa'
|
||||
expose: []
|
||||
ports: []
|
||||
image: 'id1'
|
||||
environment: {
|
||||
'FOO': 'THIS VALUE CHANGED'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '23'
|
||||
'io.resin.supervised': 'true'
|
||||
'io.resin.service-name': 'aservice'
|
||||
}
|
||||
running: true
|
||||
createdAt: new Date(0)
|
||||
containerId: '1'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
},
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
releaseId: 2
|
||||
commit: 'afafafa'
|
||||
expose: []
|
||||
ports: []
|
||||
image: 'id1'
|
||||
environment: {
|
||||
'FOO': 'THIS VALUE CHANGED'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/aservice:/tmp/resin'
|
||||
]
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '23'
|
||||
'io.resin.supervised': 'true'
|
||||
'io.resin.service-name': 'aservice'
|
||||
}
|
||||
running: true
|
||||
createdAt: new Date(1)
|
||||
containerId: '2'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
]
|
||||
volumes: {}
|
||||
networks: { default: {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
currentState[4] = {
|
||||
local: {
|
||||
name: 'aDeviceWithDifferentName'
|
||||
config: {
|
||||
'RESIN_HOST_CONFIG_gpu_mem': '512'
|
||||
'RESIN_HOST_LOG_TO_DISPLAY': '1'
|
||||
}
|
||||
apps: [
|
||||
{
|
||||
appId: 1234
|
||||
name: 'superapp'
|
||||
commit: 'afafafa'
|
||||
releaseId: 2
|
||||
services: [
|
||||
{
|
||||
appId: 1234
|
||||
serviceId: 24
|
||||
releaseId: 2
|
||||
commit: 'afafafa'
|
||||
serviceName: 'anotherService'
|
||||
imageId: 12346
|
||||
image: 'id0'
|
||||
environment: {
|
||||
'FOO': 'bro'
|
||||
'ADDITIONAL_ENV_VAR': 'foo'
|
||||
}
|
||||
volumes: [
|
||||
'/tmp/resin-supervisor/services/1234/anotherService:/tmp/resin'
|
||||
]
|
||||
privileged: false
|
||||
restartPolicy:
|
||||
Name: 'always'
|
||||
MaximumRetryCount: 0
|
||||
labels: {
|
||||
'io.resin.app-id': '1234'
|
||||
'io.resin.service-id': '24'
|
||||
'io.resin.supervised': 'true'
|
||||
'io.resin.service-name': 'anotherService'
|
||||
}
|
||||
running: false
|
||||
createdAt: new Date()
|
||||
containerId: '2'
|
||||
networkMode: '1234_default'
|
||||
networks: { '1234_default': {} }
|
||||
command: [ 'someCommand' ]
|
||||
entrypoint: [ 'theEntrypoint' ]
|
||||
}
|
||||
]
|
||||
volumes: {}
|
||||
networks: { default: {} }
|
||||
}
|
||||
]
|
||||
}
|
||||
dependent: { apps: [], devices: [] }
|
||||
}
|
||||
|
||||
exports.availableImages = availableImages = []
|
||||
availableImages[0] = [
|
||||
{
|
||||
name: 'registry2.resin.io/superapp/afaff:latest'
|
||||
appId: 1234
|
||||
serviceId: 24
|
||||
serviceName: 'anotherService'
|
||||
imageId: 12346
|
||||
releaseId: 2
|
||||
dependent: 0
|
||||
dockerImageId: 'id0'
|
||||
},
|
||||
{
|
||||
name: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
releaseId: 2
|
||||
dependent: 0
|
||||
dockerImageId: 'id1'
|
||||
}
|
||||
]
|
||||
availableImages[1] = [
|
||||
{
|
||||
name: 'registry2.resin.io/superapp/foooo:latest'
|
||||
appId: 1234
|
||||
serviceId: 24
|
||||
serviceName: 'anotherService'
|
||||
imageId: 12347
|
||||
releaseId: 2
|
||||
dependent: 0
|
||||
dockerImageId: 'id2'
|
||||
},
|
||||
{
|
||||
name: 'registry2.resin.io/superapp/edfabc:latest'
|
||||
appId: 1234
|
||||
serviceId: 23
|
||||
serviceName: 'aservice'
|
||||
imageId: 12345
|
||||
releaseId: 2
|
||||
dependent: 0
|
||||
dockerImageId: 'id1'
|
||||
}
|
||||
]
|
||||
|
||||
availableImages[2] = [
|
||||
{
|
||||
name: 'registry2.resin.io/superapp/foooo:latest'
|
||||
appId: 1234
|
||||
serviceId: 24
|
||||
serviceName: 'anotherService'
|
||||
imageId: 12347
|
||||
releaseId: 2
|
||||
dependent: 0
|
||||
dockerImageId: 'id2'
|
||||
}
|
||||
]
|
35
test/lib/mocked-resin-api.coffee
Normal file
35
test/lib/mocked-resin-api.coffee
Normal file
@ -0,0 +1,35 @@
|
||||
express = require 'express'
|
||||
_ = require 'lodash'
|
||||
api = express()
|
||||
api.use(require('body-parser').json())
|
||||
|
||||
api.resinBackend = {
|
||||
currentId: 1
|
||||
devices: {}
|
||||
registerHandler: (req, res) ->
|
||||
console.log('/device/register called with ', req.body)
|
||||
device = req.body
|
||||
device.id = api.resinBackend.currentId++
|
||||
api.resinBackend.devices[device.id] = device
|
||||
res.status(201).json(device)
|
||||
getDeviceHandler: (req, res) ->
|
||||
uuid = req.query['$filter']?.match(/uuid eq '(.*)'/)?[1]
|
||||
if uuid?
|
||||
res.json({ d: _.filter(api.resinBackend.devices, (dev) -> dev.uuid is uuid ) })
|
||||
else
|
||||
res.json({ d: [] })
|
||||
deviceKeyHandler: (req, res) ->
|
||||
res.status(200).send(req.body.apiKey)
|
||||
}
|
||||
|
||||
|
||||
api.post '/device/register', (req, res) ->
|
||||
api.resinBackend.registerHandler(req, res)
|
||||
|
||||
api.get '/v4/device', (req, res) ->
|
||||
api.resinBackend.getDeviceHandler(req, res)
|
||||
|
||||
api.post '/api-key/device/:deviceId/device-key', (req, res) ->
|
||||
api.resinBackend.deviceKeyHandler(req, res)
|
||||
|
||||
module.exports = api
|
18
test/lib/prepare.coffee
Normal file
18
test/lib/prepare.coffee
Normal file
@ -0,0 +1,18 @@
|
||||
fs = require('fs')
|
||||
|
||||
module.exports = ->
|
||||
try
|
||||
fs.unlinkSync(process.env.DATABASE_PATH)
|
||||
|
||||
try
|
||||
fs.unlinkSync(process.env.DATABASE_PATH_2)
|
||||
|
||||
try
|
||||
fs.unlinkSync(process.env.DATABASE_PATH_3)
|
||||
|
||||
try
|
||||
fs.unlinkSync(process.env.LED_FILE)
|
||||
|
||||
try
|
||||
fs.writeFileSync('./test/data/config.json', fs.readFileSync('./test/data/testconfig.json'))
|
||||
fs.writeFileSync('./test/data/config-apibinder.json', fs.readFileSync('./test/data/testconfig-apibinder.json'))
|
Loading…
Reference in New Issue
Block a user