@@ -914,6 +914,173 @@ describe('FLE tests', function () {
914914 } ) ;
915915 } ) ;
916916
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+
9171084 context ( 'pre-6.0' , function ( ) {
9181085 skipIfServerVersion ( testServer , '>= 6.0' ) ; // FLE2 available on 6.0+
9191086
0 commit comments