import {MutableSearch, TokenType} from 'sentry/utils/tokenizeSearch'; describe('utils/tokenizeSearch', function () { describe('new MutableSearch()', function () { const cases = [ { name: 'should convert a basic query string to a query object', string: 'is:unresolved', object: { tokens: [{type: TokenType.FILTER, key: 'is', value: 'unresolved'}], }, }, { name: 'should convert quoted strings', string: 'is:unresolved browser:"Chrome 36"', object: { tokens: [ {type: TokenType.FILTER, key: 'is', value: 'unresolved'}, {type: TokenType.FILTER, key: 'browser', value: 'Chrome 36'}, ], }, }, { name: 'should populate the text query', string: 'python is:unresolved browser:"Chrome 36"', object: { tokens: [ {type: TokenType.FREE_TEXT, value: 'python'}, {type: TokenType.FILTER, key: 'is', value: 'unresolved'}, {type: TokenType.FILTER, key: 'browser', value: 'Chrome 36'}, ], }, }, { name: 'should tokenize the text query', string: 'python exception', object: { tokens: [ {type: TokenType.FREE_TEXT, value: 'python'}, {type: TokenType.FREE_TEXT, value: 'exception'}, ], }, }, { name: 'should tokenize has condition', string: 'has:user has:browser', object: { tokens: [ {type: TokenType.FILTER, key: 'has', value: 'user'}, {type: TokenType.FILTER, key: 'has', value: 'browser'}, ], }, }, { name: 'should tokenize !has condition', string: '!has:user has:browser', object: { tokens: [ {type: TokenType.FILTER, key: '!has', value: 'user'}, {type: TokenType.FILTER, key: 'has', value: 'browser'}, ], }, }, { name: 'should remove spaces in the query', string: 'python is:unresolved exception', object: { tokens: [ {type: TokenType.FREE_TEXT, value: 'python'}, {type: TokenType.FILTER, key: 'is', value: 'unresolved'}, {type: TokenType.FREE_TEXT, value: 'exception'}, ], }, }, { name: 'should tokenize the quoted tags', string: 'event.type:error title:"QueryExecutionError: Code: 141."', object: { tokens: [ {type: TokenType.FILTER, key: 'event.type', value: 'error'}, { type: TokenType.FILTER, key: 'title', value: 'QueryExecutionError: Code: 141.', }, ], }, }, { name: 'should tokenize words with :: in them', string: 'key:Resque::DirtyExit', object: { tokens: [{type: TokenType.FILTER, key: 'key', value: 'Resque::DirtyExit'}], }, }, { name: 'tokens that begin with a colon are still queries', string: 'country:canada :unresolved', object: { tokens: [ {type: TokenType.FILTER, key: 'country', value: 'canada'}, {type: TokenType.FREE_TEXT, value: ':unresolved'}, ], }, }, { name: 'correctly preserve boolean operators', string: 'country:canada Or country:newzealand', object: { tokens: [ {type: TokenType.FILTER, key: 'country', value: 'canada'}, {type: TokenType.OPERATOR, value: 'OR'}, {type: TokenType.FILTER, key: 'country', value: 'newzealand'}, ], }, }, { name: 'correctly preserve parens', string: '(country:canada Or country:newzealand) AnD province:pei', object: { tokens: [ {type: TokenType.OPERATOR, value: '('}, {type: TokenType.FILTER, key: 'country', value: 'canada'}, {type: TokenType.OPERATOR, value: 'OR'}, {type: TokenType.FILTER, key: 'country', value: 'newzealand'}, {type: TokenType.OPERATOR, value: ')'}, {type: TokenType.OPERATOR, value: 'AND'}, {type: TokenType.FILTER, key: 'province', value: 'pei'}, ], }, }, { name: 'query tags boolean and parens are all stitched back together correctly', string: '(a:a OR (b:b AND c d e)) OR f g:g', object: { tokens: [ {type: TokenType.OPERATOR, value: '('}, {type: TokenType.FILTER, key: 'a', value: 'a'}, {type: TokenType.OPERATOR, value: 'OR'}, {type: TokenType.OPERATOR, value: '('}, {type: TokenType.FILTER, key: 'b', value: 'b'}, {type: TokenType.OPERATOR, value: 'AND'}, {type: TokenType.FREE_TEXT, value: 'c'}, {type: TokenType.FREE_TEXT, value: 'd'}, {type: TokenType.FREE_TEXT, value: 'e'}, {type: TokenType.OPERATOR, value: ')'}, {type: TokenType.OPERATOR, value: ')'}, {type: TokenType.OPERATOR, value: 'OR'}, {type: TokenType.FREE_TEXT, value: 'f'}, {type: TokenType.FILTER, key: 'g', value: 'g'}, ], }, }, { name: 'correctly preserve filters with functions', string: 'country:>canada OR coronaFree():canada'}, {type: TokenType.OPERATOR, value: 'OR'}, {type: TokenType.FILTER, key: 'coronaFree()', value: ' expect(new MutableSearch(string)).toEqual(object)); } }); describe('QueryResults operations', function () { it('add tokens to query object', function () { const results = new MutableSearch([]); results.addStringFilter('a:a'); expect(results.formatString()).toEqual('a:a'); results.addFilterValues('b', ['b']); expect(results.formatString()).toEqual('a:a b:b'); results.addFilterValues('c', ['c1', 'c2']); expect(results.formatString()).toEqual('a:a b:b c:c1 c:c2'); results.addFilterValues('d', ['d']); expect(results.formatString()).toEqual('a:a b:b c:c1 c:c2 d:d'); results.addFilterValues('e', ['e1*e2\\e3']); expect(results.formatString()).toEqual('a:a b:b c:c1 c:c2 d:d e:"e1\\*e2\\e3"'); results.addStringFilter('d:d2'); expect(results.formatString()).toEqual( 'a:a b:b c:c1 c:c2 d:d e:"e1\\*e2\\e3" d:d2' ); }); it('add text searches to query object', function () { const results = new MutableSearch(['a:a']); results.addFreeText('b'); expect(results.formatString()).toEqual('a:a b'); expect(results.freeText).toEqual(['b']); results.addFreeText('c'); expect(results.formatString()).toEqual('a:a b c'); expect(results.freeText).toEqual(['b', 'c']); results.addStringFilter('d:d').addFreeText('e'); expect(results.formatString()).toEqual('a:a b c d:d e'); expect(results.freeText).toEqual(['b', 'c', 'e']); results.freeText = ['x', 'y']; expect(results.formatString()).toEqual('a:a d:d x y'); expect(results.freeText).toEqual(['x', 'y']); results.freeText = ['a b c']; expect(results.formatString()).toEqual('a:a d:d "a b c"'); expect(results.freeText).toEqual(['a b c']); results.freeText = ['invalid literal for int() with base']; expect(results.formatString()).toEqual( 'a:a d:d "invalid literal for int() with base"' ); expect(results.freeText).toEqual(['invalid literal for int() with base']); }); it('add ops to query object', function () { const results = new MutableSearch(['x', 'a:a', 'y']); results.addOp('OR'); expect(results.formatString()).toEqual('x a:a y OR'); results.addFreeText('z'); expect(results.formatString()).toEqual('x a:a y OR z'); results .addOp('(') .addStringFilter('b:b') .addOp('AND') .addStringFilter('c:c') .addOp(')'); expect(results.formatString()).toEqual('x a:a y OR z ( b:b AND c:c )'); }); it('adds tags to query', function () { const results = new MutableSearch(['tag:value']); results.addStringFilter('new:too'); expect(results.formatString()).toEqual('tag:value new:too'); }); it('setTag() replaces tags', function () { const results = new MutableSearch(['tag:value']); results.setFilterValues('tag', ['too']); expect(results.formatString()).toEqual('tag:too'); }); it('setTag() replaces tags in OR', function () { let results = new MutableSearch([ '(', 'transaction:xyz', 'OR', 'transaction:abc', ')', ]); results.setFilterValues('transaction', ['def']); expect(results.formatString()).toEqual('transaction:def'); results = new MutableSearch(['(transaction:xyz', 'OR', 'transaction:abc)']); results.setFilterValues('transaction', ['def']); expect(results.formatString()).toEqual('transaction:def'); }); it('does not remove boolean operators after setting tag values', function () { const results = new MutableSearch([ '(', 'start:xyz', 'AND', 'end:abc', ')', 'OR', '(', 'start:abc', 'AND', 'end:xyz', ')', ]); results.setFilterValues('transaction', ['def']); expect(results.formatString()).toEqual( '( start:xyz AND end:abc ) OR ( start:abc AND end:xyz ) transaction:def' ); }); it('removes tags from query object', function () { let results = new MutableSearch(['x', 'a:a', 'b:b']); results.removeFilter('a'); expect(results.formatString()).toEqual('x b:b'); results = new MutableSearch(['a:a']); results.removeFilter('a'); expect(results.formatString()).toEqual(''); results = new MutableSearch(['x', 'a:a', 'a:a2']); results.removeFilter('a'); expect(results.formatString()).toEqual('x'); results = new MutableSearch(['a:a', 'OR', 'b:b']); results.removeFilter('a'); expect(results.formatString()).toEqual('b:b'); results = new MutableSearch(['a:a', 'OR', 'a:a1', 'AND', 'b:b']); results.removeFilter('a'); expect(results.formatString()).toEqual('b:b'); results = new MutableSearch(['(a:a', 'OR', 'b:b)']); results.removeFilter('a'); expect(results.formatString()).toEqual('b:b'); results = new MutableSearch(['(a:a', 'OR', 'b:b', 'OR', 'y)']); results.removeFilter('a'); expect(results.formatString()).toEqual('( b:b OR y )'); results = new MutableSearch(['(a:a', 'OR', '(b:b1', 'OR', '(c:c', 'OR', 'b:b2)))']); results.removeFilter('b'); expect(results.formatString()).toEqual('( a:a OR c:c )'); results = new MutableSearch(['(((a:a', 'OR', 'b:b1)', 'OR', 'c:c)', 'OR', 'b:b2)']); results.removeFilter('b'); expect(results.formatString()).toEqual('( ( a:a OR c:c ) )'); results = new MutableSearch(['a:a', '(b:b1', 'OR', 'b:b2', 'OR', 'b:b3)', 'c:c']); results.removeFilter('b'); expect(results.formatString()).toEqual('a:a c:c'); }); it('can return the tag keys', function () { const results = new MutableSearch(['tag:value', 'other:value', 'additional text']); expect(results.getFilterKeys()).toEqual(['tag', 'other']); }); it('getTagValues', () => { const results = new MutableSearch([ 'tag:value', 'other:value', 'tag:value2', 'additional text', ]); expect(results.getFilterValues('tag')).toEqual(['value', 'value2']); expect(results.getFilterValues('nonexistent')).toEqual([]); }); }); describe('QueryResults.formatString', function () { const cases = [ { name: 'should convert a basic object to a query string', object: new MutableSearch(['is:unresolved']), string: 'is:unresolved', }, { name: 'should quote tags with spaces', object: new MutableSearch(['is:unresolved', 'browser:"Chrome 36"']), string: 'is:unresolved browser:"Chrome 36"', }, { name: 'should stringify the query', object: new MutableSearch(['python', 'is:unresolved', 'browser:"Chrome 36"']), string: 'python is:unresolved browser:"Chrome 36"', }, { name: 'should join tokenized queries', object: new MutableSearch(['python', 'exception']), string: 'python exception', }, { name: 'should quote tags with spaces', object: new MutableSearch([ 'oh', 'me', 'oh', 'my', 'browser:"Chrome 36"', 'browser:"Firefox 60"', ]), string: 'oh me oh my browser:"Chrome 36" browser:"Firefox 60"', }, { name: 'should quote tags with parens', object: new MutableSearch([ 'bad', 'things', 'repository_id:"UUID(\'long-value\')"', ]), string: 'bad things repository_id:"UUID(\'long-value\')"', }, { // values with quotes do not need to be quoted // furthermore, timestamps contain colons // but the backend currently does not support quoted date formats name: 'should not quote tags with colon', object: new MutableSearch(['bad', 'things', 'user:"id:123"']), string: 'bad things user:id:123', }, { name: 'should escape quote tags with double quotes', object: new MutableSearch([ 'bad', 'things', 'name:"Ernest \\"Papa\\" Hemingway"', ]), string: 'bad things name:"Ernest \\"Papa\\" Hemingway"', }, { name: 'should include blank strings', object: new MutableSearch(['bad', 'things', 'name:""']), string: 'bad things name:""', }, { name: 'correctly preserve boolean operators', object: new MutableSearch(['country:canada', 'OR', 'country:newzealand']), string: 'country:canada OR country:newzealand', }, { name: 'correctly preserve parens', object: new MutableSearch([ '(country:canada', 'OR', 'country:newzealand)', 'AND', 'province:pei', ]), string: '( country:canada OR country:newzealand ) AND province:pei', }, { name: 'query tags boolean and parens are all stitched back together correctly', object: new MutableSearch([ '(a:a', 'OR', '(b:b', 'AND', 'c', 'd', 'e))', 'OR', 'f', 'g:g', ]), string: '( a:a OR ( b:b AND c d e ) ) OR f g:g', }, { name: 'correctly preserve filters with functions', object: new MutableSearch(['country:>canada', 'OR', 'coronaFree():canada OR coronaFree(): expect(object.formatString()).toEqual(string)); } }); });