1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3use url::Url;
4use wiremock::matchers::{header, method, path};
5use wiremock::{Mock, MockServer, ResponseTemplate};
6use wiremock::{Request, Respond};
7
8mod param;
9use param::Params;
10
11pub mod task;
12use task::Task;
13
14mod status;
15use status::Status;
16
17mod user;
18use user::User;
19
20mod priority;
21use priority::Priority;
22
23mod policy;
24use policy::Policy;
25
26mod space;
27use space::Space;
28
29pub mod project;
30use project::Project;
31
32mod api;
33mod column;
34use column::Column;
35
36mod phid;
37use phid::Phid;
38
39pub fn status() -> status::StatusBuilder {
40 status::StatusBuilder::default()
41}
42
43pub fn column() -> column::ColumnDataBuilder {
44 column::ColumnDataBuilder::default()
45}
46
47pub fn project() -> project::ProjectDataBuilder {
48 project::ProjectDataBuilder::default()
49}
50
51pub fn priority() -> priority::PriorityBuilder {
52 priority::PriorityBuilder::default()
53}
54
55pub fn task() -> task::TaskDataBuilder {
56 task::TaskDataBuilder::default()
57}
58
59pub fn user() -> user::UserDataBuilder {
60 user::UserDataBuilder::default()
61}
62
63trait PhabRespond: Send + Sync {
64 fn respond(
65 &self,
66 server: &PhabMockServer,
67 params: &Params,
68 request: &Request,
69 ) -> ResponseTemplate;
70}
71
72struct AuthAndParse<R> {
73 server: PhabMockServer,
74 responder: R,
75}
76
77impl<R> Respond for AuthAndParse<R>
78where
79 R: PhabRespond,
80{
81 fn respond(&self, request: &Request) -> ResponseTemplate {
82 let params = Params::new(&request.body).expect("Failed to parse request");
83 let auth = params.get(&["api.token"]);
84
85 match auth {
86 None => ResponseTemplate::new(403).set_body_string("Missing auth token"),
87 Some(a) if a != self.server.token() => {
88 ResponseTemplate::new(403).set_body_string("Incorrect auth token")
89 }
90 _ => self.responder.respond(&self.server, ¶ms, request),
91 }
92 }
93}
94
95struct Data {
96 tasks: HashMap<u32, Task>,
97 users: Vec<User>,
98 default_priority: Priority,
99 priorities: Vec<Priority>,
100 statusses: Vec<Status>,
101 projects: Vec<Project>,
102}
103
104struct Inner {
105 server: MockServer,
106 token: String,
107 data: Mutex<Data>,
108}
109
110#[derive(Clone)]
111pub struct PhabMockServer {
112 inner: Arc<Inner>,
113}
114
115impl PhabMockServer {
116 fn auth_and_parse<R>(&self, responder: R) -> AuthAndParse<R>
117 where
118 R: PhabRespond,
119 {
120 AuthAndParse {
121 server: self.clone(),
122 responder,
123 }
124 }
125
126 async fn handle_post<R>(&self, p: &str, responder: R)
127 where
128 R: PhabRespond + 'static,
129 {
130 Mock::given(method("POST"))
131 .and(path(p))
132 .and(header("content-type", "application/x-www-form-urlencoded"))
133 .respond_with(self.auth_and_parse(responder))
134 .named("phid.lookup")
135 .mount(&self.inner.server)
136 .await;
137 }
138
139 pub async fn start() -> Self {
140 let server = MockServer::start().await;
141
142 let default_priority = Priority {
143 value: 50,
144 name: "normal".to_string(),
145 color: "yellow".to_string(),
146 };
147
148 let data = Data {
149 tasks: HashMap::new(),
150 users: Vec::new(),
151 default_priority,
152 priorities: Vec::new(),
153 statusses: Vec::new(),
154 projects: Vec::new(),
155 };
156 let m = PhabMockServer {
157 inner: Arc::new(Inner {
158 server,
159 token: "badgerbadger".to_string(),
160 data: Mutex::new(data),
161 }),
162 };
163
164 m.new_priority(10, "Low", "blue");
165 m.new_priority(100, "High", "blue");
166
167 let s = status()
168 .value("open")
169 .name("Open")
170 .color("green")
171 .special(status::Special::Default)
172 .build()
173 .unwrap();
174 m.add_status(s);
175 m.new_status("wip", "In Progress", Some("indigo"));
176 let s = status()
177 .value("closed")
178 .name("Closed")
179 .color("indigo")
180 .special(status::Special::Closed)
181 .closed(true)
182 .build()
183 .unwrap();
184 m.add_status(s);
185
186 m.handle_post("api/maniphest.search", api::maniphest::Search {})
187 .await;
188 m.handle_post("api/maniphest.info", api::maniphest::Info {})
189 .await;
190 m.handle_post("api/phid.lookup", api::phid::Lookup {}).await;
191 m.handle_post("api/project.search", api::project::Search {})
192 .await;
193 m.handle_post("api/edge.search", api::edge::Search {}).await;
194 m
195 }
196
197 pub fn uri(&self) -> Url {
198 self.inner.server.uri().parse().expect("uri not a url")
199 }
200
201 pub fn token(&self) -> &str {
202 &self.inner.token
203 }
204
205 pub async fn n_requests(&self) -> usize {
206 self.inner
207 .server
208 .received_requests()
209 .await
210 .map(|v| v.len())
211 .unwrap_or_default()
212 }
213
214 pub async fn requests(&self) -> Option<Vec<wiremock::Request>> {
215 self.inner.server.received_requests().await
216 }
217
218 pub fn add_task(&self, task: Task) {
219 let mut data = self.inner.data.lock().unwrap();
220 data.tasks.insert(task.id, task);
221 }
222
223 pub fn add_project(&self, project: Project) {
224 let mut data = self.inner.data.lock().unwrap();
225 data.projects.push(project);
226 }
227
228 pub fn add_user(&self, u: User) {
229 let mut data = self.inner.data.lock().unwrap();
230 data.users.push(u);
231 }
232
233 pub fn add_status(&self, s: Status) {
234 let mut data = self.inner.data.lock().unwrap();
235 data.statusses.push(s);
236 }
237
238 pub fn add_priority(&self, p: Priority) {
239 let mut data = self.inner.data.lock().unwrap();
240 data.priorities.push(p);
241 }
242
243 pub fn get_task(&self, id: u32) -> Option<Task> {
244 let data = self.inner.data.lock().unwrap();
245 match data.tasks.get(&id) {
246 Some(t) => Some(t.clone()),
247 None => None,
248 }
249 }
250
251 pub fn find_task(&self, phid: &Phid) -> Option<Task> {
252 let data = self.inner.data.lock().unwrap();
253 data.tasks
254 .values()
255 .find(|t| t.phid == *phid)
256 .map(Clone::clone)
257 }
258
259 pub fn get_project(&self, id: u32) -> Option<Project> {
260 let data = self.inner.data.lock().unwrap();
261 data.projects.iter().find(|p| p.id == id).map(Clone::clone)
262 }
263
264 pub fn find_project(&self, phid: &Phid) -> Option<Project> {
265 let data = self.inner.data.lock().unwrap();
266 data.projects
267 .iter()
268 .find(|p| p.phid == *phid)
269 .map(Clone::clone)
270 }
271
272 pub fn default_status(&self) -> Status {
273 let data = self.inner.data.lock().unwrap();
274 data.statusses
275 .iter()
276 .find(|s| s.special == Some(status::Special::Default))
277 .map(Clone::clone)
278 .expect("No default status")
279 }
280
281 pub fn default_priority(&self) -> Priority {
282 let data = self.inner.data.lock().unwrap();
283 data.default_priority.clone()
284 }
285
286 pub fn new_user(&self, name: &str, full_name: &str) -> User {
287 let u = user::UserDataBuilder::default()
288 .full_name(full_name)
289 .name(name)
290 .build()
291 .unwrap();
292 self.add_user(u.clone());
293 u
294 }
295
296 pub fn new_priority(&self, value: u32, name: &str, color: &str) -> Priority {
297 let p = priority()
298 .value(value)
299 .name(name)
300 .color(color)
301 .build()
302 .unwrap();
303 self.add_priority(p.clone());
304 p
305 }
306
307 pub fn new_status(&self, value: &str, name: &str, color: Option<&str>) -> Status {
308 let mut s = status();
309 if let Some(color) = color {
310 s.color(color);
311 }
312 let status = s.value(value).name(name).build().unwrap();
313 self.add_status(status.clone());
314 status
315 }
316
317 pub fn new_simple_task(&self, id: u32, user: &User) -> Task {
318 let task = task()
319 .id(id)
320 .full_name(format!("Task T{}", id))
321 .description(format!("Description of task T{}", id))
322 .author(user.clone())
323 .owner(user.clone())
324 .priority(self.default_priority())
325 .status(self.default_status())
326 .build()
327 .unwrap();
328 self.add_task(task.clone());
329 task
330 }
331}