1616
1717package com .mongodb .kafka .connect ;
1818
19+ import static com .mongodb .kafka .connect .sink .MongoSinkConfig .TOPIC_OVERRIDE_CONFIG ;
20+ import static com .mongodb .kafka .connect .sink .MongoSinkTopicConfig .TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG ;
21+ import static com .mongodb .kafka .connect .sink .MongoSinkTopicConfig .TIMESERIES_GRANULARITY_CONFIG ;
22+ import static com .mongodb .kafka .connect .sink .MongoSinkTopicConfig .TIMESERIES_METAFIELD_CONFIG ;
23+ import static com .mongodb .kafka .connect .sink .MongoSinkTopicConfig .TIMESERIES_TIMEFIELD_CONFIG ;
24+ import static com .mongodb .kafka .connect .util .MongoClientHelper .isAtleastFiveDotZero ;
1925import static java .lang .String .format ;
2026import static java .util .Arrays .asList ;
2127import static java .util .Collections .emptyList ;
2228import static java .util .Collections .singletonList ;
2329import static org .junit .jupiter .api .Assertions .assertFalse ;
2430import static org .junit .jupiter .api .Assertions .assertTrue ;
31+ import static org .junit .jupiter .api .Assumptions .assumeFalse ;
2532import static org .junit .jupiter .api .Assumptions .assumeTrue ;
2633
2734import java .util .ArrayList ;
4249import org .bson .Document ;
4350
4451import com .mongodb .ConnectionString ;
52+ import com .mongodb .MongoCredential ;
4553import com .mongodb .client .MongoClient ;
4654import com .mongodb .client .MongoClients ;
4755import com .mongodb .client .MongoDatabase ;
@@ -199,6 +207,116 @@ void testSinkConfigValidationCollectionBasedPrivileges() {
199207 assertValidSink (properties , MongoSinkConfig .CONNECTION_URI_CONFIG );
200208 }
201209
210+ @ Test
211+ @ DisplayName ("Ensure sink timeseries validation works as expected" )
212+ void testSinkConfigValidationTimeseries () {
213+ assumeTrue (isAtleastFiveDotZero (getMongoClient ()));
214+
215+ // Missing timefield
216+ Map <String , String > properties = createSinkProperties ();
217+ properties .put (TIMESERIES_GRANULARITY_CONFIG , "hours" );
218+ assertInvalidSink (properties , TIMESERIES_GRANULARITY_CONFIG );
219+
220+ properties .put (TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG , "1" );
221+ assertInvalidSink (properties , TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG );
222+
223+ properties .put (TIMESERIES_METAFIELD_CONFIG , "meta" );
224+ assertInvalidSink (properties , TIMESERIES_METAFIELD_CONFIG );
225+
226+ properties .put (TIMESERIES_TIMEFIELD_CONFIG , "ts" );
227+ assertValidSink (properties );
228+
229+ // Confirm collection created
230+ assertTrue (collectionExists ());
231+
232+ // Create normal collection confirm invalid.
233+ dropDatabases ();
234+ getMongoClient ().getDatabase (DEFAULT_DATABASE_NAME ).createCollection ("test" );
235+ assertInvalidSink (properties , TIMESERIES_TIMEFIELD_CONFIG );
236+ }
237+
238+ @ Test
239+ @ DisplayName ("Ensure sink timeseries validation works as expected when using regex config" )
240+ void testSinkConfigValidationTimeseriesRegex () {
241+ assumeTrue (isAtleastFiveDotZero (getMongoClient ()));
242+
243+ // Missing timefield
244+ Map <String , String > properties = createSinkRegexProperties ();
245+ properties .put (TIMESERIES_GRANULARITY_CONFIG , "hours" );
246+ assertInvalidSink (properties );
247+ assertInvalidSink (properties , TIMESERIES_GRANULARITY_CONFIG );
248+
249+ properties .put (TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG , "1" );
250+ assertInvalidSink (properties , TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG );
251+
252+ properties .put (TIMESERIES_METAFIELD_CONFIG , "meta" );
253+ assertInvalidSink (properties , TIMESERIES_METAFIELD_CONFIG );
254+
255+ properties .put (TIMESERIES_TIMEFIELD_CONFIG , "ts" );
256+ assertValidSink (properties );
257+
258+ // Confirm no collection created
259+ assertFalse (collectionExists ());
260+ }
261+
262+ @ Test
263+ @ DisplayName (
264+ "Ensure sink timeseries validation works as expected when using regex config with overrides" )
265+ void testSinkConfigValidationTimeseriesRegexWithOverrides () {
266+ assumeTrue (isAtleastFiveDotZero (getMongoClient ()));
267+
268+ Map <String , String > properties = createSinkRegexProperties ();
269+ properties .put (MongoSinkTopicConfig .COLLECTION_CONFIG , "test" );
270+ properties .put (
271+ format (TOPIC_OVERRIDE_CONFIG , "topic-test" , TIMESERIES_GRANULARITY_CONFIG ), "hours" );
272+ assertInvalidSink (properties , TIMESERIES_GRANULARITY_CONFIG );
273+
274+ properties .put (
275+ format (TOPIC_OVERRIDE_CONFIG , "topic-test" , TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG ), "1" );
276+ assertInvalidSink (properties , TIMESERIES_EXPIRE_AFTER_SECONDS_CONFIG );
277+
278+ properties .put (
279+ format (TOPIC_OVERRIDE_CONFIG , "topic-test" , TIMESERIES_METAFIELD_CONFIG ), "meta" );
280+ assertInvalidSink (properties , TIMESERIES_METAFIELD_CONFIG );
281+
282+ properties .put (format (TOPIC_OVERRIDE_CONFIG , "topic-test" , TIMESERIES_TIMEFIELD_CONFIG ), "ts" );
283+ assertValidSink (properties );
284+
285+ // Confirm collection created thanks to override name
286+ assertTrue (collectionExists ());
287+ }
288+
289+ @ Test
290+ @ DisplayName ("Ensure sink validation when timeseries not supported" )
291+ void testSinkConfigValidationTimeseriesNotSupported () {
292+ assumeFalse (isAtleastFiveDotZero (getMongoClient ()));
293+
294+ Map <String , String > properties = createSinkProperties ();
295+ properties .put (TIMESERIES_TIMEFIELD_CONFIG , "ts" );
296+ assertInvalidSink (properties , TIMESERIES_TIMEFIELD_CONFIG );
297+ }
298+
299+ @ Test
300+ @ DisplayName ("Ensure sink validation timeseries auth permissions" )
301+ void testSinkConfigAuthValidationTimeseries () {
302+ assumeTrue (isAuthEnabled ());
303+ assumeTrue (isAtleastFiveDotZero (getMongoClient ()));
304+
305+ Map <String , String > properties = createSinkProperties (getConnectionStringForCustomUser ());
306+ properties .put (MongoSinkTopicConfig .TIMESERIES_TIMEFIELD_CONFIG , "ts" );
307+
308+ // Missing permissions
309+ createUserFromDocument (format ("{ role: 'read', db: '%s'}" , getDatabaseName ()));
310+ assertInvalidSink (properties );
311+
312+ // Add permissions
313+ dropUserAndRoles ();
314+ createUserFromDocument (format ("{ role: 'readWrite', db: '%s'}" , getDatabaseName ()));
315+
316+ assertValidSink (properties );
317+ assertTrue (collectionExists ());
318+ }
319+
202320 @ Test
203321 @ DisplayName (
204322 "Ensure sink validation passes with specific collection based privileges with a different auth db" )
@@ -385,7 +503,7 @@ private boolean collectionExists(final String databaseName, final String collect
385503 }
386504
387505 private void createUser (final String role ) {
388- createUser (getConnectionString (). getCredential (). getSource (), role );
506+ createUser (getAuthSource (), role );
389507 }
390508
391509 private void createUser (final String databaseName , final String role ) {
@@ -409,7 +527,7 @@ private void createUserFromDocument(final String role) {
409527
410528 private void createUserFromDocument (final List <String > roles ) {
411529 getMongoClient ()
412- .getDatabase (getConnectionString (). getCredential (). getSource ())
530+ .getDatabase (getAuthSource ())
413531 .runCommand (
414532 Document .parse (
415533 format (
@@ -422,7 +540,7 @@ private void createUserWithCustomRole(final List<String> privileges) {
422540 }
423541
424542 private void createUserWithCustomRole (final List <String > privileges , final List <String > roles ) {
425- createUserWithCustomRole (getConnectionString (). getCredential (). getSource (), privileges , roles );
543+ createUserWithCustomRole (getAuthSource (), privileges , roles );
426544 }
427545
428546 private void createUserWithCustomRole (
@@ -441,7 +559,7 @@ private void dropUserAndRoles() {
441559 if (isAuthEnabled ()) {
442560 List <MongoDatabase > databases =
443561 asList (
444- getMongoClient ().getDatabase (getConnectionString (). getCredential (). getSource ()),
562+ getMongoClient ().getDatabase (getAuthSource ()),
445563 getMongoClient ().getDatabase (CUSTOM_DATABASE ));
446564
447565 for (final MongoDatabase database : databases ) {
@@ -475,7 +593,7 @@ private MongoClient getMongoClient() {
475593 }
476594
477595 private String getConnectionStringForCustomUser () {
478- return getConnectionStringForCustomUser (getConnectionString (). getCredential (). getSource ());
596+ return getConnectionStringForCustomUser (getAuthSource ());
479597 }
480598
481599 private String getConnectionStringForCustomUser (final String authSource ) {
@@ -486,8 +604,7 @@ private String getConnectionStringForCustomUser(final String authSource) {
486604 format ("%s%s:%s@%s" , scheme , CUSTOM_USER , CUSTOM_PASSWORD , hostsAndQuery );
487605 userConnectionString =
488606 userConnectionString .replace (
489- format ("authSource=%s" , getConnectionString ().getCredential ().getSource ()),
490- format ("authSource=%s" , authSource ));
607+ format ("authSource=%s" , getAuthSource ()), format ("authSource=%s" , authSource ));
491608
492609 if (!userConnectionString .contains ("authSource" )) {
493610 String separator = userConnectionString .contains ("/?" ) ? "&" : "?" ;
@@ -501,6 +618,12 @@ private boolean isAuthEnabled() {
501618 return getConnectionString ().getCredential () != null ;
502619 }
503620
621+ private String getAuthSource () {
622+ return Optional .ofNullable (getConnectionString ().getCredential ())
623+ .map (MongoCredential ::getSource )
624+ .orElseThrow (() -> new AssertionError ("No auth credential" ));
625+ }
626+
504627 private boolean isReplicaSetOrSharded () {
505628 try (MongoClient mongoClient = MongoClients .create (getConnectionString ())) {
506629 Document isMaster =
0 commit comments