Compare commits
1 Commits
developmen
...
feature/S3
Author | SHA1 | Date |
---|---|---|
Dora Korpar | 5744565347 |
|
@ -0,0 +1,247 @@
|
|||
const UUID = require('uuid');
|
||||
|
||||
const errors = require('../errors');
|
||||
|
||||
/**
|
||||
* Format of xml request:
|
||||
*
|
||||
* <ONotificationConfiguration>
|
||||
* <QueueConfiguration>
|
||||
* <Event>array</Event>
|
||||
* <Filter>
|
||||
* <S3Key>
|
||||
* <FilterRule>
|
||||
* <Name>string</Name>
|
||||
* <Value>string</Value>
|
||||
* </FilterRule>
|
||||
* </S3Key>
|
||||
* </Filter>
|
||||
* <Id>string</Id>
|
||||
* <Queue>string</Queue>
|
||||
* </QueueConfiguration>
|
||||
* </NotificationConfiguration>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format of config:
|
||||
*
|
||||
* config = {
|
||||
* queueConfig: [
|
||||
* {
|
||||
* events: array,
|
||||
* queueArn: string,
|
||||
* filterRules: [
|
||||
* {
|
||||
* name: string,
|
||||
* value: string
|
||||
* },
|
||||
* {
|
||||
* name: string,
|
||||
* value:string
|
||||
* },
|
||||
* ],
|
||||
* id: string
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
|
||||
class NotificationConfiguration {
|
||||
/**
|
||||
* Create a Notification Configuration instance
|
||||
* @param {string} xml - parsed configuration xml
|
||||
* @return {object} - NotificationConfiguration instance
|
||||
*/
|
||||
constructor(xml) {
|
||||
this._parsedXml = xml;
|
||||
this._config = {};
|
||||
this._ids = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification configuration
|
||||
* @return {object} - contains error if parsing failed
|
||||
*/
|
||||
getValidatedNotificationConfiguration() {
|
||||
const validationError = this._parseNotificationConfig();
|
||||
if (validationError) {
|
||||
this._config.error = validationError;
|
||||
}
|
||||
return this._config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that notification configuration is valid
|
||||
* @return {error | undefined} - error if parsing failed, else undefined
|
||||
*/
|
||||
_parseNotificationConfig() {
|
||||
if (!this._parsedXml || this._parsedXml === '') {
|
||||
return errors.MalformedXML.customizeDescription(
|
||||
'request xml is undefined or empty');
|
||||
}
|
||||
const notificationConfig = this._parsedXml.NotificationConfiguration;
|
||||
if (!notificationConfig || notificationConfig === '') {
|
||||
return errors.MalformedXML.customizeDescription(
|
||||
'request xml does not include NotificationConfiguration');
|
||||
}
|
||||
const queueConfig = notificationConfig.QueueConfiguration;
|
||||
if (!queueConfig || !queueConfig[0] || queueConfig[0] === '') {
|
||||
return errors.MalformedXML.customizeDescription(
|
||||
'request xml does not include QueueConfiguration');
|
||||
}
|
||||
this._config.queueConfig = [];
|
||||
let parseError;
|
||||
queueConfig.forEach(c => {
|
||||
const eventObj = this._parseEvents(c.Event);
|
||||
const filterObj = this._parseFilter(c.Filter);
|
||||
const idObj = this._parseId(c.Id);
|
||||
const arnObj = this._parseArn(c.QueueArn);
|
||||
|
||||
if (eventObj.error) {
|
||||
parseError = eventObj.error;
|
||||
} else if (filterObj.error) {
|
||||
parseError = filterObj.error;
|
||||
} else if (idObj.error) {
|
||||
parseError = idObj.error;
|
||||
} else if (arnObj.error) {
|
||||
parseError = arnObj.error;
|
||||
} else {
|
||||
this._config.queueConfig.push({
|
||||
events: eventObj.events,
|
||||
queueArn: arnObj.arn,
|
||||
id: idObj.id,
|
||||
filterRules: filterObj.filterRules,
|
||||
});
|
||||
}
|
||||
});
|
||||
return parseError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that events array is valid
|
||||
* @param {array} events - event array
|
||||
* @return {object} - contains error if parsing failed or events array
|
||||
*/
|
||||
_parseEvents(events) {
|
||||
const eventsObj = {
|
||||
events: [],
|
||||
};
|
||||
if (!events || !events[0]) {
|
||||
eventsObj.error = errors.MalformedXML.customizeDescription(
|
||||
'each queue configuration must contain an event');
|
||||
return eventsObj;
|
||||
}
|
||||
const supportedEvents = {
|
||||
ObjectCreated: ['*', 'Put', 'Post', 'Copy', 'CompleteMultipartUpload'],
|
||||
ObjectRemoved: ['*', 'Delete', 'DeleteMarkerCreated'],
|
||||
};
|
||||
events.forEach(e => {
|
||||
const eventSplit = e.split(':');
|
||||
if (!(eventSplit[0] === 's3') ||
|
||||
!supportedEvents[eventSplit[1]] ||
|
||||
!supportedEvents[eventSplit[1]].includes(eventSplit[2])) {
|
||||
eventsObj.error = errors.MalformedXML.customizeDescription(
|
||||
'event array contains invalid or unsupported event');
|
||||
}
|
||||
eventsObj.events.push(e);
|
||||
});
|
||||
return eventsObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that filter array is valid
|
||||
* @param {array} filter - filter array
|
||||
* @return {object} - contains error if parsing failed or filter array
|
||||
*/
|
||||
_parseFilter(filter) {
|
||||
const filterObj = {
|
||||
filterRules: [],
|
||||
};
|
||||
if (filter && filter[0]) {
|
||||
if (!filter[0].S3Key || !filter[0].S3Key[0]) {
|
||||
filterObj.error = errors.MalformedXML.customizeDescription(
|
||||
'if included, queue configuration filter must contain S3Key');
|
||||
return filterObj;
|
||||
}
|
||||
const filterRules = filter[0].S3Key[0];
|
||||
if (!filterRules.FilterRule || !filterRules.FilterRule[0]) {
|
||||
filterObj.error = errors.MalformedXML.customizeDescription(
|
||||
'if included, queue configuration filter must contain a rule');
|
||||
return filterObj;
|
||||
}
|
||||
const ruleArray = filterRules.FilterRule;
|
||||
ruleArray.forEach(r => {
|
||||
if (!r.Name || !r.Name[0] || !r.Value || !r.Value[0]) {
|
||||
filterObj.error = errors.MalformedXML.customizeDescription(
|
||||
'each included filter must contain a name and value');
|
||||
} else if (!['Prefix', 'Suffix'].includes(r.Name[0])) {
|
||||
filterObj.error = errors.MalformedXML.customizeDescription(
|
||||
'filter Name must be one of Prefix or Suffix');
|
||||
} else {
|
||||
filterObj.filterRules.push({
|
||||
name: r.Name[0],
|
||||
value: r.Value[0],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return filterObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that id string is valid
|
||||
* @param {string} id - id string (optional)
|
||||
* @return {object} - contains error if parsing failed or id
|
||||
*/
|
||||
_parseId(id) {
|
||||
const idObj = {};
|
||||
idObj.propName = 'ruleID';
|
||||
if (id && id[0].length > 255) {
|
||||
idObj.error = errors.InvalidArgument.customizeDescription(
|
||||
'queue configuration ID is greater than 255 characters long');
|
||||
return idObj;
|
||||
}
|
||||
if (!id || !id[0] || id[0] === '') {
|
||||
// id is optional property, so create one if not provided or is ''
|
||||
// We generate 48-character alphanumeric, unique id for rule
|
||||
idObj.id = Buffer.from(UUID.v4()).toString('base64');
|
||||
} else {
|
||||
idObj.id = id[0];
|
||||
}
|
||||
// Each ID in a list of rules must be unique.
|
||||
if (this._ids.includes(idObj.id)) {
|
||||
idObj.error = errors.InvalidRequest.customizeDescription(
|
||||
'queue configuration ID must be unique');
|
||||
return idObj;
|
||||
}
|
||||
this._ids.push(idObj.id);
|
||||
return idObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that arn string is valid
|
||||
* @param {string} arn - queue arn
|
||||
* @return {object} - contains error if parsing failed or queue arn
|
||||
*/
|
||||
_parseArn(arn) {
|
||||
const arnObj = {};
|
||||
if (!arn || !arn[0]) {
|
||||
arnObj.error = errors.MalformedXML.customizeDescription(
|
||||
'each queue configuration must contain a queue arn');
|
||||
return arnObj;
|
||||
}
|
||||
const splitArn = arn[0].split(':');
|
||||
if (splitArn.length !== 6 ||
|
||||
!(splitArn[0] === 'arn') ||
|
||||
!(splitArn[1] === 'scality') ||
|
||||
!(splitArn[2] === 'bucketnotif')) {
|
||||
arnObj.error = errors.MalformedXML.customizeDescription(
|
||||
'queue arn is invalid');
|
||||
}
|
||||
// remaining 3 parts of arn are evaluated in cloudserver
|
||||
arnObj.arn = arn[0];
|
||||
return arnObj;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NotificationConfiguration;
|
|
@ -0,0 +1,216 @@
|
|||
const assert = require('assert');
|
||||
const { parseString } = require('xml2js');
|
||||
|
||||
const NotificationConfiguration =
|
||||
require('../../../lib/models/NotificationConfiguration.js');
|
||||
|
||||
function checkError(parsedXml, err, errMessage, cb) {
|
||||
const config = new NotificationConfiguration(parsedXml).
|
||||
getValidatedNotificationConfiguration();
|
||||
assert.strictEqual(config.error[err], true);
|
||||
assert.strictEqual(config.error.description, errMessage);
|
||||
cb();
|
||||
}
|
||||
|
||||
function generateEvent(testParams) {
|
||||
const event = [];
|
||||
if (testParams.key === 'Event') {
|
||||
if (Array.isArray(testParams.value)) {
|
||||
testParams.value.forEach(v => {
|
||||
event.push(`${event}<Event>${v}</Event>`);
|
||||
});
|
||||
} else {
|
||||
event.push(`<Event>${testParams.value}</Event>`);
|
||||
}
|
||||
} else {
|
||||
event.push('<Event>s3:ObjectCreated:*</Event>');
|
||||
}
|
||||
return event.join('');
|
||||
}
|
||||
|
||||
function generateFilter(testParams) {
|
||||
let filter = '';
|
||||
if (testParams.key === 'Filter') {
|
||||
filter = `<Filter>${testParams.value}</Filter>`;
|
||||
}
|
||||
if (testParams.key === 'S3Key') {
|
||||
filter = `<Filter><S3Key>${testParams.value}</S3Key></Filter>`;
|
||||
}
|
||||
if (testParams.key === 'FilterRule') {
|
||||
if (Array.isArray(testParams.value)) {
|
||||
testParams.value.forEach(v => {
|
||||
filter = `${filter}<Filter><S3Key><FilterRule>${v}` +
|
||||
'</FilterRule></S3Key></Filter>';
|
||||
});
|
||||
} else {
|
||||
filter = `<Filter><S3Key><FilterRule>${testParams.value}` +
|
||||
'</FilterRule></S3Key></Filter>';
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
function generateXml(testParams) {
|
||||
const id = testParams.key === 'Id' ? `<Id>${testParams.value}</Id>` : '<Id>queue-id</Id>';
|
||||
const arn = testParams.key === 'QueueArn' ?
|
||||
`<QueueArn>${testParams.value}</QueueArn>` :
|
||||
'<QueueArn>arn:scality:bucketnotif:::target</QueueArn>';
|
||||
const event = generateEvent(testParams);
|
||||
const filter = generateFilter(testParams);
|
||||
let queueConfig = `<QueueConfiguration>${id}${arn}${event}${filter}` +
|
||||
'</QueueConfiguration>';
|
||||
if (testParams.key === 'QueueConfiguration') {
|
||||
if (testParams.value === 'double') {
|
||||
queueConfig = `${queueConfig}${queueConfig}`;
|
||||
} else {
|
||||
queueConfig = testParams.value;
|
||||
}
|
||||
}
|
||||
const notification = testParams.key === 'NotificationConfiguration' ? '' :
|
||||
`<NotificationConfiguration>${queueConfig}</NotificationConfiguration>`;
|
||||
return notification;
|
||||
}
|
||||
|
||||
function generateParsedXml(testParams, cb) {
|
||||
const xml = generateXml(testParams);
|
||||
parseString(xml, (err, parsedXml) => {
|
||||
assert.equal(err, null, 'Error parsing xml');
|
||||
cb(parsedXml);
|
||||
});
|
||||
}
|
||||
|
||||
const failTests = [
|
||||
{
|
||||
name: 'fail with empty configuration',
|
||||
params: { key: 'NotificationConfiguration' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'request xml is undefined or empty',
|
||||
},
|
||||
{
|
||||
name: 'fail with empty QueueConfiguration',
|
||||
params: { key: 'QueueConfiguration', value: '<QueueArn>arn:scality:bucketnotif:::target</QueueArn>' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'request xml does not include QueueConfiguration',
|
||||
},
|
||||
{
|
||||
name: 'fail with invalid id',
|
||||
params: { key: 'Id', value: 'a'.repeat(256) },
|
||||
error: 'InvalidArgument',
|
||||
errorMessage: 'queue configuration ID is greater than 255 characters long',
|
||||
},
|
||||
{
|
||||
name: 'fail with repeated id',
|
||||
params: { key: 'QueueConfiguration', value: 'double' },
|
||||
error: 'InvalidRequest',
|
||||
errorMessage: 'queue configuration ID must be unique',
|
||||
},
|
||||
{
|
||||
name: 'fail with empty QueueArn',
|
||||
params: { key: 'QueueArn', value: '' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'each queue configuration must contain a queue arn',
|
||||
},
|
||||
{
|
||||
name: 'fail with invalid QueueArn',
|
||||
params: { key: 'QueueArn', value: 'arn:scality:bucketnotif:target' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'queue arn is invalid',
|
||||
},
|
||||
{
|
||||
name: 'fail with invalid QueueArn partition',
|
||||
params: { key: 'QueueArn', value: 'arn:aws:bucketnotif:::target' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'queue arn is invalid',
|
||||
},
|
||||
{
|
||||
name: 'fail with empty event',
|
||||
params: { key: 'Event', value: '' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'each queue configuration must contain an event',
|
||||
},
|
||||
{
|
||||
name: 'fail with invalid event',
|
||||
params: { key: 'Event', value: 's3:BucketCreated:Put' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'event array contains invalid or unsupported event',
|
||||
},
|
||||
{
|
||||
name: 'fail with unsupported event',
|
||||
params: { key: 'Event', value: 's3:Replication:OperationNotTracked' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'event array contains invalid or unsupported event',
|
||||
},
|
||||
{
|
||||
name: 'fail with filter that does not contain S3Key',
|
||||
params: { key: 'Filter', value: '<FilterRule><Name>Prefix</Name><Value>logs/</Value></FilterRule>' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'if included, queue configuration filter must contain S3Key',
|
||||
},
|
||||
{
|
||||
name: 'fail with filter that does not contain a rule',
|
||||
params: { key: 'S3Key', value: '<Name>Prefix</Name><Value>logs/</Value>' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'if included, queue configuration filter must contain a rule',
|
||||
},
|
||||
{
|
||||
name: 'fail with filter rule that does not contain name and value',
|
||||
params: { key: 'FilterRule', value: '<Value>noname</Value>' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'each included filter must contain a name and value',
|
||||
},
|
||||
{
|
||||
name: 'fail with invalid name in filter rule',
|
||||
params: { key: 'FilterRule', value: '<Name>Invalid</Name><Value>logs/</Value>' },
|
||||
error: 'MalformedXML',
|
||||
errorMessage: 'filter Name must be one of Prefix or Suffix',
|
||||
},
|
||||
];
|
||||
|
||||
const passTests = [
|
||||
{
|
||||
name: 'pass with multiple events in one queue configuration',
|
||||
params: {
|
||||
key: 'Event', value: ['s3:ObjectCreated:Put', 's3:ObjectCreated:Copy'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'pass with multiple filter rules',
|
||||
params: {
|
||||
key: 'FilterRule',
|
||||
value: ['<Name>Prefix</Name><Value>logs/</Value>', '<Name>Suffix</Name><Value>.pdf</Value>'] },
|
||||
},
|
||||
{
|
||||
name: 'pass with no id',
|
||||
params: { key: 'Id', value: '' },
|
||||
},
|
||||
{
|
||||
name: 'pass with basic config', params: {},
|
||||
},
|
||||
];
|
||||
|
||||
describe.only('NotificationConfiguration class getValidatedNotificationConfiguration',
|
||||
() => {
|
||||
it('should return MalformedXML error if request xml is empty', done => {
|
||||
const errMessage = 'request xml is undefined or empty';
|
||||
checkError('', 'MalformedXML', errMessage, done);
|
||||
});
|
||||
|
||||
failTests.forEach(test => {
|
||||
it(`should ${test.name}`, done => {
|
||||
generateParsedXml(test.params, xml => {
|
||||
checkError(xml, test.error, test.errorMessage, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
passTests.forEach(test => {
|
||||
it(`should ${test.name}`, done => {
|
||||
generateParsedXml(test.params, xml => {
|
||||
const config = new NotificationConfiguration(xml).
|
||||
getValidatedNotificationConfiguration();
|
||||
assert.ifError(config.error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue