Auto-merge for PR #519 via VersionBot

Add some unit tests to the multicontainer supervisor
This commit is contained in:
resin-io-versionbot[bot] 2018-05-01 16:00:06 +00:00 committed by GitHub
commit 06a5e0986c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 3412 additions and 17 deletions

15
.gitignore vendored
View File

@ -4,17 +4,14 @@
/meta-resin/ /meta-resin/
*.swp *.swp
/data/ /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/ /build/
/dist/ /dist/
tools/dind/config/ tools/dind/config/
tools/dind/config.json* tools/dind/config.json*
tools/dind/apps.json tools/dind/apps.json
test/data/config.json
test/data/config-apibinder.json
test/data/*.sqlite
test/data/led_file
/coverage/
report.xml

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## v7.5.2 - 2018-05-01
* Add some more unit tests to the multicontainer supervisor #519 [Pablo Carranza Velez]
## v7.5.1 - 2018-04-29 ## v7.5.1 - 2018-04-29
* Remove trailing slashes from working directories of services #637 [Cameron Diver] * Remove trailing slashes from working directories of services #637 [Cameron Diver]

View File

@ -1,7 +1,7 @@
{ {
"name": "resin-supervisor", "name": "resin-supervisor",
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.", "description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
"version": "7.5.1", "version": "7.5.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": { "repository": {
"type": "git", "type": "git",
@ -11,8 +11,9 @@
"start": "./entry.sh", "start": "./entry.sh",
"build": "webpack", "build": "webpack",
"lint": "resin-lint src/ test/", "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", "versionist": "versionist",
"test": "npm run lint && mocha -r ts-node/register -r coffee-script/register test/**/*" "coverage": "istanbul report text && istanbul report html"
}, },
"dependencies": { "dependencies": {
"@types/lodash": "^4.14.108", "@types/lodash": "^4.14.108",
@ -28,7 +29,7 @@
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"body-parser": "^1.12.0", "body-parser": "^1.12.0",
"buffer-equal-constant-time": "^1.0.1", "buffer-equal-constant-time": "^1.0.1",
"chai": "^4.1.2", "chai-events": "0.0.1",
"coffee-loader": "^0.7.3", "coffee-loader": "^0.7.3",
"coffee-script": "~1.11.0", "coffee-script": "~1.11.0",
"copy-webpack-plugin": "^4.2.3", "copy-webpack-plugin": "^4.2.3",
@ -39,6 +40,7 @@
"duration-js": "^4.0.0", "duration-js": "^4.0.0",
"event-stream": "^3.0.20", "event-stream": "^3.0.20",
"express": "^4.0.0", "express": "^4.0.0",
"istanbul": "^0.4.5",
"json-mask": "^0.3.8", "json-mask": "^0.3.8",
"knex": "~0.12.3", "knex": "~0.12.3",
"lockfile": "^1.0.1", "lockfile": "^1.0.1",
@ -47,13 +49,14 @@
"memoizee": "^0.4.1", "memoizee": "^0.4.1",
"mixpanel": "0.0.20", "mixpanel": "0.0.20",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^5.0.5", "mocha": "^5.1.1",
"mochainon": "^2.0.0", "mochainon": "^2.0.0",
"network-checker": "~0.0.5", "network-checker": "~0.0.5",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"null-loader": "^0.1.1", "null-loader": "^0.1.1",
"pinejs-client": "^2.4.0", "pinejs-client": "^2.4.0",
"pubnub": "^3.7.13", "pubnub": "^3.7.13",
"register-coffee-coverage": "0.0.1",
"request": "^2.51.0", "request": "^2.51.0",
"resin-lint": "^1.3.1", "resin-lint": "^1.3.1",
"resin-register-device": "^3.0.0", "resin-register-device": "^3.0.0",

View File

@ -165,6 +165,10 @@ module.exports = class Logger
@opts = opts @opts = opts
@_startBackend() @_startBackend()
stop: =>
if @backend?
@backend.stop()
_startBackend: => _startBackend: =>
if checkTruthy(@opts.nativeLogger) if checkTruthy(@opts.nativeLogger)
@backend = new NativeLoggerBackend() @backend = new NativeLoggerBackend()

19
test/00-init.coffee Normal file
View 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()
})

View 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
View 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
View 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

View File

@ -1,5 +1,6 @@
{ expect } = require 'chai' m = require 'mochainon'
Service = require '../../src/compose/service' { expect } = m.chai
Service = require '../src/compose/service'
describe 'compose/service.cofee', -> describe 'compose/service.cofee', ->

View 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'

View 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()

View File

@ -2,7 +2,7 @@ _ = require 'lodash'
m = require 'mochainon' m = require 'mochainon'
{ expect } = m.chai { expect } = m.chai
validation = require '../../src/lib/validation' validation = require '../src/lib/validation'
almostTooLongText = _.map([0...255], -> 'a').join('') almostTooLongText = _.map([0...255], -> 'a').join('')

21
test/08-blink.spec.coffee Normal file
View 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')

View 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'
})

View 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' ])

View 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()

View 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)

View 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'

View 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
View 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
View File

@ -0,0 +1 @@
foobardevice

2
test/data/etc/os-release Normal file
View File

@ -0,0 +1,2 @@
PRETTY_NAME="Resin OS 2.0.6 (fake)"
VARIANT_ID="dev"

View File

@ -0,0 +1,2 @@
PRETTY_NAME="Resin OS 1.27.0 (fake)"
VARIANT_ID="dev"

View File

@ -0,0 +1 @@
PRETTY_NAME="Resin OS 2.0.6 (fake)"

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
keep me in the repo

View 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"}

View 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"}

View 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'
}
]

View 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
View 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'))