use dropshot::ApiDescription;
use dropshot::ErrorStatusCode;
use dropshot::HttpError;
use dropshot::HttpResponseError;
use dropshot::HttpResponseOk;
use dropshot::Path;
use dropshot::RequestContext;
use dropshot::endpoint;
use dropshot::test_util::TestContext;
use http::Method;
use http::StatusCode;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::common;
#[derive(Debug, thiserror::Error, Serialize, JsonSchema)]
pub(crate) enum EnumError {
#[error("overfull \\hbox (badness {badness}) at line {line}")]
OverfullHbox { badness: i32, line: u32 },
#[error("{internal_message}")]
HttpError {
message: String,
error_code: Option<String>,
#[serde(skip)]
internal_message: String,
#[serde(skip)]
status: ErrorStatusCode,
},
}
impl From<HttpError> for EnumError {
fn from(e: HttpError) -> Self {
EnumError::HttpError {
message: e.external_message,
error_code: e.error_code,
internal_message: e.internal_message,
status: e.status_code,
}
}
}
impl HttpResponseError for EnumError {
fn status_code(&self) -> ErrorStatusCode {
match self {
EnumError::OverfullHbox { .. } => {
ErrorStatusCode::INTERNAL_SERVER_ERROR
}
EnumError::HttpError { status, .. } => *status,
}
}
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
enum DeserializedEnumError {
OverfullHbox { badness: i32, line: u32 },
HttpError { message: String, error_code: Option<String> },
}
#[derive(Debug, thiserror::Error, Serialize, JsonSchema)]
#[error("{message}")]
pub(crate) struct StructError {
message: String,
kind: ErrorKind,
#[serde(skip)]
status: ErrorStatusCode,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, Eq, PartialEq)]
enum ErrorKind {
CantGetYeFlask,
Other,
}
#[derive(Debug, Deserialize, Eq, PartialEq)]
struct DeserializedStructError {
message: String,
kind: ErrorKind,
}
impl From<HttpError> for StructError {
fn from(e: HttpError) -> Self {
Self {
message: e.external_message,
kind: ErrorKind::Other,
status: e.status_code,
}
}
}
impl HttpResponseError for StructError {
fn status_code(&self) -> ErrorStatusCode {
self.status
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub(crate) struct HandlerPathParam {
should_error: bool,
}
#[endpoint {
method = GET,
path = "/test/enum-error/{should_error}",
}]
async fn enum_error_handler(
_rqctx: RequestContext<()>,
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, EnumError> {
enum_error_inner(path)
}
#[endpoint {
method = GET,
path = "/test/struct-error/{should_error}",
}]
async fn struct_error_handler(
_rqctx: RequestContext<()>,
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, StructError> {
struct_error_inner(path)
}
#[endpoint {
method = GET,
path = "/test/dropshot-error/",
}]
async fn dropshot_error_handler(
_rqctx: RequestContext<()>,
) -> Result<HttpResponseOk<()>, HttpError> {
Err(HttpError::for_internal_error("something bad happened".to_string()))
}
fn enum_error_inner(
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, EnumError> {
let HandlerPathParam { should_error } = path.into_inner();
if should_error {
Err(EnumError::OverfullHbox { badness: 10000, line: 42 })
} else {
Ok(HttpResponseOk(()))
}
}
fn struct_error_inner(
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, StructError> {
let HandlerPathParam { should_error } = path.into_inner();
if should_error {
Err(StructError {
kind: ErrorKind::CantGetYeFlask,
message: "can't get ye flask".to_string(),
status: ErrorStatusCode::NOT_FOUND,
})
} else {
Ok(HttpResponseOk(()))
}
}
pub(crate) fn api() -> ApiDescription<()> {
let mut api = ApiDescription::new();
api.register(enum_error_handler).unwrap();
api.register(struct_error_handler).unwrap();
api.register(dropshot_error_handler).unwrap();
api
}
#[dropshot::api_description]
pub(crate) trait CustomErrorApi {
type Context;
#[endpoint {
method = GET,
path = "/test/enum-error/{should_error}",
}]
async fn enum_error(
rqctx: RequestContext<Self::Context>,
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, EnumError>;
#[endpoint {
method = GET,
path = "/test/struct-error/{should_error}",
}]
async fn struct_error(
rqctx: RequestContext<Self::Context>,
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, StructError>;
#[endpoint {
method = GET,
path = "/test/dropshot-error/",
}]
async fn dropshot_error(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<()>, HttpError>;
}
enum ApiImpl {}
impl CustomErrorApi for ApiImpl {
type Context = ();
async fn enum_error(
_rqctx: RequestContext<()>,
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, EnumError> {
enum_error_inner(path)
}
async fn struct_error(
_rqctx: RequestContext<()>,
path: Path<HandlerPathParam>,
) -> Result<HttpResponseOk<()>, StructError> {
struct_error_inner(path)
}
async fn dropshot_error(
_rqctx: RequestContext<()>,
) -> Result<HttpResponseOk<()>, HttpError> {
Err(HttpError::for_internal_error("something bad happened".to_string()))
}
}
#[tokio::test]
async fn test_enum_user_error() {
let api = api();
let testctx = common::test_setup_with_context(
"test_enum_user_error",
api,
(),
dropshot::HandlerTaskMode::Detached,
);
do_enum_user_error_test(testctx).await;
}
async fn do_enum_user_error_test(testctx: TestContext<()>) {
let json = testctx
.client_testctx
.with_error_type::<DeserializedEnumError>()
.make_request_error(
Method::GET,
"/test/enum-error/true",
StatusCode::INTERNAL_SERVER_ERROR,
)
.await;
assert_eq!(
dbg!(json),
DeserializedEnumError::OverfullHbox { badness: 10000, line: 42 }
);
testctx.teardown().await;
}
#[tokio::test]
async fn test_enum_extractor_error() {
let api = api();
let testctx = common::test_setup_with_context(
"test_enum_extractor_error",
api,
(),
dropshot::HandlerTaskMode::Detached,
);
let json = testctx
.client_testctx
.with_error_type::<DeserializedEnumError>()
.make_request_error(
Method::GET,
"/test/enum-error/asdf",
StatusCode::BAD_REQUEST,
)
.await;
assert_eq!(
dbg!(json),
DeserializedEnumError::HttpError {
message:
"bad parameter in URL path: unable to parse 'asdf' as bool"
.to_string(),
error_code: None
},
);
testctx.teardown().await;
}
#[tokio::test]
async fn test_struct_user_error() {
let api = api();
let testctx = common::test_setup_with_context(
"test_struct_user_error",
api,
(),
dropshot::HandlerTaskMode::Detached,
);
do_struct_user_error_test(testctx).await;
}
async fn do_struct_user_error_test(testctx: TestContext<()>) {
let json = testctx
.client_testctx
.with_error_type::<DeserializedStructError>()
.make_request_error(
Method::GET,
"/test/struct-error/true",
StatusCode::NOT_FOUND,
)
.await;
assert_eq!(
dbg!(json),
DeserializedStructError {
message: "can't get ye flask".to_string(),
kind: ErrorKind::CantGetYeFlask,
}
);
testctx.teardown().await;
}
#[tokio::test]
async fn test_struct_extractor_error() {
let api = api();
let testctx = common::test_setup_with_context(
"test_struct_extractor_error",
api,
(),
dropshot::HandlerTaskMode::Detached,
);
let json = testctx
.client_testctx
.with_error_type::<DeserializedStructError>()
.make_request_error(
Method::GET,
"/test/struct-error/this-wont-work",
StatusCode::BAD_REQUEST,
)
.await;
assert_eq!(
dbg!(json),
DeserializedStructError {
message:
"bad parameter in URL path: unable to parse 'this-wont-work' as bool"
.to_string(),
kind: ErrorKind::Other,
},
);
testctx.teardown().await;
}
#[test]
fn test_trait_based_api() {
custom_error_api_mod::stub_api_description().unwrap();
custom_error_api_mod::api_description::<ApiImpl>().unwrap();
}
#[tokio::test]
async fn test_trait_enum_user_error() {
let api = custom_error_api_mod::api_description::<ApiImpl>().unwrap();
let testctx = common::test_setup_with_context(
"test_trait_enum_user_error",
api,
(),
dropshot::HandlerTaskMode::Detached,
);
do_struct_user_error_test(testctx).await;
}
#[tokio::test]
async fn test_trait_struct_user_error() {
let api = custom_error_api_mod::api_description::<ApiImpl>().unwrap();
let testctx = common::test_setup_with_context(
"test_trait_struct_user_error",
api,
(),
dropshot::HandlerTaskMode::Detached,
);
do_struct_user_error_test(testctx).await;
}