feat: test webhooks
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Aravinth Manivannan 2022-12-28 13:36:30 +05:30
parent 0c6199494b
commit d919bad570
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88
3 changed files with 206 additions and 98 deletions

View File

@ -136,8 +136,10 @@ pub fn services(cfg: &mut web::ServiceConfig) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::{http::StatusCode, test}; use actix_web::{error::ResponseError, http::StatusCode, test};
use hmac::Mac;
use crate::ctx::api::v1::gitea::{HmacSha256, WebhookPayload};
use crate::db::GiteaWebhook; use crate::db::GiteaWebhook;
use crate::tests; use crate::tests;
use crate::*; use crate::*;
@ -153,6 +155,7 @@ mod tests {
let (_dir, ctx) = tests::get_ctx().await; let (_dir, ctx) = tests::get_ctx().await;
let _ = ctx.delete_user(NAME, PASSWORD).await; let _ = ctx.delete_user(NAME, PASSWORD).await;
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
let page = ctx.add_test_site(NAME.into()).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(ctx).await; let app = get_app!(ctx).await;
@ -185,6 +188,95 @@ mod tests {
check_status!(list_all_webhooks_resp, StatusCode::OK); check_status!(list_all_webhooks_resp, StatusCode::OK);
let hooks: Vec<GiteaWebhook> = let hooks: Vec<GiteaWebhook> =
actix_web::test::read_body_json(list_all_webhooks_resp).await; actix_web::test::read_body_json(list_all_webhooks_resp).await;
assert_eq!(vec![hook], hooks); assert_eq!(vec![hook.clone()], hooks);
let webhook_url = format!("{}?auth={}", V1_API_ROUTES.gitea.webhook, hook.auth_token);
// test webhook
let mut webhook_payload = WebhookPayload::default();
webhook_payload.reference = format!("refs/origin/{}", page.branch);
webhook_payload.repository.html_url = page.repo;
let body = serde_json::to_string(&webhook_payload).unwrap();
let body = body.as_bytes();
let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(&body);
let res = mac.finalize();
let sig = res.into_bytes();
let sig = hex::encode(&sig[..]);
let post_to_webhook_resp = test::call_service(
&app,
post_request!(&webhook_payload, &webhook_url)
.insert_header(("X-Gitea-Delivery", "foobar213randomuuid"))
.insert_header(("X-Gitea-Signature", sig.clone()))
.insert_header(("X-Gitea-Event", "push"))
.cookie(cookies.clone())
.to_request(),
)
.await;
check_status!(post_to_webhook_resp, StatusCode::OK);
// no webhook
let fake_webhook_url = format!(
"{}?auth={}",
V1_API_ROUTES.gitea.webhook, hook.gitea_webhook_secret
);
let body = serde_json::to_string(&webhook_payload).unwrap();
let body = body.as_bytes();
let mut mac =
HmacSha256::new_from_slice(b"nosecret").expect("HMAC can take key of any size");
mac.update(&body);
let res = mac.finalize();
let fake_sig = res.into_bytes();
let fake_sig = hex::encode(&fake_sig[..]);
let post_to_no_exist_webhook_resp = test::call_service(
&app,
post_request!(&webhook_payload, &fake_webhook_url)
.insert_header(("X-Gitea-Delivery", "foobar213randomuuid"))
.insert_header(("X-Gitea-Signature", fake_sig))
.insert_header(("X-Gitea-Event", "push"))
.cookie(cookies.clone())
.to_request(),
)
.await;
let err = ServiceError::WebhookNotFound;
assert_eq!(post_to_no_exist_webhook_resp.status(), err.status_code());
let resp_err: ErrorToResponse =
actix_web::test::read_body_json(post_to_no_exist_webhook_resp).await;
assert_eq!(resp_err.error, err.to_string());
// no website
let mut webhook_payload = WebhookPayload::default();
webhook_payload.reference = format!("refs/origin/{}", page.branch);
webhook_payload.repository.html_url = "foo".into();
let body = serde_json::to_string(&webhook_payload).unwrap();
let body = body.as_bytes();
let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(&body);
let res = mac.finalize();
let sig = res.into_bytes();
let sig = hex::encode(&sig[..]);
let post_to_no_website_webhook_resp = test::call_service(
&app,
post_request!(&webhook_payload, &webhook_url)
.insert_header(("X-Gitea-Delivery", "foobar213randomuuid"))
.insert_header(("X-Gitea-Signature", sig.clone()))
.insert_header(("X-Gitea-Event", "push"))
.cookie(cookies.clone())
.to_request(),
)
.await;
let err = ServiceError::WebsiteNotFound;
assert_eq!(post_to_no_website_webhook_resp.status(), err.status_code());
let resp_err: ErrorToResponse =
actix_web::test::read_body_json(post_to_no_website_webhook_resp).await;
assert_eq!(resp_err.error, err.to_string());
} }
} }

View File

@ -22,127 +22,128 @@ use sha2::Sha256;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::ctx::Ctx; use crate::ctx::Ctx;
use crate::errors::ServiceError;
use crate::errors::ServiceResult; use crate::errors::ServiceResult;
type HmacSha256 = Hmac<Sha256>; pub type HmacSha256 = Hmac<Sha256>;
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct CommitPerson { pub struct CommitPerson {
name: String, pub name: String,
email: String, pub email: String,
username: String, pub username: String,
} }
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
pub struct Commit { pub struct Commit {
id: String, pub id: String,
message: String, pub message: String,
url: String, pub url: String,
author: CommitPerson, pub author: CommitPerson,
committer: CommitPerson, pub committer: CommitPerson,
verification: serde_json::Value, pub verification: serde_json::Value,
timestamp: String, pub timestamp: String,
added: serde_json::Value, pub added: serde_json::Value,
removed: serde_json::Value, pub removed: serde_json::Value,
modified: serde_json::Value, pub modified: serde_json::Value,
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Person { pub struct Person {
id: usize, pub id: usize,
login: String, pub login: String,
full_name: String, pub full_name: String,
email: String, pub email: String,
avatar_url: String, pub avatar_url: String,
language: String, pub language: String,
is_admin: bool, pub is_admin: bool,
last_login: String, pub last_login: String,
created: String, pub created: String,
restricted: bool, pub restricted: bool,
active: bool, pub active: bool,
prohibit_login: bool, pub prohibit_login: bool,
location: String, pub location: String,
website: String, pub website: String,
description: String, pub description: String,
visibility: String, pub visibility: String,
followers_count: usize, pub followers_count: usize,
following_count: usize, pub following_count: usize,
starred_repos_count: usize, pub starred_repos_count: usize,
username: String, pub username: String,
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Permissions { pub struct Permissions {
admin: bool, pub admin: bool,
push: bool, pub push: bool,
pull: bool, pub pull: bool,
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct InternalTracker { pub struct InternalTracker {
enable_time_tracker: bool, pub enable_time_tracker: bool,
allow_only_contributors_to_track_time: bool, pub allow_only_contributors_to_track_time: bool,
enable_issue_dependencies: bool, pub enable_issue_dependencies: bool,
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Repository { pub struct Repository {
id: usize, pub id: usize,
owner: Person, pub owner: Person,
name: String, pub name: String,
full_name: String, pub full_name: String,
description: String, pub description: String,
empty: bool, pub empty: bool,
private: bool, pub private: bool,
fork: bool, pub fork: bool,
template: bool, pub template: bool,
parent: Option<serde_json::Value>, pub parent: Option<serde_json::Value>,
mirror: bool, pub mirror: bool,
size: usize, pub size: usize,
html_url: String, pub html_url: String,
ssh_url: String, pub ssh_url: String,
clone_url: String, pub clone_url: String,
original_url: String, pub original_url: String,
website: String, pub website: String,
stars_count: usize, pub stars_count: usize,
forks_count: usize, pub forks_count: usize,
watchers_count: usize, pub watchers_count: usize,
open_issues_count: usize, pub open_issues_count: usize,
open_pr_counter: usize, pub open_pr_counter: usize,
release_counter: usize, pub release_counter: usize,
default_branch: String, pub default_branch: String,
archived: bool, pub archived: bool,
created_at: String, pub created_at: String,
updated_at: String, pub updated_at: String,
permissions: Permissions, pub permissions: Permissions,
has_issues: bool, pub has_issues: bool,
internal_tracker: InternalTracker, pub internal_tracker: InternalTracker,
has_wiki: bool, pub has_wiki: bool,
has_pull_requests: bool, pub has_pull_requests: bool,
has_projects: bool, pub has_projects: bool,
ignore_whitespace_conflicts: bool, pub ignore_whitespace_conflicts: bool,
allow_merge_commits: bool, pub allow_merge_commits: bool,
allow_rebase: bool, pub allow_rebase: bool,
allow_rebase_explicit: bool, pub allow_rebase_explicit: bool,
allow_squash_merge: bool, pub allow_squash_merge: bool,
default_merge_style: String, pub default_merge_style: String,
avatar_url: String, pub avatar_url: String,
internal: bool, pub internal: bool,
mirror_interval: String, pub mirror_interval: String,
mirror_updated: String, pub mirror_updated: String,
repo_transfer: Option<serde_json::Value>, pub repo_transfer: Option<serde_json::Value>,
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct WebhookPayload { pub struct WebhookPayload {
#[serde(rename(serialize = "ref", deserialize = "ref"))] #[serde(rename(serialize = "ref", deserialize = "ref"))]
reference: String, pub reference: String,
before: String, pub before: String,
after: String, pub after: String,
compare_url: String, pub compare_url: String,
repository: Repository, pub repository: Repository,
pusher: Person, pub pusher: Person,
sender: Person, pub sender: Person,
} }
impl Ctx { impl Ctx {
@ -168,10 +169,9 @@ impl Ctx {
&payload.repository.clone_url, &payload.repository.clone_url,
] { ] {
if self.db.site_with_repository_exists(url).await? { if self.db.site_with_repository_exists(url).await? {
let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes()) let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes())?;
.expect("HMAC can take key of any size");
mac.update(&body); mac.update(&body);
mac.verify_slice(&sig[..]).unwrap(); mac.verify_slice(&sig[..])?;
let site = self.db.get_site_from_repo_url(url).await?; let site = self.db.get_site_from_repo_url(url).await?;
if payload.reference.contains(&site.branch) { if payload.reference.contains(&site.branch) {
@ -195,6 +195,6 @@ impl Ctx {
"[webhook][forgejo/gitea] stray update from {} repository", "[webhook][forgejo/gitea] stray update from {} repository",
payload.repository.html_url payload.repository.html_url
); );
Ok(()) Err(ServiceError::WebsiteNotFound)
} }
} }

View File

@ -28,6 +28,8 @@ use argon2_creds::errors::CredsError;
use config::ConfigError as ConfigErrorInner; use config::ConfigError as ConfigErrorInner;
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use git2::Error as GitError; use git2::Error as GitError;
use hmac::digest::InvalidLength;
use hmac::digest::MacError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::ParseError; use url::ParseError;
@ -184,6 +186,20 @@ pub enum ServiceError {
WebhookNotFound, WebhookNotFound,
} }
impl From<InvalidLength> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(_: InvalidLength) -> ServiceError {
ServiceError::InternalServerError
}
}
impl From<MacError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(_: MacError) -> ServiceError {
ServiceError::WebhookNotFound
}
}
impl From<ParseError> for ServiceError { impl From<ParseError> for ServiceError {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn from(_: ParseError) -> ServiceError { fn from(_: ParseError) -> ServiceError {