This is an automated email from the ASF dual-hosted git repository.

david-streamlio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar-connectors.git


The following commit(s) were added to refs/heads/master by this push:
     new 16bb0fdc [improve][test] Add Debezium MongoDB source integration test 
(#58)
16bb0fdc is described below

commit 16bb0fdc332086e5c9437a6cf3c0c3f3fd88f6bb
Author: David Kjerrumgaard <[email protected]>
AuthorDate: Thu Jul 9 14:29:28 2026 -0700

    [improve][test] Add Debezium MongoDB source integration test (#58)
    
    * [improve][test] Add Debezium MongoDB source integration test
    
    The debezium/mongodb module had no tests. Add a Testcontainers-based
    integration test following the existing DebeziumMysqlSourceTest
    pattern: a single-node MongoDB replica set container plus a Pulsar
    container for Debezium offset storage, seeded with two documents, and
    an assertion that the initial snapshot produces CDC records.
    
    * [improve][test] Bound record reads so an under-delivering connector 
cannot hang CI
    
    AbstractKafkaConnectSource.read() loops until a record is available and
    never returns null. Calling it on the test thread inside an Awaitility
    untilAsserted block meant a connector that produced fewer records than
    expected blocked forever: Awaitility cannot interrupt a blocked
    assertion, so its atMost(60s) never fired and the CI job ran until its
    own 45-minute timeout.
    
    Read each record on a separate thread with a per-record deadline, and
    add a test-level timeOut as a backstop. Under-delivery now fails in
    seconds with a message pointing at the connector, instead of hanging.
---
 debezium/mongodb/build.gradle.kts                  |   5 +
 .../mongodb/DebeziumMongoDbSourceTest.java         | 168 +++++++++++++++++++++
 2 files changed, 173 insertions(+)

diff --git a/debezium/mongodb/build.gradle.kts 
b/debezium/mongodb/build.gradle.kts
index d4292a98..975fdb60 100644
--- a/debezium/mongodb/build.gradle.kts
+++ b/debezium/mongodb/build.gradle.kts
@@ -25,4 +25,9 @@ dependencies {
     implementation(libs.pulsar.io.core)
     implementation(project(":debezium:pulsar-io-debezium-core"))
     implementation(libs.debezium.connector.mongodb)
+
+    testImplementation(libs.testcontainers.mongodb)
+    testImplementation(libs.testcontainers.pulsar)
+    testImplementation(libs.pulsar.client)
+    testImplementation(libs.mongodb.driver.sync)
 }
diff --git 
a/debezium/mongodb/src/test/java/org/apache/pulsar/io/debezium/mongodb/DebeziumMongoDbSourceTest.java
 
b/debezium/mongodb/src/test/java/org/apache/pulsar/io/debezium/mongodb/DebeziumMongoDbSourceTest.java
new file mode 100644
index 00000000..fb6e0b6e
--- /dev/null
+++ 
b/debezium/mongodb/src/test/java/org/apache/pulsar/io/debezium/mongodb/DebeziumMongoDbSourceTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.io.debezium.mongodb;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoCollection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pulsar.client.api.PulsarClient;
+import org.apache.pulsar.common.schema.KeyValue;
+import org.apache.pulsar.functions.api.Record;
+import org.apache.pulsar.io.core.SourceContext;
+import org.bson.Document;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.containers.PulsarContainer;
+import org.testcontainers.utility.DockerImageName;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+@Slf4j
+public class DebeziumMongoDbSourceTest {
+
+    private static final String PULSAR_IMAGE =
+            System.getenv().getOrDefault("PULSAR_TEST_IMAGE", 
"apachepulsar/pulsar:4.1.3");
+
+    /** Documents seeded before open(), each of which the initial snapshot 
must emit. */
+    private static final int EXPECTED_RECORDS = 2;
+
+    private static final int READ_TIMEOUT_SECONDS = 120;
+
+    private MongoDBContainer mongoContainer;
+    private PulsarContainer pulsarContainer;
+    private PulsarClient pulsarClient;
+    private DebeziumMongoDbSource source;
+    private ExecutorService readerExecutor;
+
+    @BeforeMethod
+    public void setup() throws Exception {
+        readerExecutor = Executors.newSingleThreadExecutor();
+        // MongoDBContainer starts a single-node replica set, which Debezium 
requires
+        // for change streams.
+        mongoContainer = new 
MongoDBContainer(DockerImageName.parse("mongo:7.0"));
+        mongoContainer.start();
+
+        pulsarContainer = new 
PulsarContainer(DockerImageName.parse(PULSAR_IMAGE));
+        pulsarContainer.start();
+
+        pulsarClient = PulsarClient.builder()
+                .serviceUrl(pulsarContainer.getPulsarBrokerUrl())
+                .build();
+
+        // Create test collection and insert initial data
+        try (MongoClient client = 
MongoClients.create(mongoContainer.getConnectionString())) {
+            MongoCollection<Document> products = 
client.getDatabase("testdb").getCollection("products");
+            products.insertOne(new Document("name", 
"widget").append("description", "A small widget"));
+            products.insertOne(new Document("name", 
"gadget").append("description", "A fancy gadget"));
+        }
+
+        source = new DebeziumMongoDbSource();
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void cleanup() throws Exception {
+        if (readerExecutor != null) {
+            // shutdownNow: a reader may still be blocked in read(), which 
never returns null
+            readerExecutor.shutdownNow();
+        }
+        if (source != null) {
+            try {
+                source.close();
+            } catch (Exception e) {
+                log.warn("Failed to close source", e);
+            }
+        }
+        if (pulsarClient != null) {
+            pulsarClient.close();
+        }
+        if (pulsarContainer != null) {
+            pulsarContainer.stop();
+        }
+        if (mongoContainer != null) {
+            mongoContainer.stop();
+        }
+    }
+
+    @Test(timeOut = 600_000)
+    public void testMongoDbCdcEvents() throws Exception {
+        String pulsarServiceUrl = pulsarContainer.getPulsarBrokerUrl();
+
+        SourceContext sourceContext = mock(SourceContext.class);
+        when(sourceContext.getPulsarClient()).thenReturn(pulsarClient);
+        when(sourceContext.getPulsarClientBuilder()).thenReturn(
+                PulsarClient.builder().serviceUrl(pulsarServiceUrl));
+        when(sourceContext.getTenant()).thenReturn("public");
+        when(sourceContext.getNamespace()).thenReturn("default");
+        
when(sourceContext.getSourceName()).thenReturn("debezium-mongodb-test");
+        when(sourceContext.getSecret(anyString())).thenReturn(null);
+
+        Map<String, Object> config = new HashMap<>();
+        config.put("mongodb.connection.string", 
mongoContainer.getConnectionString());
+        config.put("topic.prefix", "mongodb1");
+        config.put("collection.include.list", "testdb.products");
+
+        source.open(config, sourceContext);
+
+        // Debezium performs an initial snapshot of existing data.
+        // We should receive CDC records for the 2 documents we inserted.
+        int received = 0;
+        for (int i = 0; i < EXPECTED_RECORDS; i++) {
+            Record<KeyValue<byte[], byte[]>> record = readOne();
+            assertNotNull(record.getValue());
+            log.info("Received CDC record: key={}", 
record.getKey().orElse(null));
+            record.ack();
+            received++;
+        }
+        assertEquals(received, EXPECTED_RECORDS);
+    }
+
+    /**
+     * Reads a single record, failing if none arrives in time.
+     *
+     * <p>{@code AbstractKafkaConnectSource.read()} loops until a record is 
available and never
+     * returns null, so calling it on the test thread means an 
under-delivering connector hangs
+     * the test — and, since Awaitility cannot interrupt a blocked assertion, 
the whole CI job
+     * until its own timeout. Run it on a separate thread so a missing record 
surfaces as a
+     * prompt, diagnosable failure instead.
+     */
+    private Record<KeyValue<byte[], byte[]>> readOne() throws Exception {
+        Future<Record<KeyValue<byte[], byte[]>>> future = 
readerExecutor.submit(() -> source.read());
+        try {
+            return future.get(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        } catch (TimeoutException e) {
+            future.cancel(true);
+            throw new AssertionError("Timed out after " + READ_TIMEOUT_SECONDS
+                    + "s waiting for a CDC record from the initial snapshot. "
+                    + "The connector produced no record; see the Debezium logs 
above.", e);
+        }
+    }
+}

Reply via email to