addons-linter/tests/test.linter.js

1396 строки
43 KiB
JavaScript

import fs from 'fs';
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 CSSScanner from 'scanners/css';
import FilenameScanner from 'scanners/filename';
import JSONScanner from 'scanners/json';
import { oneLine } from 'common-tags';
import { Xpi } from 'io';
import {
fakeMessageData,
unexpectedSuccess,
validManifestJSON } from './helpers';
const fakeCheckFileExists = () => {
return Promise.resolve({
isDirectory: () => {
return true;
},
isFile: () => {
return true;
},
});
};
class FakeIOBase {
getFile() {
return Promise.resolve('');
}
getFiles() {
return Promise.resolve({});
}
getFilesByExt() {
return Promise.resolve([]);
}
setScanFileCallback() {
}
}
describe('Linter', () => {
it('should detect an invalid file with ENOENT', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.handleError = sinon.stub();
const fakeError = new Error('soz');
fakeError.code = 'ENOENT';
const fakeLstat = () => {
return Promise.reject(fakeError);
};
return addonLinter.checkFileExists(addonLinter.packagePath, fakeLstat)
.then(unexpectedSuccess)
.catch((err) => {
expect(err).toBeInstanceOf(Error);
expect(err.message).toContain('Path "foo" is not a file');
});
});
it('should detect other errors during lstat', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.handleError = sinon.stub();
const fakeError = new TypeError('soz');
const fakeLstat = () => {
return Promise.reject(fakeError);
};
return addonLinter.checkFileExists(addonLinter.packagePath, fakeLstat)
.then(unexpectedSuccess)
.catch((err) => {
expect(err).toBeInstanceOf(TypeError);
expect(err.message).toContain('soz');
});
});
it('should reject if not a file', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.handleError = sinon.stub();
const isFileSpy = sinon.spy(() => {
return false;
});
const isDirSpy = sinon.spy(() => {
return false;
});
const fakeLstat = () => {
return Promise.resolve({
isFile: isFileSpy,
isDirectory: isDirSpy,
});
};
return addonLinter.checkFileExists(addonLinter.packagePath, fakeLstat)
.then(unexpectedSuccess)
.catch((err) => {
expect(err).toBeInstanceOf(Error);
expect(err.message).toContain('Path "bar" is not a file or directory');
sinon.assert.calledOnce(isFileSpy);
});
});
it('should provide output via output prop', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.collector.addError(fakeMessageData);
const output = addonLinter.output;
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', () => {
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);
return addonLinter.scan()
.catch(() => {
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', () => {
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);
return addonLinter.scan()
.then(() => {
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)
// - install.rdf
// - prefs.html
it('should scan all files', () => {
const addonLinter = new Linter({ _: ['tests/fixtures/old.xpi'] });
// Stub print to prevent output.
addonLinter.print = sinon.stub();
const getFileSpy = sinon.spy(addonLinter, 'scanFile');
return addonLinter.scan()
.then(() => {
sinon.assert.callOrder(
getFileSpy.withArgs('components/main.js'),
getFileSpy.withArgs('components/secondary.js'),
getFileSpy.withArgs('install.rdf'),
getFileSpy.withArgs('prefs.html'));
});
});
it('should optionally scan a single file', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_scan_file'],
scanFile: ['subdir/test.js'],
});
// Stub print to prevent output.
addonLinter.print = sinon.stub();
const getFileSpy = sinon.spy(addonLinter, 'scanFile');
return addonLinter.scan()
.then(() => {
sinon.assert.callOrder(
getFileSpy.withArgs('manifest.json'),
getFileSpy.withArgs('subdir/test.js'));
});
});
it('Eslint ignore patterns and .eslintignorerc should be ignored', () => {
// 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();
return addonLinter.scan()
.then(() => {
expect(addonLinter.collector.scannedFiles).toEqual({
'index.js': ['javascript'],
'bower_components/bar.js': ['javascript'],
'node_modules/foo.js': ['javascript'],
'manifest.json': ['json'],
});
});
});
it('should optionally scan selected files', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_scan_file'],
scanFile: ['subdir/test.js', 'subdir/test2.js'],
});
// Stub print to prevent output.
addonLinter.print = sinon.stub();
const getFileSpy = sinon.spy(addonLinter, 'scanFile');
return addonLinter.scan()
.then(() => {
sinon.assert.callOrder(
getFileSpy.withArgs('manifest.json'),
getFileSpy.withArgs('subdir/test.js'),
getFileSpy.withArgs('subdir/test2.js'));
});
});
it('should raise an error if selected file are not found', () => {
const files = ['subdir/test3.js', 'subdir/test4.js'];
const addonLinter = new Linter({
_: ['tests/fixtures/webextension_scan_file'],
scanFile: files,
});
// Stub print to prevent output.
addonLinter.print = sinon.stub();
return addonLinter.scan().then(() => {
expect(false).toBe(true);
}, (err) => {
expect(err.message).toEqual(
`Selected file(s) not found: ${files.join(', ')}`
);
});
});
it('should throw when message.type is undefined', () => {
const addonLinter = new Linter({ _: ['tests/fixtures/webextension.zip'] });
addonLinter.io = { files: { whatever: {} } };
addonLinter.io.getFile = () => Promise.resolve();
addonLinter.getScanner = sinon.stub();
class fakeScanner {
scan() {
return Promise.resolve({
linterMessages: [{ message: 'whatever' }],
scannedFiles: [],
});
}
}
addonLinter.getScanner.returns(fakeScanner);
return addonLinter.scanFile('whatever')
.then(unexpectedSuccess)
.catch((err) => {
expect(err.message).toContain('message.type must be defined');
});
});
it('should see an error if scanFiles() blows up', () => {
const addonLinter = new Linter({ _: ['foo'] });
addonLinter.checkFileExists = fakeCheckFileExists;
// Stub handleError to prevent output.
addonLinter.handleError = sinon.stub();
addonLinter.scanFiles = () => {
return Promise.reject(new Error('scanFiles explosion'));
};
class FakeXpi extends FakeIOBase {
getFilesByExt() {
return Promise.resolve(['foo.js', 'bar.js']);
}
}
return addonLinter.scan({ _Xpi: FakeXpi })
.then(unexpectedSuccess)
.catch((err) => {
expect(err).toBeInstanceOf(Error);
expect(err.message).toContain('scanFiles explosion');
});
});
it('should call addError when Xpi rejects with dupe entry', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.collector.addError = sinon.stub();
addonLinter.print = sinon.stub();
class FakeXpi extends FakeIOBase {
getFiles() {
return Promise.reject(
new Error('DuplicateZipEntry the zip has dupes!'));
}
getFilesByExt() {
return this.getMetadata();
}
}
return addonLinter.scan({ _Xpi: FakeXpi })
.then(unexpectedSuccess)
.catch(() => {
sinon.assert.calledWith(
addonLinter.collector.addError,
messages.DUPLICATE_XPI_ENTRY);
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('should return CSSScanner', () => {
const addonLinter = new Linter({ _: ['foo'] });
const Scanner = addonLinter.getScanner('foo.css');
expect(Scanner).toEqual(CSSScanner);
});
it('should return JSONScanner', () => {
const addonLinter = new Linter({ _: ['foo'] });
const Scanner = addonLinter.getScanner('locales/en.json');
expect(Scanner).toEqual(JSONScanner);
});
const shouldBeFilenameScanned = [
'__MACOSX/foo.txt',
'wat.dll',
'META-INF/manifest.mf',
];
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);
});
it('should print scanFile if any', () => {
const addonLinter = new Linter({
_: ['foo'],
scanFile: ['testfile.js'],
});
const textOutputSpy = sinon.spy(addonLinter, 'textOutput');
addonLinter.config.output = 'text';
let logData = '';
const fakeConsole = {
// eslint-disable-next-line no-return-assign
log: sinon.spy((...args) => logData += `${args.join(' ')}\n`),
};
addonLinter.print(fakeConsole);
sinon.assert.calledOnce(textOutputSpy);
sinon.assert.calledOnce(fakeConsole.log);
expect(logData).toContain('Selected files: testfile.js');
});
});
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(oneLine`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) {
expect(false).toBe(true);
}
});
});
describe('Linter.getAddonMetadata()', () => {
it('should init with null metadata', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
});
addonLinter.print = sinon.stub();
expect(addonLinter.addonMetadata).toBe(null);
return addonLinter.scan()
.then(() => {
return addonLinter.getAddonMetadata();
})
.then((metadata) => {
expect(Object.keys(metadata).length).toBeGreaterThan(0);
});
});
it('should cache and return cached addonMetadata', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/webextension.zip'],
});
addonLinter.io = new Xpi(addonLinter.packagePath);
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 });
}
return getMetadata()
.then(() => {
sinon.assert.notCalled(fakeLog.debug);
expect(typeof addonLinter.addonMetadata).toBe('object');
})
.then(() => getMetadata())
.then(() => {
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', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.io = {
getFiles: () => {
return Promise.resolve({
'manifest.json': {},
});
},
getFileAsString: () => {
return Promise.resolve(validManifestJSON({}));
},
};
return addonLinter.getAddonMetadata()
.then((metadata) => {
expect(metadata.type).toEqual(constants.PACKAGE_EXTENSION);
});
});
it('should pass selfHosted flag to ManifestJSONParser', () => {
const addonLinter = new Linter({ _: ['bar'], selfHosted: true });
addonLinter.io = {
getFiles: () => {
return Promise.resolve({
'manifest.json': {},
});
},
getFileAsString: () => {
return Promise.resolve(validManifestJSON({}));
},
};
const FakeManifestParser = sinon.spy(ManifestJSONParser);
return addonLinter.getAddonMetadata({
ManifestJSONParser: FakeManifestParser,
})
.then(() => {
sinon.assert.calledOnce(FakeManifestParser);
expect(FakeManifestParser.firstCall.args[2].selfHosted).toEqual(true);
});
});
it('should collect an error if manifest.json and install.rdf found', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.io = {
getFiles: () => {
return Promise.resolve({
'install.rdf': {},
'manifest.json': {},
});
},
};
return addonLinter.getAddonMetadata()
.then(() => {
const errors = addonLinter.collector.errors;
expect(errors.length).toEqual(2);
expect(errors[0].code).toEqual(messages.MULTIPLE_MANIFESTS.code);
expect(errors[1].code).toEqual(messages.TYPE_NOT_DETERMINED.code);
});
});
it('should collect notices if no manifest', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.io = {
getFiles: () => {
return Promise.resolve({});
},
};
return addonLinter.getAddonMetadata()
.then(() => {
const notices = addonLinter.collector.notices;
expect(notices.length).toEqual(2);
expect(notices[0].code).toEqual(messages.TYPE_NO_MANIFEST_JSON.code);
expect(notices[1].code).toEqual(messages.TYPE_NO_INSTALL_RDF.code);
});
});
});
describe('Linter.extractMetadata()', () => {
const fakeConsole = {
error: sinon.stub(),
log: sinon.stub(),
};
it('should use Directory class if isDirectory() is true', () => {
const addonLinter = new Linter({ _: ['foo'] });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = () => {
return Promise.resolve(fakeMetadata);
};
addonLinter.checkFileExists = () => {
return Promise.resolve({
isFile: () => {
return false;
},
isDirectory: () => {
return true;
},
});
};
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
class FakeDirectory extends FakeIOBase {
}
return addonLinter.extractMetadata({
_Directory: FakeDirectory,
_console: fakeConsole,
}).then((metadata) => {
expect(metadata).toEqual(fakeMetadata);
expect(addonLinter.io).toBeInstanceOf(FakeDirectory);
});
});
it('should use Crx class if filename ends in .crx', () => {
const addonLinter = new Linter({ _: ['foo.crx'] });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = () => {
return Promise.resolve(fakeMetadata);
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
class FakeCrx extends FakeIOBase {
getFilesByExt() {
return Promise.resolve(['foo.js', 'bar.js']);
}
}
return addonLinter.extractMetadata({ _Crx: FakeCrx, _console: fakeConsole })
.then((metadata) => {
expect(metadata).toEqual(fakeMetadata);
expect(addonLinter.io).toBeInstanceOf(FakeCrx);
});
});
it('should configure a file filter on the IO object', () => {
const shouldScanFile = sinon.spy(() => true);
const addonLinter = new Linter({
_: ['foo.crx'],
shouldScanFile,
});
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = () => {
return Promise.resolve(fakeMetadata);
};
addonLinter.checkFileExists = () => {
return Promise.resolve({
isFile: () => {
return false;
},
isDirectory: () => {
return true;
},
});
};
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
const setScanFileCallback = sinon.stub();
class FakeDirectory extends FakeIOBase {
setScanFileCallback(...args) {
setScanFileCallback(...args);
}
}
return addonLinter.extractMetadata({ _Directory: FakeDirectory })
.then(() => {
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', () => {
const addonLinter = new Linter({ _: ['foo'] });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = () => {
return Promise.resolve(fakeMetadata);
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
addonLinter.markSpecialFiles = (addonMetadata) => {
return Promise.resolve(addonMetadata);
};
class FakeXpi extends FakeIOBase {
}
return addonLinter.extractMetadata({
_Xpi: FakeXpi,
_console: fakeConsole,
}).then((metadata) => {
expect(metadata).toEqual(fakeMetadata);
});
});
it('should return errors as part of metadata JSON.', () => {
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 = () => {
return Promise.resolve(fakeMetadata);
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
addonLinter.markSpecialFiles = (addonMetadata) => {
return Promise.resolve(addonMetadata);
};
class FakeXpi extends FakeIOBase {
}
return addonLinter.extractMetadata({
_Xpi: FakeXpi,
_console: fakeConsole,
}).then(() => {
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
// - install.rdf
// - package.json
// - README.md
it('should flag empty files in a ZIP.', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty-with-library.zip'],
});
const markEmptyFilesSpy = sinon.spy(addonLinter, '_markEmptyFiles');
return addonLinter.extractMetadata({ _console: fakeConsole })
.then((metadata) => {
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
// - install.rdf
// - package.json
// - README.md
it('should flag known JS libraries in a ZIP.', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty-with-library.zip'],
});
const markJSFilesSpy = sinon.spy(addonLinter, '_markJSLibs');
return addonLinter.extractMetadata({ _console: fakeConsole })
.then((metadata) => {
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', () => {
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 {
getFile(path) {
return Promise.resolve(
fs.readFileSync(`tests/fixtures/jslibs/${fakeFiles[path]}`));
}
getFiles() {
const files = {};
Object.keys(fakeFiles).forEach((filename) => {
files[filename] = { uncompressedSize: 5 };
});
return Promise.resolve(files);
}
getFilesByExt() {
return Promise.resolve(Object.keys(fakeFiles));
}
}
return addonLinter.extractMetadata({
_console: fakeConsole,
_Xpi: FakeXpi,
}).then((metadata) => {
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.notices;
expect(notices.length).toEqual(3);
expect(notices[2].code).toEqual(messages.KNOWN_LIBRARY.code);
});
});
it('should not scan known JS libraries', () => {
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 {
getFile(path) {
return Promise.resolve(
fs.readFileSync(`tests/fixtures/jslibs/${fakeFiles[path]}`));
}
getFiles() {
const files = {};
Object.keys(fakeFiles).forEach((filename) => {
files[filename] = { uncompressedSize: 5 };
});
return Promise.resolve(files);
}
getFilesByExt() {
return Promise.resolve(Object.keys(fakeFiles));
}
}
return addonLinter.extractMetadata({
_console: fakeConsole,
_Xpi: FakeXpi,
}).then(() => {
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
// - install.rdf
// - package.json
// - README.md
it('should flag banned JS libraries in a ZIP.', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/angular-bad-library.zip'],
});
const markBannedSpy = sinon.spy(addonLinter, '_markBannedLibs');
return addonLinter.extractMetadata({ _console: fakeConsole })
.then((metadata) => {
sinon.assert.calledOnce(markBannedSpy);
expect(Object.keys(metadata.jsLibs).length).toEqual(2);
expect(metadata.jsLibs).toEqual({
'data/angular-1.2.28.min.js': 'angularjs.1.2.28.angular.min.js',
'data/jquery-3.2.1.min.js': 'jquery.3.2.1.jquery.min.js',
});
const errors = addonLinter.collector.errors;
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.warnings;
expect(warnings.length).toEqual(1);
expect(warnings[0].code).toEqual(messages.UNADVISED_LIBRARY.code);
});
it('should use size attribute if uncompressedSize is undefined', () => {
const addonLinter = new Linter({ _: ['bar'] });
addonLinter.checkFileExists = () => {
return Promise.resolve({
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 {
getFiles() {
return Promise.resolve({
'dictionaries/something': { size: 5 },
whatever: { size: 0 },
});
}
}
return addonLinter.extractMetadata({
_Directory: FakeDirectory,
_console: fakeConsole,
}).then((metadata) => {
sinon.assert.calledOnce(markEmptyFilesSpy);
expect(metadata.emptyFiles).toEqual(['whatever']);
});
});
it('should error if no size attributes are found', () => {
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 {
getFiles() {
return Promise.resolve({
'dictionaries/something': { uncompressedSize: 5 },
whatever: {},
});
}
}
return addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole })
.catch((err) => {
sinon.assert.calledOnce(markEmptyFilesSpy);
expect(err.message).toEqual('No size available for whatever');
});
});
it('should error if file size of a non-binary file is too large', () => {
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 },
};
getFile(filename) {
return this.getFileAsString(filename);
}
getFiles() {
return Promise.resolve(this.files);
}
getFilesByExt(type) {
return Promise.resolve(type === 'js' ? ['myfile.js'] : ['myfile.css']);
}
getFileAsString(filename) {
return Promise.resolve((filename === constants.MANIFEST_JSON) ?
validManifestJSON() : 'const foo = "bar";');
}
}
return addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole })
.then(() => {
expect(addonLinter.collector.errors[0].code).toEqual(
messages.FILE_TOO_LARGE.code
);
// CSS and JS files that are too large should be flagged.
expect(addonLinter.collector.errors.length).toBe(2);
});
});
it('should ignore large binary files', () => {
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);
}
getFiles() {
return Promise.resolve(this.files);
}
getFilesByExt(type) {
return Promise.resolve(type === 'json' ? ['manifest.json'] : ['myfile.jpg']);
}
getFileAsString(filename) {
return Promise.resolve((filename === constants.MANIFEST_JSON) ?
validManifestJSON() : '');
}
}
return addonLinter.scan({ _Xpi: FakeXpi, _console: fakeConsole })
.then(() => {
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/
// 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/
// 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', () => {
const addonLinter = new Linter({
_: ['tests/fixtures/empty-with-library.zip'],
});
addonLinter.print = sinon.stub();
return addonLinter.scan({ _console: fakeConsole })
.then(() => {
expect(addonLinter.output.metadata.totalScannedFileSize).toEqual(9421);
});
});
it('should flag files with badwords.', () => {
var addonLinter = new Linter({
_: ['tests/fixtures/webextension_badwords.zip'],
});
var markBadwordusageSpy = sinon.spy(addonLinter, '_markBadwordUsage');
return addonLinter.scan({_console: fakeConsole})
.then(() => {
sinon.assert.calledTwice(markBadwordusageSpy);
var errors = addonLinter.collector.notices;
expect(errors.length).toEqual(1);
expect(errors[0].code).toEqual(
messages.MOZILLA_COND_OF_USE.code);
});
});
});
describe('Linter.run()', () => {
const fakeConsole = {
log: sinon.stub(),
};
it('should run extractMetadata() when metadata is true', () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
const fakeMetadata = { type: 1, somethingelse: 'whatever' };
addonLinter.toJSON = sinon.stub();
addonLinter.getAddonMetadata = () => {
return Promise.resolve(fakeMetadata);
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
sinon.stub(addonLinter, 'markSpecialFiles').callsFake((addonMetadata) => {
return Promise.resolve(addonMetadata);
});
class FakeXpi extends FakeIOBase {
}
return addonLinter.run({ _Xpi: FakeXpi, _console: fakeConsole })
.then(() => {
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', () => {
const addonLinter = new Linter({ _: ['foo'], metadata: false });
addonLinter.scan = sinon.stub();
addonLinter.scan.returns(Promise.resolve());
return addonLinter.run({ _console: fakeConsole })
.then(() => {
sinon.assert.calledOnce(addonLinter.scan);
});
});
it('should surface errors when metadata is true', () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
addonLinter.toJSON = sinon.stub();
addonLinter.handleError = sinon.spy();
addonLinter.getAddonMetadata = () => {
return Promise.reject(new Error('metadata explosion'));
};
addonLinter.checkFileExists = fakeCheckFileExists;
addonLinter.checkMinNodeVersion = () => {
return Promise.resolve();
};
class FakeXpi extends FakeIOBase {
}
return addonLinter.run({ _Xpi: FakeXpi, _console: fakeConsole })
.then(unexpectedSuccess)
.catch((err) => {
sinon.assert.calledOnce(addonLinter.handleError);
expect(err).toBeInstanceOf(Error);
expect(err.message).toContain('metadata explosion');
});
});
it('should resolve to the linting results object', () => {
const addonLinter = new Linter({ _: ['foo'], metadata: false });
addonLinter.scan = sinon.stub();
addonLinter.scan.returns(Promise.resolve());
return addonLinter.run({ _console: fakeConsole })
.then((result) => {
expect(result).toEqual(addonLinter.output);
});
});
it('should resolve to the linting results when metadata is true', () => {
const addonLinter = new Linter({ _: ['foo'], metadata: true });
addonLinter.extractMetadata = sinon.stub();
addonLinter.extractMetadata.returns(Promise.resolve());
return addonLinter.run({ _console: fakeConsole })
.then((result) => {
expect(result).toEqual(addonLinter.output);
});
});
});