@@ -914,6 +914,173 @@ describe('FLE tests', function () {
914
914
} ) ;
915
915
} ) ;
916
916
917
+ context ( '8.2+' , function ( ) {
918
+ skipIfServerVersion ( testServer , '< 8.2' ) ;
919
+
920
+ context (
921
+ 'Queryable Encryption Prefix/Suffix/Substring Support' ,
922
+ function ( ) {
923
+ // Substring prefix support is enterprise-only 8.2+
924
+ skipIfCommunityServer ( testServer ) ;
925
+
926
+ let shell : TestShell ;
927
+ let uri : string ;
928
+
929
+ const testCollection = 'qeSubstringTest' ;
930
+
931
+ before ( async function ( ) {
932
+ shell = this . startTestShell ( {
933
+ args : [ '--nodb' , `--cryptSharedLibPath=${ cryptLibrary } ` ] ,
934
+ } ) ;
935
+ uri = JSON . stringify ( await testServer . connectionString ( ) ) ;
936
+ await shell . waitForPrompt ( ) ;
937
+
938
+ // Shared setup for all substring search tests - create collection once
939
+ await shell . executeLine ( `{
940
+ opts = {
941
+ keyVaultNamespace: '${ dbname } .__keyVault',
942
+ kmsProviders: { local: { key: 'A'.repeat(128) } },
943
+ bypassQueryAnalysis: false
944
+ };
945
+
946
+ autoMongo = Mongo(${ uri } , { ...opts });
947
+ autoMongo.getDB('${ dbname } ').test.drop();
948
+
949
+ keyId = autoMongo.getKeyVault().createKey('local');
950
+
951
+ substringOptions = {
952
+ strMinQueryLength: 2,
953
+ strMaxQueryLength: 10,
954
+ strMaxLength: 60,
955
+ };
956
+
957
+ autoMongo.getClientEncryption().createEncryptedCollection('${ dbname } ', '${ testCollection } ', {
958
+ provider: 'local',
959
+ createCollectionOptions: {
960
+ encryptedFields: {
961
+ fields: [{
962
+ keyId,
963
+ path: 'data',
964
+ bsonType: 'string',
965
+ queries: [{
966
+ queryType: 'substringPreview',
967
+ ...substringOptions,
968
+ caseSensitive: false,
969
+ diacriticSensitive: false,
970
+ contention: 4
971
+ }]
972
+ }]
973
+ }
974
+ }
975
+ });
976
+
977
+ coll = autoMongo.getDB('${ dbname } ').${ testCollection } ;
978
+
979
+ // Setup explicit encryption client
980
+ explicitMongo = Mongo(${ uri } , { ...opts, bypassQueryAnalysis: true });
981
+ ce = explicitMongo.getClientEncryption();
982
+ ecoll = explicitMongo.getDB('${ dbname } ').${ testCollection } ;
983
+
984
+ explicitOpts = {
985
+ algorithm: 'TextPreview',
986
+ contentionFactor: 4,
987
+ textOptions: { caseSensitive: false, diacriticSensitive: false, substring: substringOptions }
988
+ };
989
+ }` ) ;
990
+ } ) ;
991
+
992
+ after ( async function ( ) {
993
+ await shell . executeLine ( `${ testCollection } .drop()` ) ;
994
+ } ) ;
995
+
996
+ afterEach ( async function ( ) {
997
+ await shell . executeLine ( `${ testCollection } .deleteMany({})` ) ;
998
+ } ) ;
999
+
1000
+ it ( 'allows queryable encryption with prefix searches' , async function ( ) {
1001
+ // Insert test data for prefix searches
1002
+ await shell . executeLine ( `{
1003
+ coll.insertOne({ data: 'admin_user_123.txt' });
1004
+ coll.insertOne({ data: 'admin_super_456.pdf' });
1005
+ coll.insertOne({ data: 'user_regular_789.pdf' });
1006
+ coll.insertOne({ data: 'guest_access_000.txt' });
1007
+
1008
+ // Add explicit encryption data
1009
+ ecoll.insertOne({ data: ce.encrypt(keyId, 'admin_explicit_test.pdf', explicitOpts) });
1010
+ }` ) ;
1011
+ const prefixResults = await shell . executeLine (
1012
+ 'coll.find({$expr: { $and: [{$encStrContains: {substring: "admin_", input: "$data"}}] }}, { __safeContent__: 0 }).toArray()'
1013
+ ) ;
1014
+ expect ( prefixResults ) . to . have . length ( 2 ) ;
1015
+ expect ( prefixResults ) . to . include ( 'admin_user_123.txt' ) ;
1016
+ expect ( prefixResults ) . to . include ( 'admin_super_456.pdf' ) ;
1017
+ expect ( prefixResults ) . to . include ( 'admin_explicit_test.pdf' ) ;
1018
+ } ) ;
1019
+
1020
+ it ( 'allows queryable encryption with suffix searches' , async function ( ) {
1021
+ // Insert test data for suffix searches
1022
+ await shell . executeLine ( `{
1023
+ coll.insertOne({ data: 'admin_user_123.txt' });
1024
+ coll.insertOne({ data: 'admin_super_456.pdf' });
1025
+ coll.insertOne({ data: 'user_regular_789.pdf' });
1026
+ coll.insertOne({ data: 'guest_access_000.txt' });
1027
+
1028
+ // Add explicit encryption data
1029
+ ecoll.insertOne({ data: ce.encrypt(keyId, 'admin_explicit_test.pdf', explicitOpts) });
1030
+ }` ) ;
1031
+
1032
+ const suffixResults = await shell . executeLine (
1033
+ 'coll.find({$expr: { $and: [{$encStrContains: {substring: ".pdf", input: "$data"}}] }}, { __safeContent__: 0 }).toArray()'
1034
+ ) ;
1035
+ expect ( suffixResults ) . to . have . length ( 3 ) ;
1036
+ expect ( suffixResults ) . to . include ( 'admin_super_456.pdf' ) ;
1037
+ expect ( suffixResults ) . to . include ( 'user_regular_789.pdf' ) ;
1038
+ expect ( suffixResults ) . to . include ( 'admin_explicit_test.pdf' ) ;
1039
+ } ) ;
1040
+
1041
+ it ( 'allows queryable encryption with substring searches' , async function ( ) {
1042
+ // Insert test data for substring searches
1043
+ // Insert test data for prefix searches
1044
+ await shell . executeLine ( `{
1045
+ coll.insertOne({ data: 'admin_user_123.txt' });
1046
+ coll.insertOne({ data: 'admin_super_456.pdf' });
1047
+ coll.insertOne({ data: 'user_regular_789.pdf' });
1048
+ coll.insertOne({ data: 'guest_access_000.txt' });
1049
+
1050
+ // Add explicit encryption data
1051
+ ecoll.insertOne({ data: ce.encrypt(keyId, 'explicit_user', explicitOpts) });
1052
+ }` ) ;
1053
+ // Test substring search returning multiple documents
1054
+ const substringResults = await shell . executeLine (
1055
+ 'coll.find({$expr: { $and: [{$encStrContains: {substring: "user", input: "$data"}}] }}, { __safeContent__: 0 }).toArray()'
1056
+ ) ;
1057
+ expect ( substringResults ) . to . have . length ( 2 ) ;
1058
+ expect ( substringResults ) . to . include ( 'user_regular_789.pdf' ) ;
1059
+ expect ( substringResults ) . to . include ( 'admin_user_123.txt' ) ;
1060
+
1061
+ const testingSubstringResult = await shell . executeLine (
1062
+ 'coll.find({$expr: { $and: [{$encStrContains: {substring: "user", input: "$data"}}] }}, { __safeContent__: 0 }).toArray()'
1063
+ ) ;
1064
+ expect ( testingSubstringResult ) . to . have . length ( 3 ) ;
1065
+ expect ( testingSubstringResult ) . to . include ( 'user_regular_789.pdf' ) ;
1066
+ expect ( testingSubstringResult ) . to . include ( 'admin_user_123.txt' ) ;
1067
+ expect ( testingSubstringResult ) . to . include ( 'explicit_user' ) ;
1068
+
1069
+ // Test explicit encryption substring search
1070
+ const explicitSubstringResult = await shell . executeLine ( `
1071
+ ecoll.findOne({$expr: { $and: [{$encStrContains: {substring:
1072
+ ce.encrypt(keyId, 'user', { ...explicitOpts, queryType: 'substringPreview' }), input: '$data'}}] }},
1073
+ { __safeContent__: 0 })
1074
+ ` ) ;
1075
+ expect ( explicitSubstringResult ) . to . have . length ( 3 ) ;
1076
+ expect ( explicitSubstringResult ) . to . include ( 'user_regular_789.pdf' ) ;
1077
+ expect ( explicitSubstringResult ) . to . include ( 'admin_user_123.txt' ) ;
1078
+ expect ( explicitSubstringResult ) . to . include ( 'explicit_user' ) ;
1079
+ } ) ;
1080
+ }
1081
+ ) ;
1082
+ } ) ;
1083
+
917
1084
context ( 'pre-6.0' , function ( ) {
918
1085
skipIfServerVersion ( testServer , '>= 6.0' ) ; // FLE2 available on 6.0+
919
1086
0 commit comments