diff --git a/src/motherduck_destination_server.cpp b/src/motherduck_destination_server.cpp index 78edbb2..f86c72e 100644 --- a/src/motherduck_destination_server.cpp +++ b/src/motherduck_destination_server.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -85,12 +86,23 @@ get_encryption_key(const std::string &filename, return encryption_key_it->second; } +void validate_file(const std::string &file_path) { + std::ifstream fs(file_path.c_str()); + if (fs.good()) { + fs.close(); + return; + } + throw std::invalid_argument("File <" + file_path + + "> is missing or inaccessible"); +} + void process_file( duckdb::Connection &con, const std::string &filename, const std::string &decryption_key, std::vector &utf8_columns, const std::string &null_value, const std::function &process_view) { + validate_file(filename); auto table = decryption_key.empty() ? read_unencrypted_csv(filename, utf8_columns, null_value) : read_encrypted_csv(filename, decryption_key, utf8_columns, diff --git a/test/integration/test_server.cpp b/test/integration/test_server.cpp index 351029e..3930270 100644 --- a/test/integration/test_server.cpp +++ b/test/integration/test_server.cpp @@ -24,16 +24,16 @@ bool NO_FAIL(const grpc::Status &status) { return status.ok(); } -bool IS_FAIL(const grpc::Status &status, const std::string &expected_error) { - if (!status.ok() && status.error_message() != expected_error) { - fprintf(stderr, "Query failed with unexpected message: %s\n", - status.error_message().c_str()); +bool REQUIRE_FAIL(const grpc::Status &status, + const std::string &expected_error) { + if (!status.ok()) { + REQUIRE(status.error_message() == expected_error); + return true; } - return !status.ok(); + return false; } + #define REQUIRE_NO_FAIL(result) REQUIRE(NO_FAIL((result))) -#define REQUIRE_FAIL(result, expected_error) \ - REQUIRE(IS_FAIL(result, expected_error)) TEST_CASE("ConfigurationForm", "[integration]") { DestinationSdkImpl service; @@ -817,4 +817,29 @@ TEST_CASE("Truncate fails if synced_column is missing") { auto status = service.Truncate(nullptr, &request, &response); REQUIRE_FAIL(status, "Synced column is required"); +} + +TEST_CASE("reading inaccessible or nonexistent files fails") { + DestinationSdkImpl service; + + const std::string bad_file_name = TEST_RESOURCES_DIR + "nonexistent.csv"; + ::fivetran_sdk::WriteBatchRequest request; + + auto token = std::getenv("motherduck_token"); + REQUIRE(token); + (*request.mutable_configuration())["motherduck_token"] = token; + (*request.mutable_configuration())["motherduck_database"] = "fivetran_test"; + request.mutable_csv()->set_encryption(::fivetran_sdk::Encryption::AES); + request.mutable_csv()->set_compression(::fivetran_sdk::Compression::ZSTD); + define_test_table(request, "unused_table"); + + request.add_replace_files(bad_file_name); + (*request.mutable_keys())[bad_file_name] = "whatever"; + + ::fivetran_sdk::WriteBatchResponse response; + auto status = service.WriteBatch(nullptr, &request, &response); + const auto expected = + "WriteBatch endpoint failed for schema <>, table :File <" + + bad_file_name + "> is missing or inaccessible"; + REQUIRE_FAIL(status, expected); } \ No newline at end of file