blob: a2c128473baa752e7f8c15fcb6011a259f4ec12c [file] [log] [blame]
/**
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
const fs = require('node:fs');
const path = require('node:path');
const semver = require('semver');
const rewire = require('rewire');
const { events, PlatformJson } = require('cordova-common');
const { spy: emitSpyHelper } = require('../common');
const knownPlatforms = require('../../src/platforms/platforms');
const { tmpDir, getFixture } = require('../helpers');
const temp_dir = tmpDir('plugman-install-test');
const project = path.join(temp_dir, 'android_install');
const plugins_dir = path.join(__dirname, 'plugins');
const plugins_install_dir = path.join(project, 'cordova', 'plugins');
function pluginDir (pluginId) {
const base = pluginId.length === 1
? path.join(plugins_dir, 'dependencies')
: plugins_dir;
return path.join(base, pluginId);
}
const results = {};
const TIMEOUT = 9000;
const existsSync = fs.existsSync;
// Mocked functions for tests
const fake = {
existsSync: {
noPlugins (path) {
// fake installed plugin directories as 'not found'
if (path.slice(-5) !== '.json' && path.indexOf(plugins_install_dir) >= 0) {
return false;
}
return existsSync(path);
}
},
fetch: {
dependencies (id, dir) {
if (id === pluginDir('A')) { return Promise.resolve(id); } // full path to plugin
return Promise.resolve(path.join(plugins_dir, 'dependencies', id));
}
}
};
describe('plugman/install', () => {
let install = require('../../src/plugman/install');
let fetchSpy;
let execaSpy;
beforeAll(() => {
let api;
return getFixture('androidApp').copyTo(project)
.then(_ => {
results.emit_results = [];
events.on('results', result => results.emit_results.push(result));
// Every time when addPlugin is called it will return some truthy value
const returnValues = [true, {}, [], 'foo', () => {}][Symbol.iterator]();
api = knownPlatforms.getPlatformApi('android', project);
const addPluginOrig = api.addPlugin;
spyOn(api, 'addPlugin').and.callFake(function () {
return addPluginOrig.apply(api, arguments)
.then(_ => returnValues.next());
});
return install('android', project, pluginDir('org.test.plugins.dummyplugin'));
})
.then(result => {
expect(result).toBeTruthy();
return install('android', project, pluginDir('com.cordova.engine'));
}).then(result => {
expect(result).toBeTruthy();
return install('android', project, pluginDir('org.test.plugins.childbrowser'));
}).then(result => {
expect(result).toBeTruthy();
return install('android', project, pluginDir('com.adobe.vars'), plugins_install_dir, { cli_variables: { API_KEY: 'batman' } });
}).then(result => {
expect(result).toBeTruthy();
return install('android', project, pluginDir('org.test.defaultvariables'), plugins_install_dir, { cli_variables: { API_KEY: 'batman' } });
}).then(result => {
expect(result).toBeTruthy();
api.addPlugin.and.callThrough();
events.removeAllListeners('results');
});
}, 2 * TIMEOUT);
afterAll(() => {
fs.rmSync(temp_dir, { recursive: true, force: true });
});
beforeEach(() => {
install = rewire('../../src/plugman/install');
fetchSpy = jasmine.createSpy('plugmanFetch').and.returnValue(Promise.resolve(pluginDir('com.cordova.engine')));
install.__set__({ plugmanFetch: fetchSpy });
execaSpy = jasmine.createSpy('execa');
execaSpy.and.returnValue(Promise.resolve({ stdout: '' }));
install.__set__('execa', execaSpy);
spyOn(fs, 'mkdirSync');
spyOn(fs, 'writeFileSync');
spyOn(fs, 'cpSync');
spyOn(fs, 'rmSync');
spyOn(PlatformJson.prototype, 'addInstalledPluginToPrepareQueue');
});
describe('success', () => {
it('Test 002 : should emit a results event with platform-agnostic <info>', () => {
// org.test.plugins.childbrowser
expect(results.emit_results[0]).toBe('No matter what platform you are installing to, this notice is very important.');
}, TIMEOUT);
it('Test 003 : should emit a results event with platform-specific <info>', () => {
// org.test.plugins.childbrowser
expect(results.emit_results[1]).toBe('Please make sure you read this because it is very important to complete the installation of your plugin.');
}, TIMEOUT);
it('Test 004 : should interpolate variables into <info> tags', () => {
// VariableBrowser
expect(results.emit_results[2]).toBe('Remember that your api key is batman!');
}, TIMEOUT);
it('Test 005 : should call fetch if provided plugin cannot be resolved locally', () => {
fetchSpy.and.returnValue(Promise.resolve(pluginDir('org.test.plugins.dummyplugin')));
spyOn(fs, 'existsSync').and.callFake(fake.existsSync.noPlugins);
return install('android', project, 'CLEANYOURSHORTS')
.then(() => {
expect(fetchSpy).toHaveBeenCalled();
});
});
describe('engine versions', () => {
let satisfies;
beforeEach(() => {
satisfies = spyOn(semver, 'satisfies').and.returnValue(true);
spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false);
});
it('Test 007 : should check version if plugin has engine tag', () => {
execaSpy.and.returnValue(Promise.resolve({ stdout: '2.5.0' }));
return install('android', project, pluginDir('com.cordova.engine'))
.then(() => {
expect(satisfies).toHaveBeenCalledWith('2.5.0', '>=1.0.0', true);
});
}, TIMEOUT);
it('Test 008 : should check version and munge it a little if it has "rc" in it so it plays nice with semver (introduce a dash in it)', () => {
execaSpy.and.returnValue(Promise.resolve({ stdout: '3.0.0rc1' }));
return install('android', project, pluginDir('com.cordova.engine'))
.then(() => {
expect(satisfies).toHaveBeenCalledWith('3.0.0-rc1', '>=1.0.0', true);
});
}, TIMEOUT);
it('Test 009 : should check specific platform version over cordova version if specified', () => {
execaSpy.and.returnValue(Promise.resolve({ stdout: '3.1.0' }));
return install('android', project, pluginDir('com.cordova.engine-android'))
.then(() => {
expect(satisfies).toHaveBeenCalledWith('3.1.0', '>=3.1.0', true);
});
}, TIMEOUT);
it('Test 010 : should check platform sdk version if specified', () => {
const cordovaVersion = require('../../package.json').version.replace(/-dev|-nightly.*$/, '');
execaSpy.and.returnValue(Promise.resolve({ stdout: '18' }));
return install('android', project, pluginDir('com.cordova.engine-android'))
.then(() => {
expect(satisfies.calls.count()).toBe(3);
// <engine name="cordova" VERSION=">=3.0.0"/>
expect(satisfies.calls.argsFor(0)).toEqual([cordovaVersion, '>=3.0.0', true]);
// <engine name="cordova-android" VERSION=">=3.1.0"/>
expect(satisfies.calls.argsFor(1)).toEqual(['18.0.0', '>=3.1.0', true]);
// <engine name="android-sdk" VERSION=">=18"/>
expect(satisfies.calls.argsFor(2)).toEqual(['18.0.0', '>=18', true]);
});
}, TIMEOUT);
it('Test 011 : should check engine versions', () => {
return install('android', project, pluginDir('com.cordova.engine'))
.then(() => {
const plugmanVersion = require('../../package.json').version.replace(/-dev|-nightly.*$/, '');
const cordovaVersion = require('../../package.json').version.replace(/-dev|-nightly.*$/, '');
expect(satisfies.calls.count()).toBe(4);
// <engine name="cordova" version=">=2.3.0"/>
expect(satisfies.calls.argsFor(0)).toEqual([cordovaVersion, '>=2.3.0', true]);
// <engine name="cordova-plugman" version=">=0.10.0" />
expect(satisfies.calls.argsFor(1)).toEqual([plugmanVersion, '>=0.10.0', true]);
// <engine name="mega-fun-plugin" version=">=1.0.0" scriptSrc="megaFunVersion" platform="*" />
expect(satisfies.calls.argsFor(2)).toEqual([null, '>=1.0.0', true]);
// <engine name="mega-boring-plugin" version=">=3.0.0" scriptSrc="megaBoringVersion" platform="ios|android" />
expect(satisfies.calls.argsFor(3)).toEqual([null, '>=3.0.0', true]);
});
}, TIMEOUT);
it('Test 012 : should not check custom engine version that is not supported for platform', () => {
return install('blackberry10', project, pluginDir('com.cordova.engine'))
.then(() => {
// Version >=3.0.0 of `mega-boring-plugin` is specified with platform="ios|android"
expect(satisfies.calls.count()).toBe(3);
expect(satisfies).not.toHaveBeenCalledWith(jasmine.anything(), '>=3.0.0', true);
});
}, TIMEOUT);
});
describe('with dependencies', () => {
let emit;
beforeEach(() => {
spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false);
spyOn(fs, 'existsSync').and.callFake(fake.existsSync.noPlugins);
fetchSpy.and.callFake(fake.fetch.dependencies);
emit = spyOn(events, 'emit');
execaSpy.and.returnValue(Promise.resolve({ stdout: '9.0.0' }));
class PlatformApiMock {
static addPlugin () { return Promise.resolve(); }
}
spyOn(knownPlatforms, 'getPlatformApi').and.returnValue(PlatformApiMock);
});
it('Test 015 : should install specific version of dependency', () => {
// Plugin I depends on C@1.0.0
emit.calls.reset();
return install('android', project, pluginDir('I'))
.then(() => {
expect(fetchSpy).toHaveBeenCalledWith('C@1.0.0', jasmine.any(String), jasmine.any(Object));
expect(emitSpyHelper.getInstall(emit)).toEqual([
'Install start for "C" on android.',
'Install start for "I" on android.'
]);
}, TIMEOUT);
}, TIMEOUT);
it('Test 016 : should install any dependent plugins if missing', () => {
emit.calls.reset();
return install('android', project, pluginDir('A'))
.then(() => {
expect(emitSpyHelper.getInstall(emit)).toEqual([
'Install start for "C" on android.',
'Install start for "D" on android.',
'Install start for "A" on android.'
]);
});
}, TIMEOUT);
it('Test 017 : should install any dependent plugins from registry when url is not defined', () => {
emit.calls.reset();
return install('android', project, pluginDir('A'))
.then(() => {
expect(emitSpyHelper.getInstall(emit)).toEqual([
'Install start for "C" on android.',
'Install start for "D" on android.',
'Install start for "A" on android.'
]);
});
}, TIMEOUT);
it('Test 018 : should process all dependent plugins with alternate routes to the same plugin', () => {
// Plugin F depends on A, C, D and E
emit.calls.reset();
return install('android', project, pluginDir('F'))
.then(() => {
expect(emitSpyHelper.getInstall(emit)).toEqual([
'Install start for "C" on android.',
'Install start for "D" on android.',
'Install start for "A" on android.',
'Install start for "D" on android.',
'Install start for "F" on android.'
]);
});
}, TIMEOUT);
it('Test 019 : should throw if there is a cyclic dependency', () => {
return expectAsync(
install('android', project, pluginDir('G'))
).toBeRejectedWithError(
'Cyclic dependency from G to H'
);
}, TIMEOUT);
it('Test 020 : install subdir relative to top level plugin if no fetch meta', () => {
return install('android', project, pluginDir('B'))
.then(() => {
expect(emitSpyHelper.getInstall(emit)).toEqual([
'Install start for "D" on android.',
'Install start for "E" on android.',
'Install start for "B" on android.'
]);
});
}, TIMEOUT);
it('Test 021 : install uses meta data (if available) of top level plugin source', () => {
// Fake metadata so plugin 'B' appears from 'meta/B'
const meta = require('../../src/plugman/util/metadata');
spyOn(meta, 'get_fetch_metadata').and.callFake(() => {
return {
source: { type: 'dir', url: path.join(pluginDir('B'), '..', 'meta') }
};
});
return install('android', project, pluginDir('B'))
.then(() => {
expect(emitSpyHelper.getInstall(emit)).toEqual([
'Install start for "D" on android.',
'Install start for "E" on android.',
'Install start for "B" on android.'
]);
const copy = emitSpyHelper.startsWith(emit, 'Copying from');
expect(copy.length).toBe(3);
expect(copy[0].indexOf(path.normalize('meta/D')) > 0).toBe(true);
expect(copy[1].indexOf(path.normalize('meta/subdir/E')) > 0).toBe(true);
});
}, TIMEOUT);
});
});
describe('failure', () => {
it('Test 023 : should throw if variables are missing', () => {
spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false);
return expectAsync(
install('android', project, pluginDir('com.adobe.vars'))
).toBeRejectedWithError(
'Variable(s) missing: API_KEY'
);
}, TIMEOUT);
it('Test 025 :should not fail when trying to install plugin less than minimum version. Skip instead ', () => {
spyOn(semver, 'satisfies').and.returnValue(false);
execaSpy.and.returnValue(Promise.resolve({ stdout: '0.0.1' }));
return install('android', project, pluginDir('com.cordova.engine'))
.then(result => {
expect(result).toBe(true);
});
}, TIMEOUT);
it('Test 026 : should throw if the engine scriptSrc escapes out of the plugin dir.', () => {
spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false);
return expectAsync(
install('android', project, pluginDir('org.test.invalid.engine.script'))
).toBeRejectedWithError(/^Security violation:/);
}, TIMEOUT);
it('Test 027 : should throw if a non-default cordova engine platform attribute is not defined.', () => {
spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false);
return expectAsync(
install('android', project, pluginDir('org.test.invalid.engine.no.platform'))
).toBeRejectedWithError();
}, TIMEOUT);
it('Test 028 : should throw if a non-default cordova engine scriptSrc attribute is not defined.', () => {
spyOn(PlatformJson.prototype, 'isPluginInstalled').and.returnValue(false);
return expectAsync(
install('android', project, pluginDir('org.test.invalid.engine.no.scriptSrc'))
).toBeRejectedWithError();
}, TIMEOUT);
});
});