mirror of
https://github.com/Dahlgren/arma-server-web-admin.git
synced 2024-08-30 17:22:10 +00:00
Virtual server folder with keys management
This commit is contained in:
parent
7b6d0c6425
commit
2e75733174
1
.github/workflows/node.yml
vendored
1
.github/workflows/node.yml
vendored
@ -14,6 +14,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
node-version:
|
||||||
- 14.x
|
- 14.x
|
||||||
|
@ -13,6 +13,14 @@ module.exports = {
|
|||||||
'@mod1',
|
'@mod1',
|
||||||
'@mod2',
|
'@mod2',
|
||||||
],
|
],
|
||||||
|
virtualServer: {
|
||||||
|
enabled: false, // If virtual servers should be used
|
||||||
|
fileExtensions: [ // Extra files in root of server folder that should be copied to virtual servers
|
||||||
|
'.json'
|
||||||
|
],
|
||||||
|
folders: [ // Extra folders in root of server folder that should be linked to virtual servers
|
||||||
|
]
|
||||||
|
},
|
||||||
admins: [], // add steam IDs here to enable #login without password
|
admins: [], // add steam IDs here to enable #login without password
|
||||||
auth: { // If both username and password is set, HTTP Basic Auth will be used. You may use an array to specify more than one user.
|
auth: { // If both username and password is set, HTTP Basic Auth will be used. You may use an array to specify more than one user.
|
||||||
username: '', // Username for HTTP Basic Auth
|
username: '', // Username for HTTP Basic Auth
|
||||||
|
@ -5,6 +5,8 @@ var slugify = require('slugify')
|
|||||||
|
|
||||||
var ArmaServer = require('arma-server')
|
var ArmaServer = require('arma-server')
|
||||||
|
|
||||||
|
var virtualServer = require('./virtualServer')
|
||||||
|
|
||||||
var queryInterval = 5000
|
var queryInterval = 5000
|
||||||
var queryTypes = {
|
var queryTypes = {
|
||||||
arma1: 'arma',
|
arma1: 'arma',
|
||||||
@ -132,6 +134,29 @@ Server.prototype.start = function () {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
if (self.config.virtualServer && self.config.virtualServer.enabled) {
|
||||||
|
virtualServer.create(self.config, self.mods)
|
||||||
|
.then((serverFolder) => {
|
||||||
|
self.virtualServerFolder = serverFolder
|
||||||
|
self.path = serverFolder
|
||||||
|
self.realStart()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error creating virtual server folder:', err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.path = self.config.path
|
||||||
|
self.realStart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Server.prototype.realStart = function () {
|
||||||
|
if (this.instance) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
var parameters = this.getParameters()
|
var parameters = this.getParameters()
|
||||||
var server = new ArmaServer.Server({
|
var server = new ArmaServer.Server({
|
||||||
additionalConfigurationOptions: this.getAdditionalConfigurationOptions(),
|
additionalConfigurationOptions: this.getAdditionalConfigurationOptions(),
|
||||||
@ -152,7 +177,7 @@ Server.prototype.start = function () {
|
|||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
passwordAdmin: this.admin_password,
|
passwordAdmin: this.admin_password,
|
||||||
path: this.config.path,
|
path: this.path,
|
||||||
persistent: this.persistent ? 1 : 0,
|
persistent: this.persistent ? 1 : 0,
|
||||||
platform: this.config.type,
|
platform: this.config.type,
|
||||||
players: this.max_players,
|
players: this.max_players,
|
||||||
@ -171,8 +196,13 @@ Server.prototype.start = function () {
|
|||||||
self.instance = null
|
self.instance = null
|
||||||
|
|
||||||
self.stopHeadlessClients()
|
self.stopHeadlessClients()
|
||||||
|
.then(() => {
|
||||||
self.emit('state')
|
if (self.virtualServerFolder) {
|
||||||
|
virtualServer.remove(self.virtualServerFolder)
|
||||||
|
self.virtualServerFolder = null
|
||||||
|
}
|
||||||
|
self.emit('state')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pid = instance.pid
|
this.pid = instance.pid
|
||||||
@ -207,7 +237,7 @@ Server.prototype.startHeadlessClients = function () {
|
|||||||
mods: self.mods,
|
mods: self.mods,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
password: self.password,
|
password: self.password,
|
||||||
path: self.config.path,
|
path: self.path,
|
||||||
platform: self.config.type,
|
platform: self.config.type,
|
||||||
port: self.port
|
port: self.port
|
||||||
})
|
})
|
||||||
@ -248,10 +278,29 @@ Server.prototype.stop = function (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Server.prototype.stopHeadlessClients = function () {
|
Server.prototype.stopHeadlessClients = function () {
|
||||||
this.headlessClientInstances.map(function (headlessClientInstance) {
|
var self = this
|
||||||
headlessClientInstance.kill()
|
return Promise.all(this.headlessClientInstances.map(function (headlessClientInstance) {
|
||||||
|
var handled = false
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
headlessClientInstance.on('close', function () {
|
||||||
|
if (!handled) {
|
||||||
|
handled = true
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
if (!handled) {
|
||||||
|
handled = true
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
headlessClientInstance.kill()
|
||||||
|
})
|
||||||
|
})).then(function () {
|
||||||
|
self.headlessClientInstances = []
|
||||||
})
|
})
|
||||||
this.headlessClientInstances = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Server.prototype.toJSON = function () {
|
Server.prototype.toJSON = function () {
|
||||||
|
139
lib/virtualServer.js
Normal file
139
lib/virtualServer.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
var fs = require('fs')
|
||||||
|
var fsExtra = require('fs.extra')
|
||||||
|
var _ = require('lodash')
|
||||||
|
var glob = require('glob')
|
||||||
|
var os = require('os')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
const requiredFileExtensions = [
|
||||||
|
'.dll',
|
||||||
|
'.exe',
|
||||||
|
'.so',
|
||||||
|
'.txt' // Steam app id
|
||||||
|
]
|
||||||
|
|
||||||
|
const serverFolders = [
|
||||||
|
'addons',
|
||||||
|
'aow',
|
||||||
|
'argo',
|
||||||
|
'battleye',
|
||||||
|
'contact',
|
||||||
|
'csla',
|
||||||
|
'curator',
|
||||||
|
'dll',
|
||||||
|
'dta',
|
||||||
|
'enoch',
|
||||||
|
'expansion',
|
||||||
|
'heli',
|
||||||
|
'jets',
|
||||||
|
'kart',
|
||||||
|
'linux64',
|
||||||
|
'mark',
|
||||||
|
'mpmissions',
|
||||||
|
'orange',
|
||||||
|
'tacops',
|
||||||
|
'tank'
|
||||||
|
]
|
||||||
|
|
||||||
|
function copyKeys (config, serverFolder, mods) {
|
||||||
|
// Copy needed keys, file symlinks on Windows are sketchy
|
||||||
|
const keysFolder = path.join(serverFolder, 'keys')
|
||||||
|
return fs.promises.mkdir(keysFolder, { recursive: true })
|
||||||
|
.then(() => {
|
||||||
|
const defaultKeysPath = path.join(config.path, 'keys')
|
||||||
|
const defaultKeysPromise = fs.promises.readdir(defaultKeysPath)
|
||||||
|
.then((files) => files.filter((file) => path.extname(file) === '.bikey'))
|
||||||
|
.then((files) => files.map((file) => path.join(defaultKeysPath, file)))
|
||||||
|
|
||||||
|
const modKeysPromise = Promise.all(mods.map(mod => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const modPath = path.join(config.path, mod)
|
||||||
|
glob(`${modPath}/**/*.bikey`, function (err, files) {
|
||||||
|
if (err) {
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(files)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})).then((modsFiles) => modsFiles.flat())
|
||||||
|
|
||||||
|
return Promise.all([defaultKeysPromise, modKeysPromise].map((promise) => {
|
||||||
|
return promise.then((keyFiles) => {
|
||||||
|
return Promise.all(keyFiles.map((keyFile) => {
|
||||||
|
return fs.promises.copyFile(keyFile, path.join(keysFolder, path.basename(keyFile)))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})).catch((err) => {
|
||||||
|
console.error('Error copying keys:', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFiles (config, serverFolder) {
|
||||||
|
const configFileExtensions = (config.virtualServer && config.virtualServer.fileExtensions) || []
|
||||||
|
const allowedFileExtensions = _.uniq(requiredFileExtensions.concat(configFileExtensions))
|
||||||
|
|
||||||
|
return fs.promises.readdir(config.path)
|
||||||
|
.then((files) => {
|
||||||
|
// Copy needed files, file symlinks on Windows are sketchy
|
||||||
|
const serverFiles = files.filter((file) => allowedFileExtensions.indexOf(path.extname(file)) >= 0 || path.basename(file) === 'arma3server' || path.basename(file) === 'arma3server_x64')
|
||||||
|
return Promise.all(serverFiles.map((file) => {
|
||||||
|
return fs.promises.copyFile(path.join(config.path, file), path.join(serverFolder, file))
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModFolders (config, serverFolder, mods) {
|
||||||
|
// Create virtual folders from default Arma and mods
|
||||||
|
const configFolders = (config.virtualServer && config.virtualServer.folders) || []
|
||||||
|
const serverMods = config.serverMods || []
|
||||||
|
const symlinkFolders = _.uniq(serverFolders
|
||||||
|
.concat(mods)
|
||||||
|
.concat(configFolders)
|
||||||
|
.concat(serverMods)
|
||||||
|
.map(function (folder) {
|
||||||
|
return folder.split(path.sep)[0]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.all(symlinkFolders.map((symlink) => {
|
||||||
|
return fs.promises.access(path.join(config.path, symlink))
|
||||||
|
.then(() => {
|
||||||
|
return fs.promises.symlink(path.join(config.path, symlink), path.join(serverFolder, symlink), 'junction')
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Could create symlink for', symlink, 'due to', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.create = function (config, mods) {
|
||||||
|
return fs.promises.mkdtemp(path.join(os.tmpdir(), 'arma-server-'))
|
||||||
|
.then((serverFolder) => {
|
||||||
|
console.log('Created virtual server folder:', serverFolder)
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
copyKeys(config, serverFolder, mods),
|
||||||
|
copyFiles(config, serverFolder),
|
||||||
|
createModFolders(config, serverFolder, mods)
|
||||||
|
]).then(() => {
|
||||||
|
return serverFolder
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.remove = function (folder, cb) {
|
||||||
|
if (folder) {
|
||||||
|
fsExtra.rmrf(folder, function (err) {
|
||||||
|
if (err) {
|
||||||
|
console.log('Error removing virtual server folder', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
273
test/lib/virtualServer.js
Normal file
273
test/lib/virtualServer.js
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
var async = require('async')
|
||||||
|
var fs = require('fs')
|
||||||
|
var fsExtra = require('fs.extra')
|
||||||
|
var path = require('path')
|
||||||
|
var os = require('os')
|
||||||
|
var should = require('should')
|
||||||
|
|
||||||
|
var virtualServer = require('../../lib/virtualServer.js')
|
||||||
|
|
||||||
|
var basicServerFiles = [
|
||||||
|
'@mod/addons/addon.pbo',
|
||||||
|
'@mod/keys/mod.bikey',
|
||||||
|
'@mod/optionals/@nested_mod/addons/nested_addon.pbo',
|
||||||
|
'@mod/optionals/@nested_mod/keys/nested_mod.bikey',
|
||||||
|
'addons/data_f.pbo',
|
||||||
|
'arma3server',
|
||||||
|
'arma3server.exe',
|
||||||
|
'arma3server_x64',
|
||||||
|
'arma3server_x64.exe',
|
||||||
|
'dta/product.bin',
|
||||||
|
'keys/a3.bikey',
|
||||||
|
'libsteam.so',
|
||||||
|
'mpmissions/test.vr.pbo',
|
||||||
|
'steam.dll',
|
||||||
|
'steam_appid.txt'
|
||||||
|
]
|
||||||
|
|
||||||
|
function createEmptyFile (file, cb) {
|
||||||
|
fs.open(file, 'w', function (err, fd) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.close(fd, cb)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTempServerFolder (files, cb) {
|
||||||
|
fs.mkdtemp(path.join(os.tmpdir(), 'arma-server-test-'), function (err, serverFolder) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
async.forEach(files, function (file, cb) {
|
||||||
|
var fileFolder = path.dirname(file)
|
||||||
|
if (fileFolder) {
|
||||||
|
fsExtra.mkdirp(path.join(serverFolder, fileFolder), function (err) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmptyFile(path.join(serverFolder, file), cb)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
createEmptyFile(path.join(serverFolder, file), cb)
|
||||||
|
}
|
||||||
|
}, function (err, files) {
|
||||||
|
cb(err, serverFolder)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VirtualServer', function () {
|
||||||
|
var serverFolder
|
||||||
|
var tempServerFolder
|
||||||
|
|
||||||
|
function createVirtualServer (mods, done) {
|
||||||
|
createTempServerFolder(basicServerFiles, function (err, folder) {
|
||||||
|
if (err) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverFolder = folder
|
||||||
|
|
||||||
|
return virtualServer.create({
|
||||||
|
path: serverFolder
|
||||||
|
}, mods)
|
||||||
|
.then(function (serverFolder) {
|
||||||
|
tempServerFolder = serverFolder
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
.catch(done)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeVirtualServer (done) {
|
||||||
|
if (tempServerFolder) {
|
||||||
|
virtualServer.remove(tempServerFolder, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fsExtra.rmrf(serverFolder, done)
|
||||||
|
})
|
||||||
|
} else if (serverFolder) {
|
||||||
|
fsExtra.rmrf(serverFolder, done)
|
||||||
|
} else {
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForFile (file, cb) {
|
||||||
|
fs.access(path.join(tempServerFolder, file), function (err) {
|
||||||
|
should.not.exist(err)
|
||||||
|
cb(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForLink (file, cb) {
|
||||||
|
fs.lstat(path.join(tempServerFolder, file), function (err, stats) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
should(stats.isSymbolicLink()).be.exactly(true)
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('basic server', function () {
|
||||||
|
before(function (done) {
|
||||||
|
var mods = []
|
||||||
|
createVirtualServer(mods, done)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
removeVirtualServer(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy Linux binary', function (done) {
|
||||||
|
checkForFile('arma3server.exe', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy Linux x64 binary', function (done) {
|
||||||
|
checkForFile('arma3server_x64.exe', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy Linux libsteam.so', function (done) {
|
||||||
|
checkForFile('libsteam.so', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy Windows binary', function (done) {
|
||||||
|
checkForFile('arma3server.exe', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy Windows x64 binary', function (done) {
|
||||||
|
checkForFile('arma3server_x64.exe', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy Windows steam.dll', function (done) {
|
||||||
|
checkForFile('steam.dll', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should link addons folder', function (done) {
|
||||||
|
checkForLink('addons', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have addons folder with data_f.pbo', function (done) {
|
||||||
|
checkForFile('addons/data_f.pbo', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should link dta folder', function (done) {
|
||||||
|
checkForLink('dta', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have dta folder with product.bin', function (done) {
|
||||||
|
checkForFile('dta/product.bin', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have keys folder with a3.bikey', function (done) {
|
||||||
|
checkForFile('keys/a3.bikey', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should link mpmissions folder', function (done) {
|
||||||
|
checkForLink('mpmissions', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have mpmissions folder with test.vr.pbo', function (done) {
|
||||||
|
checkForFile('mpmissions/test.vr.pbo', done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mod', function () {
|
||||||
|
before(function (done) {
|
||||||
|
var mods = [
|
||||||
|
'@mod'
|
||||||
|
]
|
||||||
|
createVirtualServer(mods, done)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
removeVirtualServer(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should link @mod folder', function (done) {
|
||||||
|
checkForLink('@mod', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have @mod folder with addons folder containing addon.pbo', function (done) {
|
||||||
|
checkForFile('@mod/addons/addon.pbo', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have keys folder with mod.bikey', function (done) {
|
||||||
|
checkForFile('keys/mod.bikey', done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('nested mod', function () {
|
||||||
|
before(function (done) {
|
||||||
|
var mods = [
|
||||||
|
path.join('@mod', 'optionals', '@nested_mod')
|
||||||
|
]
|
||||||
|
createVirtualServer(mods, done)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
removeVirtualServer(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should link @mod', function (done) {
|
||||||
|
checkForLink('@mod', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have @mod folder with optionals @nested_mod folder with addons folder containing nested_addon.pbo', function (done) {
|
||||||
|
checkForFile('@mod/optionals/@nested_mod/addons/nested_addon.pbo', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not have mod.bikey in keys', function (done) {
|
||||||
|
fs.access(path.join(tempServerFolder, 'keys', 'mod.bikey'), function (err) {
|
||||||
|
should.exist(err)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have keys folder with nested_mod.bikey', function (done) {
|
||||||
|
checkForFile('keys/nested_mod.bikey', done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('multiple mods', function () {
|
||||||
|
before(function (done) {
|
||||||
|
var mods = [
|
||||||
|
'@mod',
|
||||||
|
path.join('@mod', 'optionals', '@nested_mod')
|
||||||
|
]
|
||||||
|
createVirtualServer(mods, done)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
removeVirtualServer(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should link @mod', function (done) {
|
||||||
|
checkForLink('@mod', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have @mod folder with addons folder containing addon.pbo', function (done) {
|
||||||
|
checkForFile('@mod/addons/addon.pbo', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have @mod folder with optionals @nested_mod folder with addons folder containing nested_addon.pbo', function (done) {
|
||||||
|
checkForFile('@mod/optionals/@nested_mod/addons/nested_addon.pbo', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have keys folder with nested_mod.bikey', function (done) {
|
||||||
|
checkForFile('keys/mod.bikey', done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have keys folder with nested_mod.bikey', function (done) {
|
||||||
|
checkForFile('keys/nested_mod.bikey', done)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user