@iobroker/testing
Version:
Shared utilities for adapter and module testing in ioBroker
275 lines (274 loc) • 13.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.validatePackageFiles = validatePackageFiles;
const typeguards_1 = require("alcalzone-shared/typeguards");
const chai_1 = require("chai");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Tests if the adapter files are valid.
* This is meant to be executed in a mocha context.
*/
function validatePackageFiles(adapterDir) {
const packageJsonPath = path.join(adapterDir, 'package.json');
const ioPackageJsonPath = path.join(adapterDir, 'io-package.json');
// This allows us to skip tests that require valid JSON files
const invalidFiles = {
'package.json': false,
'io-package.json': false,
};
function skipIfInvalid(...filenames) {
if (filenames.some(f => invalidFiles[f])) {
return this.skip();
}
}
function markAsInvalid(filename) {
if (this.currentTest.state === 'failed' && invalidFiles[filename] === false) {
invalidFiles[filename] = true;
console.error(`Skipping subsequent tests including "${filename}" because they require valid JSON files!`);
}
}
/** Ensures that a given property exists on the target object */
function ensurePropertyExists(propertyPath, targetObj) {
const propertyParts = propertyPath.split('.');
it(`The property "${propertyPath}" exists`, () => {
let prev = targetObj;
for (const part of propertyParts) {
(0, chai_1.expect)(prev[part]).to.not.be.undefined;
prev = prev[part];
}
});
}
describe(`Validate the package files`, () => {
describe(`Ensure they are readable`, () => {
for (const filename of ['package.json', 'io-package.json']) {
const packagePath = path.join(adapterDir, filename);
describe(`${filename}`, () => {
afterEach(function () {
markAsInvalid.call(this, filename);
});
beforeEach(function () {
skipIfInvalid.call(this, filename);
});
it('exists', () => {
(0, chai_1.expect)(fs.existsSync(packagePath), `${filename} is missing in the adapter dir. Please create it!`).to.be.true;
});
it('contains valid JSON', () => {
(0, chai_1.expect)(() => {
JSON.parse(fs.readFileSync(packagePath, 'utf8'));
}, `${filename} contains invalid JSON!`).not.to.throw();
});
it('is an object', () => {
(0, chai_1.expect)(require(packagePath), `${filename} must contain an object!`).to.be.an('object');
});
});
}
});
describe(`Check contents of package.json`, () => {
beforeEach(function () {
skipIfInvalid.call(this, 'package.json');
});
const packageContent = require(packageJsonPath);
const iopackContent = require(ioPackageJsonPath);
const requiredProperties = [
'name',
'version',
'description',
'author',
'license',
'repository',
'repository.type',
];
requiredProperties.forEach(prop => ensurePropertyExists(prop, packageContent));
it('The package name is correct', () => {
let name = packageContent.name;
(0, chai_1.expect)(name).to.match(/^iobroker\./, `The npm package name must start with lowercase "iobroker."!`);
name = name.replace(/^iobroker\./, '');
(0, chai_1.expect)(name).to.match(/[-a-z0-9_]+/, `The adapter name must only contain lowercase letters, numbers, "-" and "_"!`);
(0, chai_1.expect)(name).to.match(/^[a-z]/, `The adapter name must start with a letter!`);
(0, chai_1.expect)(name).to.match(/[a-z0-9]$/, `The adapter name must end with a letter or number!`);
});
if (!iopackContent.common.onlyWWW) {
it(`property main is defined for non onlyWWW adapters`, () => {
(0, chai_1.expect)(packageContent.main).to.not.be.undefined;
});
}
it(`The repository type is "git"`, () => {
(0, chai_1.expect)(packageContent.repository.type).to.equal('git');
});
it('npm is not listed as a dependency', () => {
for (const depType of [
'dependencies',
'devDependencies',
'optionalDependencies',
'peerDependencies',
]) {
if ((0, typeguards_1.isObject)(packageContent[depType]) && 'npm' in packageContent[depType]) {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw new chai_1.AssertionError(`npm must not be listed in ${depType}, found "${packageContent[depType].npm}"!`);
}
}
});
});
describe(`Check contents of io-package.json`, () => {
beforeEach(function () {
skipIfInvalid.call(this, 'io-package.json');
});
const iopackContent = require(ioPackageJsonPath);
const requiredProperties = [
'common.name',
'common.titleLang',
'common.version',
'common.news',
'common.desc',
'common.icon',
'common.extIcon',
'common.type',
'common.authors',
'native',
];
requiredProperties.forEach(prop => ensurePropertyExists(prop, iopackContent));
it(`The title does not contain "adapter" or "iobroker"`, () => {
if (!iopackContent.title) {
return;
}
(0, chai_1.expect)(iopackContent.common.title).not.to.match(/iobroker|adapter/i);
});
it(`titleLang is an object to support multiple languages`, () => {
(0, chai_1.expect)(iopackContent.common.titleLang).to.be.an('object');
});
it(`titleLang does not contain "adapter" or "iobroker"`, () => {
for (const title of Object.values(iopackContent.common.titleLang)) {
(0, chai_1.expect)(title).not.to.match(/iobroker|adapter/i);
}
});
it(`The description is an object to support multiple languages`, () => {
(0, chai_1.expect)(iopackContent.common.desc).to.be.an('object');
});
it(`common.authors is an array that is not empty`, () => {
const authors = iopackContent.common.authors;
(0, chai_1.expect)((0, typeguards_1.isArray)(authors)).to.be.true;
(0, chai_1.expect)(authors.length).to.be.at.least(1);
});
it(`common.news is an object that contains maximum 20 entries`, () => {
const news = iopackContent.common.news;
(0, chai_1.expect)((0, typeguards_1.isObject)(news)).to.be.true;
(0, chai_1.expect)(Object.keys(news).length).to.be.at.most(20);
});
if (iopackContent.common.licenseInformation) {
it(`if common.licenseInformation exists, it is an object with required properties`, () => {
(0, chai_1.expect)(iopackContent.common.licenseInformation).to.be.an('object');
(0, chai_1.expect)(iopackContent.common.licenseInformation.type).to.be.oneOf([
'free',
'commercial',
'paid',
'limited',
]);
if (iopackContent.common.licenseInformation.type !== 'free') {
(0, chai_1.expect)(iopackContent.common.licenseInformation.link, 'License link is missing').to.not.be
.undefined;
}
});
it(`common.license should not exist together with common.licenseInformation`, () => {
(0, chai_1.expect)(iopackContent.common.license, 'common.license must be removed').to.be.undefined;
});
}
else {
it(`common.license must exist without common.licenseInformation`, () => {
(0, chai_1.expect)(iopackContent.common.license, 'common.licenseInformation (preferred) or common.license (deprecated) must exist').to.not.be.undefined;
});
}
if (iopackContent.common.tier != undefined) {
it(`common.tier must be 1, 2 or 3`, () => {
(0, chai_1.expect)(iopackContent.common.tier).to.be.at.least(1);
(0, chai_1.expect)(iopackContent.common.tier).to.be.at.most(3);
});
}
// If the adapter has a configuration page, check that a supported admin UI is used
const hasNoConfigPage = iopackContent.common.noConfig === true ||
iopackContent.common.noConfig === 'true' ||
iopackContent.common.adminUI?.config === 'none';
if (!hasNoConfigPage) {
it('The adapter uses a supported admin UI', () => {
const hasSupportedUI = !!iopackContent.common.materialize ||
iopackContent.common.adminUI?.config === 'html' ||
iopackContent.common.adminUI?.config === 'json' ||
iopackContent.common.adminUI?.config === 'materialize';
(0, chai_1.expect)(hasSupportedUI, 'Unsupported Admin UI, must be html, materialize or JSON config!').to.be
.true;
});
}
});
describe(`Compare contents of package.json and io-package.json`, () => {
beforeEach(function () {
skipIfInvalid.call(this, 'package.json', 'io-package.json');
});
const packageContent = require(packageJsonPath);
const iopackContent = require(ioPackageJsonPath);
it('The name matches', () => {
(0, chai_1.expect)(`iobroker.${iopackContent.common.name}`).to.equal(packageContent.name);
});
it('The version matches', () => {
(0, chai_1.expect)(iopackContent.common.version).to.equal(packageContent.version);
});
it('The license matches', () => {
if (iopackContent.common.licenseInformation) {
(0, chai_1.expect)(iopackContent.common.licenseInformation.license).to.equal(packageContent.license);
}
else {
(0, chai_1.expect)(iopackContent.common.license).to.equal(packageContent.license);
}
});
});
});
// describe(`Check additional files`, () => {
// it("README.md exists", () => {
// expect(
// fs.existsSync(path.join(adapterDir, "README.md")),
// `README.md is missing in the adapter dir. Please create it!`,
// ).to.be.true;
// });
// it("LICENSE exists or is present in the README.md", () => {
// const licenseExists = fs.existsSync(path.join(adapterDir, "LICENSE"));
// if (licenseExists) return;
// const readmeContent = fs.readFileSync(path.join(adapterDir, "README.md"), "utf8");
// expect(readmeContent).to.match(
// /## LICENSE/i,
// `The license should be in a file "LICENSE" or be included in "README.md" as a 2nd level headline!`,
// );
// });
// });
}