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)]
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::tests;
use crate::*;
@ -153,6 +155,7 @@ mod tests {
let (_dir, ctx) = tests::get_ctx().await;
let _ = ctx.delete_user(NAME, 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 app = get_app!(ctx).await;
@ -185,6 +188,95 @@ mod tests {
check_status!(list_all_webhooks_resp, StatusCode::OK);
let hooks: Vec<GiteaWebhook> =
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 crate::ctx::Ctx;
use crate::errors::ServiceError;
use crate::errors::ServiceResult;
type HmacSha256 = Hmac<Sha256>;
pub type HmacSha256 = Hmac<Sha256>;
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct CommitPerson {
name: String,
email: String,
username: String,
pub name: String,
pub email: String,
pub username: String,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
pub struct Commit {
id: String,
message: String,
url: String,
author: CommitPerson,
committer: CommitPerson,
verification: serde_json::Value,
timestamp: String,
added: serde_json::Value,
removed: serde_json::Value,
modified: serde_json::Value,
pub id: String,
pub message: String,
pub url: String,
pub author: CommitPerson,
pub committer: CommitPerson,
pub verification: serde_json::Value,
pub timestamp: String,
pub added: serde_json::Value,
pub removed: serde_json::Value,
pub modified: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Person {
id: usize,
login: String,
full_name: String,
email: String,
avatar_url: String,
language: String,
is_admin: bool,
last_login: String,
created: String,
restricted: bool,
active: bool,
prohibit_login: bool,
location: String,
website: String,
description: String,
visibility: String,
followers_count: usize,
following_count: usize,
starred_repos_count: usize,
username: String,
pub id: usize,
pub login: String,
pub full_name: String,
pub email: String,
pub avatar_url: String,
pub language: String,
pub is_admin: bool,
pub last_login: String,
pub created: String,
pub restricted: bool,
pub active: bool,
pub prohibit_login: bool,
pub location: String,
pub website: String,
pub description: String,
pub visibility: String,
pub followers_count: usize,
pub following_count: usize,
pub starred_repos_count: usize,
pub username: String,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Permissions {
admin: bool,
push: bool,
pull: bool,
pub admin: bool,
pub push: bool,
pub pull: bool,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct InternalTracker {
enable_time_tracker: bool,
allow_only_contributors_to_track_time: bool,
enable_issue_dependencies: bool,
pub enable_time_tracker: bool,
pub allow_only_contributors_to_track_time: bool,
pub enable_issue_dependencies: bool,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Repository {
id: usize,
owner: Person,
name: String,
full_name: String,
description: String,
empty: bool,
private: bool,
fork: bool,
template: bool,
parent: Option<serde_json::Value>,
mirror: bool,
size: usize,
html_url: String,
ssh_url: String,
clone_url: String,
original_url: String,
website: String,
stars_count: usize,
forks_count: usize,
watchers_count: usize,
open_issues_count: usize,
open_pr_counter: usize,
release_counter: usize,
default_branch: String,
archived: bool,
created_at: String,
updated_at: String,
permissions: Permissions,
has_issues: bool,
internal_tracker: InternalTracker,
has_wiki: bool,
has_pull_requests: bool,
has_projects: bool,
ignore_whitespace_conflicts: bool,
allow_merge_commits: bool,
allow_rebase: bool,
allow_rebase_explicit: bool,
allow_squash_merge: bool,
default_merge_style: String,
avatar_url: String,
internal: bool,
mirror_interval: String,
mirror_updated: String,
repo_transfer: Option<serde_json::Value>,
pub id: usize,
pub owner: Person,
pub name: String,
pub full_name: String,
pub description: String,
pub empty: bool,
pub private: bool,
pub fork: bool,
pub template: bool,
pub parent: Option<serde_json::Value>,
pub mirror: bool,
pub size: usize,
pub html_url: String,
pub ssh_url: String,
pub clone_url: String,
pub original_url: String,
pub website: String,
pub stars_count: usize,
pub forks_count: usize,
pub watchers_count: usize,
pub open_issues_count: usize,
pub open_pr_counter: usize,
pub release_counter: usize,
pub default_branch: String,
pub archived: bool,
pub created_at: String,
pub updated_at: String,
pub permissions: Permissions,
pub has_issues: bool,
pub internal_tracker: InternalTracker,
pub has_wiki: bool,
pub has_pull_requests: bool,
pub has_projects: bool,
pub ignore_whitespace_conflicts: bool,
pub allow_merge_commits: bool,
pub allow_rebase: bool,
pub allow_rebase_explicit: bool,
pub allow_squash_merge: bool,
pub default_merge_style: String,
pub avatar_url: String,
pub internal: bool,
pub mirror_interval: String,
pub mirror_updated: String,
pub repo_transfer: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct WebhookPayload {
#[serde(rename(serialize = "ref", deserialize = "ref"))]
reference: String,
before: String,
after: String,
compare_url: String,
repository: Repository,
pusher: Person,
sender: Person,
pub reference: String,
pub before: String,
pub after: String,
pub compare_url: String,
pub repository: Repository,
pub pusher: Person,
pub sender: Person,
}
impl Ctx {
@ -168,10 +169,9 @@ impl Ctx {
&payload.repository.clone_url,
] {
if self.db.site_with_repository_exists(url).await? {
let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes())
.expect("HMAC can take key of any size");
let mut mac = HmacSha256::new_from_slice(hook.gitea_webhook_secret.as_bytes())?;
mac.update(&body);
mac.verify_slice(&sig[..]).unwrap();
mac.verify_slice(&sig[..])?;
let site = self.db.get_site_from_repo_url(url).await?;
if payload.reference.contains(&site.branch) {
@ -195,6 +195,6 @@ impl Ctx {
"[webhook][forgejo/gitea] stray update from {} repository",
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 derive_more::{Display, Error};
use git2::Error as GitError;
use hmac::digest::InvalidLength;
use hmac::digest::MacError;
use serde::{Deserialize, Serialize};
use url::ParseError;
@ -184,6 +186,20 @@ pub enum ServiceError {
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 {
#[cfg(not(tarpaulin_include))]
fn from(_: ParseError) -> ServiceError {