This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-kamelets.git
The following commit(s) were added to refs/heads/main by this push:
new 838ad9185 Fix #542: add a Citrus integration test for couchbase-source
(#3008)
838ad9185 is described below
commit 838ad91855084de9ad65be0d0f25a102bfc62e7e
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Sep 7 11:37:10 2026 +0200
Fix #542: add a Citrus integration test for couchbase-source (#3008)
Follow-up to #3007, which shipped the Kamelet without a test because there
was no Couchbase container in the toolchain. There is one -- it just needs
setting up by hand.
Citrus 5.0.0 has no built-in Couchbase container type, so this uses the
generic container with couchbase/server:community-7.6.2. Three details were
not optional:
- Ports are bound one-to-one. The Couchbase SDK bootstraps from the
management port and then reconnects using the addresses the cluster
advertises, so a remapped host port leaves the client unable to reach the
data service.
- The wait strategy needs a full-line regex. Testcontainers matches log
wait patterns against the whole line, so "Starting Couchbase Server" never
matches and the container times out after five minutes.
- initCluster.groovy avoids Groovy GStrings entirely. Citrus resolves
dollar-brace expressions in these scripts as its own test variables, so a
GString fails with "Unknown variable".
The script initialises the cluster, creates a bucket, adds a primary index
and seeds one document over plain HTTP, so the test module needs no
Couchbase SDK on its classpath. It polls for the management port itself
because the container logs its startup line well before that port serves
requests.
The statement in application.properties is
"SELECT META().id AS __id, * FROM _default" rather than the more obvious
"SELECT * FROM <bucket>". Both parts matter: CouchbaseConsumer skips any
row without the __id alias, and it runs the query in the bucket scope, so
the keyspace is the collection.
Passes locally in about 35 seconds:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in CouchbaseIT
SUCCESS (38088ms) couchbase-source-route-test
Claude-Session: https://claude.ai/code/session_01Tz352mt7yzGWLfvYTBo2Dk
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../src/test/java/CouchbaseIT.java | 33 ++++++++
.../resources/couchbase/application.properties | 24 ++++++
.../couchbase-source-route.citrus.it.yaml | 60 ++++++++++++++
.../couchbase/couchbase-source-route.yaml | 32 ++++++++
.../test/resources/couchbase/initCluster.groovy | 96 ++++++++++++++++++++++
5 files changed, 245 insertions(+)
diff --git a/tests/camel-kamelets-itest/src/test/java/CouchbaseIT.java
b/tests/camel-kamelets-itest/src/test/java/CouchbaseIT.java
new file mode 100644
index 000000000..101dc584c
--- /dev/null
+++ b/tests/camel-kamelets-itest/src/test/java/CouchbaseIT.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+import java.util.stream.Stream;
+
+import org.citrusframework.api.common.TestLoader;
+import org.citrusframework.junit.jupiter.CitrusSupport;
+import org.citrusframework.junit.jupiter.CitrusTestFactory;
+import org.citrusframework.junit.jupiter.CitrusTestFactorySupport;
+import org.junit.jupiter.api.DynamicTest;
+
+@CitrusSupport
+public class CouchbaseIT {
+
+ @CitrusTestFactory
+ public Stream<DynamicTest> couchbase() {
+ return
CitrusTestFactorySupport.factory(TestLoader.YAML).packageScan("couchbase");
+ }
+}
diff --git
a/tests/camel-kamelets-itest/src/test/resources/couchbase/application.properties
b/tests/camel-kamelets-itest/src/test/resources/couchbase/application.properties
new file mode 100644
index 000000000..3d426efcb
--- /dev/null
+++
b/tests/camel-kamelets-itest/src/test/resources/couchbase/application.properties
@@ -0,0 +1,24 @@
+# ---------------------------------------------------------------------------
+# 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.
+# ---------------------------------------------------------------------------
+
+couchbase.protocol=http
+couchbase.hostname=localhost
+couchbase.port=8091
+couchbase.bucket=kamelets
+couchbase.username=Administrator
+couchbase.password=password
+couchbase.statement=SELECT META().id AS __id, * FROM _default
diff --git
a/tests/camel-kamelets-itest/src/test/resources/couchbase/couchbase-source-route.citrus.it.yaml
b/tests/camel-kamelets-itest/src/test/resources/couchbase/couchbase-source-route.citrus.it.yaml
new file mode 100644
index 000000000..ccf378dc7
--- /dev/null
+++
b/tests/camel-kamelets-itest/src/test/resources/couchbase/couchbase-source-route.citrus.it.yaml
@@ -0,0 +1,60 @@
+# ---------------------------------------------------------------------------
+# 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.
+# ---------------------------------------------------------------------------
+
+name: couchbase-source-route-test
+actions:
+ # Create Couchbase infrastructure.
+ #
+ # Citrus has no built-in Couchbase container type, so this uses the generic
+ # one. The ports are bound one-to-one on purpose: the Couchbase SDK
bootstraps
+ # from the management port and then reconnects using the addresses the
cluster
+ # advertises, so a remapped host port makes the client unable to reach the
+ # data service.
+ - testcontainers:
+ start:
+ container:
+ name: "couchbase"
+ image: "couchbase/server:community-7.6.2"
+ startUpTimeout: 300
+ portBindings:
+ - "8091:8091"
+ - "8093:8093"
+ - "11210:11210"
+ waitFor:
+ logMessage: ".*Starting Couchbase Server.*"
+
+ # Initialise the cluster, create the bucket and seed one document
+ - groovy:
+ script:
+ file: "couchbase/initCluster.groovy"
+
+ # Create Camel JBang integration (source)
+ - camel:
+ cli:
+ run:
+ waitForRunningState: false
+ integration:
+ file: "couchbase/couchbase-source-route.yaml"
+ systemProperties:
+ file: "couchbase/application.properties"
+
+ # Verify the Couchbase source picked up the seeded document
+ - camel:
+ cli:
+ verify:
+ integration: "couchbase-source-route"
+ logMessage: "hello-from-couchbase"
diff --git
a/tests/camel-kamelets-itest/src/test/resources/couchbase/couchbase-source-route.yaml
b/tests/camel-kamelets-itest/src/test/resources/couchbase/couchbase-source-route.yaml
new file mode 100644
index 000000000..129232091
--- /dev/null
+++
b/tests/camel-kamelets-itest/src/test/resources/couchbase/couchbase-source-route.yaml
@@ -0,0 +1,32 @@
+# ---------------------------------------------------------------------------
+# 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.
+# ---------------------------------------------------------------------------
+
+- route:
+ from:
+ uri: "kamelet:couchbase-source"
+ parameters:
+ protocol: "{{couchbase.protocol}}"
+ couchbaseHostname: "{{couchbase.hostname}}"
+ couchbasePort: "{{couchbase.port}}"
+ bucket: "{{couchbase.bucket}}"
+ username: "{{couchbase.username}}"
+ password: "{{couchbase.password}}"
+ statement: "{{couchbase.statement}}"
+ delay: 2000
+ steps:
+ - to:
+ uri: "kamelet:log-sink"
diff --git
a/tests/camel-kamelets-itest/src/test/resources/couchbase/initCluster.groovy
b/tests/camel-kamelets-itest/src/test/resources/couchbase/initCluster.groovy
new file mode 100644
index 000000000..2ec58c195
--- /dev/null
+++ b/tests/camel-kamelets-itest/src/test/resources/couchbase/initCluster.groovy
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+// Brings a freshly started Couchbase container to the point where the
+// couchbase-source Kamelet can poll it: initialise the cluster, create a
+// bucket, add a primary index and seed one document.
+//
+// Two deliberate constraints:
+// - plain HTTP rather than the Couchbase SDK, so the test module does not
need
+// the SDK on its classpath
+// - string concatenation rather than Groovy GStrings, because Citrus resolves
+// dollar-brace expressions in these scripts as its own test variables
+
+def admin = 'http://localhost:8091'
+def query = 'http://localhost:8093/query/service'
+def user = 'Administrator'
+def pass = 'password'
+def bucket = 'kamelets'
+def basic = 'Basic ' + (user + ':' + pass).bytes.encodeBase64().toString()
+
+def call = { String url, String body, boolean auth ->
+ def conn = new URL(url).openConnection()
+ conn.requestMethod = 'POST'
+ conn.doOutput = true
+ conn.connectTimeout = 10000
+ conn.readTimeout = 60000
+ conn.setRequestProperty('Content-Type',
'application/x-www-form-urlencoded')
+ if (auth) {
+ conn.setRequestProperty('Authorization', basic)
+ }
+ conn.outputStream.withWriter { it << body }
+ def code = conn.responseCode
+ if (code >= 400) {
+ throw new IllegalStateException('POST ' + url + ' failed with ' + code)
+ }
+ return code
+}
+
+// The container logs "Starting Couchbase Server" well before the management
+// port serves requests, so wait for readiness here rather than in the
container
+// wait strategy.
+def ready = false
+for (int i = 0; i < 60 && !ready; i++) {
+ try {
+ def conn = new URL(admin + '/pools').openConnection()
+ conn.connectTimeout = 2000
+ conn.readTimeout = 2000
+ ready = conn.responseCode == 200
+ } catch (Exception ignored) {
+ // not up yet
+ }
+ if (!ready) {
+ sleep(2000)
+ }
+}
+if (!ready) {
+ throw new IllegalStateException('Couchbase management port did not become
available at ' + admin)
+}
+
+call(admin + '/pools/default', 'memoryQuota=512&indexMemoryQuota=512', false)
+call(admin + '/node/controller/setupServices', 'services=kv%2Cn1ql%2Cindex',
false)
+call(admin + '/settings/web', 'port=SAME&username=' + user + '&password=' +
pass, false)
+call(admin + '/settings/indexes', 'storageMode=forestdb', true)
+call(admin + '/pools/default/buckets', 'name=' + bucket +
'&ramQuota=128&bucketType=couchbase', true)
+
+// The bucket is created asynchronously and the query service only sees the
+// keyspace once it has warmed up, so retry the index creation until it lands.
+def indexed = false
+for (int i = 0; i < 30 && !indexed; i++) {
+ try {
+ call(query, 'statement=' + URLEncoder.encode('CREATE PRIMARY INDEX ON
`' + bucket + '`', 'UTF-8'), true)
+ indexed = true
+ } catch (Exception ignored) {
+ sleep(2000)
+ }
+}
+if (!indexed) {
+ throw new IllegalStateException('Could not create the primary index on ' +
bucket)
+}
+
+def insert = 'INSERT INTO `' + bucket + '` (KEY, VALUE) VALUES ("doc1",
{"message":"hello-from-couchbase"})'
+call(query, 'statement=' + URLEncoder.encode(insert, 'UTF-8'), true)