From ba820e5661a159e4ad5aa93979a356a0e6f9c5ee Mon Sep 17 00:00:00 2001 From: Alexander Chan Date: Tue, 24 Apr 2018 18:16:36 -0700 Subject: [PATCH] bf: ZENKO-250 correctly evaluate regex pattern Original code will evaluate regex in `/pattern/` syntax incorrectly Adds parser to have MD search recognize if a regex is in `/pattern/` syntax or a simple string --- .../apiUtils/bucket/parseLikeExpression.js | 19 +++++++ lib/api/apiUtils/bucket/parseWhere.js | 13 +++-- .../aws-node-sdk/test/mdSearch/basicSearch.js | 50 +++++++++++++++++ .../test/mdSearch/utils/helpers.js | 14 +++-- tests/unit/api/parseLikeExpression.js | 53 +++++++++++++++++++ 5 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 lib/api/apiUtils/bucket/parseLikeExpression.js create mode 100644 tests/unit/api/parseLikeExpression.js diff --git a/lib/api/apiUtils/bucket/parseLikeExpression.js b/lib/api/apiUtils/bucket/parseLikeExpression.js new file mode 100644 index 0000000000..d5fc2a6c3a --- /dev/null +++ b/lib/api/apiUtils/bucket/parseLikeExpression.js @@ -0,0 +1,19 @@ +/** + * parse LIKE expressions + * @param {string} regex - regex pattern + * @return {object} MongoDB search object + */ +function parseLikeExpression(regex) { + if (typeof regex !== 'string') { + return null; + } + const split = regex.split('/'); + if (split.length < 3 || split[0] !== '') { + return { $regex: regex }; + } + const pattern = split.slice(1, split.length - 1).join('/'); + const regexOpt = split[split.length - 1]; + return { $regex: pattern, $options: regexOpt }; +} + +module.exports = parseLikeExpression; diff --git a/lib/api/apiUtils/bucket/parseWhere.js b/lib/api/apiUtils/bucket/parseWhere.js index 281697076e..d61a002946 100644 --- a/lib/api/apiUtils/bucket/parseWhere.js +++ b/lib/api/apiUtils/bucket/parseWhere.js @@ -1,3 +1,4 @@ +const parseLikeExpression = require('./parseLikeExpression'); /* This code is based on code from https://github.com/olehch/sqltomongo @@ -53,7 +54,7 @@ function parseWhere(root) { const e2 = parseWhere(root[operator][1]); // eslint-disable-next-line - return { '$and' : [ + return { '$and' : [ e1, e2, ] }; @@ -62,15 +63,21 @@ function parseWhere(root) { const e2 = parseWhere(root[operator][1]); // eslint-disable-next-line - return { '$or' : [ + return { '$or' : [ e1, e2, ] }; } const field = root[operator][0]; + const value = root[operator][1]; const expr = exprMapper[operator]; const obj = {}; - obj[`value.${field}`] = { [expr]: root[operator][1] }; + + if (operator === 'LIKE') { + obj[`value.${field}`] = parseLikeExpression(value); + } else { + obj[`value.${field}`] = { [expr]: value }; + } return obj; } diff --git a/tests/functional/aws-node-sdk/test/mdSearch/basicSearch.js b/tests/functional/aws-node-sdk/test/mdSearch/basicSearch.js index e99a67ec35..d544f3bd11 100644 --- a/tests/functional/aws-node-sdk/test/mdSearch/basicSearch.js +++ b/tests/functional/aws-node-sdk/test/mdSearch/basicSearch.js @@ -45,6 +45,25 @@ runIfMongo('Basic search', () => { encodedSearch, objectKey, done); }); + it('should list object with regex searched for system metadata', done => { + const encodedSearch = encodeURIComponent('key LIKE "find.*"'); + return runAndCheckSearch(s3Client, bucketName, + encodedSearch, objectKey, done); + }); + + it('should list object with regex searched for system metadata with flags', + done => { + const encodedSearch = encodeURIComponent('key LIKE "/FIND.*/i"'); + return runAndCheckSearch(s3Client, bucketName, + encodedSearch, objectKey, done); + }); + + it('should return empty when no object match regex', done => { + const encodedSearch = encodeURIComponent('key LIKE "/NOTFOUND.*/i"'); + return runAndCheckSearch(s3Client, bucketName, + encodedSearch, null, done); + }); + it('should list object with searched for user metadata', done => { const encodedSearch = encodeURIComponent(`x-amz-meta-food="${userMetadata.food}"`); @@ -99,3 +118,34 @@ runIfMongo('Search when no objects in bucket', () => { encodedSearch, null, done); }); }); + +runIfMongo('Invalid regular expression searches', () => { + const bucketName = `noobjectbucket${Date.now()}`; + before(done => { + s3Client.createBucket({ Bucket: bucketName }, done); + }); + + after(done => { + s3Client.deleteBucket({ Bucket: bucketName }, done); + }); + + it('should return error if pattern is invalid', done => { + const encodedSearch = encodeURIComponent('key LIKE "/((helloworld/"'); + const testError = { + code: 'InternalError', + message: 'We encountered an internal error. Please try again.', + }; + return runAndCheckSearch(s3Client, bucketName, + encodedSearch, testError, done); + }); + + it('should return error if regex flag is invalid', done => { + const encodedSearch = encodeURIComponent('key LIKE "/((helloworld/ii"'); + const testError = { + code: 'InternalError', + message: 'We encountered an internal error. Please try again.', + }; + return runAndCheckSearch(s3Client, bucketName, + encodedSearch, testError, done); + }); +}); diff --git a/tests/functional/aws-node-sdk/test/mdSearch/utils/helpers.js b/tests/functional/aws-node-sdk/test/mdSearch/utils/helpers.js index 9eb62caea8..a23434f907 100644 --- a/tests/functional/aws-node-sdk/test/mdSearch/utils/helpers.js +++ b/tests/functional/aws-node-sdk/test/mdSearch/utils/helpers.js @@ -20,23 +20,29 @@ testUtils.runIfMongo = process.env.S3METADATA === 'mongodb' ? describe : describe.skip; testUtils.runAndCheckSearch = (s3Client, bucketName, encodedSearch, - keyToFind, done) => { + testResult, done) => { const searchRequest = s3Client.listObjects({ Bucket: bucketName }); searchRequest.on('build', () => { searchRequest.httpRequest.path = `${searchRequest.httpRequest.path}?search=${encodedSearch}`; }); searchRequest.on('success', res => { - if (keyToFind) { + if (testResult) { assert(res.data.Contents[0], 'should be Contents listed'); - assert.strictEqual(res.data.Contents[0].Key, keyToFind); + assert.strictEqual(res.data.Contents[0].Key, testResult); assert.strictEqual(res.data.Contents.length, 1); } else { assert.strictEqual(res.data.Contents.length, 0); } return done(); }); - searchRequest.on('error', done); + searchRequest.on('error', err => { + if (testResult) { + assert.strictEqual(err.code, testResult.code); + assert.strictEqual(err.message, testResult.message); + } + return done(); + }); searchRequest.send(); }; diff --git a/tests/unit/api/parseLikeExpression.js b/tests/unit/api/parseLikeExpression.js new file mode 100644 index 0000000000..e4663a09f5 --- /dev/null +++ b/tests/unit/api/parseLikeExpression.js @@ -0,0 +1,53 @@ +const assert = require('assert'); +const parseLikeExpression = + require('../../../lib/api/apiUtils/bucket/parseLikeExpression'); + +describe('parseLikeExpression', () => { + const tests = [ + { + input: '', + output: { $regex: '' }, + }, + { + input: 'ice-cream-cone', + output: { $regex: 'ice-cream-cone' }, + }, + { + input: '/ice-cream-cone/', + output: { $regex: 'ice-cream-cone', $options: '' }, + }, + { + input: '/ice-cream-cone/i', + output: { $regex: 'ice-cream-cone', $options: 'i' }, + }, + { + input: 'an/ice-cream-cone/', + output: { $regex: 'an/ice-cream-cone/' }, + }, + { + input: '///', + output: { $regex: '/', $options: '' }, + }, + ]; + tests.forEach(test => it('should return correct MongoDB query object: ' + + `"${test.input}" => ${JSON.stringify(test.output)}`, () => { + const res = parseLikeExpression(test.input); + assert.deepStrictEqual(res, test.output); + })); + const badInputTests = [ + { + input: null, + output: null, + }, + { + input: 1235, + output: null, + }, + ]; + badInputTests.forEach(test => it( + 'should return null if input is not a string ' + + `"${test.input}" => ${JSON.stringify(test.output)}`, () => { + const res = parseLikeExpression(test.input); + assert.deepStrictEqual(res, test.output); + })); +});