phabricator_mock/
lib.rs

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, &params, 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}