@@ -13,25 +13,25 @@ describe("OrderByQueryRangeStrategy", function () {
1313 strategy = new OrderByQueryRangeStrategy ( ) ;
1414 } ) ;
1515
16- describe ( "filterPartitionRanges" , function ( ) {
17- const createRange = ( id : string , min : string , max : string ) : PartitionKeyRange => ( {
18- id,
19- minInclusive : min ,
20- maxExclusive : max ,
21- ridPrefix : parseInt ( id ) ,
22- throughputFraction : 1 ,
23- status : "online" ,
24- parents : [ ] ,
25- } ) ;
16+ const createRange = ( id : string , min : string , max : string ) : PartitionKeyRange => ( {
17+ id,
18+ minInclusive : min ,
19+ maxExclusive : max ,
20+ ridPrefix : parseInt ( id ) ,
21+ throughputFraction : 1 ,
22+ status : "online" ,
23+ parents : [ ] ,
24+ } ) ;
2625
27- const createContinuationRange = (
28- range : PartitionKeyRange ,
29- token : string ,
30- ) : PartitionRangeWithContinuationToken => ( {
31- range,
32- continuationToken : token ,
33- } ) ;
26+ const createContinuationRange = (
27+ range : PartitionKeyRange ,
28+ token : string ,
29+ ) : PartitionRangeWithContinuationToken => ( {
30+ range,
31+ continuationToken : token ,
32+ } ) ;
3433
34+ describe ( "filterPartitionRanges" , function ( ) {
3535 describe ( "Edge Cases" , function ( ) {
3636 it ( "should return empty result when targetRanges is null" , function ( ) {
3737 const result = strategy . filterPartitionRanges ( null as any ) ;
@@ -750,4 +750,281 @@ describe("OrderByQueryRangeStrategy", function () {
750750 expect ( strategy . getStrategyType ( ) ) . toBe ( "OrderByQuery" ) ;
751751 } ) ;
752752 } ) ;
753+
754+ describe ( "formatValueForSQL escaping via filterPartitionRanges" , function ( ) {
755+ /**
756+ * Helper to get the filteringCondition produced for a given orderByItem value.
757+ * Uses a single range so the target range filter (\>= for ASC) is returned.
758+ */
759+ function getFilterCondition ( value : any ) : string | undefined {
760+ const ranges = [ createRange ( "1" , "" , "FF" ) ] ;
761+ const contRanges = [ createContinuationRange ( ranges [ 0 ] , "token1" ) ] ;
762+ const queryInfo = {
763+ queryInfo : {
764+ queryInfo : {
765+ orderBy : [ "Ascending" ] ,
766+ orderByExpressions : [ "c.field1" ] ,
767+ } ,
768+ } ,
769+ orderByItems : [ { item : value } ] ,
770+ } ;
771+ const result = strategy . filterPartitionRanges ( ranges , contRanges , queryInfo ) ;
772+ return result . rangeTokenPairs [ 0 ] ?. filteringCondition ;
773+ }
774+
775+ // --- Type handling ---
776+ it ( "should not alter strings without special characters" , function ( ) {
777+ const condition = getFilterCondition ( "normal string" ) ;
778+ expect ( condition ) . toContain ( "'normal string'" ) ;
779+ } ) ;
780+
781+ it ( "should handle null values" , function ( ) {
782+ expect ( getFilterCondition ( null ) ) . toContain ( "null" ) ;
783+ } ) ;
784+
785+ it ( "should handle number values" , function ( ) {
786+ expect ( getFilterCondition ( 42 ) ) . toContain ( "42" ) ;
787+ } ) ;
788+
789+ it ( "should handle boolean values" , function ( ) {
790+ expect ( getFilterCondition ( true ) ) . toContain ( "true" ) ;
791+ } ) ;
792+
793+ it ( "should handle empty string" , function ( ) {
794+ expect ( getFilterCondition ( "" ) ) . toContain ( "''" ) ;
795+ } ) ;
796+
797+ // --- Core backslash escaping ---
798+ it ( "should escape \\u2013 to prevent SQL unicode interpretation (original bug)" , function ( ) {
799+ const value = "Gold\u005cu2013Foran" ; // Gold\u2013Foran
800+ const condition = getFilterCondition ( value ) ;
801+ expect ( condition ) . toContain ( "'Gold\\\\u2013Foran'" ) ;
802+ } ) ;
803+
804+ it ( "should escape a single backslash" , function ( ) {
805+ const condition = getFilterCondition ( "\\" ) ;
806+ expect ( condition ) . toContain ( "'\\\\'" ) ;
807+ } ) ;
808+
809+ it ( "should escape trailing backslash" , function ( ) {
810+ const condition = getFilterCondition ( "path\\" ) ;
811+ expect ( condition ) . toContain ( "'path\\\\'" ) ;
812+ } ) ;
813+
814+ it ( "should escape triple backslash (odd count)" , function ( ) {
815+ const condition = getFilterCondition ( "\\\\\\" ) ;
816+ expect ( condition ) . toContain ( "'\\\\\\\\\\\\'" ) ;
817+ } ) ;
818+
819+ it ( "should escape 20 consecutive backslashes" , function ( ) {
820+ const condition = getFilterCondition ( "\\" . repeat ( 20 ) ) ;
821+ expect ( condition ) . toContain ( `'${ "\\\\" . repeat ( 20 ) } '` ) ;
822+ } ) ;
823+
824+ // --- Core quote escaping ---
825+ it ( "should escape a single quote as \\u0027" , function ( ) {
826+ const condition = getFilterCondition ( "'" ) ;
827+ expect ( condition ) . toContain ( "'\\u0027'" ) ;
828+ } ) ;
829+
830+ it ( "should escape consecutive single quotes" , function ( ) {
831+ const condition = getFilterCondition ( "'''" ) ;
832+ expect ( condition ) . toContain ( "'\\u0027\\u0027\\u0027'" ) ;
833+ } ) ;
834+
835+ it ( "should escape doubled quotes in context" , function ( ) {
836+ const condition = getFilterCondition ( "it''s a ''test''" ) ;
837+ expect ( condition ) . toContain ( "'it\\u0027\\u0027s a \\u0027\\u0027test\\u0027\\u0027'" ) ;
838+ } ) ;
839+
840+ // --- Backslash + quote interaction (the critical ambiguity case) ---
841+ it ( "should escape both backslash and quote in one string" , function ( ) {
842+ const condition = getFilterCondition ( "it's a \\test" ) ;
843+ expect ( condition ) . toContain ( "'it\\u0027s a \\\\test'" ) ;
844+ } ) ;
845+
846+ it ( "should escape backslash immediately before quote (\\' ambiguity)" , function ( ) {
847+ const condition = getFilterCondition ( "\\'" ) ;
848+ expect ( condition ) . toContain ( "'\\\\\\u0027'" ) ;
849+ } ) ;
850+
851+ it ( "should escape quote immediately before backslash" , function ( ) {
852+ const condition = getFilterCondition ( "'\\" ) ;
853+ expect ( condition ) . toContain ( "'\\u0027\\\\'" ) ;
854+ } ) ;
855+
856+ it ( "should escape backslash-quote-backslash sandwich" , function ( ) {
857+ const condition = getFilterCondition ( "\\'\\" ) ;
858+ expect ( condition ) . toContain ( "'\\\\\\u0027\\\\'" ) ;
859+ } ) ;
860+
861+ it ( "should escape 5 backslash-quote pairs" , function ( ) {
862+ const condition = getFilterCondition ( "\\'" . repeat ( 5 ) ) ;
863+ expect ( condition ) . toContain ( `'${ "\\\\\\u0027" . repeat ( 5 ) } '` ) ;
864+ } ) ;
865+
866+ it ( "should escape triple backslash followed by quote" , function ( ) {
867+ const condition = getFilterCondition ( "\\\\\\'" ) ;
868+ expect ( condition ) . toContain ( "'\\\\\\\\\\\\\\u0027'" ) ;
869+ } ) ;
870+
871+ // --- Unicode escape sequences in stored data ---
872+ it ( "should escape literal \\u0027 in value (our escape target)" , function ( ) {
873+ const value = "has\u005cu0027literal" ;
874+ const condition = getFilterCondition ( value ) ;
875+ expect ( condition ) . toContain ( "'has\\\\u0027literal'" ) ;
876+ } ) ;
877+
878+ it ( "should escape literal \\u005c (unicode for backslash itself)" , function ( ) {
879+ const value = "\u005cu005c" ;
880+ const condition = getFilterCondition ( value ) ;
881+ expect ( condition ) . toContain ( "'\\\\u005c'" ) ;
882+ } ) ;
883+
884+ it ( "should escape incomplete unicode \\u00" , function ( ) {
885+ const value = "\u005cu00" ;
886+ const condition = getFilterCondition ( value ) ;
887+ expect ( condition ) . toContain ( "'\\\\u00'" ) ;
888+ } ) ;
889+
890+ it ( "should escape \\u followed by quote" , function ( ) {
891+ const condition = getFilterCondition ( "\\u'" ) ;
892+ expect ( condition ) . toContain ( "'\\\\u\\u0027'" ) ;
893+ } ) ;
894+
895+ // --- Backslash + escape letters ---
896+ it ( "should escape all 26 backslash+letter combinations" , function ( ) {
897+ const letters = "abcdefghijklmnopqrstuvwxyz" ;
898+ const value = letters
899+ . split ( "" )
900+ . map ( ( c ) => `\\${ c } ` )
901+ . join ( "" ) ;
902+ const condition = getFilterCondition ( value ) ;
903+ const expected = letters
904+ . split ( "" )
905+ . map ( ( c ) => `\\\\${ c } ` )
906+ . join ( "" ) ;
907+ expect ( condition ) . toContain ( `'${ expected } '` ) ;
908+ } ) ;
909+
910+ it ( "should escape \\r\\n\\t together" , function ( ) {
911+ const condition = getFilterCondition ( "\\r\\n\\t" ) ;
912+ expect ( condition ) . toContain ( "'\\\\r\\\\n\\\\t'" ) ;
913+ } ) ;
914+
915+ it ( "should escape double-backslash before escape letters (\\\\n\\\\t\\\\r)" , function ( ) {
916+ const condition = getFilterCondition ( "\\\\n\\\\t\\\\r" ) ;
917+ expect ( condition ) . toContain ( "'\\\\\\\\n\\\\\\\\t\\\\\\\\r'" ) ;
918+ } ) ;
919+
920+ // --- Nested combos: escapes + quotes interleaved ---
921+ it ( "should escape quote wrapping an escape sequence ('\\n')" , function ( ) {
922+ const condition = getFilterCondition ( "'\\n'" ) ;
923+ expect ( condition ) . toContain ( "'\\u0027\\\\n\\u0027'" ) ;
924+ } ) ;
925+
926+ it ( "should escape \\n\\\\\\n (escape, backslash-pair, escape)" , function ( ) {
927+ const condition = getFilterCondition ( "\\n\\\\\\n" ) ;
928+ expect ( condition ) . toContain ( "'\\\\n\\\\\\\\\\\\n'" ) ;
929+ } ) ;
930+
931+ it ( "should escape \\t\\n'\\t\\n (interleaved escapes with quote)" , function ( ) {
932+ const condition = getFilterCondition ( "\\t\\n'\\t\\n" ) ;
933+ expect ( condition ) . toContain ( "'\\\\t\\\\n\\u0027\\\\t\\\\n'" ) ;
934+ } ) ;
935+
936+ it ( "should escape \\u0027\\n\\u0027 (literal unicode-quotes with escape between)" , function ( ) {
937+ const value = "\u005cu0027\\n\u005cu0027" ;
938+ const condition = getFilterCondition ( value ) ;
939+ expect ( condition ) . toContain ( "'\\\\u0027\\\\n\\\\u0027'" ) ;
940+ } ) ;
941+
942+ // --- Hex-like and regex ---
943+ it ( "should escape hex-like sequences \\x41\\x42\\x43" , function ( ) {
944+ const condition = getFilterCondition ( "\\x41\\x42\\x43" ) ;
945+ expect ( condition ) . toContain ( "'\\\\x41\\\\x42\\\\x43'" ) ;
946+ } ) ;
947+
948+ it ( "should escape regex-like pattern \\d+\\.\\d+\\.\\d+" , function ( ) {
949+ const condition = getFilterCondition ( "\\d+\\.\\d+\\.\\d+" ) ;
950+ expect ( condition ) . toContain ( "'\\\\d+\\\\.\\\\d+\\\\.\\\\d+'" ) ;
951+ } ) ;
952+
953+ // --- Realistic values ---
954+ it ( "should escape Windows path with quotes" , function ( ) {
955+ const condition = getFilterCondition ( "C:\\Program Files\\O'Reilly\\book\\chapter1.txt" ) ;
956+ expect ( condition ) . toContain (
957+ "'C:\\\\Program Files\\\\O\\u0027Reilly\\\\book\\\\chapter1.txt'" ,
958+ ) ;
959+ } ) ;
960+
961+ it ( "should escape SQL query embedded as a value" , function ( ) {
962+ const value = "SELECT * FROM c WHERE c.name = 'O\\'Brien' AND c.path = '\\\\server'" ;
963+ const condition = getFilterCondition ( value ) ;
964+ expect ( condition ) . toContain (
965+ "'SELECT * FROM c WHERE c.name = \\u0027O\\\\\\u0027Brien\\u0027 AND c.path = \\u0027\\\\\\\\server\\u0027'" ,
966+ ) ;
967+ } ) ;
968+
969+ it ( "should escape log entry with mixed escapes and quotes" , function ( ) {
970+ const value = "2024-01-01\\t[WARN]\\tUser 'admin' path=C:\\temp\\n\\tStack: Error\\n" ;
971+ const condition = getFilterCondition ( value ) ;
972+ expect ( condition ) . toContain (
973+ "'2024-01-01\\\\t[WARN]\\\\tUser \\u0027admin\\u0027 path=C:\\\\temp\\\\n\\\\tStack: Error\\\\n'" ,
974+ ) ;
975+ } ) ;
976+
977+ // --- Adversarial: SQL injection ---
978+ it ( "should escape SQL injection with backslash-quote" , function ( ) {
979+ const condition = getFilterCondition ( "val\\' OR 1=1 --" ) ;
980+ expect ( condition ) . toContain ( "'val\\\\\\u0027 OR 1=1 --'" ) ;
981+ } ) ;
982+
983+ it ( "should escape SQL injection with closing literal" , function ( ) {
984+ const condition = getFilterCondition ( "') OR ('1'='1" ) ;
985+ expect ( condition ) . toContain ( "'\\u0027) OR (\\u00271\\u0027=\\u00271'" ) ;
986+ } ) ;
987+
988+ it ( "should pass through percent and SQL comment chars unchanged" , function ( ) {
989+ const condition = getFilterCondition ( "100% done; DROP TABLE --" ) ;
990+ expect ( condition ) . toContain ( "'100% done; DROP TABLE --'" ) ;
991+ } ) ;
992+
993+ // --- Kitchen sink combos ---
994+ it ( "should escape all dangerous chars combined: \\'\\\\\\u0027\\u2013\\n\\t" , function ( ) {
995+ const value = "\\'\\\\\\u0027\\u2013\\n\\t" ;
996+ const condition = getFilterCondition ( value ) ;
997+ expect ( condition ) . toContain ( "'\\\\\\u0027\\\\\\\\\\\\u0027\\\\u2013\\\\n\\\\t'" ) ;
998+ } ) ;
999+
1000+ it ( "should escape 5 quotes + 5 backslashes + 5 quotes symmetrically" , function ( ) {
1001+ const condition = getFilterCondition ( "'''''\\\\\\\\\\'''''" ) ;
1002+ const expected = "\\u0027" . repeat ( 5 ) + "\\\\" . repeat ( 5 ) + "\\u0027" . repeat ( 5 ) ;
1003+ expect ( condition ) . toContain ( `'${ expected } '` ) ;
1004+ } ) ;
1005+
1006+ it ( "should escape long string with every escape type scattered" , function ( ) {
1007+ const value =
1008+ "The quick brown fox\\n jumped over\\t the 'lazy' dog.\\\\ The path was C:\\Users\\test\\file.txt and it\\'s value had \\u2013 dashes \\u0027quotes\\u0027 end." ;
1009+ const condition = getFilterCondition ( value ) ;
1010+ expect ( condition ) . toContain ( "\\\\n jumped over\\\\t" ) ;
1011+ expect ( condition ) . toContain ( "\\u0027lazy\\u0027" ) ;
1012+ expect ( condition ) . toContain ( "C:\\\\Users\\\\test\\\\file.txt" ) ;
1013+ expect ( condition ) . toContain ( "it\\\\\\u0027s value" ) ;
1014+ expect ( condition ) . toContain ( "\\\\u2013 dashes" ) ;
1015+ expect ( condition ) . toContain ( "\\\\u0027quotes\\\\u0027" ) ;
1016+ } ) ;
1017+
1018+ // --- Double quotes (non-SQL-special, verify no interference) ---
1019+ it ( "should pass through double quotes unchanged" , function ( ) {
1020+ const condition = getFilterCondition ( '\\"hello\\"' ) ;
1021+ expect ( condition ) . toContain ( "'\\\\\"hello\\\\\"'" ) ;
1022+ } ) ;
1023+
1024+ // --- Real unicode characters (no backslash, should pass through unchanged) ---
1025+ it ( "should pass through real en-dash character unchanged" , function ( ) {
1026+ const condition = getFilterCondition ( "Gold\u2013Foran" ) ;
1027+ expect ( condition ) . toContain ( "'Gold\u2013Foran'" ) ;
1028+ } ) ;
1029+ } ) ;
7531030} ) ;
0 commit comments