| /* | |
| * 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. | |
| */ | |
| #include "iceberg/catalog/rest/error_handlers.h" | |
| #include <string_view> | |
| #include <gtest/gtest.h> | |
| #include "iceberg/catalog/rest/types.h" | |
| #include "iceberg/test/matchers.h" | |
| namespace iceberg::rest { | |
| namespace { | |
| void ExpectErrorWithMessage(const Status& status, ErrorKind kind, | |
| std::string_view message) { | |
| ASSERT_FALSE(status.has_value()); | |
| EXPECT_EQ(status.error().kind, kind); | |
| EXPECT_EQ(status.error().message, message); | |
| } | |
| } // namespace | |
| TEST(ErrorHandlersTest, DefaultErrorHandlerIncludesCodeAndType) { | |
| ErrorResponse error{ | |
| .code = 422, | |
| .type = "ValidationException", | |
| .message = "Invalid input", | |
| }; | |
| ExpectErrorWithMessage( | |
| DefaultErrorHandler::Instance()->Accept(error), ErrorKind::kRestError, | |
| "Unable to process (code: 422, type: ValidationException): Invalid input"); | |
| } | |
| TEST(ErrorHandlersTest, DefaultErrorHandlerWithCodeOnly) { | |
| ErrorResponse error{ | |
| .code = 422, | |
| .type = "", | |
| .message = "", | |
| }; | |
| ExpectErrorWithMessage(DefaultErrorHandler::Instance()->Accept(error), | |
| ErrorKind::kRestError, | |
| "Unable to process (code: 422, type: null): null"); | |
| } | |
| TEST(ErrorHandlersTest, DefaultErrorHandlerWithCodeAndMessageOnly) { | |
| ErrorResponse error{ | |
| .code = 422, | |
| .type = "", | |
| .message = "Invalid input", | |
| }; | |
| ExpectErrorWithMessage(DefaultErrorHandler::Instance()->Accept(error), | |
| ErrorKind::kRestError, | |
| "Unable to process (code: 422, type: null): Invalid input"); | |
| } | |
| TEST(ErrorHandlersTest, DefaultErrorHandlerWithCodeAndTypeOnly) { | |
| ErrorResponse error{ | |
| .code = 422, | |
| .type = "ValidationException", | |
| .message = "", | |
| }; | |
| ExpectErrorWithMessage( | |
| DefaultErrorHandler::Instance()->Accept(error), ErrorKind::kRestError, | |
| "Unable to process (code: 422, type: ValidationException): null"); | |
| } | |
| TEST(ErrorHandlersTest, NamespaceErrorHandlerFormats422AsRestError) { | |
| ErrorResponse error{ | |
| .code = 422, | |
| .type = "ValidationException", | |
| .message = "Invalid namespace", | |
| }; | |
| ExpectErrorWithMessage( | |
| NamespaceErrorHandler::Instance()->Accept(error), ErrorKind::kRestError, | |
| "Unable to process (code: 422, type: ValidationException): Invalid namespace"); | |
| } | |
| TEST(ErrorHandlersTest, TableErrorHandlerMaps404NotFoundToNotFound) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "NotFoundException", | |
| .message = "Failed to open input stream for file: metadata.json", | |
| }; | |
| EXPECT_THAT(TableErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNotFound)); | |
| EXPECT_THAT(TableErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("metadata.json")); | |
| } | |
| TEST(ErrorHandlersTest, TableErrorHandlerMaps404ToNoSuchTableByDefault) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "NoSuchTableException", | |
| .message = "Table does not exist", | |
| }; | |
| EXPECT_THAT(TableErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNoSuchTable)); | |
| } | |
| TEST(ErrorHandlersTest, CreateTableErrorHandlerMaps404ToNoSuchNamespace) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "NoSuchNamespaceException", | |
| .message = "Namespace does not exist", | |
| }; | |
| EXPECT_THAT(CreateTableErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNoSuchNamespace)); | |
| } | |
| TEST(ErrorHandlersTest, CreateTableErrorHandlerMaps409ToAlreadyExists) { | |
| ErrorResponse error{ | |
| .code = 409, | |
| .type = "AlreadyExistsException", | |
| .message = "Table already exists", | |
| }; | |
| EXPECT_THAT(CreateTableErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kAlreadyExists)); | |
| } | |
| TEST(ErrorHandlersTest, CreateTableErrorHandlerMapsServiceFailureToCommitStateUnknown) { | |
| ErrorResponse error{ | |
| .code = 503, | |
| .type = "ServiceFailureException", | |
| .message = "Service unavailable", | |
| }; | |
| EXPECT_THAT(CreateTableErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kCommitStateUnknown)); | |
| EXPECT_THAT(CreateTableErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("Service failed: 503: Service unavailable")); | |
| } | |
| TEST(ErrorHandlersTest, PlanErrorHandlerMapsUnknown404ToNoSuchPlanId) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "UnknownException", | |
| .message = "Plan does not exist", | |
| }; | |
| EXPECT_THAT(PlanErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNoSuchPlanId)); | |
| } | |
| TEST(ErrorHandlersTest, PlanErrorHandlerDelegates406ToDefaultHandler) { | |
| ErrorResponse error{ | |
| .code = 406, | |
| .type = "NotAcceptableException", | |
| .message = "Not acceptable", | |
| }; | |
| ExpectErrorWithMessage( | |
| PlanErrorHandler::Instance()->Accept(error), ErrorKind::kRestError, | |
| "Unable to process (code: 406, type: NotAcceptableException): Not acceptable"); | |
| } | |
| TEST(ErrorHandlersTest, PlanTaskErrorHandlerMapsUnknown404ToNoSuchPlanTask) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "UnknownException", | |
| .message = "Plan task does not exist", | |
| }; | |
| EXPECT_THAT(PlanTaskErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNoSuchPlanTask)); | |
| } | |
| TEST(ErrorHandlersTest, OAuthErrorHandlerMapsInvalidClientToNotAuthorized) { | |
| ErrorResponse error{ | |
| .code = 400, | |
| .type = "invalid_client", | |
| .message = "Credentials given were invalid", | |
| }; | |
| EXPECT_THAT(OAuthErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNotAuthorized)); | |
| EXPECT_THAT( | |
| OAuthErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("Not authorized: invalid_client: Credentials given were invalid")); | |
| } | |
| TEST(ErrorHandlersTest, OAuthErrorHandlerParsesOAuthErrorResponse) { | |
| auto parse_result = OAuthErrorHandler::Instance()->ParseResponse( | |
| 400, | |
| R"({"error":"invalid_client","error_description":"Credentials given were invalid"})"); | |
| ASSERT_TRUE(parse_result.has_value()); | |
| EXPECT_EQ(parse_result->code, 400); | |
| EXPECT_EQ(parse_result->type, "invalid_client"); | |
| EXPECT_EQ(parse_result->message, "Credentials given were invalid"); | |
| EXPECT_THAT(OAuthErrorHandler::Instance()->Accept(*parse_result), | |
| IsError(ErrorKind::kNotAuthorized)); | |
| } | |
| TEST(ErrorHandlersTest, OAuthErrorHandlerMapsClientErrorsToBadRequest) { | |
| ErrorResponse error{ | |
| .code = 400, | |
| .type = "invalid_grant", | |
| .message = "Grant is invalid", | |
| }; | |
| EXPECT_THAT(OAuthErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kBadRequest)); | |
| EXPECT_THAT(OAuthErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("Malformed request: invalid_grant: Grant is invalid")); | |
| } | |
| TEST(ErrorHandlersTest, ConfigErrorHandlerMapsTyped404ToNoSuchWarehouse) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "NotFoundException", | |
| .message = "Warehouse not found", | |
| }; | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kNoSuchWarehouse)); | |
| } | |
| TEST(ErrorHandlersTest, ConfigErrorHandlerDelegatesUntyped404ToDefaultHandler) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "", | |
| .message = "Not Found", | |
| }; | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kRestError)); | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("Not Found")); | |
| } | |
| TEST(ErrorHandlersTest, ConfigErrorHandlerDelegatesFallback404ToDefaultHandler) { | |
| ErrorResponse error{ | |
| .code = 404, | |
| .type = "RESTException", | |
| .message = "Not Found", | |
| }; | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kRestError)); | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("Not Found")); | |
| } | |
| TEST(ErrorHandlersTest, ConfigErrorHandlerDelegatesNon404ToDefaultHandler) { | |
| ErrorResponse error{ | |
| .code = 500, | |
| .type = "", | |
| .message = "Internal server error", | |
| }; | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| IsError(ErrorKind::kInternalServerError)); | |
| EXPECT_THAT(ConfigErrorHandler::Instance()->Accept(error), | |
| HasErrorMessage("Internal server error")); | |
| } | |
| } // namespace iceberg::rest |