add test + json errors
This commit is contained in:
+34
-6
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+37
-11
@@ -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,
|
||||||
|
"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(),
|
||||||
}
|
}
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+160
-7
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user