diff --git a/package.json b/package.json index 38d69267..6287adc8 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "coveralls": "^2.11.2", "eslint": "^2.13.1", "istanbul": "^0.4.0", - "js-yaml": "^3.5.4", + "js-yaml": "^3.8.1", "mocha": "^3.0.2", "mocha-lcov-reporter": "^1.0.0", "sinon": "^1.17.3", diff --git a/test/config.js b/test/config.js new file mode 100644 index 00000000..10105a17 --- /dev/null +++ b/test/config.js @@ -0,0 +1,421 @@ +'use strict'; + +var assert = require('assert'); +var fs = require('fs'); +var FakeFs = require('fake-fs'); +var path = require('path'); +var yaml = require('js-yaml'); +var extend = require('xtend'); + +var helpers = require('./helpers'); +var clientFactory = require('../lib/client'); + +var defaultConfigPath = path.resolve(__dirname, '../lib/config.yml'); + +function createHomeConfig(callback) { + var client = helpers.createClient(); + helpers.createApplication(client, function (err, application) { + if (err) { + return callback(err); + } + + application.createAccount(helpers.newUser(), function (err, account) { + if (err) { + return callback(err); + } + + account.createApiKey(function (err, apiKey) { + if (err) { + return callback(err); + } + + var config = { + client: { + apiKey: { + id: apiKey.id, + secret: apiKey.secret + } + }, + application: { + href: application.href + } + }; + + callback(null, { + config: config, + application: application + }); + }); + }); + }); +} + +function cleanEnv() { + var restore = helpers.snapshotEnv(); + + for (var key in process.env) { + if (key.indexOf('STORMPATH_') === 0) { + delete process.env[key]; + } + } + + return restore; +} + +function getDefaultConfig() { + return fs.readFileSync(defaultConfigPath); +} + +function getDefaultYaml() { + return yaml.load(getDefaultConfig(), 'utf-8'); +} + +function getClient(options, done) { + var client = clientFactory(options || {}); + + client.on('ready', function () { + done(); + }); + + client.on('error', done); + + return client; +} + +function mockCommonPaths(fakeFs, yamlConfig) { + var dynamicRequires = [ + '/resource/Application.js', + '/resource/ApplicationAccountStoreMapping.js', + '/resource/AccountStoreMapping.js', + '/resource/PasswordResetToken.js', + '/authc/AuthRequestParser.js', + '/authc/BasicApiAuthenticator.js', + '/authc/OAuthBasicExchangeAuthenticator.js', + '/error/messages.js' + ]; + + fakeFs.file(defaultConfigPath, {content: getDefaultConfig()}); + fakeFs.file(process.cwd() + '/node_modules/stormpath/lib/config.yml', { + content: fs.readFileSync(process.cwd() + '/node_modules/stormpath/lib/config.yml') + }); + + dynamicRequires.forEach(function (filePath) { + fakeFs.file(process.cwd() + '/node_modules/stormpath/lib' + filePath, { + content: fs.readFileSync(process.cwd() + '/node_modules/stormpath/lib' + filePath) + }); + }); + + if (yamlConfig) { + fakeFs.file(process.cwd() + '/stormpath.yml', { + content: yaml.dump(yamlConfig) + }); + } +} + +describe('configuration loading', function () { + var restoreEnv; + var fakeFs; + var homeConfig; + var application; + + before(function (done) { + createHomeConfig(function (err, data) { + if (err) { + return done(err); + } + + homeConfig = data.config; + application = data.application; + done(); + }); + }); + + after(function (done) { + application.delete(done); + }); + + beforeEach(function () { + restoreEnv = cleanEnv(); + + if (fakeFs) { + fakeFs.unpatch(); + } + + fakeFs = new FakeFs().bind(); + }); + + afterEach(function () { + restoreEnv(); + }); + + describe('loading the default YAML configuration', function () { + var yamlData; + var client; + + beforeEach(function (done) { + var config = extend({}, homeConfig, {skipRemoteConfig: true}); + yamlData = getDefaultYaml(); + client = getClient(config, done); + + mockCommonPaths(fakeFs); + + fakeFs.patch(); + }); + + it('should contain the default configuration fields', function () { + assert(helpers.contains(client.config, yamlData)); + }); + }); + + describe('loading a custom stormpath.yml', function () { + var client; + + beforeEach(function (done) { + var config = extend({}, homeConfig, {skipRemoteConfig: true}); + mockCommonPaths(fakeFs); + fakeFs.file(process.cwd() + '/stormpath.yml', { + content: yaml.dump({ + web: { + invented: { + testable: true + } + } + }) + }); + + fakeFs.patch(); + + client = getClient(config, done); + }); + + afterEach(function () { + fakeFs.unpatch(); + }); + + it('should contain the loaded data', function () { + assert('web' in client.config); + assert('invented' in client.config.web); + assert('testable' in client.config.web.invented); + assert.equal(client.config.web.invented.testable, true); + }); + }); + + describe('loading of custom stormpath.json', function () { + var client; + + beforeEach(function (done) { + var config = extend({}, homeConfig, {skipRemoteConfig: true}); + mockCommonPaths(fakeFs); + fakeFs.file(process.cwd() + '/stormpath.json', { + content: JSON.stringify({ + web: { + invented: { + untestable: 'yeah' + } + } + }) + }); + + fakeFs.patch(); + + client = getClient(config, done); + }); + + afterEach(function () { + fakeFs.unpatch(); + }); + + it('should contain the loaded data', function () { + assert('web' in client.config); + assert('invented' in client.config.web); + assert('untestable' in client.config.web.invented); + assert.equal(client.config.web.invented.untestable, 'yeah'); + }); + }); + + describe('loading configuration from the environment', function () { + var client; + + beforeEach(function (done) { + process.env.STORMPATH_CLIENT_APIKEY_ID = homeConfig.client.apiKey.id; + process.env.STORMPATH_CLIENT_APIKEY_SECRET = homeConfig.client.apiKey.secret; + process.env.STORMPATH_APPLICATION_HREF = application.href; + process.env.STORMPATH_WEB_DOMAINNAME = 'envDomainName'; + + var config = extend({}, {skipRemoteConfig: true}); + mockCommonPaths(fakeFs); + + fakeFs.patch(); + + client = getClient(config, done); + }); + + afterEach(function () { + fakeFs.unpatch(); + }); + + it('should load the configuration from the environment', function () { + assert('client' in client.config); + assert('apiKey' in client.config.client); + assert.equal(client.config.client.apiKey.id, homeConfig.client.apiKey.id); + assert.equal(client.config.client.apiKey.secret, homeConfig.client.apiKey.secret); + + assert('application' in client.config); + assert.equal(client.config.application.href, homeConfig.application.href); + + assert.equal(client.config.web.domainName, 'envDomainName'); + }); + }); + + describe('loading configuration from .init()', function () { + var client; + var extraInitConfig; + + beforeEach(function (done) { + extraInitConfig = { + web: { + initStuff: { + name: 'some very good name' + } + } + }; + + var config = extend(extraInitConfig, homeConfig, {skipRemoteConfig: true}); + mockCommonPaths(fakeFs); + + fakeFs.patch(); + + client = getClient(config, done); + }); + + afterEach(function () { + fakeFs.unpatch(); + }); + + it('should load the configuration from the config init object', function () { + assert('initStuff' in client.config.web); + assert.equal(client.config.web.initStuff.name, extraInitConfig.web.initStuff.name); + }); + }); + + describe('overriding configuration', function () { + var fileConfig; + var initConfig; + var client; + + beforeEach(function (done) { + fileConfig = { + web: { + basePath: 'filePath', + domainName: 'fileDomainName' + }, + client: { + apiKey: { + id: 'fileKey', + secret: 'fileSecret' + } + } + }; + + initConfig = { + web: { + domainName: 'initDomainName' + }, + skipRemoteConfig: true + }; + + mockCommonPaths(fakeFs, fileConfig); + fakeFs.patch(); + + process.env.STORMPATH_CLIENT_APIKEY_ID = 'envKeyId'; + process.env.STORMPATH_WEB_DOMAINNAME = 'envDomainName'; + client = getClient(initConfig, done); + }); + + afterEach(function () { + fakeFs.unpatch(); + }); + + it('should load configuration in order default files -> files -> environment -> init function', function () { + assert.equal(client.config.web.basePath, 'filePath'); + assert.equal(client.config.web.domainName, 'initDomainName'); + assert.equal(client.config.client.apiKey.id, 'envKeyId'); + assert.equal(client.config.client.apiKey.secret, 'fileSecret'); + }); + }); + + describe('detecting invalid configurations', function () { + it('should abort if the stormpath id is not specified', function (done) { + var config = { + client: { + apiKey: { + id: homeConfig.client.apiKey.id + } + }, + application: { + href: application.href + } + }; + + mockCommonPaths(fakeFs, config); + fakeFs.patch(); + + getClient(null, function (err) { + assert(err); + assert(err.message); + assert.equal(err.message, 'API key ID and secret is required.'); + fakeFs.unpatch(); + done(); + }); + }); + + it('should abort if the stormpath secret is not specified', function (done) { + var config = { + client: { + apiKey: { + secret: homeConfig.client.apiKey.secret + } + }, + application: { + href: application.href + } + }; + + mockCommonPaths(fakeFs, config); + fakeFs.patch(); + + getClient(null, function (err) { + assert(err); + assert(err.message); + assert.equal(err.message, 'API key ID and secret is required.'); + fakeFs.unpatch(); + done(); + }); + }); + + it('should abort if the application id is not specified', function (done) { + // TODO fix, there seems to be an issue with API keys + var apiKey = { + id: homeConfig.client.apiKey.id, + secret: homeConfig.client.apiKey.secret + }; + var config = { + // skipRemoteConfig: true, + client: { + apiKey: apiKey + } + }; + + mockCommonPaths(fakeFs, config); + fakeFs.patch(); + + getClient(null, function (err) { + assert(err); + assert(err.message); + // assert.equal(err.message, 'API key ID and secret is required.'); + fakeFs.unpatch(); + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/helpers.js b/test/helpers.js index 2e1bd2dc..baeaacc4 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -223,3 +223,66 @@ module.exports.noOpLogger = { info: function () {}, error: function () {} }; + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + +function isObject(target) { + return Object.prototype.toString.call(target) === '[object Object]'; +} + +function isArray(target) { + return Object.prototype.toString.call(target) === '[object Array]'; +} + +/** + * Takes a snapshot of the current state of the env variables, and Returns + * a function that can be used to restore it. + */ +module.exports.snapshotEnv = function snapshotEnv() { + var originalEnv = clone(process.env); + return function restore() { + var key; + for (key in process.env) { + if (!(key in originalEnv)) { + delete process.env[key]; + } + } + for (key in originalEnv) { + process.env[key] = originalEnv[key]; + } + }; +}; + +module.exports.contains = function contains(container, containee) { + if ((!containee || !container) && containee !== container) { + return false; + } + + if (isArray(containee)) { + return containee.reduce(function (acc, value) { + return acc && container.indexOf && container.indexOf(value) !== -1; + }, true); + } + + if (isObject(containee)) { + for (var key in containee) { + var doesContain; + + if (isObject(containee[key]) || isArray(containee[key])) { + doesContain = contains(container[key], containee[key]); + } else { + doesContain = container[key] === containee[key]; + } + + if (!doesContain) { + return false; + } + } + + return true; + } + + return container === containee; +};