66from datetime import datetime
77from unittest .mock import Mock , PropertyMock
88
9+ import pytest
10+
11+ from airbyte_cdk import AirbyteTracedException , FailureType
912from airbyte_cdk .sources .file_based .availability_strategy .default_file_based_availability_strategy import (
1013 DefaultFileBasedAvailabilityStrategy ,
1114)
@@ -61,7 +64,9 @@ def test_given_file_extension_does_not_match_when_check_availability_and_parsabi
6164
6265 def test_not_available_given_no_files (self ) -> None :
6366 """
64- If no files are returned, then the stream is not available.
67+ If no files are returned, then the stream is not available, and the
68+ reason is the actionable `EMPTY_STREAM` message rather than a Python
69+ traceback.
6570 """
6671 self ._stream .get_files .return_value = []
6772
@@ -71,6 +76,62 @@ def test_not_available_given_no_files(self) -> None:
7176
7277 assert not is_available
7378 assert "No files were identified in the stream" in reason
79+ assert "Traceback" not in reason
80+ assert "raise CheckAvailabilityError" not in reason
81+
82+ def test_check_availability_returns_actionable_reason_when_no_files (self ) -> None :
83+ """
84+ `check_availability` (used by file-transfer / permissions-transfer modes)
85+ must also return the actionable reason rather than a traceback string.
86+ """
87+ self ._stream .get_files .return_value = []
88+
89+ is_available , reason = self ._strategy .check_availability (self ._stream , Mock (), Mock ())
90+
91+ assert not is_available
92+ assert "No files were identified in the stream" in reason
93+ assert "Traceback" not in reason
94+ assert "raise CheckAvailabilityError" not in reason
95+
96+ def test_airbyte_traced_exception_from_stream_reader_propagates (self ) -> None :
97+ """
98+ When the underlying stream reader raises an `AirbyteTracedException`
99+ (e.g. invalid credentials), the actionable message must propagate
100+ unchanged instead of being wrapped in a generic `ERROR_LISTING_FILES`
101+ reason.
102+ """
103+ self ._stream .get_files .side_effect = AirbyteTracedException (
104+ internal_message = "raw provider error" ,
105+ message = "Could not authenticate with Google drive. Please check your credentials." ,
106+ failure_type = FailureType .config_error ,
107+ )
108+
109+ with pytest .raises (AirbyteTracedException ) as exc_info :
110+ self ._strategy .check_availability_and_parsability (self ._stream , Mock (), Mock ())
111+
112+ assert (
113+ exc_info .value .message
114+ == "Could not authenticate with Google drive. Please check your credentials."
115+ )
116+ assert exc_info .value .failure_type == FailureType .config_error
117+
118+ def test_airbyte_traced_exception_propagates_in_check_availability (self ) -> None :
119+ """
120+ Same propagation guarantee as above for `check_availability`.
121+ """
122+ self ._stream .get_files .side_effect = AirbyteTracedException (
123+ internal_message = "raw provider error" ,
124+ message = "Could not authenticate with Google drive. Please check your credentials." ,
125+ failure_type = FailureType .config_error ,
126+ )
127+
128+ with pytest .raises (AirbyteTracedException ) as exc_info :
129+ self ._strategy .check_availability (self ._stream , Mock (), Mock ())
130+
131+ assert (
132+ exc_info .value .message
133+ == "Could not authenticate with Google drive. Please check your credentials."
134+ )
74135
75136 def test_parse_records_is_not_called_with_parser_max_n_files_for_parsability_set (self ) -> None :
76137 """
0 commit comments