addons-linter/tests/unit/test.linter.js

1682 строки
51 KiB
JavaScript

/* eslint-disable max-classes-per-file */
import fs from 'fs';
import { oneLine } from 'common-tags';
import { Xpi } from 'addons-scanner-utils/dist/io';
import {
DuplicateZipEntryError,
InvalidZipFileError,
} from 'addons-scanner-utils/dist/errors';
import { createFakeStderr } from 'addons-scanner-utils/dist/test-helpers';
import Linter from 'linter';
import * as constants from 'const';
import * as messages from 'messages';
import ManifestJSONParser from 'parsers/manifestjson';
import BinaryScanner from 'scanners/binary';
import FilenameScanner from 'scanners/filename';
import JavaScriptScanner from 'scanners/javascript';
import JSONScanner from 'scanners/json';
import LangpackScanner from 'scanners/langpack';
import { AddonsLinterUserError } from 'utils';
import {
fakeMessageData,
validManifestJSON,
validStaticThemeManifestJSON,
assertHasMatchingError,
} from './helpers';
const fakeCheckFileExists = async () => {
return {
isDirectory: () => {
return true;
},
isFile: () => {
return true;
},
};
};
class FakeIOBase {
async getFile() {
return '';
}
async getFiles() {
return {};
}
async getFilesByExt() {
return [];
}
setScanFileCallback() {}
}
describe('Linter', () => {
describe('validateConfig', () => {
it('should throw on invalid manifest version range options', async () => {
const addonLinter = new Linter({
_: ['foo'],
minManifestVersion: 3,
maxManifestVersion: 2,
});
sinon.spy(addonLinter, 'validateConfig');
await expect(addonLinter.run()).rejects.toThrow(AddonsLinterUserError);
sinon.assert.calledOnce(addonLinter.validateConfig);
await expect(addonLinter.run()).rejects.toThrow(
/Invalid manifest version range requested/
);
sinon.assert.calledTwice(addonLinter.validateConfig);
});
});
it('should detect an invalid file with ENOENT', async () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.handleError = sinon.stub();
const fakeError = new Error('soz');
fakeError.code = 'ENOENT';
const fakeLstat = async () => {
throw fakeError;
};
await expect(
addonLinter.checkFileExists(addonLinter.packagePath, fakeLstat)
).rejects.toThrow('Path "foo" is not a file');
});
it('should detect other errors during lstat', async () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.handleError = sinon.stub();
const fakeError = new TypeError('soz');
const fakeLstat = async () => {
throw fakeError;
};
await expect(
addonLinter.checkFileExists(addonLinter.packagePath, fakeLstat)
).rejects.toThrow(fakeError);
});
it('should reject if not a file', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.handleError = sinon.stub();
const isFileSpy = sinon.spy(() => {
return false;
});
const isDirSpy = sinon.spy(() => {
return false;
});
const fakeLstat = async () => {
return {
isFile: isFileSpy,
isDirectory: isDirSpy,
};
};
const expectedError = new Error(
'Path "bar" is not a file or directory or does not exist.'
);
await expect(
addonLinter.checkFileExists(addonLinter.packagePath, fakeLstat)
).rejects.toThrow(expectedError);
sinon.assert.calledOnce(isFileSpy);
});
it('should provide output via output prop', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError(fakeMessageData);
const { output } = addonLinter;
expect(output.count).toEqual(1);
expect(output.summary.errors).toEqual(1);
expect(output.summary.notices).toEqual(0);
expect(output.summary.warnings).toEqual(0);
});
it('should collect an error when not an xpi/zip', async () => {
const addonLinter = new Linter({ _: ['tests/fixtures/not-a-zip.zip'] });
// Stub print to prevent output.
addonLinter.print = sinon.stub();
expect(addonLinter.collector.errors.length).toEqual(0);
await addonLinter.scan();
expect(addonLinter.collector.errors.length).toEqual(1);
expect(addonLinter.collector.errors[0].code).toEqual(
messages.BAD_ZIPFILE.code
);
});
// Uses an extension with a mozIndexedDB warning in it.
it('should send JSScanner messages to the collector', async () => {
const addonLinter = new Linter({ _: ['tests/fixtures/webext_mozdb.zip'] });
// Stub print to prevent output.
addonLinter.print = sinon.stub();
expect(addonLinter.collector.warnings.length).toEqual(0);
await addonLinter.scan();
expect(addonLinter.collector.warnings.length).toBeGreaterThan(0);
});
// Test to make sure we can all files inside an add-on, not just one of each.
//
// Uses our example xpi, with the following file layout:
//
// - chrome.manifest
// - chrome/
// - components/
// - main.js (has a mozIndexedDB assignment)
// - secondary.js (nothing bad)
// - prefs.html
it('should scan all files', async () => {
const addonLinter = new Linter({ _: ['tests/fixtures/old.xpi'] });
// Stub print to prevent output.
addonLinter.print = sinon.stub();
const getFileSpy = sinon.spy(addonLinter, 'scanFile');
await addonLinter.scan();
sinon.assert.callOrder(
getFileSpy.withArgs('components/main.js'),
getFileSpy.withArgs('components/secondary.js'),
getFileSpy.withArgs('prefs.html')
);
});
it.each(['mjs', 'jsm'])('should scan %s files', async (fileExtension) => {
const filename = `file.${fileExtension}`;
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_scan_file'],
});
// Stub print to prevent output.
addonLinter.print = sinon.stub();
const getFileSpy = sinon.spy(addonLinter, 'scanFile');
await addonLinter.scan();
sinon.assert.callOrder(
getFileSpy.withArgs(filename),
getFileSpy.withArgs('manifest.json')
);
});
it('Eslint ignore patterns and .eslintignorerc should be ignored', async () => {
// Verify https://github.com/mozilla/addons-linter/issues/1288 is fixed
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_node_modules_bower'],
});
// Stub print to prevent output.
addonLinter.print = sinon.stub();
await addonLinter.scan();
expect(addonLinter.collector.scannedFiles).toEqual({
'index.js': ['javascript'],
'bower_components/bar.js': ['javascript'],
'node_modules/foo.js': ['javascript'],
'manifest.json': ['json'],
});
});
it('Eslint shouldnt ignore dotfiles', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_hidden_files/'],
});
addonLinter.print = sinon.stub();
await addonLinter.scan();
expect(addonLinter.collector.scannedFiles).toEqual({
'.hidden.js': ['javascript'],
});
});
it('should throw when message.type is undefined', async () => {
const addonLinter = new Linter({ _: ['tests/fixtures/webextension.zip'] });
addonLinter.io = { files: { whatever: {} } };
addonLinter.io.getFile = () => Promise.resolve();
addonLinter.getScanner = sinon.stub();
class FakeScanner {
async scan() {
return {
linterMessages: [{ message: 'whatever' }],
scannedFiles: [],
};
}
}
addonLinter.getScanner.returns(FakeScanner);
await expect(addonLinter.scanFile('whatever')).rejects.toThrow(
'message.type must be defined'
);
});
it('should see an error if scanFiles() blows up', async () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.checkFileExists = fakeCheckFileExists;
// Stub handleError to prevent output.
addonLinter.handleError = sinon.stub();
addonLinter.scanFiles = async () => {
throw new Error('scanFiles explosion');
};
class FakeXpi extends FakeIOBase {
async getFilesByExt() {
return ['foo.js', 'bar.js'];
}
}
await expect(addonLinter.scan({ _Xpi: FakeXpi })).rejects.toThrow(
'scanFiles explosion'
);
});
it('should call addError when Xpi rejects with dupe entry', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.collector.addError = sinon.stub();
addonLinter.print = sinon.stub();
const expectedError = new DuplicateZipEntryError('the zip has dupes!');
class FakeXpi extends FakeIOBase {
async getFiles() {
throw expectedError;
}
getFilesByExt() {
return this.getMetadata();
}
}
await addonLinter.scan({ _Xpi: FakeXpi });
sinon.assert.calledWith(
addonLinter.collector.addError,
messages.DUPLICATE_XPI_ENTRY
);
sinon.assert.calledOnce(addonLinter.print);
});
it('should call addError when Xpi rejects with InvalidZipFileError', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.collector.addError = sinon.stub();
addonLinter.print = sinon.stub();
const expectedError = new InvalidZipFileError(
'invalid characters in fileName: fake\\file.txt'
);
class FakeXpi extends FakeIOBase {
async getFiles() {
throw expectedError;
}
getFilesByExt() {
return this.getMetadata();
}
}
await addonLinter.scan({ _Xpi: FakeXpi });
sinon.assert.calledWith(addonLinter.collector.addError, {
...messages.INVALID_XPI_ENTRY,
message: expectedError.message,
});
sinon.assert.calledOnce(addonLinter.print);
});
it('should throw if invalid type is passed to colorize', () => {
const addonLinter = new Linter({ _: ['bar'] });
expect(() => {
addonLinter.colorize('whatever');
}).toThrow(/colorize passed invalid type/);
});
});
describe('Linter.getScanner()', () => {
it('should return BinaryScanner', () => {
const addonLinter = new Linter({ _: ['foo'] });
const Scanner = addonLinter.getScanner('foo.whatever');
expect(Scanner).toEqual(BinaryScanner);
});
it.each(['foo.js', 'bar.jsm', 'baz.mjs'])(
'should return JavaScriptScanner for file: %s',
(file) => {
const addonLinter = new Linter({ _: ['foo'] });
const Scanner = addonLinter.getScanner(file);
expect(Scanner).toEqual(JavaScriptScanner);
}
);
it('should return JSONScanner', () => {
const addonLinter = new Linter({ _: ['foo'] });
const Scanner = addonLinter.getScanner('locales/en.json');
expect(Scanner).toEqual(JSONScanner);
});
it('should return LangpackScanner', () => {
const addonLinter = new Linter({ _: ['foo'] });
let Scanner = addonLinter.getScanner('foo.properties');
expect(Scanner).toEqual(LangpackScanner);
Scanner = addonLinter.getScanner('foo.ftl');
expect(Scanner).toEqual(LangpackScanner);
Scanner = addonLinter.getScanner('foo.dtd');
expect(Scanner).toEqual(LangpackScanner);
});
const shouldBeFilenameScanned = [
'__MACOSX/foo.txt',
'wat.dll',
'META-INF/manifest.mf',
'mozilla-recommendation.json',
'some/subfolder/mozilla-recommendation.json',
];
shouldBeFilenameScanned.forEach((filename) => {
it(`should return FilenameScanner for ${filename}`, () => {
const addonLinter = new Linter({ _: ['foo'] });
const Scanner = addonLinter.getScanner(filename);
expect(Scanner).toEqual(FilenameScanner);
});
});
});
describe('Linter.handleError()', () => {
it('should show stack if config.stack is true', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.config.stack = true;
const fakeError = new Error('Errol the error');
fakeError.stack = 'fake stack city limits';
const fakeConsole = {
error: sinon.stub(),
};
addonLinter.handleError(fakeError, fakeConsole);
sinon.assert.calledWith(fakeConsole.error, fakeError.stack);
});
it('should show colorized error', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.chalk = {};
addonLinter.chalk.red = sinon.stub();
const fakeError = new Error('Errol the error');
fakeError.stack = 'fake stack city limits';
const fakeConsole = {
error: sinon.stub(),
};
addonLinter.handleError(fakeError, fakeConsole);
sinon.assert.calledOnce(fakeConsole.error);
sinon.assert.calledWith(addonLinter.chalk.red, 'Errol the error');
});
});
describe('Linter.print()', () => {
it('should print as json when config.output is json', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.config.output = 'json';
addonLinter.toJSON = sinon.stub();
const fakeConsole = {
log: sinon.stub(),
};
addonLinter.print(fakeConsole);
sinon.assert.calledOnce(addonLinter.toJSON);
sinon.assert.calledOnce(fakeConsole.log);
});
it('should print as json when config.output is text', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.textOutput = sinon.stub();
addonLinter.config.output = 'text';
const fakeConsole = {
log: sinon.stub(),
};
addonLinter.print(fakeConsole);
sinon.assert.calledOnce(addonLinter.textOutput);
sinon.assert.calledOnce(fakeConsole.log);
});
it('should not print anything if config.output is none', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.textOutput = sinon.stub();
addonLinter.toJSON = sinon.stub();
addonLinter.config.output = 'none';
const fakeConsole = {
log: sinon.stub(),
};
addonLinter.print(fakeConsole);
sinon.assert.notCalled(addonLinter.textOutput);
sinon.assert.notCalled(addonLinter.toJSON);
sinon.assert.notCalled(fakeConsole.log);
});
});
describe('Linter.toJSON()', () => {
it('should pass correct args to JSON.stringify for pretty printing', () => {
const addonLinter = new Linter({ _: ['foo'] });
const fakeJSON = {
stringify: sinon.stub(),
};
addonLinter.toJSON({ pretty: true, _JSON: fakeJSON });
sinon.assert.calledWith(fakeJSON.stringify, sinon.match.any, null, 4);
});
it('should output metadata when config.output is json', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.config.output = 'json';
addonLinter.addonMetadata = {
meta: 'data',
};
const fakeJSON = {
stringify: sinon.stub(),
};
addonLinter.toJSON({ pretty: true, _JSON: fakeJSON });
expect(fakeJSON.stringify.firstCall.args[0].metadata.meta).toEqual('data');
});
it('should pass correct args to JSON.stringify for normal printing', () => {
const addonLinter = new Linter({ _: ['foo'] });
const fakeJSON = {
stringify: sinon.stub(),
};
addonLinter.toJSON({ pretty: false, _JSON: fakeJSON });
sinon.assert.calledWith(fakeJSON.stringify, sinon.match.any);
});
it('should provide JSON via toJSON()', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError(fakeMessageData);
const json = addonLinter.toJSON();
const parsedJSON = JSON.parse(json);
expect(parsedJSON.count).toEqual(1);
expect(parsedJSON.summary.errors).toEqual(1);
expect(parsedJSON.summary.notices).toEqual(0);
expect(parsedJSON.summary.warnings).toEqual(0);
});
});
describe('Linter.textOutput()', () => {
// Return a large number from terminalWidth() so text doesn't wrap,
// forcing the strings we check for to be far apart.
function terminalWidth() {
return 1000;
}
function mediumTerminalWidth() {
return 77;
}
function smallTerminalWidth() {
return 59;
}
function uselesslyTinyTerminalWidth() {
return 1;
}
it('should have error in textOutput()', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError({
code: 'WHATEVER_ERROR',
message: 'whatever error message',
description: 'whatever error description',
});
const text = addonLinter.textOutput(terminalWidth);
expect(addonLinter.output.summary.errors).toEqual(1);
expect(text).toContain('Validation Summary:');
expect(text).toContain('WHATEVER_ERROR');
expect(text).toContain('whatever error message');
expect(text).toContain('whatever error description');
});
it('should have notice message in textOutput()', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addNotice({
code: 'WHATEVER_NOTICE',
message: 'whatever notice message',
description: 'whatever notice description',
});
const text = addonLinter.textOutput(terminalWidth);
expect(addonLinter.output.summary.notices).toEqual(1);
expect(text).toContain('Validation Summary:');
expect(text).toContain('WHATEVER_NOTICE');
expect(text).toContain('whatever notice message');
expect(text).toContain('whatever notice description');
});
it('should have warning in textOutput()', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addWarning({
code: 'WHATEVER_WARNING',
message: 'whatever warning message',
description: 'whatever warning description',
});
const text = addonLinter.textOutput(terminalWidth);
expect(addonLinter.output.summary.warnings).toEqual(1);
expect(text).toContain('Validation Summary:');
expect(text).toContain('WHATEVER_WARNING');
expect(text).toContain('whatever warning message');
expect(text).toContain('whatever warning description');
});
it('should remove description when terminal is <78 columns wide', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError({
code: 'WHATEVER_ERROR',
message: 'whatever error message',
description: 'whatever error description',
});
const text = addonLinter.textOutput(mediumTerminalWidth);
expect(addonLinter.output.summary.errors).toEqual(1);
expect(text).not.toContain('Description');
expect(text).not.toContain('whatever error description');
});
it('should remove columns, description, and lines when terminal is < 60 columns wide', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError({
code: 'WHATEVER_ERROR',
message: 'whatever error message',
description: 'whatever error description',
column: 5,
line: 20,
});
const text = addonLinter.textOutput(smallTerminalWidth);
expect(addonLinter.output.summary.errors).toEqual(1);
expect(text).not.toContain('Description');
expect(text).not.toContain('whatever error description');
expect(text).not.toContain('Column');
expect(text).not.toContain('5');
expect(text).not.toContain('Line');
expect(text).not.toContain('20');
});
it('should survive even a 1 column terminal', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError({
code: 'WHATEVER_ERROR',
message: 'whatever error message',
description: 'whatever error description',
column: 5,
line: 20,
});
try {
addonLinter.textOutput(uselesslyTinyTerminalWidth);
expect(addonLinter.output.summary.errors).toEqual(1);
} catch (e) {
// eslint-disable-next-line jest/no-conditional-expect
expect(false).toBe(true);
}
});
});
describe('Linter.getAddonMetadata()', () => {
it('should init with null metadata', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
});
addonLinter.print = sinon.stub();
expect(addonLinter.addonMetadata).toBe(null);
await addonLinter.scan();
const metadata = await addonLinter.getAddonMetadata();
expect(Object.keys(metadata).length).toBeGreaterThan(0);
});
it('should cache and return cached addonMetadata', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
});
addonLinter.io = new Xpi({
filePath: addonLinter.packagePath,
stderr: createFakeStderr(),
});
addonLinter.print = sinon.stub();
// This should only be called when the addonMetadata _is_ populated.
const fakeLog = {
debug: sinon.stub(),
info: sinon.stub(),
error: sinon.stub(),
warn: sinon.stub(),
};
function getMetadata() {
return addonLinter.getAddonMetadata({ _log: fakeLog });
}
await getMetadata();
sinon.assert.notCalled(fakeLog.debug);
expect(typeof addonLinter.addonMetadata).toBe('object');
await getMetadata();
sinon.assert.calledOnce(fakeLog.debug);
sinon.assert.calledWith(
fakeLog.debug,
'Metadata already set; returning cached metadata.'
);
});
it('should look at JSON when parsing manifest.json', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.io = {
getFiles: async () => {
return {
'manifest.json': {},
};
},
getFileAsString: async () => {
return validManifestJSON({});
},
};
const metadata = await addonLinter.getAddonMetadata();
expect(metadata.type).toEqual(constants.PACKAGE_EXTENSION);
});
it('should pass selfHosted flag to ManifestJSONParser', async () => {
const addonLinter = new Linter({ _: ['bar'], selfHosted: true });
addonLinter.io = {
getFiles: async () => {
return {
'manifest.json': {},
};
},
getFileAsString: async () => {
return validManifestJSON({});
},
};
const FakeManifestParser = sinon.spy(ManifestJSONParser);
await addonLinter.getAddonMetadata({
ManifestJSONParser: FakeManifestParser,
});
sinon.assert.calledOnce(FakeManifestParser);
expect(FakeManifestParser.firstCall.args[2].selfHosted).toEqual(true);
});
it.each([
[true, ['META-INF/manifest.mf']],
[false, ['META-INF/manifest.non-signature-file']],
[false, ['ANOTHER-DIR/manifest.mf']],
[false, ['manifest.mf']],
])(
'should pass isAlreadySigned flag set as %p to ManifestJSONParser with packaged files: %p',
async (isAlreadySigned, files) => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.io = {
getFiles: async () => {
return {
'manifest.json': {},
...files.reduce((acc, k) => {
acc[k] = {};
return acc;
}, {}),
};
},
getFileAsString: async () => {
return validManifestJSON({});
},
};
const FakeManifestParser = sinon.spy(ManifestJSONParser);
await addonLinter.getAddonMetadata({
ManifestJSONParser: FakeManifestParser,
});
sinon.assert.calledOnce(FakeManifestParser);
expect(FakeManifestParser.firstCall.args[2].isAlreadySigned).toEqual(
isAlreadySigned
);
}
);
it('should error if no manifest', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.io = {
getFiles: async () => {
return {};
},
};
await addonLinter.getAddonMetadata();
const { errors } = addonLinter.collector;
expect(errors.length).toEqual(1);
expect(errors[0].code).toEqual(messages.TYPE_NO_MANIFEST_JSON.code);
});
it('should validate static theme images defined in the manifest', async () => {
const files = { 'manifest.json': {} };
const getFiles = async () => files;
// Spy the manifest parser.
let spyManifestParser;
const FakeManifestParser = sinon.spy((...args) => {
spyManifestParser = new ManifestJSONParser(...args);
spyManifestParser.validateStaticThemeImages = sinon.spy();
return spyManifestParser;
});
// Test on a static theme manifest.
const addonLinterTheme = new Linter({ _: ['bar'] });
addonLinterTheme.io = {
getFiles,
getFileAsString: async () => {
return validStaticThemeManifestJSON({});
},
};
const themeMetadata = await addonLinterTheme.getAddonMetadata({
ManifestJSONParser: FakeManifestParser,
});
expect(themeMetadata.type).toEqual(constants.PACKAGE_EXTENSION);
sinon.assert.calledOnce(FakeManifestParser);
sinon.assert.calledOnce(spyManifestParser.validateStaticThemeImages);
// Test on a non theme manifest.
const addonLinterExt = new Linter({ _: ['bar'] });
addonLinterExt.io = {
getFiles,
getFileAsString: async () => {
return validManifestJSON({});
},
};
const extMetadata = await addonLinterExt.getAddonMetadata({
ManifestJSONParser: FakeManifestParser,
});
expect(extMetadata.type).toEqual(constants.PACKAGE_EXTENSION);
sinon.assert.calledTwice(FakeManifestParser);
sinon.assert.notCalled(spyManifestParser.validateStaticThemeImages);
});
});
describe('Linter.extractMetadata()', () => {
const fakeConsole = {
error: sinon.stub(),
log: sinon.stub(),
};
it('should use Directory class if isDirectory() is true', async () => {
const addonLinter = new Linter({ _: ['foo'] });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = async () => {
return fakeMetadata;
};
addonLinter.checkFileExists = async () => {
return {
isFile: () => {
return false;
},
isDirectory: () => {
return true;
},
};
};
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
class FakeDirectory extends FakeIOBase {}
const metadata = await addonLinter.extractMetadata({
_Directory: FakeDirectory,
_console: fakeConsole,
});
expect(metadata).toEqual(fakeMetadata);
expect(addonLinter.io).toBeInstanceOf(FakeDirectory);
});
it('should use Crx class if filename ends in .crx', async () => {
const addonLinter = new Linter({ _: ['foo.crx'] });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = async () => {
return fakeMetadata;
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
class FakeCrx extends FakeIOBase {
getFilesByExt() {
return Promise.resolve(['foo.js', 'bar.js']);
}
}
const metadata = await addonLinter.extractMetadata({
_Crx: FakeCrx,
_console: fakeConsole,
});
expect(metadata).toEqual(fakeMetadata);
expect(addonLinter.io).toBeInstanceOf(FakeCrx);
});
it('should configure a file filter on the IO object', async () => {
const shouldScanFile = sinon.spy(() => true);
const addonLinter = new Linter({
_: ['foo.crx'],
shouldScanFile,
});
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = async () => {
return fakeMetadata;
};
addonLinter.checkFileExists = async () => {
return {
isFile: () => {
return false;
},
isDirectory: () => {
return true;
},
};
};
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
const setScanFileCallback = sinon.stub();
class FakeDirectory extends FakeIOBase {
setScanFileCallback(...args) {
setScanFileCallback(...args);
}
}
await addonLinter.extractMetadata({ _Directory: FakeDirectory });
expect(addonLinter.io).toBeInstanceOf(FakeDirectory);
sinon.assert.calledOnce(setScanFileCallback);
expect(typeof setScanFileCallback.firstCall.args[0]).toEqual('function');
sinon.assert.notCalled(shouldScanFile);
setScanFileCallback.firstCall.args[0]();
sinon.assert.calledOnce(shouldScanFile);
});
it('should return metadata', async () => {
const addonLinter = new Linter({ _: ['foo'] });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = async () => {
return fakeMetadata;
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
addonLinter.markSpecialFiles = async (addonMetadata) => {
return addonMetadata;
};
class FakeXpi extends FakeIOBase {}
const metadata = await addonLinter.extractMetadata({
_Xpi: FakeXpi,
_console: fakeConsole,
});
expect(metadata).toEqual(fakeMetadata);
});
it('should return errors as part of metadata JSON.', async () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
// Invoke an error so we can make sure we see it in the
// output.
addonLinter.collector.addError({
code: 'FAKE_METADATA_ERROR',
message: 'Fake metadata error',
description: 'Fake metadata error description',
});
const fakeMetadata = { type: 1 };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = async () => {
return fakeMetadata;
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
addonLinter.markSpecialFiles = async (addonMetadata) => {
return addonMetadata;
};
class FakeXpi extends FakeIOBase {}
await addonLinter.extractMetadata({
_Xpi: FakeXpi,
_console: fakeConsole,
});
sinon.assert.calledOnce(addonLinter.toJSON);
const inputObject = addonLinter.toJSON.firstCall.args[0].input;
expect(inputObject.hasErrors).toEqual(true);
expect(inputObject.metadata).toEqual(fakeMetadata);
expect(inputObject.errors.length).toEqual(1);
expect(inputObject.errors[0].code).toEqual('FAKE_METADATA_ERROR');
});
// Uses our empty-with-library extension, with the following file layout:
//
// - bootstrap.js
// - data/
// - change-text.js
// - empty.js (empty file)
// - jquery-3.2.1.min.js (minified jQuery)
// - index.js
// - package.json
// - README.md
it('should flag empty files in a ZIP.', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty-with-library.zip'],
});
const markEmptyFilesSpy = sinon.spy(addonLinter, '_markEmptyFiles');
const metadata = await addonLinter.extractMetadata({
_console: fakeConsole,
});
sinon.assert.calledOnce(markEmptyFilesSpy);
expect(metadata.emptyFiles).toEqual(['data/empty.js']);
});
// Uses our empty-with-library extension, with the following file layout:
//
// - bootstrap.js
// - data/
// - change-text.js
// - empty.js (empty file)
// - jquery-3.2.1.min.js (minified jQuery)
// - index.js
// - package.json
// - README.md
it('should flag known JS libraries in a ZIP.', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty-with-library.zip'],
});
const markJSFilesSpy = sinon.spy(addonLinter, '_markJSLibs');
const metadata = await addonLinter.extractMetadata({
_console: fakeConsole,
});
sinon.assert.calledOnce(markJSFilesSpy);
expect(Object.keys(metadata.jsLibs).length).toEqual(1);
expect(metadata.jsLibs).toEqual({
'data/jquery-3.2.1.min.js': 'jquery.3.2.1.jquery.min.js',
});
});
it('should flag known JS libraries', async () => {
const addonLinter = new Linter({ _: ['foo'] });
const markJSFilesSpy = sinon.spy(addonLinter, '_markJSLibs');
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.scanFiles = () => Promise.resolve();
// suppress output.
addonLinter.print = sinon.stub();
const fakeFiles = {
'angular.js': 'angular-1.2.28.min.js',
'my/real/files/notalib.js': 'not-a-library.js',
'my/real/files/alsonotalib.js': 'not-a-library.js',
'my/nested/library/path/j.js': 'jquery-3.2.1.min.js',
};
class FakeXpi extends FakeIOBase {
async getFile(filename) {
return this.getFileAsString(filename);
}
async getFiles() {
const files = {};
files['manifest.json'] = { uncompressedSize: 839 };
Object.keys(fakeFiles).forEach((filename) => {
files[filename] = { uncompressedSize: 5 };
});
return files;
}
async getFilesByExt() {
return Object.keys(fakeFiles).concat([constants.MANIFEST_JSON]);
}
async getFileAsString(filename) {
return filename === constants.MANIFEST_JSON
? validManifestJSON()
: fs.readFileSync(
`tests/fixtures/jslibs/${fakeFiles[filename]}`,
'utf-8'
);
}
}
const metadata = await addonLinter.extractMetadata({
_console: fakeConsole,
_Xpi: FakeXpi,
});
sinon.assert.calledOnce(markJSFilesSpy);
expect(Object.keys(metadata.jsLibs).length).toEqual(1);
expect(metadata.jsLibs).toEqual({
'my/nested/library/path/j.js': 'jquery.3.2.1.jquery.min.js',
});
const { notices } = addonLinter.collector;
expect(notices.length).toEqual(1);
expect(notices[0].code).toEqual(messages.KNOWN_LIBRARY.code);
});
it('should not scan known JS libraries', async () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.scan = () => Promise.resolve();
// suppress output.
addonLinter.print = sinon.stub();
const fakeFiles = {
'my/nested/library/path/j.js': 'jquery-3.2.1.min.js',
};
class FakeXpi extends FakeIOBase {
async getFile(path) {
return fs.readFileSync(
`tests/fixtures/jslibs/${fakeFiles[path]}`,
'utf-8'
);
}
async getFiles() {
const files = {};
Object.keys(fakeFiles).forEach((filename) => {
files[filename] = { uncompressedSize: 5 };
});
return files;
}
async getFilesByExt() {
return Object.keys(fakeFiles);
}
}
await addonLinter.extractMetadata({
_console: fakeConsole,
_Xpi: FakeXpi,
});
expect(addonLinter.collector.warnings.length).toBe(0);
});
// Uses our angular-bad-library extension, with the following file layout:
//
// - bootstrap.js
// - data/
// - angular-1.2.28.min.js (minified Angular)
// - change-text.js
// - empty.js (empty file)
// - jquery-3.2.1.min.js (minified jQuery)
// - index.js
// - package.json
// - README.md
it('should flag banned JS libraries in a ZIP.', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/angular-bad-library.zip'],
});
const markBannedSpy = sinon.spy(addonLinter, '_markBannedLibs');
const metadata = await addonLinter.extractMetadata({
_console: fakeConsole,
});
sinon.assert.calledOnce(markBannedSpy);
expect(Object.keys(metadata.jsLibs).length).toEqual(2);
expect(metadata.jsLibs).toEqual({
'data/angular.min.js': 'angularjs.1.3.3.angular.min.js',
'data/jquery-3.2.1.min.js': 'jquery.3.2.1.jquery.min.js',
});
const { errors } = addonLinter.collector;
expect(errors.length).toEqual(1);
expect(errors[0].code).toEqual(messages.BANNED_LIBRARY.code);
});
it('should flag unadvised JS libraries in a ZIP.', () => {
const addonLinter = new Linter({
_: ['fake.zip'],
});
const fakeUnadvisedLibs = ['test_unadvised_fake_lib.js'];
const fakeMetadata = {
jsLibs: {
'data/unadvised_fake_lib.js': 'test_unadvised_fake_lib.js',
'data/jquery-3.2.1.min.js': 'jquery.3.2.1.jquery.min.js',
},
};
addonLinter._markBannedLibs(fakeMetadata, fakeUnadvisedLibs);
expect(Object.keys(fakeMetadata.jsLibs).length).toEqual(2);
expect(fakeMetadata.jsLibs).toEqual({
'data/unadvised_fake_lib.js': 'test_unadvised_fake_lib.js',
'data/jquery-3.2.1.min.js': 'jquery.3.2.1.jquery.min.js',
});
const { warnings } = addonLinter.collector;
expect(warnings.length).toEqual(1);
expect(warnings[0].code).toEqual(messages.UNADVISED_LIBRARY.code);
});
it('should flag potentially minified JS files', async () => {
const addonLinter = new Linter({ _: ['foo'] });
const markUnknownOrMinifiedCodeSpy = sinon.spy(
addonLinter,
'_markUnknownOrMinifiedCode'
);
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.scanFiles = () => Promise.resolve();
// suppress output.
addonLinter.print = sinon.stub();
const read = (filename) => {
return fs.readFileSync(`tests/fixtures/jslibs/${filename}`, 'utf-8');
};
const fakeFiles = {
// Regular library, should be in `jsLibs` and not matched as a minified
// file.
'jquery.js': read('jquery-3.2.1.min.js'),
// Regular libraries but modified so the hashes differ. They should
// be matched by `markUnknownOrMinifiedCode`.
'modified-jquery.js': read('jquery-3.2.1-modified.js'),
'modified-angular.js': read('angular-1.2.28-modified.js'),
// sourceMap(URL) matching is a good indicator for minified code.
'minified-with-sourcemap.js': read('minified-with-sourcemap.js'),
'sourcemap-with-external-url.js': oneLine`
//# sourceMappingURL=http://example.com/path/to/your/sourcemap.map`,
// Should match indentation detection, it's less than 500 chars long.
'minified-no-nl.js': "(function(){alert('foo')});".repeat(10),
// Should not match because > 20% of lines that are properly indented
'minified-less-than-20percent.js': read(
'minified-less-than-20percent.js'
).trim(),
};
class FakeXpi extends FakeIOBase {
getFile(filename) {
return this.getFileAsString(filename);
}
async getFiles() {
const files = {};
Object.keys(fakeFiles).forEach((filename) => {
files[filename] = { uncompressedSize: 5 };
});
return files;
}
async getFilesByExt() {
return Object.keys(fakeFiles);
}
async getFileAsString(filename) {
return fakeFiles[filename];
}
}
const metadata = await addonLinter.extractMetadata({
_console: fakeConsole,
_Xpi: FakeXpi,
});
sinon.assert.calledOnce(markUnknownOrMinifiedCodeSpy);
expect(metadata.unknownMinifiedFiles).toEqual([
'modified-jquery.js',
'modified-angular.js',
'minified-with-sourcemap.js',
'sourcemap-with-external-url.js',
'minified-no-nl.js',
]);
expect(metadata.jsLibs).toEqual({
'jquery.js': 'jquery.3.2.1.jquery.min.js',
});
});
it('should use size attribute if uncompressedSize is undefined', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = async () => {
return {
isFile: () => {
return false;
},
isDirectory: () => {
return true;
},
};
};
addonLinter.scanFiles = () => Promise.resolve();
// suppress output.
addonLinter.print = sinon.stub();
const markEmptyFilesSpy = sinon.spy(addonLinter, '_markEmptyFiles');
class FakeDirectory extends FakeIOBase {
async getFiles() {
return {
'dictionaries/something': { size: 5 },
whatever: { size: 0 },
};
}
}
const metadata = await addonLinter.extractMetadata({
_Directory: FakeDirectory,
_console: fakeConsole,
});
sinon.assert.calledOnce(markEmptyFilesSpy);
expect(metadata.emptyFiles).toEqual(['whatever']);
});
it('should error if no size attributes are found', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.scanFiles = () => Promise.resolve();
// suppress output.
addonLinter.print = sinon.stub();
const markEmptyFilesSpy = sinon.spy(addonLinter, '_markEmptyFiles');
class FakeXpi extends FakeIOBase {
async getFiles() {
return {
'dictionaries/something': { uncompressedSize: 5 },
whatever: {},
};
}
}
await expect(
addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole })
).rejects.toThrow('No size available for whatever');
sinon.assert.calledOnce(markEmptyFilesSpy);
});
it('should error if file size of a non-binary file is too large', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = fakeCheckFileExists;
// suppress output.
addonLinter.print = sinon.stub();
const largeFileSize = constants.MAX_FILE_SIZE_TO_PARSE_MB * 1024 * 1024 + 1;
class FakeXpi extends FakeIOBase {
files = {
'manifest.json': { uncompressedSize: 839 },
'myfile.css': { uncompressedSize: largeFileSize },
'myfile.js': { uncompressedSize: largeFileSize },
'some.exe': { uncompressedSize: largeFileSize },
};
getFile(filename) {
return this.getFileAsString(filename);
}
async getFiles() {
return this.files;
}
async getFilesByExt(type) {
return type === 'js' ? ['myfile.js'] : ['myfile.css'];
}
async getFileAsString(filename) {
return filename === constants.MANIFEST_JSON
? validManifestJSON()
: 'const foo = "bar";';
}
}
await addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole });
expect(addonLinter.collector.errors[0].code).toEqual(
messages.FILE_TOO_LARGE.code
);
// JS files that are too large should be flagged. As of March 2024, we no
// longer report "large" CSS files.
expect(addonLinter.collector.errors.length).toBe(1);
});
it('should ignore large binary files', async () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = fakeCheckFileExists;
// suppress output.
addonLinter.print = sinon.stub();
const largeFileSize = constants.MAX_FILE_SIZE_TO_PARSE_MB * 1024 * 1024 * 4;
class FakeXpi extends FakeIOBase {
files = {
'manifest.json': { uncompressedSize: 839 },
'myfile.jpg': { uncompressedSize: largeFileSize },
};
getFile(filename) {
return this.getFileAsString(filename);
}
async getFiles() {
return this.files;
}
async getFilesByExt(type) {
return type === 'json' ? ['manifest.json'] : ['myfile.jpg'];
}
async getFileAsString(filename) {
return filename === constants.MANIFEST_JSON ? validManifestJSON() : '';
}
}
await addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole });
expect(addonLinter.collector.errors.length).toBe(0);
});
// Total zip size is 96080 but only a handful of files are actually
// scanned.
// Archive: tests/fixtures/empty-with-library.zip
// Skipped Length Date Time Name
// ------- ------- ---------- ----- ----
// 593 2015-11-28 19:46 bootstrap.js
// X 0 2017-05-09 15:09 data/
// X 6148 2017-05-09 15:09 data/.DS_Store
// X 0 2017-05-09 15:09 __MACOSX/
// X 0 2017-05-09 15:09 __MACOSX/data/
// X 120 2017-05-09 15:09 __MACOSX/data/._.DS_Store
// 1420 2015-11-28 19:46 data/change-text.js
// 0 2015-11-28 19:46 data/empty.js
// X 86659 2017-03-20 20:01 data/jquery-3.2.1.min.js
// 195 2017-03-20 20:01 __MACOSX/data/._jquery-3.2.1.min.js
// 421 2015-11-28 19:46 index.js
// 218 2016-06-30 16:10 manifest.json
// 277 2015-11-28 19:46 package.json
// X 29 2015-11-28 19:46 README.md
// ------- -------
// 96080 14 files
it('should collect total size of all scanned files', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty-with-library.zip'],
});
addonLinter.print = sinon.stub();
await addonLinter.scan({ _console: fakeConsole });
expect(addonLinter.output.metadata.totalScannedFileSize).toEqual(2929);
});
it('should flag coin miners', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_badwords.zip'],
});
const markCoinMinerUsageSpy = sinon.spy(addonLinter, '_markCoinMinerUsage');
// suppress output.
addonLinter.print = sinon.stub();
addonLinter.checkFileExists = fakeCheckFileExists;
class FakeXpi extends FakeIOBase {
files = {
'coinhive_disguised_as_preferences.js': { uncompressedSize: 20 },
'included_in_manifest.json': { uncompressedSize: 20 },
'coinhive_disguised_renamed.js': { uncompressedSize: 20 },
'coinhive.min.js': { uncompressedSize: 20 },
};
getFile(filename) {
return this.getFileAsString(filename);
}
async getFiles() {
return this.files;
}
async getFileAsString(filename) {
const contents = fs.readFileSync(
`tests/fixtures/coinminers/${filename}`,
'utf-8'
);
return contents;
}
}
await addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole });
// Only manifest and js files, binary files like the .png are ignored
sinon.assert.callCount(markCoinMinerUsageSpy, 4);
const { warnings } = addonLinter.collector;
expect(warnings.length).toEqual(5);
assertHasMatchingError(warnings, {
code: 'COINMINER_USAGE_DETECTED',
column: 284,
line: 2,
instancePath: 'CoinHive.CONFIG',
file: 'coinhive_disguised_as_preferences.js',
});
assertHasMatchingError(warnings, {
code: 'COINMINER_USAGE_DETECTED',
column: 119556,
line: 26,
instancePath: 'CryptonightWASMWrapper',
file: 'coinhive_disguised_as_preferences.js',
});
assertHasMatchingError(warnings, {
code: 'COINMINER_USAGE_DETECTED',
column: 30,
line: 12,
file: 'included_in_manifest.json',
});
assertHasMatchingError(warnings, {
code: 'COINMINER_USAGE_DETECTED',
column: 134338,
line: 1,
instancePath: 'CryptonightWASMWrapper',
file: 'coinhive_disguised_renamed.js',
});
assertHasMatchingError(warnings, {
code: 'COINMINER_USAGE_DETECTED',
column: undefined,
line: undefined,
instancePath: undefined,
file: 'coinhive.min.js',
});
});
});
describe('Linter.run()', () => {
const fakeConsole = {
log: sinon.stub(),
};
it('should run extractMetadata() when metadata is true', async () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = async () => {
return fakeMetadata;
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
sinon
.stub(addonLinter, 'markSpecialFiles')
.callsFake(async (addonMetadata) => {
return addonMetadata;
});
class FakeXpi extends FakeIOBase {}
await addonLinter.run({ _Xpi: FakeXpi, _console: fakeConsole });
sinon.assert.calledOnce(addonLinter.toJSON);
sinon.assert.calledOnce(addonLinter.markSpecialFiles);
expect(addonLinter.toJSON.firstCall.args[0].input).toEqual({
hasErrors: false,
metadata: fakeMetadata,
});
});
it('should run scan() when metadata is false', async () => {
const addonLinter = new Linter({ _: ['foo'], metadata: false });
addonLinter.scan = sinon.stub();
addonLinter.scan.returns(Promise.resolve());
await addonLinter.run({ _console: fakeConsole });
sinon.assert.calledOnce(addonLinter.scan);
});
it('should surface errors when metadata is true', async () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
const expectedError = new Error('metadata explosion');
addonLinter.toJSON = sinon.stub();
addonLinter.handleError = sinon.spy();
addonLinter.getAddonMetadata = async () => {
throw expectedError;
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
class FakeXpi extends FakeIOBase {}
await expect(
addonLinter.run({ _Xpi: FakeXpi, _console: fakeConsole })
).rejects.toThrow(expectedError);
sinon.assert.calledOnce(addonLinter.handleError);
});
it('should resolve to the linting results object', async () => {
const addonLinter = new Linter({ _: ['foo'], metadata: false });
addonLinter.scan = sinon.stub();
addonLinter.scan.returns(Promise.resolve());
const result = await addonLinter.run({ _console: fakeConsole });
expect(result).toEqual(addonLinter.output);
});
it('should resolve to the linting results when metadata is true', async () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
addonLinter.extractMetadata = sinon.stub();
addonLinter.extractMetadata.returns(Promise.resolve());
const result = await addonLinter.run({ _console: fakeConsole });
expect(result).toEqual(addonLinter.output);
});
it('should pass disabled linting rules to Scanner', async () => {
const addonLinter = new Linter({ _: ['tests/fixtures/good.zip'] });
const fakeScanner = sinon.stub().returns({
scan: () => Promise.resolve({ linterMessages: [], scannedFiles: [] }),
});
addonLinter.getScanner = sinon.stub().returns(fakeScanner);
addonLinter.config.disableLinterRules = 'foo,bar, baz';
await addonLinter.run({ _console: fakeConsole });
sinon.assert.calledWithNew(fakeScanner);
sinon.assert.calledWithExactly(
fakeScanner,
sinon.match.string, // fileData
sinon.match.string, // filename
sinon.match({ disabledRules: 'foo,bar, baz' })
);
});
describe('auto-close', () => {
class FakeXpi extends FakeIOBase {
static closeWasCalled = false;
constructor() {
super();
FakeXpi.closeWasCalled = false;
}
close() {
FakeXpi.closeWasCalled = true;
}
}
it('should not close the IO object when auto-close feature is enabled (default)', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
disableXpiAutoclose: false,
});
await addonLinter.run({ _Xpi: FakeXpi });
expect(FakeXpi.closeWasCalled).toEqual(false);
});
it('should close the IO object when auto-close feature is disabled', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
disableXpiAutoclose: true,
});
await addonLinter.run({ _Xpi: FakeXpi });
expect(FakeXpi.closeWasCalled).toEqual(true);
});
it('should not close the IO object when auto-close feature is enabled (default) and metadata is true', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
metadata: true,
disableXpiAutoclose: false,
});
await addonLinter.run({ _Xpi: FakeXpi });
expect(FakeXpi.closeWasCalled).toEqual(false);
});
it('should close the IO object when auto-close feature is disabled and metadata is true', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
metadata: true,
disableXpiAutoclose: true,
});
await addonLinter.run({ _Xpi: FakeXpi });
expect(FakeXpi.closeWasCalled).toEqual(true);
});
it('should work with empty ZIP files', async () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty.zip'],
disableXpiAutoclose: true,
});
await addonLinter.run();
const { errors } = addonLinter.collector;
expect(errors.length).toEqual(1);
expect(errors[0].code).toEqual(messages.TYPE_NO_MANIFEST_JSON.code);
});
});
});