use async_trait::async_trait;
use chrono::{DateTime, Utc};
#[cfg(not(target_arch = "wasm32"))]
use futures_util::future::BoxFuture;
#[cfg(target_arch = "wasm32")]
use futures_util::future::LocalBoxFuture;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use crate::error::Result;
use crate::runtime::{MaybeSend, MaybeSendSync};
use crate::state::{UploadMetadata, UploadState};
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait Hook: MaybeSendSync {
fn name(&self) -> &str;
fn events(&self) -> &[HookEvent];
async fn pre_hook(&self, _ctx: &HookContext) -> Result<PreHookResult> {
Ok(PreHookResult::proceed())
}
async fn post_hook(&self, _ctx: &HookContext) -> Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum HookEvent {
PreCreate,
PostCreate,
PreReceive,
PostReceive,
PreFinish,
PostFinish,
PreTerminate,
PostTerminate,
}
impl HookEvent {
pub fn all() -> &'static [HookEvent] {
&[
HookEvent::PreCreate,
HookEvent::PostCreate,
HookEvent::PreReceive,
HookEvent::PostReceive,
HookEvent::PreFinish,
HookEvent::PostFinish,
HookEvent::PreTerminate,
HookEvent::PostTerminate,
]
}
pub fn is_pre(&self) -> bool {
matches!(
self,
HookEvent::PreCreate
| HookEvent::PreReceive
| HookEvent::PreFinish
| HookEvent::PreTerminate
)
}
pub fn as_str(&self) -> &'static str {
match self {
HookEvent::PreCreate => "pre-create",
HookEvent::PostCreate => "post-create",
HookEvent::PreReceive => "pre-receive",
HookEvent::PostReceive => "post-receive",
HookEvent::PreFinish => "pre-finish",
HookEvent::PostFinish => "post-finish",
HookEvent::PreTerminate => "pre-terminate",
HookEvent::PostTerminate => "post-terminate",
}
}
}
impl std::fmt::Display for HookEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HookContext {
event: HookEvent,
upload: HookUpload,
request: HookRequestInfo,
}
impl HookContext {
pub fn new(event: HookEvent, upload: impl Into<HookUpload>, request: HookRequestInfo) -> Self {
Self {
event,
upload: upload.into(),
request,
}
}
#[must_use]
pub fn event(&self) -> HookEvent {
self.event
}
#[must_use]
pub fn upload(&self) -> &HookUpload {
&self.upload
}
#[must_use]
pub fn request(&self) -> &HookRequestInfo {
&self.request
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HookUpload {
id: String,
offset: u64,
length: Option<u64>,
created_at: DateTime<Utc>,
expires_at: Option<DateTime<Utc>>,
is_partial: bool,
is_final: bool,
parts: Option<Vec<String>>,
metadata: UploadMetadata,
}
impl HookUpload {
pub fn new(id: impl Into<String>) -> Self {
UploadState::new(id).into()
}
pub fn from_state(state: &UploadState) -> Self {
Self {
id: state.id().to_string(),
offset: state.offset(),
length: state.length(),
created_at: *state.created_at(),
expires_at: state.expires_at().copied(),
is_partial: state.is_partial(),
is_final: state.is_final(),
parts: state.parts().map(|parts| parts.to_vec()),
metadata: state.metadata().clone(),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn offset(&self) -> u64 {
self.offset
}
pub fn length(&self) -> Option<u64> {
self.length
}
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
pub fn expires_at(&self) -> Option<&DateTime<Utc>> {
self.expires_at.as_ref()
}
pub fn is_partial(&self) -> bool {
self.is_partial
}
pub fn is_final(&self) -> bool {
self.is_final
}
pub fn parts(&self) -> Option<&[String]> {
self.parts.as_deref()
}
pub fn metadata(&self) -> &UploadMetadata {
&self.metadata
}
pub fn is_complete(&self) -> bool {
match self.length {
Some(length) => self.offset >= length,
None => false,
}
}
pub fn is_expired(&self) -> bool {
crate::expiration::ProtocolExpiration::from_parts(
self.expires_at,
self.is_complete(),
self.is_partial,
)
.is_expired()
}
fn set_metadata(&mut self, metadata: UploadMetadata) {
self.metadata = metadata;
}
}
impl From<UploadState> for HookUpload {
fn from(state: UploadState) -> Self {
Self::from_state(&state)
}
}
impl From<&UploadState> for HookUpload {
fn from(state: &UploadState) -> Self {
Self::from_state(state)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HookRequestInfo {
pub method: String,
pub path: String,
#[serde(default)]
pub headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct PreHookResult {
proceed: bool,
metadata: Option<UploadMetadata>,
reject_status: Option<u16>,
reject_message: Option<String>,
response_headers: HashMap<String, String>,
}
impl PreHookResult {
#[must_use]
pub fn proceed() -> Self {
Self {
proceed: true,
..Default::default()
}
}
#[must_use]
pub fn proceed_with_metadata(metadata: impl Into<UploadMetadata>) -> Self {
Self::proceed().with_metadata(metadata)
}
#[must_use]
pub fn reject(status: u16, message: impl Into<String>) -> Self {
Self {
proceed: false,
reject_status: Some(status),
reject_message: Some(message.into()),
..Default::default()
}
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.response_headers.insert(name.into(), value.into());
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: impl Into<UploadMetadata>) -> Self {
self.metadata = Some(metadata.into());
self
}
#[must_use]
pub fn proceeds(&self) -> bool {
self.proceed
}
#[must_use]
pub fn metadata(&self) -> Option<&UploadMetadata> {
self.metadata.as_ref()
}
#[must_use]
pub fn reject_status(&self) -> Option<u16> {
self.reject_status
}
#[must_use]
pub fn reject_message(&self) -> Option<&str> {
self.reject_message.as_deref()
}
#[must_use]
pub fn response_headers(&self) -> &HashMap<String, String> {
&self.response_headers
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
pub trait HookExecutor: MaybeSendSync {
async fn execute_pre(&self, ctx: &HookContext) -> Result<PreHookResult>;
async fn execute_post(&self, ctx: &HookContext);
}
pub(crate) async fn execute_post_best_effort<H>(hooks: &H, ctx: &HookContext)
where
H: HookExecutor + ?Sized,
{
hooks.execute_post(ctx).await;
}
#[cfg(target_arch = "wasm32")]
#[allow(dead_code)]
fn assert_hook_chain_accepts_non_send_closure() {
use std::cell::Cell;
use std::rc::Rc;
let calls = Rc::new(Cell::new(0));
let _chain = HookChain::new().on_pre_create(move |_| {
let calls = Rc::clone(&calls);
async move {
calls.set(calls.get() + 1);
Ok(PreHookResult::proceed())
}
});
}
pub struct HookChain {
hooks: Vec<Arc<dyn Hook>>,
}
impl std::fmt::Debug for HookChain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names: Vec<&str> = self.hooks.iter().map(|hook| hook.name()).collect();
f.debug_struct("HookChain").field("hooks", &names).finish()
}
}
impl HookChain {
pub fn new() -> Self {
Self { hooks: Vec::new() }
}
#[must_use]
pub fn with_hook<H: Hook + 'static>(mut self, hook: H) -> Self {
self.hooks.push(Arc::new(hook));
self
}
#[must_use]
pub fn with_shared_hook(mut self, hook: Arc<dyn Hook>) -> Self {
self.hooks.push(hook);
self
}
}
impl Default for HookChain {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_arch = "wasm32"))]
type PreHookFut = BoxFuture<'static, Result<PreHookResult>>;
#[cfg(target_arch = "wasm32")]
type PreHookFut = LocalBoxFuture<'static, Result<PreHookResult>>;
#[cfg(not(target_arch = "wasm32"))]
type PostHookFut = BoxFuture<'static, Result<()>>;
#[cfg(target_arch = "wasm32")]
type PostHookFut = LocalBoxFuture<'static, Result<()>>;
#[cfg(not(target_arch = "wasm32"))]
type PreHookFn = Arc<dyn Fn(HookContext) -> PreHookFut + Send + Sync>;
#[cfg(target_arch = "wasm32")]
type PreHookFn = Arc<dyn Fn(HookContext) -> PreHookFut>;
#[cfg(not(target_arch = "wasm32"))]
type PostHookFn = Arc<dyn Fn(HookContext) -> PostHookFut + Send + Sync>;
#[cfg(target_arch = "wasm32")]
type PostHookFn = Arc<dyn Fn(HookContext) -> PostHookFut>;
pub struct FnHook {
name: String,
events: Vec<HookEvent>,
pre: Option<PreHookFn>,
post: Option<PostHookFn>,
}
impl std::fmt::Debug for FnHook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FnHook")
.field("name", &self.name)
.field("events", &self.events)
.field("has_pre", &self.pre.is_some())
.field("has_post", &self.post.is_some())
.finish()
}
}
impl FnHook {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
events: Vec::new(),
pre: None,
post: None,
}
}
#[must_use]
pub fn for_event(mut self, event: HookEvent) -> Self {
if !self.events.contains(&event) {
self.events.push(event);
}
self
}
#[must_use]
pub fn for_events(mut self, events: impl IntoIterator<Item = HookEvent>) -> Self {
for event in events {
if !self.events.contains(&event) {
self.events.push(event);
}
}
self
}
#[must_use]
pub fn with_pre<F, Fut>(mut self, f: F) -> Self
where
F: Fn(HookContext) -> Fut + MaybeSendSync + 'static,
Fut: std::future::Future<Output = Result<PreHookResult>> + MaybeSend + 'static,
{
self.pre = Some(Arc::new(move |ctx| Box::pin(f(ctx))));
self
}
#[must_use]
pub fn with_post<F, Fut>(mut self, f: F) -> Self
where
F: Fn(HookContext) -> Fut + MaybeSendSync + 'static,
Fut: std::future::Future<Output = Result<()>> + MaybeSend + 'static,
{
self.post = Some(Arc::new(move |ctx| Box::pin(f(ctx))));
self
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl Hook for FnHook {
fn name(&self) -> &str {
&self.name
}
fn events(&self) -> &[HookEvent] {
&self.events
}
async fn pre_hook(&self, ctx: &HookContext) -> Result<PreHookResult> {
match &self.pre {
Some(f) => f(ctx.clone()).await,
None => Ok(PreHookResult::proceed()),
}
}
async fn post_hook(&self, ctx: &HookContext) -> Result<()> {
match &self.post {
Some(f) => f(ctx.clone()).await,
None => Ok(()),
}
}
}
macro_rules! hook_chain_on_methods {
(
$( pre $pre_name:ident => $pre_event:ident ),* $(,)?
;
$( post $post_name:ident => $post_event:ident ),* $(,)?
) => {
impl HookChain {
$(
#[doc = concat!("Registers a closure to run on [`HookEvent::", stringify!($pre_event), "`].")]
#[must_use]
pub fn $pre_name<F, Fut>(self, f: F) -> Self
where
F: Fn(HookContext) -> Fut + MaybeSendSync + 'static,
Fut: std::future::Future<Output = Result<PreHookResult>> + MaybeSend + 'static,
{
self.with_hook(
FnHook::new(stringify!($pre_name))
.for_event(HookEvent::$pre_event)
.with_pre(f),
)
}
)*
$(
#[doc = concat!("Registers a closure to run on [`HookEvent::", stringify!($post_event), "`].")]
#[must_use]
pub fn $post_name<F, Fut>(self, f: F) -> Self
where
F: Fn(HookContext) -> Fut + MaybeSendSync + 'static,
Fut: std::future::Future<Output = Result<()>> + MaybeSend + 'static,
{
self.with_hook(
FnHook::new(stringify!($post_name))
.for_event(HookEvent::$post_event)
.with_post(f),
)
}
)*
}
};
}
hook_chain_on_methods! {
pre on_pre_create => PreCreate,
pre on_pre_receive => PreReceive,
pre on_pre_finish => PreFinish,
pre on_pre_terminate => PreTerminate
;
post on_post_create => PostCreate,
post on_post_receive => PostReceive,
post on_post_finish => PostFinish,
post on_post_terminate => PostTerminate,
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl HookExecutor for HookChain {
async fn execute_pre(&self, ctx: &HookContext) -> Result<PreHookResult> {
let mut result = PreHookResult::proceed();
let mut current_upload = ctx.upload.clone();
let mut metadata_changed = false;
for hook in &self.hooks {
if !hook.events().contains(&ctx.event) {
continue;
}
if !ctx.event.is_pre() {
continue;
}
let hook_ctx = HookContext {
event: ctx.event,
upload: current_upload.clone(),
request: ctx.request.clone(),
};
let hook_result = hook.pre_hook(&hook_ctx).await?;
for (k, v) in hook_result.response_headers {
result.response_headers.insert(k, v);
}
if !hook_result.proceed {
return Ok(PreHookResult {
proceed: false,
metadata: None,
reject_status: hook_result.reject_status,
reject_message: hook_result.reject_message,
response_headers: result.response_headers,
});
}
if ctx.event.allows_metadata_replacement()
&& let Some(metadata) = hook_result.metadata
{
current_upload.set_metadata(metadata);
metadata_changed = true;
}
}
if metadata_changed {
result.metadata = Some(current_upload.metadata().clone());
}
Ok(result)
}
async fn execute_post(&self, ctx: &HookContext) {
for hook in &self.hooks {
if !hook.events().contains(&ctx.event) {
continue;
}
if ctx.event.is_pre() {
continue;
}
if let Err(e) = hook.post_hook(ctx).await {
tracing::warn!(
hook = hook.name(),
event = ctx.event.as_str(),
error = %e,
"post-hook failed"
);
}
}
}
}
impl HookEvent {
fn allows_metadata_replacement(self) -> bool {
matches!(self, HookEvent::PreCreate | HookEvent::PreReceive)
}
}
#[derive(Debug, Clone, Default)]
pub struct NoopHookExecutor;
impl NoopHookExecutor {
pub fn new() -> Self {
Self
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl HookExecutor for NoopHookExecutor {
async fn execute_pre(&self, _ctx: &HookContext) -> Result<PreHookResult> {
Ok(PreHookResult::proceed())
}
async fn execute_post(&self, _ctx: &HookContext) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::StorageHandle;
struct TestHook {
name: String,
events: Vec<HookEvent>,
reject: bool,
}
impl TestHook {
fn new(name: &str, events: Vec<HookEvent>) -> Self {
Self {
name: name.to_string(),
events,
reject: false,
}
}
fn rejecting(name: &str, events: Vec<HookEvent>) -> Self {
Self {
name: name.to_string(),
events,
reject: true,
}
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl Hook for TestHook {
fn name(&self) -> &str {
&self.name
}
fn events(&self) -> &[HookEvent] {
&self.events
}
async fn pre_hook(&self, _ctx: &HookContext) -> Result<PreHookResult> {
if self.reject {
Ok(PreHookResult::reject(403, "Rejected by test hook"))
} else {
Ok(PreHookResult::proceed())
}
}
async fn post_hook(&self, _ctx: &HookContext) -> Result<()> {
Ok(())
}
}
fn make_context(event: HookEvent) -> HookContext {
HookContext::new(
event,
UploadState::new("test-id"),
HookRequestInfo::default(),
)
}
#[test]
fn hook_upload_serialization_hides_storage_facts() {
let mut state = UploadState::new("test-id").with_length(5);
let mut handle = StorageHandle::new("storage-secret");
handle.set_internal("backend-upload-id", "internal-secret");
state.set_storage_handle(handle);
let ctx = HookContext::new(HookEvent::PreCreate, state, HookRequestInfo::default());
let json = serde_json::to_value(&ctx).unwrap();
let serialized = json.to_string();
assert_eq!(json["upload"]["id"], "test-id");
assert!(json["upload"].get("storage_key").is_none());
assert!(json["upload"].get("internal").is_none());
assert!(!serialized.contains("storage-secret"));
assert!(!serialized.contains("internal-secret"));
}
#[test]
fn hook_upload_expiration_matches_completed_upload_policy() {
let expired_at = Utc::now() - chrono::Duration::minutes(1);
let mut completed = UploadState::new("completed")
.with_length(5)
.with_expiration(expired_at);
completed.set_offset(5);
let mut completed_partial = UploadState::new("completed-partial")
.with_length(5)
.with_expiration(expired_at)
.with_partial();
completed_partial.set_offset(5);
assert!(!HookUpload::from_state(&completed).is_expired());
assert!(HookUpload::from_state(&completed_partial).is_expired());
}
#[tokio::test]
async fn test_hook_chain_empty() {
let chain = HookChain::new();
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
}
#[tokio::test]
async fn test_hook_chain_proceed() {
let chain = HookChain::new()
.with_hook(TestHook::new("hook1", vec![HookEvent::PreCreate]))
.with_hook(TestHook::new("hook2", vec![HookEvent::PreCreate]));
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
}
#[tokio::test]
async fn test_hook_chain_reject() {
let chain = HookChain::new()
.with_hook(TestHook::new("hook1", vec![HookEvent::PreCreate]))
.with_hook(TestHook::rejecting("hook2", vec![HookEvent::PreCreate]));
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(!result.proceed);
assert_eq!(result.reject_status, Some(403));
}
#[tokio::test]
async fn test_hook_event_filtering() {
let chain =
HookChain::new().with_hook(TestHook::rejecting("hook1", vec![HookEvent::PreReceive]));
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed); }
#[tokio::test]
async fn test_noop_executor() {
let executor = NoopHookExecutor::new();
let ctx = make_context(HookEvent::PreCreate);
let result = executor.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
assert!(result.metadata.is_none());
executor.execute_post(&ctx).await;
}
#[test]
fn test_hook_event_is_pre() {
assert!(HookEvent::PreCreate.is_pre());
assert!(HookEvent::PreReceive.is_pre());
assert!(HookEvent::PreFinish.is_pre());
assert!(HookEvent::PreTerminate.is_pre());
assert!(!HookEvent::PostCreate.is_pre());
assert!(!HookEvent::PostReceive.is_pre());
assert!(!HookEvent::PostFinish.is_pre());
assert!(!HookEvent::PostTerminate.is_pre());
}
#[test]
fn test_pre_hook_result_builders() {
let proceed = PreHookResult::proceed();
assert!(proceed.proceed);
assert!(proceed.metadata.is_none());
let mut metadata = UploadMetadata::new();
metadata.insert("filename", "test.txt");
let proceed_with_metadata = PreHookResult::proceed_with_metadata(metadata);
assert!(proceed_with_metadata.proceed);
assert!(proceed_with_metadata.metadata.is_some());
let reject = PreHookResult::reject(400, "Bad request");
assert!(!reject.proceed);
assert_eq!(reject.reject_status, Some(400));
assert_eq!(reject.reject_message, Some("Bad request".to_string()));
}
use std::sync::atomic::{AtomicUsize, Ordering};
#[tokio::test]
async fn on_pre_create_runs_closure() {
let chain = HookChain::new()
.on_pre_create(|_ctx| async { Ok(PreHookResult::reject(418, "teapot")) });
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(!result.proceed);
assert_eq!(result.reject_status, Some(418));
}
#[tokio::test]
async fn on_pre_create_ignores_other_events() {
let chain = HookChain::new()
.on_pre_create(|_ctx| async { Ok(PreHookResult::reject(500, "should not fire")) });
let ctx = make_context(HookEvent::PreReceive);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
}
#[tokio::test]
async fn on_post_finish_runs_closure() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let chain = HookChain::new().on_post_finish(move |_ctx| {
let c = counter_clone.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
Ok(())
}
});
let ctx = make_context(HookEvent::PostFinish);
chain.execute_post(&ctx).await;
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn chain_mixes_closure_and_trait_hooks() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let chain = HookChain::new()
.with_hook(TestHook::new("trait-hook", vec![HookEvent::PreCreate]))
.on_pre_create(move |_ctx| {
let c = counter_clone.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
Ok(PreHookResult::proceed())
}
});
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn pre_hook_closure_can_replace_metadata() {
let chain = HookChain::new().on_pre_create(|ctx| async move {
let mut metadata = ctx.upload.metadata().clone();
metadata.insert("injected", "yes");
Ok(PreHookResult::proceed_with_metadata(metadata))
});
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
let updated = result.metadata.expect("metadata should be carried through");
assert_eq!(
updated.get("injected").and_then(|v| v.as_str()),
Some("yes")
);
}
#[tokio::test]
async fn metadata_replacement_is_visible_to_later_pre_hooks() {
let chain = HookChain::new()
.on_pre_create(|ctx| async move {
assert!(ctx.upload.metadata().get("injected").is_none());
let mut metadata = ctx.upload.metadata().clone();
metadata.insert("injected", "yes");
Ok(PreHookResult::proceed_with_metadata(metadata))
})
.on_pre_create(|ctx| async move {
assert_eq!(
ctx.upload
.metadata()
.get("injected")
.and_then(|v| v.as_str()),
Some("yes")
);
Ok(PreHookResult::proceed())
});
let ctx = make_context(HookEvent::PreCreate);
let result = chain.execute_pre(&ctx).await.unwrap();
assert_eq!(
result
.metadata
.unwrap()
.get("injected")
.and_then(|v| v.as_str()),
Some("yes")
);
}
#[tokio::test]
async fn metadata_replacement_is_ignored_for_gate_only_pre_hooks() {
let chain = HookChain::new()
.on_pre_finish(|ctx| async move {
assert!(ctx.upload.metadata().get("uncommitted").is_none());
let mut metadata = ctx.upload.metadata().clone();
metadata.insert("uncommitted", "yes");
Ok(PreHookResult::proceed_with_metadata(metadata))
})
.on_pre_finish(|ctx| async move {
assert!(ctx.upload.metadata().get("uncommitted").is_none());
Ok(PreHookResult::proceed())
});
let ctx = make_context(HookEvent::PreFinish);
let result = chain.execute_pre(&ctx).await.unwrap();
assert!(result.proceed);
assert!(result.metadata.is_none());
}
#[tokio::test]
async fn multiple_closures_for_same_event_run_in_order() {
let counter = Arc::new(AtomicUsize::new(0));
let c1 = counter.clone();
let c2 = counter.clone();
let chain = HookChain::new()
.on_post_create(move |_ctx| {
let c = c1.clone();
async move {
assert_eq!(c.load(Ordering::SeqCst), 0);
c.store(1, Ordering::SeqCst);
Ok(())
}
})
.on_post_create(move |_ctx| {
let c = c2.clone();
async move {
assert_eq!(c.load(Ordering::SeqCst), 1);
c.store(2, Ordering::SeqCst);
Ok(())
}
});
let ctx = make_context(HookEvent::PostCreate);
chain.execute_post(&ctx).await;
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn fn_hook_for_events_subscribes_multiple() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let hook = FnHook::new("multi")
.for_events([HookEvent::PostCreate, HookEvent::PostFinish])
.with_post(move |_ctx| {
let c = counter_clone.clone();
async move {
c.fetch_add(1, Ordering::SeqCst);
Ok(())
}
});
let chain = HookChain::new().with_hook(hook);
chain
.execute_post(&make_context(HookEvent::PostCreate))
.await;
chain
.execute_post(&make_context(HookEvent::PostFinish))
.await;
chain
.execute_post(&make_context(HookEvent::PostReceive))
.await;
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
}