add test + json errors

This commit is contained in:
2026-05-31 21:30:17 +00:00
parent 227fcfaafb
commit 7e3b49ad76
3 changed files with 231 additions and 24 deletions
+34 -6
View File
@@ -87,13 +87,9 @@ where
} }
fn check_sig_header(secret_key: &[u8], sig_header: &[u8], body: &[u8]) -> Result<(), AppError> { fn check_sig_header(secret_key: &[u8], sig_header: &[u8], body: &[u8]) -> Result<(), AppError> {
let sig_header_decoded = hex::decode(sig_header).map_err(|err| anyhow!(err))?; let sig_header_decoded = hex::decode(sig_header).map_err(|_| AppError::WebHookSigHeaderInvalidErr)?;
let webhook_sig_header_secret = let mut mac = Hmac::<Sha256>::new_from_slice(secret_key).map_err(|err| anyhow!(err))?;
std::env::var("WEBHOOK_SIG_HEADER_SECRET").map_err(|err| anyhow!(err))?;
let mut mac = Hmac::<Sha256>::new_from_slice(&webhook_sig_header_secret.into_bytes())
.map_err(|err| anyhow!(err))?;
mac.update(body); mac.update(body);
@@ -105,3 +101,35 @@ fn check_sig_header(secret_key: &[u8], sig_header: &[u8], body: &[u8]) -> Result
false => Err(AppError::WebHookSigHeaderInvalidErr), false => Err(AppError::WebHookSigHeaderInvalidErr),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn valid_json_bytes_parse_to_value() {
let body = serde_json::to_vec(
&json!({"action": "created", "pull_request": {"id": 1}, "comment": {"body": "hi"}}),
)
.unwrap();
let Json(value) = Json::<Value>::from_bytes(&body).unwrap();
assert_eq!(value["action"], "created");
assert_eq!(value["pull_request"]["id"], 1);
assert_eq!(value["comment"]["body"], "hi");
}
#[test]
fn malformed_json_bytes_return_malformed_error() {
let body = b"not valid json";
let result = Json::<Value>::from_bytes(body);
assert!(result.is_err());
}
#[test]
fn empty_body_returns_malformed_error() {
let body = b"";
let result = Json::<Value>::from_bytes(body);
assert!(result.is_err());
}
}
+38 -12
View File
@@ -15,6 +15,12 @@ pub enum AppError {
#[error("WebHook sig header is invalid")] #[error("WebHook sig header is invalid")]
WebHookSigHeaderInvalidErr, WebHookSigHeaderInvalidErr,
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Wrong type for field: {0}")]
WrongFieldType(String),
#[error(transparent)] #[error(transparent)]
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
} }
@@ -22,19 +28,39 @@ pub enum AppError {
impl IntoResponse for AppError { impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
match self { match self {
AppError::MalformedJsonErr => (StatusCode::BAD_REQUEST, "Malformed Json"), AppError::MalformedJsonErr => {
(StatusCode::BAD_REQUEST, "Malformed Json".to_string()).into_response()
}
AppError::BadJsonStructErr => ( AppError::BadJsonStructErr => (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"Json not contains mandatory fields", "Json not contains mandatory fields".to_string(),
), )
AppError::WebHookSigHeaderNotFoundErr => { .into_response(),
(StatusCode::BAD_REQUEST, "WebHook sig header not found") AppError::WebHookSigHeaderNotFoundErr => (
} StatusCode::BAD_REQUEST,
AppError::WebHookSigHeaderInvalidErr => { "WebHook sig header not found".to_string(),
(StatusCode::BAD_REQUEST, "WebHook sig header is invalid") )
} .into_response(),
AppError::Other(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"), AppError::WebHookSigHeaderInvalidErr => (
} StatusCode::UNAUTHORIZED,
.into_response() "WebHook sig header is invalid".to_string(),
)
.into_response(),
AppError::MissingField(ref field) => (
StatusCode::BAD_REQUEST,
format!("Missing required field: {}", field),
)
.into_response(),
AppError::WrongFieldType(ref field) => (
StatusCode::BAD_REQUEST,
format!("Wrong type for field: {}", field),
)
.into_response(),
AppError::Other(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
)
.into_response(),
}
} }
} }
+160 -7
View File
@@ -1,8 +1,8 @@
use anyhow::anyhow;
use serde_json::Value; use serde_json::Value;
use crate::errors::AppError; use crate::errors::AppError;
#[derive(Debug, PartialEq)]
pub enum WebhookType { pub enum WebhookType {
Review(u64, String), Review(u64, String),
} }
@@ -15,9 +15,9 @@ impl TryFrom<Value> for WebhookType {
let comment = json.get("comment"); let comment = json.get("comment");
let action = json let action = json
.get("action") .get("action")
.ok_or(anyhow!("action not found"))? .ok_or(AppError::MissingField("action".into()))?
.as_str() .as_str()
.ok_or(anyhow!("error while action"))?; .ok_or(AppError::WrongFieldType("action".into()))?;
if action != "created" { if action != "created" {
return Err(AppError::BadJsonStructErr); return Err(AppError::BadJsonStructErr);
@@ -26,16 +26,16 @@ impl TryFrom<Value> for WebhookType {
if let (Some(pull_request), Some(comment)) = (pull_request, comment) { if let (Some(pull_request), Some(comment)) = (pull_request, comment) {
let comment_body = comment let comment_body = comment
.get("body") .get("body")
.ok_or(anyhow!("comment body not found"))? .ok_or(AppError::MissingField("comment.body".into()))?
.as_str() .as_str()
.ok_or(anyhow!("error while get pr comment"))? .ok_or(AppError::WrongFieldType("comment.body".into()))?
.to_string(); .to_string();
let pr_id = pull_request let pr_id = pull_request
.get("id") .get("id")
.ok_or(anyhow!("pr id not found"))? .ok_or(AppError::MissingField("pull_request.id".into()))?
.as_u64() .as_u64()
.ok_or(anyhow!("error while get pr id"))?; .ok_or(AppError::WrongFieldType("pull_request.id".into()))?;
return Ok(WebhookType::Review(pr_id, comment_body)); return Ok(WebhookType::Review(pr_id, comment_body));
} }
@@ -43,3 +43,156 @@ impl TryFrom<Value> for WebhookType {
Err(AppError::BadJsonStructErr) Err(AppError::BadJsonStructErr)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn valid_webhook_parses_review() {
let payload = json!({
"action": "created",
"pull_request": { "id": 42 },
"comment": { "body": "LGTM" }
});
let result = WebhookType::try_from(payload).unwrap();
assert_eq!(result, WebhookType::Review(42, "LGTM".into()));
}
#[test]
fn missing_action_returns_error() {
let payload = json!({
"pull_request": { "id": 1 },
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::MissingField(ref f) if f == "action"));
}
#[test]
fn action_not_created_returns_bad_json_struct() {
let payload = json!({
"action": "updated",
"pull_request": { "id": 1 },
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::BadJsonStructErr));
}
#[test]
fn action_not_a_string_returns_error() {
let payload = json!({
"action": 123,
"pull_request": { "id": 1 },
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::WrongFieldType(ref f) if f == "action"));
}
#[test]
fn missing_pull_request_returns_bad_json_struct() {
let payload = json!({
"action": "created",
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::BadJsonStructErr));
}
#[test]
fn missing_comment_returns_bad_json_struct() {
let payload = json!({
"action": "created",
"pull_request": { "id": 1 }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::BadJsonStructErr));
}
#[test]
fn missing_pr_id_returns_error() {
let payload = json!({
"action": "created",
"pull_request": { "number": 1 },
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::MissingField(ref f) if f == "pull_request.id"));
}
#[test]
fn pr_id_not_a_number_returns_error() {
let payload = json!({
"action": "created",
"pull_request": { "id": "not-a-number" },
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::WrongFieldType(ref f) if f == "pull_request.id"));
}
#[test]
fn missing_comment_body_returns_error() {
let payload = json!({
"action": "created",
"pull_request": { "id": 1 },
"comment": { "text": "no body" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::MissingField(ref f) if f == "comment.body"));
}
#[test]
fn comment_body_not_a_string_returns_error() {
let payload = json!({
"action": "created",
"pull_request": { "id": 1 },
"comment": { "body": 999 }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::WrongFieldType(ref f) if f == "comment.body"));
}
#[test]
fn null_pull_request_returns_error() {
let payload = json!({
"action": "created",
"pull_request": null,
"comment": { "body": "ok" }
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::MissingField(ref f) if f == "pull_request.id"));
}
#[test]
fn null_comment_returns_error() {
let payload = json!({
"action": "created",
"pull_request": { "id": 1 },
"comment": null
});
let err = WebhookType::try_from(payload).unwrap_err();
assert!(matches!(err, AppError::MissingField(ref f) if f == "comment.body"));
}
#[test]
fn large_pr_id_parses_correctly() {
let payload = json!({
"action": "created",
"pull_request": { "id": 18446744073709551615u64 },
"comment": { "body": "max u64" }
});
let result = WebhookType::try_from(payload).unwrap();
assert_eq!(result, WebhookType::Review(18446744073709551615, "max u64".into()));
}
#[test]
fn full_webhook_payload_parses() {
let payload: Value = serde_json::from_str(include_str!("../docs/webhook_pr_body.json")).unwrap();
let result = WebhookType::try_from(payload).unwrap();
assert_eq!(result, WebhookType::Review(1, "Test comment".into()));
}
}