Skip to content

Commit

Permalink
test: add unit tests for after, error, and finally hooks
Browse files Browse the repository at this point in the history
Signed-off-by: Maxim Fischuk <[email protected]>
  • Loading branch information
MaximFischuk authored and MaximusFk committed Dec 17, 2024
1 parent 1a0d540 commit 0b6edcc
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 3 deletions.
4 changes: 2 additions & 2 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,13 +396,13 @@ impl Client {
Ok(None) => { /* INFO: just continue execution */ }
Err(error) => {
drop(invoke_hook_context);
context.merge_missing(&hook_context.evaluation_context);
context.merge_missing(hook_context.evaluation_context);
return (context, Err(error));
}
}
}

context.merge_missing(&hook_context.evaluation_context);
context.merge_missing(hook_context.evaluation_context);
(context, Ok(()))
}

Expand Down
237 changes: 236 additions & 1 deletion src/hooks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ pub use logging::LoggingHook;
/// They operate similarly to middleware in many web frameworks.
///
/// https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md
#[cfg_attr(feature = "test-util", mockall::automock)] // Specified lifetimes manually to make it work with mockall
#[cfg_attr(
feature = "test-util",
mockall::automock,
allow(clippy::ref_option_ref)
)] // Specified lifetimes manually to make it work with mockall
#[async_trait::async_trait]
pub trait Hook: Send + Sync + 'static {
/// This method is called before the flag evaluation.
Expand Down Expand Up @@ -347,6 +351,191 @@ mod tests {
assert!(result.is_ok());
}

#[spec(
number = "4.3.6",
text = "The after stage MUST run after flag resolution occurs. It accepts a hook context (required), evaluation details (required) and hook hints (optional). It has no return value."
)]
#[tokio::test]
async fn after_hook() {
let mut mock_hook = MockHook::new();

let mut api = OpenFeature::singleton_mut().await;
let mut client = api.create_client();
let mut mock_provider = MockFeatureProvider::default();

let mut seq = mockall::Sequence::new();

mock_provider.expect_hooks().return_const(vec![]);
mock_provider.expect_initialize().return_const(());
mock_provider
.expect_resolve_bool_value()
.once()
.in_sequence(&mut seq)
.return_const(Ok(ResolutionDetails::new(true)));

api.set_provider(mock_provider).await;
drop(api);

mock_hook.expect_before().returning(|_, _| Ok(None));

mock_hook
.expect_after()
.once()
.in_sequence(&mut seq)
.return_const(Ok(()));

mock_hook.expect_finally().return_const(());

// evaluation
client = client.with_hook(mock_hook);

let flag_key = "flag";
let eval_ctx = EvaluationContext::default().with_custom_field("is", "a test");

let result = client.get_bool_value(flag_key, Some(&eval_ctx), None).await;

assert!(result.is_ok());
}

#[spec(
number = "4.3.7",
text = "The error hook MUST run when errors are encountered in the before stage, the after stage or during flag resolution. It accepts hook context (required), exception representing what went wrong (required), and hook hints (optional). It has no return value."
)]
#[tokio::test]
async fn error_hook() {
// error on `before` hook
{
let mut mock_hook = MockHook::new();

let mut api = OpenFeature::singleton_mut().await;
let mut client = api.create_client();
let mut mock_provider = MockFeatureProvider::default();

let mut seq = mockall::Sequence::new();

mock_provider.expect_hooks().return_const(vec![]);
mock_provider.expect_initialize().return_const(());
mock_provider.expect_resolve_bool_value().never();

api.set_provider(mock_provider).await;
drop(api);

mock_hook.expect_before().returning(|_, _| error());

mock_hook
.expect_error()
.once()
.in_sequence(&mut seq)
.return_const(());

mock_hook.expect_finally().return_const(());

// evaluation
client = client.with_hook(mock_hook);

let flag_key = "flag";
let eval_ctx = EvaluationContext::default().with_custom_field("is", "a test");

let result = client.get_bool_value(flag_key, Some(&eval_ctx), None).await;

assert!(result.is_err());
}

// error on evaluation
{
let mut mock_hook = MockHook::new();

let mut api = OpenFeature::singleton_mut().await;
let mut client = api.create_client();
let mut mock_provider = MockFeatureProvider::default();

let mut seq = mockall::Sequence::new();

mock_provider.expect_hooks().return_const(vec![]);
mock_provider.expect_initialize().return_const(());

mock_hook.expect_before().returning(|_, _| Ok(None));

mock_provider
.expect_resolve_bool_value()
.once()
.in_sequence(&mut seq)
.return_const(error());

mock_hook
.expect_error()
.once()
.in_sequence(&mut seq)
.return_const(());

mock_hook.expect_finally().return_const(());

api.set_provider(mock_provider).await;
drop(api);

// evaluation
client = client.with_hook(mock_hook);

let flag_key = "flag";
let eval_ctx = EvaluationContext::default().with_custom_field("is", "a test");

let result = client.get_bool_value(flag_key, Some(&eval_ctx), None).await;

assert!(result.is_err());
}
}

#[spec(
number = "4.3.8",
text = "The finally hook MUST run after the before, after, and error stages. It accepts a hook context (required), evaluation details (required) and hook hints (optional). It has no return value."
)]
#[tokio::test]
async fn finally_hook() {
let mut mock_hook = MockHook::new();

let mut api = OpenFeature::singleton_mut().await;
let mut client = api.create_client();
let mut mock_provider = MockFeatureProvider::default();

let mut seq = mockall::Sequence::new();

mock_provider.expect_hooks().return_const(vec![]);
mock_provider.expect_initialize().return_const(());
mock_provider
.expect_resolve_bool_value()
.return_const(Ok(ResolutionDetails::new(true)));

api.set_provider(mock_provider).await;
drop(api);

mock_hook
.expect_before()
.once()
.in_sequence(&mut seq)
.returning(|_, _| Ok(None));
mock_hook
.expect_after()
.once()
.in_sequence(&mut seq)
.return_const(Ok(()));

mock_hook
.expect_finally()
.once()
.in_sequence(&mut seq)
.return_const(());

// evaluation
client = client.with_hook(mock_hook);

let flag_key = "flag";
let eval_ctx = EvaluationContext::default().with_custom_field("is", "a test");

let result = client.get_bool_value(flag_key, Some(&eval_ctx), None).await;

assert!(result.is_ok());
}

#[spec(
number = "4.4.1",
text = "The API, Client, Provider, and invocation MUST have a method for registering hooks."
Expand Down Expand Up @@ -468,6 +657,52 @@ mod tests {
#[test]
fn error_hook_not_throw_checked_by_type_system() {}

#[spec(
number = "4.4.5",
text = "If an error occurs in the before or after hooks, the error hooks MUST be invoked."
)]
#[tokio::test]
async fn error_hook_invoked_on_error() {
let mut mock_hook = MockHook::new();

let mut api = OpenFeature::singleton_mut().await;
let mut client = api.create_client();
let mut mock_provider = MockFeatureProvider::default();

let mut seq = mockall::Sequence::new();

mock_provider.expect_hooks().return_const(vec![]);
mock_provider.expect_initialize().return_const(());
mock_provider.expect_resolve_bool_value().never();

api.set_provider(mock_provider).await;
drop(api);

mock_hook
.expect_before()
.once()
.in_sequence(&mut seq)
.returning(|_, _| error());

mock_hook
.expect_error()
.once()
.in_sequence(&mut seq)
.return_const(());

mock_hook.expect_finally().return_const(());

// evaluation
client = client.with_hook(mock_hook);

let flag_key = "flag";
let eval_ctx = EvaluationContext::default().with_custom_field("is", "a test");

let result = client.get_bool_value(flag_key, Some(&eval_ctx), None).await;

assert!(result.is_err());
}

#[spec(
number = "4.4.6",
text = "If an error occurs during the evaluation of before or after hooks, any remaining hooks in the before or after stages MUST NOT be invoked."
Expand Down

0 comments on commit 0b6edcc

Please sign in to comment.