feat: test webhooks
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
0c6199494b
commit
d919bad570
3 changed files with 206 additions and 98 deletions
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue