Skip to content

Commit 239d443

Browse files
devin-ai-integration[bot]bot_apk
andcommitted
fix(standard-tests): validate CONNECTION_STATUS in test_docker_image_build_and_check
Co-Authored-By: bot_apk <apk@cognition.ai>
1 parent 1256a1f commit 239d443

2 files changed

Lines changed: 157 additions & 5 deletions

File tree

airbyte_cdk/test/standard_tests/docker_base.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
ConfiguredAirbyteCatalog,
2323
ConfiguredAirbyteStream,
2424
DestinationSyncMode,
25+
Status,
2526
SyncMode,
2627
)
2728
from airbyte_cdk.models.connector_metadata import MetadataFile
@@ -210,15 +211,16 @@ def test_docker_image_build_and_check(
210211
"""Run `docker_image` acceptance tests.
211212
212213
This test builds the connector image and runs the `check` command inside the container.
214+
It validates that the connector emits exactly one `CONNECTION_STATUS` message whose
215+
status matches the scenario's expected outcome (`SUCCEEDED` or `FAILED`). Scenarios
216+
whose `acceptance-test-config.yml` entry has `status: "failed"` are exercised too,
217+
so that `check` against bad configs is validated end-to-end rather than skipped.
213218
214219
Note:
215220
- It is expected for docker image caches to be reused between test runs.
216221
- In the rare case that image caches need to be cleared, please clear
217222
the local docker image cache using `docker image prune -a` command.
218223
"""
219-
if scenario.expected_outcome.expect_exception():
220-
pytest.skip("Skipping test_docker_image_build_and_check (expected to fail).")
221-
222224
tag = "dev-latest"
223225
connector_root = self.get_connector_root_dir()
224226
metadata = MetadataFile.from_file(connector_root / "metadata.yaml")
@@ -233,11 +235,14 @@ def test_docker_image_build_and_check(
233235
no_verify=False,
234236
)
235237

238+
expect_success = scenario.expected_outcome.expect_success()
239+
expect_exception = scenario.expected_outcome.expect_exception()
240+
236241
container_config_path = "/secrets/config.json"
237242
with scenario.with_temp_config_file(
238243
connector_root=connector_root,
239244
) as temp_config_file:
240-
_ = run_docker_airbyte_command(
245+
result = run_docker_airbyte_command(
241246
[
242247
"docker",
243248
"run",
@@ -249,7 +254,50 @@ def test_docker_image_build_and_check(
249254
"--config",
250255
container_config_path,
251256
],
252-
raise_if_errors=True,
257+
# Only raise on trace errors when we expect a successful check. When the
258+
# scenario expects the check to fail, the connector is supposed to exit
259+
# cleanly with a `CONNECTION_STATUS: FAILED` message rather than raising.
260+
raise_if_errors=expect_success,
261+
)
262+
263+
self._assert_check_result_matches_expected_outcome(result, scenario)
264+
265+
@staticmethod
266+
def _assert_check_result_matches_expected_outcome(
267+
result: EntrypointOutput,
268+
scenario: ConnectorTestScenario,
269+
) -> None:
270+
"""Assert that the `check` output matches the scenario's expected outcome.
271+
272+
The connector must emit exactly one `CONNECTION_STATUS` message. When the scenario
273+
expects success, the status must be `SUCCEEDED`; when it expects an exception (i.e.
274+
`acceptance-test-config.yml` declares `status: "failed"` or `status: "exception"`),
275+
the status must be `FAILED`. When the expected outcome is `ALLOW_ANY`, only the
276+
presence of a single `CONNECTION_STATUS` message is validated.
277+
"""
278+
status_messages = result.connection_status_messages
279+
assert len(status_messages) == 1, (
280+
"Expected exactly one CONNECTION_STATUS message but got "
281+
f"{len(status_messages)}:\n"
282+
+ "\n".join(str(msg) for msg in status_messages)
283+
+ result.get_formatted_error_message()
284+
)
285+
286+
connection_status = status_messages[0].connectionStatus
287+
assert connection_status is not None, (
288+
"Expected CONNECTION_STATUS message to have a connectionStatus payload. Got: \n"
289+
+ "\n".join(str(msg) for msg in status_messages)
290+
)
291+
292+
if scenario.expected_outcome.expect_success():
293+
assert connection_status.status == Status.SUCCEEDED, (
294+
"Expected CONNECTION_STATUS to be SUCCEEDED but got "
295+
f"{connection_status.status}. Message: {connection_status.message!r}"
296+
)
297+
elif scenario.expected_outcome.expect_exception():
298+
assert connection_status.status == Status.FAILED, (
299+
"Expected CONNECTION_STATUS to be FAILED but got "
300+
f"{connection_status.status}. Message: {connection_status.message!r}"
253301
)
254302

255303
@pytest.mark.skipif(
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""Unit tests for `DockerConnectorTestSuite._assert_check_result_matches_expected_outcome`.
3+
4+
These tests cover the `CONNECTION_STATUS` validation added to
5+
`test_docker_image_build_and_check` (see airbyte-internal-issues#16212). They
6+
construct `EntrypointOutput` objects directly with synthetic messages rather
7+
than invoking Docker, which lets us exercise the assertion logic in isolation
8+
without requiring the Docker CLI.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import json
14+
from contextlib import AbstractContextManager, nullcontext
15+
16+
import pytest
17+
18+
from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput
19+
from airbyte_cdk.test.models import ConnectorTestScenario
20+
from airbyte_cdk.test.standard_tests.docker_base import DockerConnectorTestSuite
21+
22+
23+
def _connection_status_message(status: str, message: str | None = None) -> str:
24+
payload: dict[str, object] = {"status": status}
25+
if message is not None:
26+
payload["message"] = message
27+
return json.dumps({"type": "CONNECTION_STATUS", "connectionStatus": payload})
28+
29+
30+
_SUCCEEDED = _connection_status_message("SUCCEEDED")
31+
_FAILED = _connection_status_message("FAILED", "bad credentials")
32+
33+
34+
@pytest.mark.parametrize(
35+
("scenario_status", "messages", "expectation"),
36+
[
37+
pytest.param(
38+
"succeed",
39+
[_SUCCEEDED],
40+
nullcontext(),
41+
id="succeed_scenario_with_succeeded_status_passes",
42+
),
43+
pytest.param(
44+
"succeed",
45+
[_FAILED],
46+
pytest.raises(AssertionError, match="SUCCEEDED"),
47+
id="succeed_scenario_with_failed_status_raises__gap_1",
48+
),
49+
pytest.param(
50+
"failed",
51+
[_FAILED],
52+
nullcontext(),
53+
id="failed_scenario_with_failed_status_passes__gap_2",
54+
),
55+
pytest.param(
56+
"failed",
57+
[_SUCCEEDED],
58+
pytest.raises(AssertionError, match="FAILED"),
59+
id="failed_scenario_with_succeeded_status_raises",
60+
),
61+
pytest.param(
62+
"exception",
63+
[_FAILED],
64+
nullcontext(),
65+
id="exception_scenario_with_failed_status_passes",
66+
),
67+
pytest.param(
68+
None,
69+
[_SUCCEEDED],
70+
nullcontext(),
71+
id="no_expectation_accepts_succeeded",
72+
),
73+
pytest.param(
74+
None,
75+
[_FAILED],
76+
nullcontext(),
77+
id="no_expectation_accepts_failed",
78+
),
79+
pytest.param(
80+
"succeed",
81+
[],
82+
pytest.raises(AssertionError, match="Expected exactly one CONNECTION_STATUS"),
83+
id="missing_connection_status_message_raises",
84+
),
85+
pytest.param(
86+
"succeed",
87+
[_SUCCEEDED, _SUCCEEDED],
88+
pytest.raises(AssertionError, match="Expected exactly one CONNECTION_STATUS"),
89+
id="multiple_connection_status_messages_raises",
90+
),
91+
],
92+
)
93+
def test_assert_check_result_matches_expected_outcome(
94+
scenario_status: str | None,
95+
messages: list[str],
96+
expectation: AbstractContextManager[object],
97+
) -> None:
98+
output = EntrypointOutput(messages=messages)
99+
scenario = ConnectorTestScenario(status=scenario_status)
100+
101+
with expectation:
102+
DockerConnectorTestSuite._assert_check_result_matches_expected_outcome(
103+
output, scenario
104+
)

0 commit comments

Comments
 (0)