Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added settings manager #1645

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"body-parser": "^1.14.2",
"colors": "^1.1.2",
"commander": "^2.9.0",
"deep-equal": "^1.0.1",
"deepcopy": "^0.6.1",
"express": "^4.13.4",
"intersect": "^1.0.1",
Expand Down
210 changes: 210 additions & 0 deletions spec/PersistentSettings.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
var request = require('request');
var deepcopy = require('deepcopy');
var DatabaseAdapter = require('../src/DatabaseAdapter');
var database = DatabaseAdapter.getDatabaseConnection('test', 'test_');
var settingsCollection = '_ServerSettings';
var logger = require('../src/logger').default;

var configuration;

describe('Persistent Settings', () => {
beforeEach((done) => {
configuration = deepcopy(defaultConfiguration);
configuration.verbose = true;
configuration.enableConfigChanges = true;
newServer().then(done);
});

describe('Upon Initialization', () => {
it('should persist settings', (done) => {
configuration.clientKey = 'local';

newServer()
.then(getPersisted)
.then(persisted => {
expect(persisted.clientKey).toEqual('local');
})
.then(done)
.catch(done.fail);
});

it('should only load mutable settings from database', (done) => {
configuration.clientKey = 'local'; // defined

updatePersisted({ logLevel: 'info', clientKey: 'persisted' })
.then(newServer)
.then(_ => {
var config = parseServerObject.config;
expect(config.logLevel).toEqual('info'); // not locked or defined, so updated
expect(config.clientKey).toEqual('local'); // configuration defined, therefore not updated
})
.then(done)
.catch(done.fail);
});

it('overwrites defined settings if lockDefinedSettings is false', (done) => {
configuration.clientKey = 'local';
configuration.lockDefinedSettings = false;

updatePersisted({ clientKey: 'persisted' })
.then(newServer)
.then(_ => {
var config = parseServerObject.config;
expect(config.clientKey).toEqual('persisted'); // defined setting was updated
})
.then(done)
.catch(done.fail);
});
});

describe('Settings Router', () => {
it('should provide error on post if config changes disabled', (done) => {
configuration.enableConfigChanges = false;
newServer()
.then(endpoint.get)
.then(res => expect(res.res.statusCode).toBe(200))
.then(_ => endpoint.post({ clientKey: 'causesError' }))
.then(res => {
expect(res.res.statusCode).toBe(403);
expect(res.body.error).toBe('Server config changes are disabled');
})
.then(done)
.catch(done.fail);
});

it('should run setting callbacks such as configureLogger', (done) => {
endpoint.post({ logLevel: 'silly' })
.then(res => {
expect(res.res.statusCode).toBe(200);
expect(res.body.logLevel).toBe('silly');
expect(logger.transports['parse-server'].level).toBe('silly');
})
.then(endpoint.get)
.then(res => {
expect(res.res.statusCode).toBe(200);
expect(res.body.logLevel).toBe('silly');
})
.then(done)
.catch(done.fail);
});

it('should not set defined setting', (done) => {
endpoint.post({ clientKey: 'alreadyDefined' })
.then(res => {
expect(res.res.statusCode).toBe(200);
expect(res.body.clientKey).toBeUndefined();
})
.then(endpoint.get)
.then(res => {
expect(res.res.statusCode).toBe(200);
expect(res.body.clientKey).toBe(configuration.clientKey);
})
.then(done)
.catch(done.fail);
});

it('should not allow access without masterKey', (done) => {
var invalidHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'invalid'
};

endpoint.post({ logLevel: 'silly' }, invalidHeaders)
.then(res => {
expect(res.res.statusCode).toBe(403);
expect(res.body.error).toBe('unauthorized');
})
.then(_ => endpoint.get(invalidHeaders))
.then(res => {
expect(res.res.statusCode).toBe(403);
expect(res.body.error).toBe('unauthorized');
})
.then(done)
.catch(done.fail);
});

it('should expose non-existant settings as null', (done) => {
delete configuration.clientKey;

database.deleteEverything()
.then(newServer)
.then(endpoint.get)
.then(res => expect(res.body.clientKey).toBe(null))
.then(done)
.catch(done.fail);
});

it('should fetch database values', (done) => {
delete configuration.clientKey;

database.deleteEverything()
.then(newServer)
.then(endpoint.get)
.then(res => expect(res.body.clientKey).toBe(null))
.then(_ => updatePersisted({ clientKey: 'persisted' }))
.then(endpoint.get)
.then(res => expect(res.body.clientKey).toBe('persisted'))
.then(done)
.catch(done.fail);
});

it('should only return modified values', (done) => {
// info is default log level
var currentLogLevel;
endpoint.get()
.then(res => currentLogLevel = res.body.logLevel)
.then(_ => endpoint.post({ logLevel: currentLogLevel }))
.then(res => expect(res.body.logLevel).toBeUndefined)
.then(done)
.catch(done.fail);
});
});
});

function newServer() {
setServerConfiguration(deepcopy(configuration));
return parseServerObject.config.settingsInitialized;
}

function updatePersisted(settings) {
settings.applicationId = configuration.appId;
return parseServerObject.config.settingsInitialized
.then(_ => database.adaptiveCollection(settingsCollection))
.then(coll => coll.upsertOne({ applicationId: configuration.appId }, { $set: settings }))
.then(_ => undefined);
}

function getPersisted() {
return parseServerObject.config.settingsInitialized
.then(_ => database.mongoFind(settingsCollection, {}, {}))
.then(results => results && results.length && results[0]);
}

var settingsUrl = 'http://localhost:8378/1/settings';
var defaultHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test'
};

var req = (method, headers, body) => new Promise((resolve, reject) => {
request[method]({
url: settingsUrl,
json: body,
headers: headers || defaultHeaders
}, (err, res, body) => {
if (err) {
reject(err);
} else {
if (typeof body === 'string') body = JSON.parse(body);
resolve({
res: res,
body: body
});
}
});
});

var endpoint = {
get: headers => req('get', headers),
post: (body, headers) => req('post', headers, body)
}
57 changes: 0 additions & 57 deletions spec/SettingsRouter.spec.js

This file was deleted.

11 changes: 6 additions & 5 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ var cache = require('../src/cache').default;
var DatabaseAdapter = require('../src/DatabaseAdapter');
var express = require('express');
var facebook = require('../src/authDataManager/facebook');
var ParseServer = require('../src/index').ParseServer;
var ParseServer = require('../src/index').default;
var path = require('path');

var databaseURI = process.env.DATABASE_URI;
Expand Down Expand Up @@ -43,9 +43,9 @@ var defaultConfiguration = {
};

// Set up a default API server for testing with default configuration.
var api = new ParseServer(defaultConfiguration);
global.parseServerObject = new ParseServer(defaultConfiguration);
Copy link
Contributor

@drew-gross drew-gross Apr 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you adjust these tests to not need global variables please? I'd suggest fetching everything you need for tests from the HTTP api rather than looking at the object itself. Then it's more of an end-to-end test as well.

Copy link
Author

@mamaso mamaso Apr 27, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is an ugly side effect of the pull/update/push promise behavior that happens in the constructor.

Adjusted so that setServerConfiguration returns the constructed parse server object instead of using a new global, is that a fair compromise?

The tests are split into two groups: testing ParseServer constructor behavior & testing the settings router e2e. Testing the constructor behavior benefits from having the constructed Parse Server object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that seems like a good solution.

var app = express();
app.use('/1', api);
app.use('/1', global.parseServerObject.app);
var server = app.listen(port);

// Prevent reinitializing the server from clobbering Cloud Code
Expand All @@ -63,8 +63,8 @@ var setServerConfiguration = configuration => {
server.close();
cache.clearCache();
app = express();
api = new ParseServer(configuration);
app.use('/1', api);
global.parseServerObject = new ParseServer(configuration);
app.use('/1', global.parseServerObject.app);
server = app.listen(port);
};

Expand Down Expand Up @@ -259,6 +259,7 @@ global.jequal = jequal;
global.range = range;
global.setServerConfiguration = setServerConfiguration;
global.defaultConfiguration = defaultConfiguration;
global.parseServerObject = global.parseServerObject;

// LiveQuery test setting
require('../src/LiveQuery/PLog').logLevel = 'NONE';
Expand Down
1 change: 1 addition & 0 deletions src/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class Config {
this.liveQueryController = cacheInfo.liveQueryController;
this.sessionLength = cacheInfo.sessionLength;
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
this.enableConfigChanges = cacheInfo.enableConfigChanges;
}

static validate(options) {
Expand Down
Loading