Skip to content

Commit

Permalink
Add delete_after_n method to When
Browse files Browse the repository at this point in the history
In situations where a component is expected to automatically retry an
endpoint manually calling `delete` on a `Mock` may not be an option.

To support this scenario, add a new `delete_after_n` method to `When`
that will remove the `Mock` from the server after it has matched the
specified number of calls.

Related to alexliesenfeld#76
Related to alexliesenfeld#96
  • Loading branch information
wfchandler committed Jul 25, 2024
1 parent 45072c9 commit fa1632d
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 3 deletions.
34 changes: 34 additions & 0 deletions src/api/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,40 @@ impl When {
self
}

/// Sets the number of requests the mock should respond to before self-destructing.
///
/// * `count` - The number of requests to respond to.
///
/// Note: the mock will no longer be available to assert against when the request
/// limit is hit.
///
/// ```
/// use httpmock::prelude::*;
/// use isahc::{prelude::*, Request};
///
/// let server = MockServer::start();
///
/// let mock = server.mock(|when, then|{
/// when.path("/test").delete_after_n(1);
/// then.status(202);
/// });
///
/// // Send a first request which deletes the mock from the server, then send another request.
/// let response1 = isahc::get(server.url("/test")).unwrap();
///
/// let response2 = isahc::get(server.url("/test")).unwrap();
///
/// // Assert
/// assert_eq!(response1.status(), 202);
/// assert_eq!(response2.status(), 404);
/// ```
pub fn delete_after_n(mut self, count: usize) -> Self {
update_cell(&self.expectations, |e| {
e.delete_after_n = Some(count);
});
self
}

/// Sets the required HTTP request body content.
///
/// * `body` - The required HTTP request body.
Expand Down
7 changes: 7 additions & 0 deletions src/common/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ pub struct RequestRequirements {
pub query_param: Option<Vec<(String, String)>>,
pub x_www_form_urlencoded_key_exists: Option<Vec<String>>,
pub x_www_form_urlencoded: Option<Vec<(String, String)>>,
pub delete_after_n: Option<usize>,

#[serde(skip_serializing, skip_deserializing)]
pub matchers: Option<Vec<MockMatcherFunction>>,
Expand Down Expand Up @@ -221,6 +222,7 @@ impl RequestRequirements {
query_param: None,
x_www_form_urlencoded: None,
x_www_form_urlencoded_key_exists: None,
delete_after_n: None,
matchers: None,
}
}
Expand Down Expand Up @@ -299,6 +301,11 @@ impl RequestRequirements {
self.query_param = Some(arg);
self
}

pub fn with_delete_after_n(mut self, arg: usize) -> Self {
self.delete_after_n = Some(arg);
self
}
}

/// A Request that is made to set a new mock.
Expand Down
57 changes: 54 additions & 3 deletions src/server/web/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,23 @@ pub(crate) fn find_mock(
let mock = mocks.get_mut(&found_id).unwrap();
mock.call_counter += 1;

return Ok(Some(mock.definition.response.clone()));
let response = mock.definition.response.clone();

if let Some(ct) = mock.definition.request.delete_after_n {
if mock.call_counter >= ct {
log::debug!(
"Deleting mock with id={} after receiving {} calls",
found_id,
mock.call_counter
);
if mock.is_static {
return Err(format!("Cannot delete static mock with ID {}", found_id));
}
mocks.remove(&found_id);
}
}

return Ok(Some(response));
}

log::debug!(
Expand Down Expand Up @@ -260,10 +276,11 @@ mod test {
use regex::Regex;

use crate::common::data::{
HttpMockRequest, MockDefinition, MockServerHttpResponse, Pattern, RequestRequirements,
ActiveMock, HttpMockRequest, MockDefinition, MockServerHttpResponse, Pattern,
RequestRequirements,
};
use crate::server::web::handlers::{
add_new_mock, read_one_mock, request_matches, validate_mock_definition, verify,
add_new_mock, find_mock, read_one_mock, request_matches, validate_mock_definition, verify,
};
use crate::server::MockServerState;
use crate::Method;
Expand Down Expand Up @@ -675,6 +692,40 @@ mod test {
assert_eq!(result2, true);
}

/// This test checks if matching "delete_after_n" is working as expected.
#[test]
fn delete_after_n_test() {
// Arrange
let state = MockServerState::default();

let req = RequestRequirements::new()
.with_path("/test-path".to_string())
.with_delete_after_n(1);

let res = MockServerHttpResponse {
body: None,
delay: None,
status: Some(200),
headers: None,
};

let mock_def = MockDefinition::new(req.clone(), res);

add_new_mock(&state, mock_def, false).unwrap();

let mock = HttpMockRequest::new("GET".to_string(), "/test-path".to_string());

// Act
let does_match = request_matches(&MockServerState::default(), Arc::new(mock.clone()), &req);
let first_run = find_mock(&state, mock.clone()).unwrap();
let second_run = find_mock(&state, mock.clone()).unwrap();

// Assert
assert_eq!(true, does_match);
assert_eq!(true, first_run.is_some());
assert_eq!(true, second_run.is_none());
}

/// This test checks if matching "path_matches" is working as expected.
#[test]
fn not_match_path_matches_test() {
Expand Down
2 changes: 2 additions & 0 deletions src/standalone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct YAMLRequestRequirements {
pub query_param: Option<Vec<NameValuePair>>,
pub x_www_form_urlencoded_key_exists: Option<Vec<String>>,
pub x_www_form_urlencoded_tuple: Option<Vec<NameValuePair>>,
pub delete_after_n: Option<usize>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -131,6 +132,7 @@ fn map_to_mock_definition(yaml_definition: YAMLMockDefinition) -> MockDefinition
query_param: to_pair_vec(yaml_definition.when.query_param),
x_www_form_urlencoded: to_pair_vec(yaml_definition.when.x_www_form_urlencoded_tuple),
x_www_form_urlencoded_key_exists: yaml_definition.when.x_www_form_urlencoded_key_exists,
delete_after_n: yaml_definition.when.delete_after_n,
matchers: None,
},
response: MockServerHttpResponse {
Expand Down

0 comments on commit fa1632d

Please sign in to comment.