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 FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default;
describe("AdaptableController", ()=>{
describe("AdapterLoader", ()=>{
it("should instantiate an adapter from string in object", (done) => {
var adapterPath = require('path').resolve("./spec/MockAdapter");
var adapter = loadAdapter({
adapter: adapterPath,
key: "value",
foo: "bar"
options: {
key: "value",
foo: "bar"
}
});
expect(adapter instanceof Object).toBe(true);
@@ -24,7 +26,6 @@ describe("AdaptableController", ()=>{
var adapter = loadAdapter(adapterPath);
expect(adapter instanceof Object).toBe(true);
expect(adapter.options).toBe(adapterPath);
done();
});
@@ -65,4 +66,22 @@ describe("AdaptableController", ()=>{
expect(adapter).toBe(originalAdapter);
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) {
this.options = options;
}
return {
options: options
};
};

View File

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

View File

@@ -1,13 +1,15 @@
var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter');
var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations;
// Make mock config
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
describe('OneSignalPushAdapter', () => {
it('can be initialized', (done) => {
// Make mock config
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
@@ -18,8 +20,16 @@ describe('OneSignalPushAdapter', () => {
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) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']);
done();
@@ -56,7 +66,7 @@ describe('OneSignalPushAdapter', () => {
it('can send push notifications', (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
// Mock android ios senders
var androidSender = jasmine.createSpy('send')
@@ -108,7 +118,7 @@ describe('OneSignalPushAdapter', () => {
});
it("can send iOS notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
@@ -135,7 +145,7 @@ describe('OneSignalPushAdapter', () => {
});
it("can send Android notifications", (done) => {
var oneSignalPushAdapter = new OneSignalPushAdapter();
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
var sendToOneSignal = jasmine.createSpy('sendToOneSignal');
oneSignalPushAdapter.sendToOneSignal = sendToOneSignal;
@@ -157,10 +167,7 @@ describe('OneSignalPushAdapter', () => {
});
it("can post the correct data", (done) => {
var pushConfig = {
oneSignalAppId:"APP ID",
oneSignalApiKey:"API KEY"
};
var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig);
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 => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve()
sendVerificationEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
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 => {
var emailAdapter = {
sendVerificationEmail: () => Promise.resolve()
sendVerificationEmail: () => Promise.resolve(),
sendMail: () => Promise.resolve()
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
@@ -131,7 +133,8 @@ describe('Parse.User testing', () => {
expect(options.appName).toEqual('emailing app');
expect(options.user.get('email')).toEqual('user@parse.com');
done();
}
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
@@ -175,7 +178,8 @@ describe('Parse.User testing', () => {
done();
});
});
}
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
@@ -232,7 +236,8 @@ describe('Parse.User testing', () => {
done();
});
});
}
},
sendMail: () => {}
}
setServerConfiguration({
serverURL: 'http://localhost:8378/1',

View File

@@ -1,36 +1,43 @@
export function loadAdapter(options, defaultAdapter) {
let adapter;
export function loadAdapter(adapter, defaultAdapter, options) {
// We have options and options have adapter key
if (options) {
// Pass an adapter as a module name, a function or an instance
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
adapter = options;
if (!adapter)
{
if (!defaultAdapter) {
return options;
}
if (options.adapter) {
adapter = options.adapter;
// Load from the default adapter when no adapter is set
return loadAdapter(defaultAdapter, undefined, options);
} else if (typeof adapter === "function") {
try {
return adapter(options);
} catch(e) {
var Adapter = adapter;
return new Adapter(options);
}
}
if (!adapter) {
adapter = defaultAdapter;
}
// This is a string, require the module
if (typeof adapter === "string") {
} else if (typeof adapter === "string") {
adapter = require(adapter);
// If it's define as a module, get the default
if (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
// if it's an function, instanciate and pass the options
if (typeof adapter === "function") {
var Adapter = adapter;
adapter = new Adapter(options);
}
// return the adapter as is as it's unusable otherwise
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 sendMail = (to, subject, text) => {
let sendMail = ({to, subject, text}) => {
let data = {
from: mailgunOptions.fromAddress,
to: to,
@@ -24,16 +24,21 @@ let SimpleMailgunAdapter = mailgunOptions => {
});
}
return {
return Object.freeze({
sendVerificationEmail: ({ link, user, appName, }) => {
let verifyMessage =
"Hi,\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;
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

View File

@@ -4,21 +4,36 @@
import * as AWS from 'aws-sdk';
import { FilesAdapter } from './FilesAdapter';
import requiredParameter from '../../requiredParameter';
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 {
// Creates an S3 session.
// Providing AWS access and secret keys is mandatory
// Region and bucket will use sane defaults if omitted
constructor(
accessKey,
secretKey,
bucket,
{ region = DEFAULT_S3_REGION,
bucketPrefix = '',
directAccess = false } = {}
) {
accessKey = requiredParameter('S3Adapter requires an accessKey'),
secretKey = requiredParameter('S3Adapter requires a secretKey'),
bucket,
{ region = DEFAULT_S3_REGION,
bucketPrefix = '',
directAccess = false } = {}) {
super();
this._region = region;

View File

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

View File

@@ -18,6 +18,10 @@ export class OneSignalPushAdapter extends PushAdapter {
this.validPushTypes = ['ios', 'android'];
this.senderMap = {};
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['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.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.mailController = cacheInfo.mailController;
this.verifyUserEmails = cacheInfo.verifyUserEmails;
this.emailAdapter = cacheInfo.emailAdapter;
this.appName = cacheInfo.appName;
this.hooksController = cacheInfo.hooksController;

View File

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

View File

@@ -16,11 +16,11 @@ import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
//import passwordReset from './passwordReset';
import PromiseRouter from './PromiseRouter';
import verifyEmail from './verifyEmail';
import loadAdapter from './Adapters/loadAdapter';
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { ClassesRouter } from './Routers/ClassesRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { FilesController } from './Controllers/FilesController';
import { MailController } from './Controllers/MailController';
import { FilesRouter } from './Routers/FilesRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter';
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
@@ -30,7 +30,7 @@ import { HooksRouter } from './Routers/HooksRouter';
import { HooksController } from './Controllers/HooksController';
import { InstallationsRouter } from './Routers/InstallationsRouter';
import { AdapterLoader } from './Adapters/AdapterLoader';
import { loadAdapter } from './Adapters/AdapterLoader';
import { LoggerController } from './Controllers/LoggerController';
import { PushController } from './Controllers/PushController';
import { PushRouter } from './Routers/PushRouter';
@@ -79,9 +79,6 @@ let validateEmailConfiguration = (verifyUserEmails, appName, emailAdapter) => {
if (!emailAdapter) {
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,
});
if (verifyUserEmails && process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1) {
emailAdapter = loadAdapter(emailAdapter);
validateEmailConfiguration(verifyUserEmails, appName, emailAdapter);
if (verifyUserEmails && (process.env.PARSE_EXPERIMENTAL_EMAIL_VERIFICATION_ENABLED || process.env.TESTING == 1)) {
let mailController = new MailController(loadAdapter(emailAdapter));
cache.apps[appId].mailController = mailController;
cache.apps[appId].verifyUserEmails = verifyUserEmails;
cache.apps[appId].emailAdapter = emailAdapter;
}
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability