Skip to main content

wae_testing/
service_mock.rs

1//! 外部服务 Mock 工具模块
2//!
3//! 提供 MockExternalService 结构体,用于模拟外部 HTTP 服务,支持请求期望配置、调用记录和验证。
4
5use crate::mock::{Mock, MockCall};
6use bytes::Bytes;
7use http::{HeaderMap, Method, StatusCode};
8use parking_lot::RwLock;
9use std::{collections::HashMap, sync::Arc};
10use wae_types::{WaeError, WaeErrorKind, WaeResult as TestingResult};
11
12/// 外部服务请求记录
13#[derive(Debug, Clone)]
14pub struct ServiceRequest {
15    /// HTTP 方法
16    pub method: Method,
17    /// 请求路径
18    pub path: String,
19    /// 请求头
20    pub headers: HeaderMap,
21    /// 请求体 (可选)
22    pub body: Option<Bytes>,
23    /// 请求时间戳
24    pub timestamp: std::time::Instant,
25}
26
27/// 外部服务响应
28#[derive(Debug, Clone)]
29pub struct ServiceResponse {
30    /// 状态码
31    pub status: StatusCode,
32    /// 响应头
33    pub headers: HeaderMap,
34    /// 响应体
35    pub body: Bytes,
36}
37
38impl ServiceResponse {
39    /// 创建成功响应
40    pub fn ok(body: impl Into<Bytes>) -> Self {
41        Self { status: StatusCode::OK, headers: HeaderMap::new(), body: body.into() }
42    }
43
44    /// 创建错误响应
45    pub fn error(status: StatusCode, body: impl Into<Bytes>) -> Self {
46        Self { status, headers: HeaderMap::new(), body: body.into() }
47    }
48
49    /// 添加响应头
50    pub fn header<K, V>(mut self, key: K, value: V) -> Self
51    where
52        K: TryInto<http::HeaderName>,
53        V: TryInto<http::HeaderValue>,
54    {
55        if let (Ok(key), Ok(value)) = (key.try_into(), value.try_into()) {
56            self.headers.append(key, value);
57        }
58        self
59    }
60}
61
62/// 服务请求匹配规则
63#[derive(Clone)]
64pub enum ServiceMatchRule {
65    /// 匹配指定路径
66    Path(String),
67    /// 匹配路径和方法
68    PathAndMethod(String, Method),
69    /// 自定义匹配函数
70    Custom(Arc<dyn Fn(&ServiceRequest) -> bool + Send + Sync>),
71}
72
73impl std::fmt::Debug for ServiceMatchRule {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            ServiceMatchRule::Path(path) => f.debug_tuple("Path").field(path).finish(),
77            ServiceMatchRule::PathAndMethod(path, method) => f.debug_tuple("PathAndMethod").field(path).field(method).finish(),
78            ServiceMatchRule::Custom(_) => f.debug_tuple("Custom").field(&"<function>").finish(),
79        }
80    }
81}
82
83/// 服务响应配置
84#[derive(Debug, Clone)]
85pub struct ServiceResponseConfig {
86    /// 匹配规则
87    pub match_rule: ServiceMatchRule,
88    /// 响应内容
89    pub response: ServiceResponse,
90}
91
92/// 外部服务期望配置
93#[derive(Debug, Default)]
94pub struct ServiceExpectation {
95    /// 期望的请求次数映射 (路径 -> 期望次数)
96    pub expected_requests: HashMap<String, usize>,
97    /// 描述信息
98    pub description: Option<String>,
99}
100
101impl ServiceExpectation {
102    /// 创建新的服务期望配置
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    /// 设置期望请求次数
108    pub fn expect_request(mut self, path: impl Into<String>, count: usize) -> Self {
109        self.expected_requests.insert(path.into(), count);
110        self
111    }
112
113    /// 设置描述
114    pub fn description(mut self, desc: impl Into<String>) -> Self {
115        self.description = Some(desc.into());
116        self
117    }
118}
119
120/// Mock 外部服务构建器
121pub struct MockExternalServiceBuilder {
122    responses: Vec<ServiceResponseConfig>,
123    expectation: ServiceExpectation,
124    requests: Arc<RwLock<Vec<ServiceRequest>>>,
125}
126
127impl MockExternalServiceBuilder {
128    /// 创建新的 Mock 外部服务构建器
129    pub fn new() -> Self {
130        Self { responses: Vec::new(), expectation: ServiceExpectation::default(), requests: Arc::new(RwLock::new(Vec::new())) }
131    }
132
133    /// 添加响应配置 (匹配路径)
134    pub fn respond_to_path(mut self, path: impl Into<String>, response: ServiceResponse) -> Self {
135        self.responses.push(ServiceResponseConfig { match_rule: ServiceMatchRule::Path(path.into()), response });
136        self
137    }
138
139    /// 添加响应配置 (匹配路径和方法)
140    pub fn respond_to_path_and_method(mut self, path: impl Into<String>, method: Method, response: ServiceResponse) -> Self {
141        self.responses
142            .push(ServiceResponseConfig { match_rule: ServiceMatchRule::PathAndMethod(path.into(), method), response });
143        self
144    }
145
146    /// 添加自定义响应配置
147    pub fn respond_with<F>(mut self, matcher: F, response: ServiceResponse) -> Self
148    where
149        F: Fn(&ServiceRequest) -> bool + Send + Sync + 'static,
150    {
151        self.responses.push(ServiceResponseConfig { match_rule: ServiceMatchRule::Custom(Arc::new(matcher)), response });
152        self
153    }
154
155    /// 设置期望
156    pub fn expect(mut self, expectation: ServiceExpectation) -> Self {
157        self.expectation = expectation;
158        self
159    }
160
161    /// 构建 Mock 外部服务
162    pub fn build(self) -> MockExternalService {
163        MockExternalService { responses: self.responses, expectation: self.expectation, requests: self.requests }
164    }
165}
166
167impl Default for MockExternalServiceBuilder {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173/// Mock 外部服务
174///
175/// 用于模拟外部 HTTP 服务,支持请求期望配置、调用记录和验证。
176pub struct MockExternalService {
177    responses: Vec<ServiceResponseConfig>,
178    expectation: ServiceExpectation,
179    requests: Arc<RwLock<Vec<ServiceRequest>>>,
180}
181
182impl MockExternalService {
183    /// 处理外部服务请求
184    pub fn handle_request(
185        &self,
186        method: Method,
187        path: String,
188        headers: HeaderMap,
189        body: Option<Bytes>,
190    ) -> TestingResult<ServiceResponse> {
191        let request = ServiceRequest {
192            method: method.clone(),
193            path: path.clone(),
194            headers: headers.clone(),
195            body: body.clone(),
196            timestamp: std::time::Instant::now(),
197        };
198
199        {
200            let mut requests = self.requests.write();
201            requests.push(request.clone());
202        }
203
204        for config in &self.responses {
205            if Self::matches(&request, &config.match_rule) {
206                return Ok(config.response.clone());
207            }
208        }
209
210        Err(WaeError::new(WaeErrorKind::MockError { reason: format!("No mock response configured for {} {}", method, path) }))
211    }
212
213    /// 异步处理外部服务请求
214    pub async fn handle_request_async(
215        &self,
216        method: Method,
217        path: String,
218        headers: HeaderMap,
219        body: Option<Bytes>,
220    ) -> TestingResult<ServiceResponse> {
221        self.handle_request(method, path, headers, body)
222    }
223
224    fn matches(request: &ServiceRequest, rule: &ServiceMatchRule) -> bool {
225        match rule {
226            ServiceMatchRule::Path(path) => request.path == *path,
227            ServiceMatchRule::PathAndMethod(path, method) => request.path == *path && request.method == *method,
228            ServiceMatchRule::Custom(matcher) => matcher(request),
229        }
230    }
231
232    /// 获取请求记录
233    pub fn requests(&self) -> Vec<ServiceRequest> {
234        self.requests.read().clone()
235    }
236
237    /// 获取请求次数
238    pub fn request_count(&self) -> usize {
239        self.requests.read().len()
240    }
241
242    /// 获取指定路径的请求次数
243    pub fn request_count_by_path(&self, path: &str) -> usize {
244        self.requests.read().iter().filter(|r| r.path == path).count()
245    }
246}
247
248impl Mock for MockExternalService {
249    fn calls(&self) -> Vec<MockCall> {
250        self.requests
251            .read()
252            .iter()
253            .map(|r| MockCall { args: vec![r.method.to_string(), r.path.clone()], timestamp: r.timestamp })
254            .collect()
255    }
256
257    fn call_count(&self) -> usize {
258        self.request_count()
259    }
260
261    fn verify(&self) -> TestingResult<()> {
262        for (path, expected) in &self.expectation.expected_requests {
263            let actual = self.request_count_by_path(path);
264            if actual != *expected {
265                return Err(WaeError::new(WaeErrorKind::AssertionFailed {
266                    message: format!("Expected {} requests for path '{}', but got {}", expected, path, actual),
267                }));
268            }
269        }
270
271        Ok(())
272    }
273
274    fn reset(&self) {
275        let mut requests = self.requests.write();
276        requests.clear();
277    }
278}