Improves AdapterLoader, enforces configuraiton on Adapters

This commit is contained in:
Florent Vilmart
2016-02-23 21:05:27 -05:00
parent 8dc37b9d30
commit 0b307bc22f
17 changed files with 176 additions and 109 deletions

View File

@@ -2,15 +2,17 @@
var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter; var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter;
var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
describe("AdaptableController", ()=>{ describe("AdapterLoader", ()=>{
it("should instantiate an adapter from string in object", (done) => { it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter"); var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter({ var adapter = loadAdapter({
adapter: adapterPath, adapter: adapterPath,
key: "value", options: {
foo: "bar" key: "value",
foo: "bar"
}
}); });
expect(adapter instanceof Object).toBe(true); expect(adapter instanceof Object).toBe(true);
@@ -24,7 +26,6 @@ describe("AdaptableController", ()=>{
var adapter = loadAdapter(adapterPath); var adapter = loadAdapter(adapterPath);
expect(adapter instanceof Object).toBe(true); expect(adapter instanceof Object).toBe(true);
expect(adapter.options).toBe(adapterPath);
done(); done();
}); });
@@ -65,4 +66,22 @@ describe("AdaptableController", ()=>{
expect(adapter).toBe(originalAdapter); expect(adapter).toBe(originalAdapter);
done(); done();
}); });
it("should fail loading an improperly configured adapter", (done) => {
var Adapter = function(options) {
if (!options.foo) {
throw "foo is required for that adapter";
}
}
var adapterOptions = {
param: "key",
doSomething: function() {}
};
expect(() => {
var adapter = loadAdapter(adapterOptions, Adapter);
expect(adapter).toEqual(adapterOptions);
}).not.toThrow("foo is required for that adapter");
done();
});
}); });

View File

@@ -1,3 +1,5 @@
module.exports = function(options) { module.exports = function(options) {
this.options = options; return {
} options: options
};
};

View File

@@ -3,6 +3,7 @@ module.exports = options => {
throw "Options were not provided" throw "Options were not provided"
} }
return { return {
sendVerificationEmail: () => Promise.resolve() sendVerificationEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
} }
} }

View File

@@ -1,13 +1,15 @@
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter'); var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations; var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations;
// Make mock config
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
describe('OneSignalPushAdapter', () => { describe('OneSignalPushAdapter', () => {
it('can be initialized', (done) => { it('can be initialized', (done) => {
// Make mock config
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
@@ -17,9 +19,17 @@ describe('OneSignalPushAdapter', () => {
expect(senderMap.android instanceof Function).toBe(true); expect(senderMap.android instanceof Function).toBe(true);
done(); done();
}); });
it('cannt be initialized if options are missing', (done) => {
expect(() => {
new OneSignalPushAdapter();
}).toThrow("Trying to initialiazed OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey");
done();
});
it('can get valid push types', (done) => { it('can get valid push types', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(); var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']); expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done(); done();
@@ -56,7 +66,7 @@ describe('OneSignalPushAdapter', () => {
it('can send push notifications', (done) => { it('can send push notifications', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(); var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
// Mock android ios senders // Mock android ios senders
var androidSender = jasmine.createSpy('send') var androidSender = jasmine.createSpy('send')
@@ -108,7 +118,7 @@ describe('OneSignalPushAdapter', () => {
}); });
it("can send iOS notifications", (done) => { it("can send iOS notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(); var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal'); var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal; oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
@@ -135,7 +145,7 @@ describe('OneSignalPushAdapter', () => {
}); });
it("can send Android notifications", (done) => { it("can send Android notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter(); var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal'); var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal; oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
@@ -157,10 +167,7 @@ describe('OneSignalPushAdapter', () => {
}); });
it("can post the correct data", (done) => { it("can post the correct data", (done) => {
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var write = jasmine.createSpy('write'); var write = jasmine.createSpy('write');

View File

@@ -51,7 +51,8 @@ describe('Parse.User testing', () => {
it('sends verification email if email verification is enabled', done => { it('sends verification email if email verification is enabled', done => {
var emailAdapter = { var emailAdapter = {
sendVerificationEmail: () => Promise.resolve() sendVerificationEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
} }
setServerConfiguration({ setServerConfiguration({
serverURL: 'http://localhost:8378/1', serverURL: 'http://localhost:8378/1',
@@ -89,7 +90,8 @@ describe('Parse.User testing', () => {
it('does not send verification email if email verification is disabled', done => { it('does not send verification email if email verification is disabled', done => {
var emailAdapter = { var emailAdapter = {
sendVerificationEmail: () => Promise.resolve() sendVerificationEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
} }
setServerConfiguration({ setServerConfiguration({
serverURL: 'http://localhost:8378/1', serverURL: 'http://localhost:8378/1',
@@ -131,7 +133,8 @@ describe('Parse.User testing', () => {
expect(options.appName).toEqual('emailing app'); expect(options.appName).toEqual('emailing app');
expect(options.user.get('email')).toEqual('user@parse.com'); expect(options.user.get('email')).toEqual('user@parse.com');
done(); done();
} },
sendMail: () => {}
} }
setServerConfiguration({ setServerConfiguration({
serverURL: 'http://localhost:8378/1', serverURL: 'http://localhost:8378/1',
@@ -175,7 +178,8 @@ describe('Parse.User testing', () => {
done(); done();
}); });
}); });
} },
sendMail: () => {}
} }
setServerConfiguration({ setServerConfiguration({
serverURL: 'http://localhost:8378/1', serverURL: 'http://localhost:8378/1',
@@ -232,7 +236,8 @@ describe('Parse.User testing', () => {
done(); done();
}); });
}); });
} },
sendMail: () => {}
} }
setServerConfiguration({ setServerConfiguration({
serverURL: 'http://localhost:8378/1', serverURL: 'http://localhost:8378/1',

View File

@@ -1,36 +1,43 @@
export function loadAdapter(options, defaultAdapter) { export function loadAdapter(adapter, defaultAdapter, options) {
let adapter;
// We have options and options have adapter key if (!adapter)
if (options) { {
// Pass an adapter as a module name, a function or an instance if (!defaultAdapter) {
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) { return options;
adapter = options;
} }
if (options.adapter) { // Load from the default adapter when no adapter is set
adapter = options.adapter; return loadAdapter(defaultAdapter, undefined, options);
} else if (typeof adapter === "function") {
try {
return adapter(options);
} catch(e) {
var Adapter = adapter;
return new Adapter(options);
} }
} } else if (typeof adapter === "string") {
if (!adapter) {
adapter = defaultAdapter;
}
// This is a string, require the module
if (typeof adapter === "string") {
adapter = require(adapter); adapter = require(adapter);
// If it's define as a module, get the default // If it's define as a module, get the default
if (adapter.default) { if (adapter.default) {
adapter = adapter.default; adapter = adapter.default;
} }
return loadAdapter(adapter, undefined, options);
} else if (adapter.module) {
return loadAdapter(adapter.module, undefined, adapter.options);
} else if (adapter.class) {
return loadAdapter(adapter.class, undefined, adapter.options);
} else if (adapter.adapter) {
return loadAdapter(adapter.adapter, undefined, adapter.options);
} else {
// Try to load the defaultAdapter with the options
// The default adapter should throw if the options are
// incompatible
try {
return loadAdapter(defaultAdapter, undefined, adapter);
} catch (e) {};
} }
// From there it's either a function or an object // return the adapter as is as it's unusable otherwise
// if it's an function, instanciate and pass the options return adapter;
if (typeof adapter === "function") {
var Adapter = adapter;
adapter = new Adapter(options);
}
return adapter;
} }
module.exports = { loadAdapter } export default loadAdapter;

View File

@@ -0,0 +1,6 @@
export class MailAdapter {
sendVerificationEmail(options) {}
sendMail(options) {}
}
export default MailAdapter;

View File

@@ -6,7 +6,7 @@ let SimpleMailgunAdapter = mailgunOptions => {
} }
let mailgun = Mailgun(mailgunOptions); let mailgun = Mailgun(mailgunOptions);
let sendMail = (to, subject, text) => { let sendMail = ({to, subject, text}) => {
let data = { let data = {
from: mailgunOptions.fromAddress, from: mailgunOptions.fromAddress,
to: to, to: to,
@@ -24,16 +24,21 @@ let SimpleMailgunAdapter = mailgunOptions => {
}); });
} }
return { return Object.freeze({
sendVerificationEmail: ({ link, user, appName, }) => { sendVerificationEmail: ({ link, user, appName, }) => {
let verifyMessage = let verifyMessage =
"Hi,\n\n" + "Hi,\n\n" +
"You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" + "You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" +
"" + "" +
"Click here to confirm it:\n" + link; "Click here to confirm it:\n" + link;
return sendMail(user.email, 'Please verify your e-mail for ' + appName, verifyMessage); return sendMail({
} to:user.email,
} subject: 'Please verify your e-mail for ' + appName,
text: verifyMessage
});
},
sendMail: sendMail
});
} }
module.exports = SimpleMailgunAdapter module.exports = SimpleMailgunAdapter

View File

@@ -4,23 +4,38 @@
import * as AWS from 'aws-sdk'; import * as AWS from 'aws-sdk';
import { FilesAdapter } from './FilesAdapter'; import { FilesAdapter } from './FilesAdapter';
import requiredParameter from '../../requiredParameter';
const DEFAULT_S3_REGION = "us-east-1"; const DEFAULT_S3_REGION = "us-east-1";
function parseS3AdapterOptions(...options) {
if (options.length === 1 && typeof options[0] == "object") {
return options;
}
const additionalOptions = options[3] || {};
return {
accessKey: options[0],
secretKey: options[1],
bucket: options[2],
region: additionalOptions.region
}
}
export class S3Adapter extends FilesAdapter { export class S3Adapter extends FilesAdapter {
// Creates an S3 session. // Creates an S3 session.
// Providing AWS access and secret keys is mandatory // Providing AWS access and secret keys is mandatory
// Region and bucket will use sane defaults if omitted // Region and bucket will use sane defaults if omitted
constructor( constructor(
accessKey, accessKey = requiredParameter('S3Adapter requires an accessKey'),
secretKey, secretKey = requiredParameter('S3Adapter requires a secretKey'),
bucket, bucket,
{ region = DEFAULT_S3_REGION, { region = DEFAULT_S3_REGION,
bucketPrefix = '', bucketPrefix = '',
directAccess = false } = {} directAccess = false } = {}) {
) {
super(); super();
this._region = region; this._region = region;
this._bucket = bucket; this._bucket = bucket;
this._bucketPrefix = bucketPrefix; this._bucketPrefix = bucketPrefix;

View File

@@ -99,9 +99,12 @@ let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
} }
export class FileLoggerAdapter extends LoggerAdapter { export class FileLoggerAdapter extends LoggerAdapter {
constructor(options = {}) { constructor(options) {
super(); super();
if (options && !options.logsFolder) {
throw "FileLoggerAdapter requires logsFolder";
}
options = options || {};
this._logsFolder = options.logsFolder || LOGS_FOLDER; this._logsFolder = options.logsFolder || LOGS_FOLDER;
// check logs folder exists // check logs folder exists

View File

@@ -18,6 +18,10 @@ export class OneSignalPushAdapter extends PushAdapter {
this.validPushTypes = ['ios', 'android']; this.validPushTypes = ['ios', 'android'];
this.senderMap = {}; this.senderMap = {};
this.OneSignalConfig = {}; this.OneSignalConfig = {};
const { oneSignalAppId, oneSignalApiKey } = pushConfig;
if (!oneSignalAppId || !oneSignalApiKey) {
throw "Trying to initialiazed OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey";
}
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId']; this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey']; this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];

View File

@@ -1,25 +0,0 @@
export default options => {
if (!options) {
return undefined;
}
if (typeof options === 'string') {
//Configuring via module name with no options
return require(options)();
}
if (!options.module && !options.class) {
//Configuring via object
return options;
}
if (options.module) {
//Configuring via module name + options
return require(options.module)(options.options)
}
if (options.class) {
//Configuring via class + options
return options.class(options.options);
}
}

View File

@@ -24,8 +24,8 @@ export class Config {
this.allowClientClassCreation = cacheInfo.allowClientClassCreation; this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.mailController = cacheInfo.mailController;
this.verifyUserEmails = cacheInfo.verifyUserEmails; this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.emailAdapter = cacheInfo.emailAdapter;
this.appName = cacheInfo.appName; this.appName = cacheInfo.appName;
this.hooksController = cacheInfo.hooksController; this.hooksController = cacheInfo.hooksController;

View File

@@ -31,7 +31,6 @@ export class AdaptableController {
} }
validateAdapter(adapter) { validateAdapter(adapter) {
if (!adapter) { if (!adapter) {
throw new Error(this.constructor.name+" requires an adapter"); throw new Error(this.constructor.name+" requires an adapter");
} }

View File

@@ -0,0 +1,29 @@
import AdaptableController from './AdaptableController';
import { MailAdapter } from '../Adapters/Email/MailAdapter';
import { randomString } from '../cryptoUtils';
import { inflate } from '../triggers';
export class MailController extends AdaptableController {
setEmailVerificationStatus(user, status) {
if (status == false) {
user._email_verify_token = randomString(25);
}
user.emailVerified = status;
}
sendVerificationEmail(user, config) {
const token = encodeURIComponent(user._email_verify_token);
const username = encodeURIComponent(user.username);
let link = `${config.mount}/verify_email?token=${token}&username=${username}`;
this.adapter.sendVerificationEmail({
appName: config.appName,
link: link,
user: inflate('_User', user),
});
}
sendMail(options) {
this.adapter.sendMail(options);
}
expectedAdapterType() {
return MailAdapter;
}
}

View File

@@ -28,8 +28,7 @@ export class UsersRouter extends ClassesRouter {
req.params.className = '_User'; req.params.className = '_User';
if (req.config.verifyUserEmails) { if (req.config.verifyUserEmails) {
req.body._email_verify_token = cryptoUtils.randomString(25); req.config.mailController.setEmailVerificationStatus(req.body, false);
req.body.emailVerified = false;
} }
let p = super.handleCreate(req); let p = super.handleCreate(req);
@@ -37,12 +36,7 @@ export class UsersRouter extends ClassesRouter {
if (req.config.verifyUserEmails) { if (req.config.verifyUserEmails) {
// Send email as fire-and-forget once the user makes it into the DB. // Send email as fire-and-forget once the user makes it into the DB.
p.then(() => { p.then(() => {
let link = req.config.mount + "/verify_email?token=" + encodeURIComponent(req.body._email_verify_token) + "&username=" + encodeURIComponent(req.body.username); req.config.mailController.sendVerificationEmail(req.body, req.config);
req.config.emailAdapter.sendVerificationEmail({
appName: req.config.appName,
link: link,
user: triggers.inflate('_User', req.body),
});
}); });
} }
return p; return p;

View File

@@ -16,11 +16,11 @@ import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
//import passwordReset from './passwordReset'; //import passwordReset from './passwordReset';
import PromiseRouter from './PromiseRouter'; import PromiseRouter from './PromiseRouter';
import verifyEmail from './verifyEmail'; import verifyEmail from './verifyEmail';
import loadAdapter from './Adapters/loadAdapter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { ClassesRouter } from './Routers/ClassesRouter'; import { ClassesRouter } from './Routers/ClassesRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController'; import { FilesController } from './Controllers/FilesController';
import { MailController } from './Controllers/MailController';
import { FilesRouter } from './Routers/FilesRouter'; import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter';
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
@@ -30,7 +30,7 @@ import { HooksRouter } from './Routers/HooksRouter';
import { HooksController } from './Controllers/HooksController'; import { HooksController } from './Controllers/HooksController';
import { InstallationsRouter } from './Routers/InstallationsRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter';
import { AdapterLoader } from './Adapters/AdapterLoader'; import { loadAdapter } from './Adapters/AdapterLoader';
import { LoggerController } from './Controllers/LoggerController'; import { LoggerController } from './Controllers/LoggerController';
import { PushController } from './Controllers/PushController'; import { PushController } from './Controllers/PushController';
import { PushRouter } from './Routers/PushRouter'; import { PushRouter } from './Routers/PushRouter';
@@ -79,9 +79,6 @@ let validateEmailConfiguration = (verifyUserEmails, appName, emailAdapter) => {
if (!emailAdapter) { if (!emailAdapter) {
throw 'User email verification was enabled, but no email adapter was provided'; throw 'User email verification was enabled, but no email adapter was provided';
} }
if (typeof emailAdapter.sendVerificationEmail !== 'function') {
throw 'Invalid email adapter: no sendVerificationEmail() function was provided';
}
} }
} }
@@ -164,11 +161,10 @@ function ParseServer({
appName: appName, appName: appName,
}); });
if (verifyUserEmails && process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1) { if (verifyUserEmails && (process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1)) {
emailAdapter = loadAdapter(emailAdapter); let mailController = new MailController(loadAdapter(emailAdapter));
validateEmailConfiguration(verifyUserEmails, appName, emailAdapter); cache.apps[appId].mailController = mailController;
cache.apps[appId].verifyUserEmails = verifyUserEmails; cache.apps[appId].verifyUserEmails = verifyUserEmails;
cache.apps[appId].emailAdapter = emailAdapter;
} }
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability