git_mob_tool/repositories/
mob_session_repo.rs

1use crate::Result;
2use crate::helpers::{CmdOutput, CommandRunner};
3
4#[cfg(test)]
5use mockall::{automock, predicate::*};
6#[cfg_attr(test, automock)]
7pub trait MobSessionRepo {
8    fn list_coauthors(&self) -> Result<Vec<String>>;
9    fn add_coauthor(&self, coauthor: &str) -> Result<()>;
10    fn clear(&self) -> Result<()>;
11}
12
13pub struct GitConfigMobRepo<Cmd> {
14    pub command_runner: Cmd,
15}
16
17impl<Cmd: CommandRunner> GitConfigMobRepo<Cmd> {
18    const COAUTHORS_MOB_SECTION: &'static str = "coauthors-mob";
19    const COAUTHOR_MOB_KEY: &'static str = "entry";
20
21    const EXIT_CODE_SUCCESS: i32 = 0;
22    const EXIT_CODE_CONFIG_INVALID_KEY: i32 = 1;
23
24    fn git_config_error<T>(output: &CmdOutput) -> Result<T> {
25        match output.status_code {
26            Some(code) => Err(format!("Git config command exited with status code: {code}").into()),
27            None => Err("Git config command terminated by signal".into()),
28        }
29    }
30}
31
32impl<Cmd: CommandRunner> MobSessionRepo for GitConfigMobRepo<Cmd> {
33    fn list_coauthors(&self) -> Result<Vec<String>> {
34        let full_key = format!("{}.{}", Self::COAUTHORS_MOB_SECTION, Self::COAUTHOR_MOB_KEY);
35
36        let output = self
37            .command_runner
38            .execute("git", &["config", "--global", "--get-all", &full_key])?;
39
40        match output.status_code {
41            Some(Self::EXIT_CODE_SUCCESS) => Ok(String::from_utf8(output.stdout)?
42                .lines()
43                .map(|x| x.into())
44                .collect()),
45
46            Some(Self::EXIT_CODE_CONFIG_INVALID_KEY) => Ok(vec![]),
47            _ => Self::git_config_error(&output),
48        }
49    }
50    fn add_coauthor(&self, coauthor: &str) -> Result<()> {
51        let full_key = format!("{}.{}", Self::COAUTHORS_MOB_SECTION, Self::COAUTHOR_MOB_KEY);
52
53        let output = self
54            .command_runner
55            .execute("git", &["config", "--global", "--add", &full_key, coauthor])?;
56
57        match output.status_code {
58            Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
59            _ => Self::git_config_error(&output),
60        }
61    }
62    fn clear(&self) -> Result<()> {
63        if self.list_coauthors()?.is_empty() {
64            return Ok(());
65        }
66
67        let section = Self::COAUTHORS_MOB_SECTION.to_owned();
68
69        let output = self
70            .command_runner
71            .execute("git", &["config", "--global", "--remove-section", &section])?;
72
73        match output.status_code {
74            Some(Self::EXIT_CODE_SUCCESS) => Ok(()),
75            _ => Self::git_config_error(&output),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use crate::helpers::MockCommandRunner;
83
84    use super::*;
85
86    fn create_mock_command_runner(
87        program: &str,
88        args: &[&str],
89        stdout: Vec<u8>,
90        stderr: Vec<u8>,
91        status_code: Option<i32>,
92    ) -> MockCommandRunner {
93        let cloned_program = program.to_string();
94        let cloned_args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
95
96        let mut mock_cmd_runner = MockCommandRunner::new();
97        mock_cmd_runner
98            .expect_execute()
99            .once()
100            .withf(move |program, args| program == cloned_program && args == cloned_args)
101            .returning(move |_, _| {
102                Ok(CmdOutput {
103                    stdout: stdout.clone(),
104                    stderr: stderr.clone(),
105                    status_code,
106                })
107            });
108        mock_cmd_runner
109    }
110
111    #[test]
112    fn test_list_coauthors() -> Result<()> {
113        let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
114        let stdout =
115            b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into();
116        let stderr = vec![];
117        let status_code = Some(0);
118        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
119        let mob_repo = GitConfigMobRepo { command_runner };
120
121        let result = mob_repo.list_coauthors()?;
122
123        assert_eq!(
124            result,
125            vec![
126                "Leo Messi <leo.messi@example.com>",
127                "Emi Martinez <emi.martinez@example.com>"
128            ]
129        );
130
131        Ok(())
132    }
133
134    #[test]
135    fn test_list_coauthors_when_mob_session_empty() -> Result<()> {
136        let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
137        let stdout = vec![];
138        let stderr = vec![];
139        let status_code = Some(1);
140        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
141        let mob_repo = GitConfigMobRepo { command_runner };
142
143        let result = mob_repo.list_coauthors()?;
144
145        assert_eq!(result, Vec::<String>::new());
146
147        Ok(())
148    }
149
150    #[test]
151    fn test_list_coauthors_when_unexpected_error() -> Result<()> {
152        let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
153        let stdout = vec![];
154        let stderr = b"uh-oh!".into();
155        let status_code = Some(129);
156        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
157        let mob_repo = GitConfigMobRepo { command_runner };
158
159        let result = mob_repo.list_coauthors();
160
161        assert!(
162            result
163                .is_err_and(|x| x.to_string() == "Git config command exited with status code: 129")
164        );
165
166        Ok(())
167    }
168
169    #[test]
170    fn test_list_coauthors_when_terminated_by_signal() -> Result<()> {
171        let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
172        let stdout = vec![];
173        let stderr = vec![];
174        let status_code = None;
175        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
176        let mob_repo = GitConfigMobRepo { command_runner };
177
178        let result = mob_repo.list_coauthors();
179
180        assert!(result.is_err_and(|x| x.to_string() == "Git config command terminated by signal"));
181
182        Ok(())
183    }
184
185    #[test]
186    fn test_add_coauthor() -> Result<()> {
187        let coauthor = "Leo Messi <leo.messi@example.com>";
188        let args = &[
189            "config",
190            "--global",
191            "--add",
192            "coauthors-mob.entry",
193            coauthor,
194        ];
195        let stdout = vec![];
196        let stderr = vec![];
197        let status_code = Some(0);
198        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
199        let mob_repo = GitConfigMobRepo { command_runner };
200
201        mob_repo.add_coauthor(coauthor)?;
202
203        Ok(())
204    }
205
206    #[test]
207    fn test_add_coauthor_when_unexpected_error() -> Result<()> {
208        let coauthor = "Leo Messi <leo.messi@example.com>";
209        let args = &[
210            "config",
211            "--global",
212            "--add",
213            "coauthors-mob.entry",
214            coauthor,
215        ];
216        let stdout = vec![];
217        let stderr = b"uh-oh!".into();
218        let status_code = Some(129);
219        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
220        let mob_repo = GitConfigMobRepo { command_runner };
221
222        let result = mob_repo.add_coauthor(coauthor);
223
224        assert!(
225            result
226                .is_err_and(|x| x.to_string() == "Git config command exited with status code: 129")
227        );
228
229        Ok(())
230    }
231
232    #[test]
233    fn test_add_coauthor_when_terminated_by_signal() -> Result<()> {
234        let coauthor = "Leo Messi <leo.messi@example.com>";
235        let args = &[
236            "config",
237            "--global",
238            "--add",
239            "coauthors-mob.entry",
240            coauthor,
241        ];
242        let stdout = vec![];
243        let stderr = vec![];
244        let status_code = None;
245        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
246        let mob_repo = GitConfigMobRepo { command_runner };
247
248        let result = mob_repo.add_coauthor(coauthor);
249
250        assert!(result.is_err_and(|x| x.to_string() == "Git config command terminated by signal"));
251
252        Ok(())
253    }
254
255    #[test]
256    fn test_clear() -> Result<()> {
257        let mut command_runner = MockCommandRunner::new();
258        command_runner
259            .expect_execute()
260            .once()
261            .withf(|program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
262            .returning(|_, _| {
263                Ok(CmdOutput {
264                    stdout: b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into(),
265                    stderr: vec![],
266                    status_code: Some(0),
267                })
268            });
269        command_runner
270            .expect_execute()
271            .once()
272            .withf(|program, args| {
273                program == "git"
274                    && args == ["config", "--global", "--remove-section", "coauthors-mob"]
275            })
276            .returning(|_, _| {
277                Ok(CmdOutput {
278                    stdout: vec![],
279                    stderr: vec![],
280                    status_code: Some(0),
281                })
282            });
283        let mob_repo = GitConfigMobRepo { command_runner };
284
285        mob_repo.clear()?;
286
287        Ok(())
288    }
289
290    #[test]
291    fn test_clear_when_mob_session_empty() -> Result<()> {
292        let args = &["config", "--global", "--get-all", "coauthors-mob.entry"];
293        let stdout = vec![];
294        let stderr = vec![];
295        let status_code = Some(1);
296        let command_runner = create_mock_command_runner("git", args, stdout, stderr, status_code);
297        let mob_repo = GitConfigMobRepo { command_runner };
298
299        mob_repo.clear()?;
300
301        Ok(())
302    }
303
304    #[test]
305    fn test_clear_when_unexpected_error() -> Result<()> {
306        let mut command_runner = MockCommandRunner::new();
307        command_runner
308            .expect_execute()
309            .once()
310            .withf( |program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
311            .returning( |_, _| {
312                Ok(CmdOutput {
313                    stdout: b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into(),
314                    stderr: vec![],
315                    status_code: Some(0),
316                })
317            });
318        command_runner
319            .expect_execute()
320            .once()
321            .withf(|program, args| {
322                program == "git"
323                    && args == ["config", "--global", "--remove-section", "coauthors-mob"]
324            })
325            .returning(|_, _| {
326                Ok(CmdOutput {
327                    stdout: vec![],
328                    stderr: b"uh-oh!".into(),
329                    status_code: Some(129),
330                })
331            });
332        let mob_repo = GitConfigMobRepo { command_runner };
333
334        let result = mob_repo.clear();
335
336        assert!(
337            result
338                .is_err_and(|x| x.to_string() == "Git config command exited with status code: 129")
339        );
340
341        Ok(())
342    }
343
344    #[test]
345    fn test_clear_when_terminated_by_signal() -> Result<()> {
346        let mut command_runner = MockCommandRunner::new();
347        command_runner
348            .expect_execute()
349            .once()
350            .withf( |program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
351            .returning( |_, _| {
352                Ok(CmdOutput {
353                    stdout: b"Leo Messi <leo.messi@example.com>\nEmi Martinez <emi.martinez@example.com>\n".into(),
354                    stderr: vec![],
355                    status_code: Some(0),
356                })
357            });
358        command_runner
359            .expect_execute()
360            .once()
361            .withf(|program, args| {
362                program == "git"
363                    && args == ["config", "--global", "--remove-section", "coauthors-mob"]
364            })
365            .returning(|_, _| {
366                Ok(CmdOutput {
367                    stdout: vec![],
368                    stderr: vec![],
369                    status_code: None,
370                })
371            });
372        let mob_repo = GitConfigMobRepo { command_runner };
373        let result = mob_repo.clear();
374
375        assert!(result.is_err_and(|x| x.to_string() == "Git config command terminated by signal"));
376
377        Ok(())
378    }
379}