diff --git a/src/mongocxx/include/mongocxx/v1/exception.hpp b/src/mongocxx/include/mongocxx/v1/exception.hpp index 14e58a3f1e..3b4db5e717 100644 --- a/src/mongocxx/include/mongocxx/v1/exception.hpp +++ b/src/mongocxx/include/mongocxx/v1/exception.hpp @@ -116,6 +116,9 @@ class exception : public std::system_error { class internal; private: + MONGOCXX_ABI_NO_EXPORT /* explicit(false) */ + exception(std::error_code ec, char const* message, std::unique_ptr impl); + MONGOCXX_ABI_NO_EXPORT /* explicit(false) */ exception(std::error_code ec, std::unique_ptr impl); diff --git a/src/mongocxx/include/mongocxx/v1/server_error.hpp b/src/mongocxx/include/mongocxx/v1/server_error.hpp index 299941ef09..1e133afd9a 100644 --- a/src/mongocxx/include/mongocxx/v1/server_error.hpp +++ b/src/mongocxx/include/mongocxx/v1/server_error.hpp @@ -66,7 +66,12 @@ class server_error : public v1::exception { /// bsoncxx::v1::document::view MONGOCXX_ABI_CDECL raw() const; + class internal; + private: + MONGOCXX_ABI_NO_EXPORT /* explicit(false) */ + server_error(int code, char const* message, std::unique_ptr impl); + MONGOCXX_ABI_NO_EXPORT void key_function() const override; }; diff --git a/src/mongocxx/lib/mongocxx/v1/exception.cpp b/src/mongocxx/lib/mongocxx/v1/exception.cpp index b7c044d7c4..a142de5aab 100644 --- a/src/mongocxx/lib/mongocxx/v1/exception.cpp +++ b/src/mongocxx/lib/mongocxx/v1/exception.cpp @@ -16,17 +16,114 @@ // +#include +#include +#include +#include +#include +#include + +#include + +#include #include #include #include #include +#include #include #include +#include + namespace mongocxx { namespace v1 { +namespace { + +bool common_equivalence(v1::source_errc errc, int v, std::error_condition const& ec) noexcept { + if (ec.category() == v1::source_error_category()) { + return v != 0 ? errc == static_cast(ec.value()) : false; + } + + if (ec.category() == v1::type_error_category()) { + return v != 0 ? v1::type_errc::runtime_error == static_cast(ec.value()) : false; + } + + return false; +} + +std::error_category const& mongoc_error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "mongoc_error_code_t"; + } + + std::string message(int v) const noexcept override { + return std::string(this->name()) + ':' + std::to_string(v); + } + + bool equivalent(int v, std::error_condition const& ec) const noexcept override { + return common_equivalence(v1::source_errc::mongoc, v, ec); + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +std::error_category const& mongocrypt_error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "mongocrypt_status_t"; + } + + std::string message(int v) const noexcept override { + return std::string(this->name()) + ':' + std::to_string(v); + } + + bool equivalent(int v, std::error_condition const& ec) const noexcept override { + return common_equivalence(v1::source_errc::mongocrypt, v, ec); + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +std::error_category const& unknown_error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "unknown"; + } + + std::string message(int v) const noexcept override { + return std::string(this->name()) + ':' + std::to_string(v); + } + + bool equivalent(int v, std::error_condition const& ec) const noexcept override { + if (ec.category() == v1::source_error_category()) { + return false; + } + + if (ec.category() == v1::type_error_category()) { + return v != 0 ? v1::type_errc::runtime_error == static_cast(ec.value()) : false; + } + + return false; + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +} // namespace + std::error_category const& source_error_category() { class type final : public std::error_category { char const* name() const noexcept override { @@ -85,7 +182,22 @@ std::error_category const& type_error_category() { return instance.value(); } -class exception::impl {}; +class exception::impl { + public: + bsoncxx::v1::array::value _error_labels; +}; + +bool exception::has_error_label(bsoncxx::v1::stdx::string_view label) const { + for (auto const& e : _impl->_error_labels) { + if (e.type_view() == bsoncxx::v1::types::b_string{label}) { + return true; + } + } + return false; +} + +exception::exception(std::error_code ec, char const* message, std::unique_ptr impl) + : std::system_error{ec, message}, _impl{std::move(impl)} {} exception::exception(std::error_code ec, std::unique_ptr impl) : std::system_error{ec}, _impl{std::move(impl)} {} @@ -99,9 +211,130 @@ exception::exception(std::error_code ec, std::unique_ptr impl) : std::syst // vtable. void exception::key_function() const {} +namespace { + +v1::exception make_exception(bson_error_t const& error) { + auto const code = static_cast(error.code); + auto const raw_category = static_cast(error.reserved); + auto const message = static_cast(error.message); + auto const has_message = message[0] != '\0'; + + // Undocumented: see mongoc-error-private.h. + switch (raw_category) { + // MONGOC_ERROR_CATEGORY_BSON / BSON_ERROR_CATEGORY + // Unlikely. Convert to MONGOC_ERROR_BSON_INVALID (18). + case 1: { + std::string what; + what += "bson error code "; + what += std::to_string(code); + if (has_message) { + what += ": "; + what += message; + } + return v1::exception::internal::make(MONGOC_ERROR_BSON_INVALID, mongoc_error_category(), what.c_str()); + } + + // MONGOC_ERROR_CATEGORY + case 2: { + if (has_message) { + return v1::exception::internal::make(code, mongoc_error_category(), message); + } else { + return v1::exception::internal::make(code, mongoc_error_category()); + } + } + + // MONGOC_ERROR_CATEGORY_SERVER + // Unlikely. Throw as `v1::exception` but use the correct error category. + case 3: { + if (has_message) { + return v1::exception::internal::make(code, v1::server_error::internal::category(), message); + } else { + return v1::exception::internal::make(code, v1::server_error::internal::category()); + } + } + + // MONGOC_ERROR_CATEGORY_CRYPT + case 4: { + if (has_message) { + return v1::exception::internal::make(code, mongocrypt_error_category(), message); + } else { + return v1::exception::internal::make(code, mongocrypt_error_category()); + } + } + + // MONGOC_ERROR_CATEGORY_SASL + // Unlikely. Convert to MONGOC_ERROR_CLIENT_AUTHENTICATE (11). + case 5: { + std::string what; + what += "sasl error code "; + what += std::to_string(code); + if (has_message) { + what += ": "; + what += message; + } + return v1::exception::internal::make( + MONGOC_ERROR_CLIENT_AUTHENTICATE, mongoc_error_category(), what.c_str()); + } + + // Unlikely. + default: { + std::string what; + what += "unknown error category "; + what += std::to_string(raw_category); + if (has_message) { + what += ": "; + what += message; + } + + return v1::exception::internal::make(code, unknown_error_category(), what.c_str()); + } + } +} + +} // namespace + exception exception::internal::make(std::error_code ec) { return {ec, bsoncxx::make_unique()}; } +exception exception::internal::make(int code, std::error_category const& category, char const* message) { + return {std::error_code{code, category}, message, bsoncxx::make_unique()}; +} + +exception exception::internal::make(int code, std::error_category const& category) { + return {std::error_code{code, category}, bsoncxx::make_unique()}; +} + +void exception::internal::set_error_labels(exception& self, bsoncxx::v1::document::view v) { + auto& _error_labels = self._impl->_error_labels; + + auto const e = v["errorLabels"]; + + if (e && e.type_id() == bsoncxx::v1::types::id::k_array) { + _error_labels = e.get_array().value; + } else { + _error_labels = bsoncxx::v1::array::value{}; + } +} + +void throw_exception(bson_error_t const& error) { + throw make_exception(error); +} + +void throw_exception(bson_error_t const& error, bsoncxx::v1::document::value doc) { + // Server-side error. + if (auto const code = doc["code"]) { + if (code.type_id() == bsoncxx::v1::types::id::k_int32) { + auto const ex = make_exception(error); + throw v1::server_error::internal::make(int{code.get_int32().value}, ex.what(), std::move(doc), ex.code()); + } + } + + // Client-side error. + auto ex = make_exception(error); + exception::internal::set_error_labels(ex, doc); + throw ex; +} + } // namespace v1 } // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v1/exception.hh b/src/mongocxx/lib/mongocxx/v1/exception.hh index a71b5cba87..b63d3896a9 100644 --- a/src/mongocxx/lib/mongocxx/v1/exception.hh +++ b/src/mongocxx/lib/mongocxx/v1/exception.hh @@ -16,8 +16,13 @@ // +#include +#include + #include +#include + #include namespace mongocxx { @@ -26,7 +31,22 @@ namespace v1 { class exception::internal { public: static MONGOCXX_ABI_EXPORT_CDECL_TESTING(exception) make(std::error_code ec); + static MONGOCXX_ABI_EXPORT_CDECL_TESTING(exception) + make(int code, std::error_category const& category, char const* message); + static MONGOCXX_ABI_EXPORT_CDECL_TESTING(exception) make(int code, std::error_category const& category); + + static MONGOCXX_ABI_EXPORT_CDECL_TESTING(void) set_error_labels(exception& self, bsoncxx::v1::document::view v); }; +[[noreturn]] MONGOCXX_ABI_EXPORT_CDECL_TESTING(void) throw_exception(bson_error_t const& error); + +[[noreturn]] MONGOCXX_ABI_EXPORT_CDECL_TESTING(void) throw_exception( + bson_error_t const& error, + bsoncxx::v1::document::value raw); + +[[noreturn]] inline void throw_exception(bson_error_t const& error, bsoncxx::v1::document::view raw) { + throw_exception(error, bsoncxx::v1::document::value{raw}); +} + } // namespace v1 } // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v1/server_error.cpp b/src/mongocxx/lib/mongocxx/v1/server_error.cpp index 5c46f15610..3873174d37 100644 --- a/src/mongocxx/lib/mongocxx/v1/server_error.cpp +++ b/src/mongocxx/lib/mongocxx/v1/server_error.cpp @@ -12,14 +12,108 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include +#include + +// + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include namespace mongocxx { namespace v1 { -class server_error::impl {}; +namespace { + +std::error_category const& server_error_category() { + class type final : public std::error_category { + char const* name() const noexcept override { + return "server"; + } + + std::string message(int v) const noexcept override { + return "server error code " + std::to_string(v); + } + + bool equivalent(int v, std::error_condition const& ec) const noexcept override { + if (ec.category() == v1::source_error_category()) { + using condition = v1::source_errc; + + auto const source = static_cast(ec.value()); + + return v != 0 ? source == condition::server : false; + } + + if (ec.category() == v1::type_error_category()) { + using condition = v1::type_errc; + + auto const type = static_cast(ec.value()); + + return v != 0 ? type == condition::runtime_error : false; + } + + return false; + } + }; + + static bsoncxx::immortal const instance; + + return instance.value(); +} + +} // namespace + +class server_error::impl { + public: + bsoncxx::v1::document::value _raw; + std::error_code _client_code; + + explicit impl(bsoncxx::v1::document::value raw) : _raw{std::move(raw)} {} + + impl(bsoncxx::v1::document::value raw, std::error_code client_code) + : _raw{std::move(raw)}, _client_code{client_code} {} +}; void server_error::key_function() const {} +std::error_code server_error::client_code() const { + return _impl->_client_code; +} + +bsoncxx::v1::document::view server_error::raw() const { + return _impl->_raw; +} + +server_error::server_error(int code, char const* message, std::unique_ptr impl) + : v1::exception{v1::exception::internal::make(code, server_error_category(), message)}, _impl{std::move(impl)} { + v1::exception::internal::set_error_labels(*this, _impl->_raw); +} + +server_error server_error::internal::make(int code, char const* message, bsoncxx::v1::document::value raw) { + return {code, message, bsoncxx::make_unique(std::move(raw))}; +} + +server_error server_error::internal::make( + int code, + char const* message, + bsoncxx::v1::document::value raw, + std::error_code client_code) { + return {code, message, bsoncxx::make_unique(std::move(raw), client_code)}; +} + +std::error_category const& server_error::internal::category() { + return v1::server_error_category(); +} + } // namespace v1 } // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v1/server_error.hh b/src/mongocxx/lib/mongocxx/v1/server_error.hh new file mode 100644 index 0000000000..91854f0672 --- /dev/null +++ b/src/mongocxx/lib/mongocxx/v1/server_error.hh @@ -0,0 +1,42 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed 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. + +#pragma once + +#include // IWYU pragma: export + +// + +#include + +#include + +#include + +namespace mongocxx { +namespace v1 { + +class server_error::internal { + public: + static MONGOCXX_ABI_EXPORT_CDECL_TESTING(server_error) + make(int code, char const* message, bsoncxx::v1::document::value raw); + + static MONGOCXX_ABI_EXPORT_CDECL_TESTING(server_error) + make(int code, char const* message, bsoncxx::v1::document::value raw, std::error_code client_code); + + static MONGOCXX_ABI_EXPORT_CDECL_TESTING(std::error_category const&) category(); +}; + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/mongoc_error.hh b/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/mongoc_error.hh index b2c4bf4088..88d4f0646f 100644 --- a/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/mongoc_error.hh +++ b/src/mongocxx/lib/mongocxx/v_noabi/mongocxx/mongoc_error.hh @@ -14,12 +14,19 @@ #pragma once +#include + +#include +#include +#include + #include +#include #include #include -#include +#include namespace mongocxx { namespace v_noabi { @@ -29,8 +36,8 @@ inline std::error_code make_error_code(int code, int) { return {code, server_error_category()}; } -inline std::error_code make_error_code(::bson_error_t const& error) { - return make_error_code(static_cast(error.code), static_cast(error.domain)); +inline std::error_code make_error_code(bson_error_t const& error) { + return v_noabi::make_error_code(static_cast(error.code), static_cast(error.domain)); } inline void set_bson_error_message(bson_error_t* error, char const* msg) { @@ -39,7 +46,7 @@ inline void set_bson_error_message(bson_error_t* error, char const* msg) { inline void make_bson_error(bson_error_t* error, operation_exception const& e) { // No way to get the domain back out of the exception, so zero out. - error->code = static_cast(e.code().value()); + error->code = static_cast(e.code().value()); error->domain = 0; set_bson_error_message(error, e.what()); } @@ -52,13 +59,23 @@ inline void make_generic_bson_error(bson_error_t* error) { } template -[[noreturn]] void throw_exception(::bson_error_t const& error) { - throw exception_type{make_error_code(error), error.message}; +[[noreturn]] void throw_exception(bson_error_t const& error) { + throw exception_type{v_noabi::make_error_code(error), error.message}; +} + +template +[[noreturn]] void throw_exception(bsoncxx::v_noabi::document::value raw_server_error, bson_error_t const& error) { + throw exception_type{v_noabi::make_error_code(error), std::move(raw_server_error), error.message}; } template -[[noreturn]] void throw_exception(bsoncxx::v_noabi::document::value raw_server_error, ::bson_error_t const& error) { - throw exception_type{make_error_code(error), std::move(raw_server_error), error.message}; +[[noreturn]] void throw_exception(v1::exception const& ex) { + if (auto const ptr = dynamic_cast(&ex)) { + throw exception_type{ + ptr->code(), bsoncxx::v_noabi::document::value{bsoncxx::v_noabi::from_v1(ptr->raw())}, ptr->what()}; + } + + throw exception_type{ex.code(), ex.what()}; } } // namespace v_noabi diff --git a/src/mongocxx/test/CMakeLists.txt b/src/mongocxx/test/CMakeLists.txt index d072dab439..85ee048022 100644 --- a/src/mongocxx/test/CMakeLists.txt +++ b/src/mongocxx/test/CMakeLists.txt @@ -106,6 +106,7 @@ set(mongocxx_test_sources_v1 v1/logger.cpp v1/read_concern.cpp v1/read_preference.cpp + v1/server_error.cpp ) set(mongocxx_test_sources_spec diff --git a/src/mongocxx/test/v1/exception.cpp b/src/mongocxx/test/v1/exception.cpp index 875cc8926a..692ade0df7 100644 --- a/src/mongocxx/test/v1/exception.cpp +++ b/src/mongocxx/test/v1/exception.cpp @@ -16,7 +16,22 @@ // -#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include #include #include @@ -58,5 +73,396 @@ TEST_CASE("type", "[mongocxx][v1][error]") { } } +TEST_CASE("ownership", "[mongocxx][v1][error][exception]") { + auto source = exception::internal::make(1, std::system_category(), "source"); + auto target = exception::internal::make(2, std::system_category(), "target"); + + exception::internal::set_error_labels(source, scoped_bson{R"({"errorLabels": ["one"]})"}.view()); + exception::internal::set_error_labels(target, scoped_bson{R"({"errorLabels": ["two"]})"}.view()); + + REQUIRE(source.code().value() == 1); + REQUIRE(source.has_error_label("one")); + REQUIRE_FALSE(source.has_error_label("two")); + + REQUIRE(target.code().value() == 2); + REQUIRE_FALSE(target.has_error_label("one")); + REQUIRE(target.has_error_label("two")); + + SECTION("copy") { + auto copy = source; + + CHECK(source.code().value() == 1); + CHECK(source.has_error_label("one")); + CHECK_FALSE(source.has_error_label("two")); + + CHECK(copy.code().value() == 1); + CHECK(copy.has_error_label("one")); + CHECK_FALSE(copy.has_error_label("two")); + + target = copy; + + CHECK(copy.code().value() == 1); + CHECK(copy.has_error_label("one")); + CHECK_FALSE(copy.has_error_label("two")); + + CHECK(target.code().value() == 1); + CHECK(target.has_error_label("one")); + CHECK_FALSE(target.has_error_label("two")); + } +} + +TEST_CASE("make", "[mongocxx][v1][error][exception]") { + SECTION("error_code") { + using std::make_error_code; + + SECTION("default") { + auto const ec = exception::internal::make(std::error_code{}).code(); + + CHECK(ec == std::error_code{}); + + // Unsupported category. + CHECK(ec != mongocxx::v1::type_errc::zero); + CHECK(ec != mongocxx::v1::type_errc::invalid_argument); + CHECK(ec != mongocxx::v1::type_errc::runtime_error); + } + + SECTION("generic") { + auto const ec = exception::internal::make(make_error_code(std::errc::invalid_argument)).code(); + + CHECK(ec == std::errc::invalid_argument); + + // Unsupported category. + CHECK(ec != mongocxx::v1::type_errc::zero); + CHECK(ec != mongocxx::v1::type_errc::invalid_argument); + CHECK(ec != mongocxx::v1::type_errc::runtime_error); + } + + SECTION("bsoncxx") { + auto const ec = + exception::internal::make(make_error_code(bsoncxx::v1::document::view::errc::invalid_data)).code(); + + // bsoncxx error category. + CHECK(ec == bsoncxx::v1::document::view::errc::invalid_data); + CHECK(ec == bsoncxx::v1::source_errc::bsoncxx); + CHECK(ec == bsoncxx::v1::type_errc::runtime_error); + + // Not from mongocxx. + CHECK(ec != v1::source_errc::mongocxx); + CHECK(ec != v1::type_errc::runtime_error); + } + } + + SECTION("int + category") { + auto const v = static_cast(std::errc::invalid_argument); + auto const ex = exception::internal::make(v, std::generic_category()); + + CHECK(ex.code() == std::errc::invalid_argument); + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring(std::generic_category().message(v))); + } + + SECTION("int + category + message") { + auto const v = static_cast(std::errc::invalid_argument); + auto const ex = exception::internal::make(v, std::generic_category(), "abc"); + + CHECK(ex.code() == std::errc::invalid_argument); + CHECK_THAT( + ex.what(), + Catch::Matchers::ContainsSubstring(std::generic_category().message(v)) && + Catch::Matchers::ContainsSubstring("abc")); + } +} + +TEST_CASE("set_error_labels", "[mongocxx][v1][error][exception]") { + auto ex = exception::internal::make(std::error_code{}); + + exception::internal::set_error_labels(ex, scoped_bson{R"({"errorLabels": ["two"]})"}.view()); + + CHECK_FALSE(ex.has_error_label("one")); + CHECK(ex.has_error_label("two")); + CHECK_FALSE(ex.has_error_label("three")); + + SECTION("replace") { + scoped_bson const doc{R"({"errorLabels": ["one", "two", "three"]})"}; + exception::internal::set_error_labels(ex, doc.view()); + CHECK(ex.has_error_label("one")); + CHECK(ex.has_error_label("two")); + CHECK(ex.has_error_label("three")); + } + + SECTION("reset") { + exception::internal::set_error_labels(ex, bsoncxx::v1::document::view{}); + + CHECK_FALSE(ex.has_error_label("one")); + CHECK_FALSE(ex.has_error_label("two")); + CHECK_FALSE(ex.has_error_label("three")); + } + + SECTION("ignore") { + scoped_bson const doc{R"({"errorLabels": ["one", 2, "three"]})"}; + exception::internal::set_error_labels(ex, doc.view()); + + CHECK(ex.has_error_label("one")); + CHECK_FALSE(ex.has_error_label("two")); + CHECK(ex.has_error_label("three")); + } +} + +TEST_CASE("throw_exception", "[mongocxx][v1][error][exception]") { + // mongoc-error-private.h + enum category : std::uint8_t { + MONGOC_ERROR_CATEGORY_BSON = 1, // BSON_ERROR_CATEGORY + MONGOC_ERROR_CATEGORY = 2, + MONGOC_ERROR_CATEGORY_SERVER = 3, + MONGOC_ERROR_CATEGORY_CRYPT = 4, + MONGOC_ERROR_CATEGORY_SASL = 5, + }; + + SECTION("bson") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY_BSON}; + throw_exception(error); + } catch (v1::exception const& ex) { + auto const& code = ex.code(); + + // Translate bson error codes from mongoc into MONGOC_ERROR_BSON_INVALID. + CHECK(code == source_errc::mongoc); + CHECK(code == v1::type_errc::runtime_error); + CHECK(code.value() == MONGOC_ERROR_BSON_INVALID); + CHECK(MONGOC_ERROR_BSON_INVALID == 18); + CHECK(code.message() == "mongoc_error_code_t:18"); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("bson error code 123: abc")); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, MONGOC_ERROR_CATEGORY_BSON}; + throw_exception(error); + } catch (v1::exception const& ex) { + CHECK_THAT( + ex.what(), + !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ") || + !Catch::Matchers::ContainsSubstring("bson error code 123: ")); + } + } + } + + SECTION("mongoc") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY}; + throw_exception(error); + } catch (v1::exception const& ex) { + auto const& code = ex.code(); + + CHECK(code == source_errc::mongoc); + CHECK(code == v1::type_errc::runtime_error); + CHECK(code.value() == 123); + CHECK(code.message() == "mongoc_error_code_t:123"); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("abc")); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, MONGOC_ERROR_CATEGORY}; + throw_exception(error); + } catch (v1::exception const& ex) { + CHECK_THAT(ex.what(), !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ")); + } + } + } + + SECTION("server") { + SECTION("plain") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY_SERVER}; + throw_exception(error); + } catch (v1::exception const& ex) { + auto const& code = ex.code(); + + CHECK(code == source_errc::server); + CHECK(code == v1::type_errc::runtime_error); + CHECK(code.message() == "server error code 123"); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("abc")); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, MONGOC_ERROR_CATEGORY_SERVER}; + throw_exception(error); + } catch (v1::exception const& ex) { + CHECK_THAT(ex.what(), !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ")); + } + } + } + + SECTION("invalid raw") { + auto const op = [](bsoncxx::v1::document::value raw) -> std::error_code { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY_SERVER}; + throw_exception(error, std::move(raw)); + } catch (v1::exception const& ex) { + return ex.code(); + } + }; + + { + auto const code = op(bsoncxx::v1::document::value{nullptr}); + + CHECK(code.message() == "server error code 123"); + CHECK(code == source_errc::server); + CHECK(code == type_errc::runtime_error); + } + + { + auto const code = op(scoped_bson{}.value()); + + CHECK(code.message() == "server error code 123"); + CHECK(code == source_errc::server); + CHECK(code == type_errc::runtime_error); + } + + { + auto const code = op(scoped_bson{R"({"x": 1})"}.value()); + + CHECK(code.message() == "server error code 123"); + CHECK(code == source_errc::server); + CHECK(code == type_errc::runtime_error); + } + } + + SECTION("valid raw") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY}; + throw_exception(error, scoped_bson{R"({"code": 456})"}.value()); + } catch (v1::server_error const& ex) { + auto const& code = ex.code(); + + CHECK(code == source_errc::server); + CHECK(code == type_errc::runtime_error); + CHECK(code.value() == 456); + CHECK(code.message() == "server error code 456"); + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("abc")); + + CHECK(ex.client_code().value() == 123); + CHECK(ex.client_code() == mongocxx::v1::source_errc::mongoc); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, MONGOC_ERROR_CATEGORY}; + throw_exception(error, scoped_bson{R"({"code": 456})"}.value()); + } catch (v1::server_error const& ex) { + CHECK_THAT(ex.what(), !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ")); + } + } + } + } + + SECTION("mongocrypt") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY_CRYPT}; + throw_exception(error); + } catch (v1::exception const& ex) { + auto const& code = ex.code(); + + CHECK(code == source_errc::mongocrypt); + CHECK(code == type_errc::runtime_error); + CHECK(code.value() == 123); + CHECK(code.message() == "mongocrypt_status_t:123"); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("abc")); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, MONGOC_ERROR_CATEGORY_CRYPT}; + throw_exception(error); + } catch (v1::exception const& ex) { + CHECK_THAT(ex.what(), !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ")); + } + } + } + + SECTION("sasl") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", MONGOC_ERROR_CATEGORY_SASL}; + throw_exception(error); + } catch (v1::exception const& ex) { + auto const& code = ex.code(); + + // Translate sasl error codes from mongoc into MONGOC_ERROR_BSON_INVALID. + CHECK(code == source_errc::mongoc); + CHECK(code == type_errc::runtime_error); + CHECK(code.value() == MONGOC_ERROR_CLIENT_AUTHENTICATE); + CHECK(MONGOC_ERROR_CLIENT_AUTHENTICATE == 11); + CHECK(code.message() == "mongoc_error_code_t:11"); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("sasl error code 123: abc")); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, MONGOC_ERROR_CATEGORY_SASL}; + throw_exception(error); + } catch (v1::exception const& ex) { + CHECK_THAT( + ex.what(), + !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ") || + !Catch::Matchers::ContainsSubstring("sasl error code 123: ")); + } + } + } + + SECTION("unknown") { + SECTION("with message") { + try { + bson_error_t const error{0, 123, "abc", UINT8_MAX}; + throw_exception(error); + } catch (v1::exception const& ex) { + auto const& code = ex.code(); + + CHECK(code != source_errc::zero); + CHECK(code != source_errc::mongocxx); + CHECK(code != source_errc::mongoc); + CHECK(code != source_errc::mongocrypt); + CHECK(code != source_errc::server); + + CHECK(code == type_errc::runtime_error); + + CHECK(code.value() == 123); + CHECK(code.message() == "unknown:123"); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("unknown error category 255: abc")); + } + } + + SECTION("no message") { + try { + bson_error_t const error{0, 123, {}, UINT8_MAX}; + throw_exception(error); + } catch (v1::exception const& ex) { + CHECK_THAT( + ex.what(), + !Catch::Matchers::ContainsSubstring(ex.code().message() + ": ") || + !Catch::Matchers::ContainsSubstring("unknown error category 255: ")); + } + } + } +} + } // namespace v1 } // namespace mongocxx diff --git a/src/mongocxx/test/v1/server_error.cpp b/src/mongocxx/test/v1/server_error.cpp new file mode 100644 index 0000000000..659ba47a49 --- /dev/null +++ b/src/mongocxx/test/v1/server_error.cpp @@ -0,0 +1,174 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed 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. + +#include + +// + +#include +#include + +#include +#include + +#include +#include + +#include + +#include +#include + +namespace mongocxx { +namespace v1 { + +TEST_CASE("ownership", "[mongocxx][v1][error][server_error]") { + auto source = server_error::internal::make(1, "one", {}); + auto target = server_error::internal::make(2, "two", {}); + + exception::internal::set_error_labels(source, scoped_bson{R"({"errorLabels": ["one"]})"}.view()); + exception::internal::set_error_labels(target, scoped_bson{R"({"errorLabels": ["two"]})"}.view()); + + REQUIRE(source.code().value() == 1); + REQUIRE(source.has_error_label("one")); + REQUIRE_FALSE(source.has_error_label("two")); + + REQUIRE(target.code().value() == 2); + REQUIRE_FALSE(target.has_error_label("one")); + REQUIRE(target.has_error_label("two")); + + SECTION("copy") { + auto copy = source; + + CHECK(source.code().value() == 1); + CHECK(source.has_error_label("one")); + CHECK_FALSE(source.has_error_label("two")); + + CHECK(copy.code().value() == 1); + CHECK(copy.has_error_label("one")); + CHECK_FALSE(copy.has_error_label("two")); + + target = copy; + + CHECK(copy.code().value() == 1); + CHECK(copy.has_error_label("one")); + CHECK_FALSE(copy.has_error_label("two")); + + CHECK(target.code().value() == 1); + CHECK(target.has_error_label("one")); + CHECK_FALSE(target.has_error_label("two")); + } +} + +TEST_CASE("make", "[mongocxx][v1][error][server_error]") { + SECTION("simple") { + auto const ex = server_error::internal::make(123, "abc", bsoncxx::v1::document::value{}); + + CHECK(ex.code() == v1::source_errc::server); + CHECK(ex.code() == v1::type_errc::runtime_error); + CHECK(ex.code().value() == 123); + CHECK(ex.code().message() == "server error code 123"); + CHECK(ex.client_code() == std::error_code{}); + + CHECK_THAT(ex.what(), Catch::Matchers::ContainsSubstring("abc")); + + CHECK(ex.raw().empty()); + } + + SECTION("raw") { + scoped_bson const doc{R"({"ok": 0, "code": 123, "errmsg": "abc"})"}; + auto const ex = server_error::internal::make(123, "abc", doc.value()); + + CHECK(ex.code() == v1::source_errc::server); + CHECK(ex.code() == v1::type_errc::runtime_error); + CHECK(ex.code().value() == 123); + CHECK(ex.code().message() == "server error code 123"); + CHECK(ex.client_code() == std::error_code{}); + + auto const raw = ex.raw(); + + REQUIRE(raw == doc.view()); + + REQUIRE(static_cast(raw["ok"])); + CHECK(raw["ok"].get_int32().value == 0); + + REQUIRE(static_cast(raw["code"])); + CHECK(raw["code"].get_int32().value == 123); + + REQUIRE(static_cast(raw["errmsg"])); + CHECK(raw["errmsg"].get_string().value == "abc"); + } + + SECTION("client_code") { + auto const ec = std::make_error_code(std::errc::invalid_argument); + scoped_bson const doc{R"({"ok": 0, "code": 123, "errmsg": "abc"})"}; + auto const ex = server_error::internal::make(123, "abc", doc.value(), ec); + + CHECK(ex.code() == v1::source_errc::server); + CHECK(ex.code() == v1::type_errc::runtime_error); + CHECK(ex.code().value() == 123); + CHECK(ex.code().message() == "server error code 123"); + CHECK(ex.client_code() == ec); + + auto const raw = ex.raw(); + + REQUIRE(raw == doc.view()); + + REQUIRE(static_cast(raw["ok"])); + CHECK(raw["ok"].get_int32().value == 0); + + REQUIRE(static_cast(raw["code"])); + CHECK(raw["code"].get_int32().value == 123); + + REQUIRE(static_cast(raw["errmsg"])); + CHECK(raw["errmsg"].get_string().value == "abc"); + } + + SECTION("errorLabels") { + SECTION("none") { + scoped_bson doc{R"({})"}; + auto const ex = server_error::internal::make(0, "", std::move(doc).value()); + CHECK_FALSE(ex.has_error_label("a")); + CHECK_FALSE(ex.has_error_label("b")); + CHECK_FALSE(ex.has_error_label("c")); + } + + SECTION("empty") { + scoped_bson doc{R"({"errorLabels": []})"}; + auto const ex = server_error::internal::make(0, "", std::move(doc).value()); + CHECK_FALSE(ex.has_error_label("a")); + CHECK_FALSE(ex.has_error_label("b")); + CHECK_FALSE(ex.has_error_label("c")); + } + + SECTION("one") { + scoped_bson doc{R"({"errorLabels": ["a"]})"}; + auto const ex = server_error::internal::make(0, "", std::move(doc).value()); + CHECK(ex.has_error_label("a")); + CHECK_FALSE(ex.has_error_label("b")); + CHECK_FALSE(ex.has_error_label("c")); + } + + SECTION("many") { + scoped_bson doc{R"({"errorLabels": ["a", "b", "c"]})"}; + auto const ex = server_error::internal::make(0, "", std::move(doc).value()); + CHECK(ex.has_error_label("a")); + CHECK(ex.has_error_label("b")); + CHECK(ex.has_error_label("c")); + } + } +} + +} // namespace v1 +} // namespace mongocxx diff --git a/src/mongocxx/test/v1/server_error.hh b/src/mongocxx/test/v1/server_error.hh new file mode 100644 index 0000000000..9c9e6e8e87 --- /dev/null +++ b/src/mongocxx/test/v1/server_error.hh @@ -0,0 +1,21 @@ +// Copyright 2009-present MongoDB, Inc. +// +// Licensed 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. + +#pragma once + +#include // IWYU pragma: export + +// + +#include // IWYU pragma: export