1use crate::client::api::SashikoApi;
7use crate::client::error::ApiError;
8use crate::client::types::{ListParams, ReviewQuery};
9use crate::models::{
10 EmailMessage, MailingList, Paginated, PatchId, Patchset, PatchsetDetail, ServerStats,
11};
12use std::sync::Mutex;
13
14type MockResult<T> = Option<Result<T, String>>;
15
16#[allow(clippy::struct_field_names)]
21pub struct MockClient {
22 patchsets_response: Mutex<MockResult<Paginated<Patchset>>>,
23 stats_response: Mutex<MockResult<ServerStats>>,
24 lists_response: Mutex<MockResult<Vec<MailingList>>>,
25 patch_detail_response: Mutex<MockResult<PatchsetDetail>>,
26 messages_response: Mutex<MockResult<Paginated<EmailMessage>>>,
27 message_detail_response: Mutex<MockResult<EmailMessage>>,
28}
29
30impl MockClient {
31 #[must_use]
33 pub fn new() -> Self {
34 Self {
35 patchsets_response: Mutex::new(None),
36 stats_response: Mutex::new(None),
37 lists_response: Mutex::new(None),
38 patch_detail_response: Mutex::new(None),
39 messages_response: Mutex::new(None),
40 message_detail_response: Mutex::new(None),
41 }
42 }
43
44 pub fn set_patchsets(&self, result: Result<Paginated<Patchset>, String>) {
46 if let Ok(mut guard) = self.patchsets_response.lock() {
47 *guard = Some(result);
48 }
49 }
50
51 pub fn set_stats(&self, result: Result<ServerStats, String>) {
53 if let Ok(mut guard) = self.stats_response.lock() {
54 *guard = Some(result);
55 }
56 }
57
58 pub fn set_lists(&self, result: Result<Vec<MailingList>, String>) {
60 if let Ok(mut guard) = self.lists_response.lock() {
61 *guard = Some(result);
62 }
63 }
64
65 pub fn set_patch_detail(&self, result: Result<PatchsetDetail, String>) {
67 if let Ok(mut guard) = self.patch_detail_response.lock() {
68 *guard = Some(result);
69 }
70 }
71
72 pub fn set_messages(&self, result: Result<Paginated<EmailMessage>, String>) {
74 if let Ok(mut guard) = self.messages_response.lock() {
75 *guard = Some(result);
76 }
77 }
78
79 pub fn set_message_detail(&self, result: Result<EmailMessage, String>) {
81 if let Ok(mut guard) = self.message_detail_response.lock() {
82 *guard = Some(result);
83 }
84 }
85
86 fn take_result<T: Clone>(lock: &Mutex<MockResult<T>>, endpoint: &str) -> Result<T, ApiError> {
88 let guard = lock
89 .lock()
90 .map_err(|e| ApiError::Configuration(format!("mock lock poisoned: {e}")))?;
91
92 match guard.as_ref() {
93 Some(Ok(val)) => Ok(val.clone()),
94 Some(Err(msg)) => Err(ApiError::Network {
95 source: msg.clone().into(),
96 remote: "mock".to_string(),
97 }),
98 None => Err(ApiError::Configuration(format!(
99 "mock: no canned response set for {endpoint}"
100 ))),
101 }
102 }
103}
104
105impl Default for MockClient {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111#[async_trait::async_trait]
112impl SashikoApi for MockClient {
113 async fn lists(&self) -> Result<Vec<MailingList>, ApiError> {
114 Self::take_result(&self.lists_response, "lists")
115 }
116
117 async fn patchsets(&self, _params: &ListParams) -> Result<Paginated<Patchset>, ApiError> {
118 Self::take_result(&self.patchsets_response, "patchsets")
119 }
120
121 async fn messages(&self, _params: &ListParams) -> Result<Paginated<EmailMessage>, ApiError> {
122 Self::take_result(&self.messages_response, "messages")
123 }
124
125 async fn patch_detail(&self, _id: &PatchId) -> Result<PatchsetDetail, ApiError> {
126 Self::take_result(&self.patch_detail_response, "patch_detail")
127 }
128
129 async fn patchset_summary(&self, _id: &PatchId) -> Result<PatchsetDetail, ApiError> {
130 Self::take_result(&self.patch_detail_response, "patchset_summary")
131 }
132
133 async fn message_detail(&self, _id: &PatchId) -> Result<EmailMessage, ApiError> {
134 Self::take_result(&self.message_detail_response, "message_detail")
135 }
136
137 async fn review(&self, _params: &ReviewQuery) -> Result<serde_json::Value, ApiError> {
138 Err(ApiError::Configuration(
139 "mock: review not implemented".to_string(),
140 ))
141 }
142
143 async fn review_log(&self, _params: &ReviewQuery) -> Result<serde_json::Value, ApiError> {
144 Err(ApiError::Configuration(
145 "mock: review_log not implemented".to_string(),
146 ))
147 }
148
149 async fn stats(&self) -> Result<ServerStats, ApiError> {
150 Self::take_result(&self.stats_response, "stats")
151 }
152
153 async fn stats_timeline(
154 &self,
155 _subsystem_id: Option<i64>,
156 ) -> Result<serde_json::Value, ApiError> {
157 Err(ApiError::Configuration(
158 "mock: stats_timeline not implemented".to_string(),
159 ))
160 }
161
162 async fn stats_reviews(&self) -> Result<serde_json::Value, ApiError> {
163 Err(ApiError::Configuration(
164 "mock: stats_reviews not implemented".to_string(),
165 ))
166 }
167
168 async fn stats_tools(&self) -> Result<serde_json::Value, ApiError> {
169 Err(ApiError::Configuration(
170 "mock: stats_tools not implemented".to_string(),
171 ))
172 }
173}
174
175#[cfg(test)]
176#[allow(clippy::expect_used)]
177mod tests {
178 use super::*;
179 use crate::models::Patchset;
180
181 #[tokio::test]
182 async fn mock_returns_canned_patchsets() {
183 let mock = MockClient::new();
184 let patchsets = Paginated {
185 items: vec![Patchset::fixture()],
186 total: 1,
187 page: 1,
188 per_page: 50,
189 };
190 mock.set_patchsets(Ok(patchsets));
191
192 let result = mock.patchsets(&ListParams::default()).await;
193 assert!(result.is_ok());
194 assert_eq!(result.expect("patchsets").items.len(), 1);
195 }
196
197 #[tokio::test]
198 async fn mock_returns_canned_error() {
199 let mock = MockClient::new();
200 mock.set_patchsets(Err("connection refused".to_string()));
201
202 let result = mock.patchsets(&ListParams::default()).await;
203 assert!(result.is_err());
204 }
205
206 #[tokio::test]
207 async fn mock_returns_error_when_not_set() {
208 let mock = MockClient::new();
209 let result = mock.patchsets(&ListParams::default()).await;
210 assert!(result.is_err());
211 assert!(matches!(result, Err(ApiError::Configuration(_))));
212 }
213
214 #[tokio::test]
215 async fn mock_stats() {
216 let mock = MockClient::new();
217 mock.set_stats(Ok(ServerStats {
218 status: "ok".to_string(),
219 version: "0.1.0".to_string(),
220 pending: 5,
221 reviewing: 2,
222 messages: 1000,
223 patchsets: 500,
224 }));
225
226 let stats = mock.stats().await.expect("stats");
227 assert_eq!(stats.status, "ok");
228 assert_eq!(stats.pending, 5);
229 }
230}