1pub(crate) mod attachment;
4pub(crate) mod children;
5pub(crate) mod comment;
6pub(crate) mod compare;
7pub(crate) mod copy;
8pub(crate) mod create;
9pub(crate) mod delete;
10pub(crate) mod download;
11pub(crate) mod edit;
12pub(crate) mod history;
13pub(crate) mod label;
14pub(crate) mod move_page;
15pub(crate) mod read;
16pub(crate) mod restriction;
17pub(crate) mod search;
18pub(crate) mod space;
19pub(crate) mod user;
20pub(crate) mod watcher;
21pub(crate) mod write;
22
23use anyhow::Result;
24use clap::{Parser, Subcommand};
25
26#[derive(Parser)]
28pub struct ConfluenceCommand {
29 #[command(subcommand)]
31 pub command: ConfluenceSubcommands,
32
33 #[command(flatten)]
35 pub instance: super::InstanceArg,
36}
37
38#[derive(Subcommand)]
40pub enum ConfluenceSubcommands {
41 Comment(comment::CommentCommand),
43 Read(read::ReadCommand),
50 Write(write::WriteCommand),
60 Edit(edit::EditCommand),
62 Search(search::SearchCommand),
64 Create(create::CreateCommand),
66 Delete(delete::DeleteCommand),
68 Move(move_page::MoveCommand),
70 Copy(copy::CopyCommand),
72 Label(label::LabelCommand),
74 Attachment(attachment::AttachmentCommand),
76 Download(download::DownloadCommand),
78 Children(children::ChildrenCommand),
80 History(history::HistoryCommand),
82 Compare(compare::CompareCommandGroup),
84 User(user::UserCommand),
86 Space(space::SpaceCommand),
88 Watcher(watcher::WatcherCommand),
90 Restriction(restriction::RestrictionCommand),
92}
93
94impl ConfluenceCommand {
95 pub async fn execute(self) -> Result<()> {
97 self.instance.apply();
98 match self.command {
99 ConfluenceSubcommands::Comment(cmd) => cmd.execute().await,
100 ConfluenceSubcommands::Read(cmd) => cmd.execute().await,
101 ConfluenceSubcommands::Write(cmd) => cmd.execute().await,
102 ConfluenceSubcommands::Edit(cmd) => cmd.execute().await,
103 ConfluenceSubcommands::Search(cmd) => cmd.execute().await,
104 ConfluenceSubcommands::Create(cmd) => cmd.execute().await,
105 ConfluenceSubcommands::Label(cmd) => cmd.execute().await,
106 ConfluenceSubcommands::Attachment(cmd) => cmd.execute().await,
107 ConfluenceSubcommands::Delete(cmd) => cmd.execute().await,
108 ConfluenceSubcommands::Move(cmd) => cmd.execute().await,
109 ConfluenceSubcommands::Copy(cmd) => cmd.execute().await,
110 ConfluenceSubcommands::Download(cmd) => cmd.execute().await,
111 ConfluenceSubcommands::Children(cmd) => cmd.execute().await,
112 ConfluenceSubcommands::History(cmd) => cmd.execute().await,
113 ConfluenceSubcommands::Compare(cmd) => cmd.execute().await,
114 ConfluenceSubcommands::User(cmd) => cmd.execute().await,
115 ConfluenceSubcommands::Space(cmd) => cmd.execute().await,
116 ConfluenceSubcommands::Watcher(cmd) => cmd.execute().await,
117 ConfluenceSubcommands::Restriction(cmd) => cmd.execute().await,
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::cli::atlassian::format::{ContentFormat, OutputFormat};
126
127 #[test]
128 fn confluence_subcommands_comment_variant() {
129 let cmd = ConfluenceCommand {
130 command: ConfluenceSubcommands::Comment(comment::CommentCommand {
131 command: comment::CommentSubcommands::List(comment::ListCommand {
132 id: "12345".to_string(),
133 kind: comment::CommentKindFilter::All,
134 limit: 25,
135 output: OutputFormat::Table,
136 }),
137 }),
138 instance: crate::cli::atlassian::InstanceArg::default(),
139 };
140 assert!(matches!(cmd.command, ConfluenceSubcommands::Comment(_)));
141 }
142
143 #[test]
144 fn confluence_subcommands_read_variant() {
145 let cmd = ConfluenceCommand {
146 command: ConfluenceSubcommands::Read(read::ReadCommand {
147 id: "12345".to_string(),
148 out_file: None,
149 output: None,
150 format: ContentFormat::Jfm,
151 version: None,
152 }),
153 instance: crate::cli::atlassian::InstanceArg::default(),
154 };
155 assert!(matches!(cmd.command, ConfluenceSubcommands::Read(_)));
156 }
157
158 #[test]
159 fn confluence_subcommands_write_variant() {
160 let cmd = ConfluenceCommand {
161 command: ConfluenceSubcommands::Write(write::WriteCommand {
162 id: "12345".to_string(),
163 file: None,
164 format: ContentFormat::Adf,
165 force: false,
166 dry_run: false,
167 }),
168 instance: crate::cli::atlassian::InstanceArg::default(),
169 };
170 assert!(matches!(cmd.command, ConfluenceSubcommands::Write(_)));
171 }
172
173 #[test]
174 fn confluence_subcommands_edit_variant() {
175 let cmd = ConfluenceCommand {
176 command: ConfluenceSubcommands::Edit(edit::EditCommand {
177 id: "12345".to_string(),
178 }),
179 instance: crate::cli::atlassian::InstanceArg::default(),
180 };
181 assert!(matches!(cmd.command, ConfluenceSubcommands::Edit(_)));
182 }
183
184 #[test]
185 fn confluence_subcommands_search_variant() {
186 let cmd = ConfluenceCommand {
187 command: ConfluenceSubcommands::Search(search::SearchCommand {
188 cql: Some("space = ENG".to_string()),
189 space: None,
190 title: None,
191 limit: 25,
192 output: OutputFormat::Table,
193 }),
194 instance: crate::cli::atlassian::InstanceArg::default(),
195 };
196 assert!(matches!(cmd.command, ConfluenceSubcommands::Search(_)));
197 }
198
199 #[test]
200 fn confluence_subcommands_create_variant() {
201 let cmd = ConfluenceCommand {
202 command: ConfluenceSubcommands::Create(create::CreateCommand {
203 file: None,
204 format: ContentFormat::Jfm,
205 space: Some("ENG".to_string()),
206 title: Some("Test".to_string()),
207 parent: None,
208 dry_run: false,
209 }),
210 instance: crate::cli::atlassian::InstanceArg::default(),
211 };
212 assert!(matches!(cmd.command, ConfluenceSubcommands::Create(_)));
213 }
214
215 #[test]
216 fn confluence_subcommands_label_variant() {
217 let cmd = ConfluenceCommand {
218 command: ConfluenceSubcommands::Label(label::LabelCommand {
219 command: label::LabelSubcommands::List(label::ListCommand {
220 id: "12345".to_string(),
221 output: OutputFormat::Table,
222 }),
223 }),
224 instance: crate::cli::atlassian::InstanceArg::default(),
225 };
226 assert!(matches!(cmd.command, ConfluenceSubcommands::Label(_)));
227 }
228
229 #[test]
230 fn confluence_subcommands_attachment_variant() {
231 let cmd = ConfluenceCommand {
232 command: ConfluenceSubcommands::Attachment(attachment::AttachmentCommand {
233 command: attachment::AttachmentSubcommands::List(attachment::ListCommand {
234 page_id: "12345".to_string(),
235 cursor: None,
236 limit: 25,
237 output: OutputFormat::Table,
238 }),
239 }),
240 instance: crate::cli::atlassian::InstanceArg::default(),
241 };
242 assert!(matches!(cmd.command, ConfluenceSubcommands::Attachment(_)));
243 }
244
245 #[test]
246 fn confluence_subcommands_delete_variant() {
247 let cmd = ConfluenceCommand {
248 command: ConfluenceSubcommands::Delete(delete::DeleteCommand {
249 id: "12345".to_string(),
250 force: true,
251 dry_run: false,
252 purge: false,
253 }),
254 instance: crate::cli::atlassian::InstanceArg::default(),
255 };
256 assert!(matches!(cmd.command, ConfluenceSubcommands::Delete(_)));
257 }
258
259 #[test]
260 fn confluence_subcommands_move_variant() {
261 let cmd = ConfluenceCommand {
262 command: ConfluenceSubcommands::Move(move_page::MoveCommand {
263 id: "12345".to_string(),
264 target: "456".to_string(),
265 position: move_page::MovePosition::Append,
266 }),
267 instance: crate::cli::atlassian::InstanceArg::default(),
268 };
269 assert!(matches!(cmd.command, ConfluenceSubcommands::Move(_)));
270 }
271
272 #[tokio::test]
276 #[allow(clippy::await_holding_lock)]
277 async fn confluence_command_execute_move_dispatch() {
278 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
280 .lock()
281 .unwrap_or_else(std::sync::PoisonError::into_inner);
282
283 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
284 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
285 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
286
287 let cmd = ConfluenceCommand {
288 command: ConfluenceSubcommands::Move(move_page::MoveCommand {
289 id: "12345".to_string(),
290 target: "456".to_string(),
291 position: move_page::MovePosition::Append,
292 }),
293 instance: crate::cli::atlassian::InstanceArg::default(),
294 };
295 let _ = cmd.execute().await;
296
297 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
298 std::env::remove_var("ATLASSIAN_EMAIL");
299 std::env::remove_var("ATLASSIAN_API_TOKEN");
300 }
301
302 #[test]
303 fn confluence_subcommands_user_variant() {
304 let cmd = ConfluenceCommand {
305 command: ConfluenceSubcommands::User(user::UserCommand {
306 command: user::UserSubcommands::Search(user::UserSearchCommand {
307 query: "alice".to_string(),
308 limit: 25,
309 output: OutputFormat::Table,
310 }),
311 }),
312 instance: crate::cli::atlassian::InstanceArg::default(),
313 };
314 assert!(matches!(cmd.command, ConfluenceSubcommands::User(_)));
315 }
316
317 #[test]
318 fn confluence_subcommands_space_variant() {
319 let cmd = ConfluenceCommand {
320 command: ConfluenceSubcommands::Space(space::SpaceCommand {
321 command: space::SpaceSubcommands::List(space::ListCommand {
322 keys: vec![],
323 r#type: None,
324 status: None,
325 cursor: None,
326 limit: 25,
327 output: OutputFormat::Table,
328 }),
329 }),
330 instance: crate::cli::atlassian::InstanceArg::default(),
331 };
332 assert!(matches!(cmd.command, ConfluenceSubcommands::Space(_)));
333 }
334
335 #[test]
336 fn confluence_subcommands_space_pages_variant() {
337 let pages = ConfluenceCommand {
338 command: ConfluenceSubcommands::Space(space::SpaceCommand {
339 command: space::SpaceSubcommands::Pages(space::PagesCommand {
340 key: "ENG".to_string(),
341 status: None,
342 sort: None,
343 cursor: None,
344 limit: 25,
345 output: OutputFormat::Table,
346 }),
347 }),
348 instance: crate::cli::atlassian::InstanceArg::default(),
349 };
350 let other = ConfluenceCommand {
351 command: ConfluenceSubcommands::Read(read::ReadCommand {
352 id: "1".to_string(),
353 out_file: None,
354 output: None,
355 format: ContentFormat::Jfm,
356 version: None,
357 }),
358 instance: crate::cli::atlassian::InstanceArg::default(),
359 };
360 for (expected, cmd) in [(true, pages), (false, other)] {
364 assert_eq!(
365 matches!(cmd.command, ConfluenceSubcommands::Space(_)),
366 expected
367 );
368 }
369 }
370
371 #[tokio::test]
376 #[allow(clippy::await_holding_lock)]
377 async fn confluence_command_execute_space_dispatch() {
378 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
379 .lock()
380 .unwrap_or_else(std::sync::PoisonError::into_inner);
381
382 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
383 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
384 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
385
386 let cmd = ConfluenceCommand {
387 command: ConfluenceSubcommands::Space(space::SpaceCommand {
388 command: space::SpaceSubcommands::List(space::ListCommand {
389 keys: vec![],
390 r#type: None,
391 status: None,
392 cursor: None,
393 limit: 25,
394 output: OutputFormat::Table,
395 }),
396 }),
397 instance: crate::cli::atlassian::InstanceArg::default(),
398 };
399 let _ = cmd.execute().await;
400
401 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
402 std::env::remove_var("ATLASSIAN_EMAIL");
403 std::env::remove_var("ATLASSIAN_API_TOKEN");
404 }
405
406 #[test]
407 fn confluence_subcommands_children_variant() {
408 let cmd = ConfluenceCommand {
409 command: ConfluenceSubcommands::Children(children::ChildrenCommand {
410 id: Some("12345".to_string()),
411 space: None,
412 recursive: false,
413 max_depth: 0,
414 output: OutputFormat::Table,
415 }),
416 instance: crate::cli::atlassian::InstanceArg::default(),
417 };
418 assert!(matches!(cmd.command, ConfluenceSubcommands::Children(_)));
419 }
420
421 #[test]
422 fn confluence_subcommands_history_variant() {
423 let cmd = ConfluenceCommand {
424 command: ConfluenceSubcommands::History(history::HistoryCommand {
425 id: "12345".to_string(),
426 since: None,
427 limit: 20,
428 output: OutputFormat::Table,
429 }),
430 instance: crate::cli::atlassian::InstanceArg::default(),
431 };
432 assert!(matches!(cmd.command, ConfluenceSubcommands::History(_)));
433 }
434
435 #[tokio::test]
440 #[allow(clippy::await_holding_lock)]
441 async fn confluence_command_execute_history_dispatch() {
442 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
444 .lock()
445 .unwrap_or_else(std::sync::PoisonError::into_inner);
446
447 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
448 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
449 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
450
451 let cmd = ConfluenceCommand {
452 command: ConfluenceSubcommands::History(history::HistoryCommand {
453 id: "12345".to_string(),
454 since: None,
455 limit: 20,
456 output: OutputFormat::Table,
457 }),
458 instance: crate::cli::atlassian::InstanceArg::default(),
459 };
460 let _ = cmd.execute().await;
461
462 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
463 std::env::remove_var("ATLASSIAN_EMAIL");
464 std::env::remove_var("ATLASSIAN_API_TOKEN");
465 }
466
467 #[tokio::test]
472 #[allow(clippy::await_holding_lock)]
473 async fn confluence_command_execute_children_dispatch() {
474 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
477 .lock()
478 .unwrap_or_else(std::sync::PoisonError::into_inner);
479
480 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
481 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
482 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
483
484 let cmd = ConfluenceCommand {
485 command: ConfluenceSubcommands::Children(children::ChildrenCommand {
486 id: Some("12345".to_string()),
487 space: None,
488 recursive: false,
489 max_depth: 0,
490 output: OutputFormat::Table,
491 }),
492 instance: crate::cli::atlassian::InstanceArg::default(),
493 };
494 let _ = cmd.execute().await;
495
496 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
497 std::env::remove_var("ATLASSIAN_EMAIL");
498 std::env::remove_var("ATLASSIAN_API_TOKEN");
499 }
500
501 #[tokio::test]
503 #[allow(clippy::await_holding_lock)]
504 async fn confluence_command_execute_attachment_dispatch() {
505 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
507 .lock()
508 .unwrap_or_else(std::sync::PoisonError::into_inner);
509
510 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
511 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
512 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
513
514 let cmd = ConfluenceCommand {
515 command: ConfluenceSubcommands::Attachment(attachment::AttachmentCommand {
516 command: attachment::AttachmentSubcommands::List(attachment::ListCommand {
517 page_id: "12345".to_string(),
518 cursor: None,
519 limit: 25,
520 output: OutputFormat::Table,
521 }),
522 }),
523 instance: crate::cli::atlassian::InstanceArg::default(),
524 };
525 let _ = cmd.execute().await;
526
527 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
528 std::env::remove_var("ATLASSIAN_EMAIL");
529 std::env::remove_var("ATLASSIAN_API_TOKEN");
530 }
531
532 #[test]
533 fn confluence_subcommands_download_variant() {
534 let cmd = ConfluenceCommand {
535 command: ConfluenceSubcommands::Download(download::DownloadCommand {
536 id: Some("12345".to_string()),
537 space: None,
538 output_dir: std::path::PathBuf::from("."),
539 format: ContentFormat::Jfm,
540 concurrency: 8,
541 max_depth: 0,
542 title_filter: None,
543 resume: false,
544 include_attachments: false,
545 on_conflict: download::OnConflict::Backup,
546 }),
547 instance: crate::cli::atlassian::InstanceArg::default(),
548 };
549 assert!(matches!(cmd.command, ConfluenceSubcommands::Download(_)));
550 }
551
552 #[test]
553 fn confluence_subcommands_download_space_variant() {
554 let cmd = ConfluenceCommand {
555 command: ConfluenceSubcommands::Download(download::DownloadCommand {
556 id: None,
557 space: Some("AD".to_string()),
558 output_dir: std::path::PathBuf::from("./AD"),
559 format: ContentFormat::Jfm,
560 concurrency: 8,
561 max_depth: 0,
562 title_filter: Some("architecture".to_string()),
563 resume: false,
564 include_attachments: false,
565 on_conflict: download::OnConflict::Backup,
566 }),
567 instance: crate::cli::atlassian::InstanceArg::default(),
568 };
569 assert!(matches!(cmd.command, ConfluenceSubcommands::Download(_)));
570 }
571
572 #[tokio::test]
577 #[allow(clippy::await_holding_lock)]
578 async fn confluence_command_execute_compare_dispatch() {
579 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
581 .lock()
582 .unwrap_or_else(std::sync::PoisonError::into_inner);
583
584 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
585 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
586 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
587
588 let cmd = ConfluenceCommand {
589 command: ConfluenceSubcommands::Compare(compare::CompareCommandGroup {
590 command: compare::CompareSubcommands::Run(compare::CompareCommand {
591 id: "12345".to_string(),
592 from: "previous".to_string(),
593 to: "latest".to_string(),
594 detail: compare::DetailArg::Outline,
595 include: "body".to_string(),
596 ignore_whitespace: true,
597 min_change_chars: 0,
598 filter_sections: Vec::new(),
599 budget: 16384,
600 output: OutputFormat::Yaml,
601 }),
602 }),
603 instance: crate::cli::atlassian::InstanceArg::default(),
604 };
605 let _ = cmd.execute().await;
606
607 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
608 std::env::remove_var("ATLASSIAN_EMAIL");
609 std::env::remove_var("ATLASSIAN_API_TOKEN");
610 }
611
612 fn set_dead_credentials() {
618 std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
619 std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
620 std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
621 }
622
623 fn clear_credentials_env() {
624 std::env::remove_var("ATLASSIAN_INSTANCE_URL");
625 std::env::remove_var("ATLASSIAN_EMAIL");
626 std::env::remove_var("ATLASSIAN_API_TOKEN");
627 }
628
629 #[tokio::test]
631 #[allow(clippy::await_holding_lock)]
632 async fn confluence_command_execute_copy_dispatch() {
633 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
634 .lock()
635 .unwrap_or_else(std::sync::PoisonError::into_inner);
636 set_dead_credentials();
637
638 let cmd = ConfluenceCommand {
639 command: ConfluenceSubcommands::Copy(copy::CopyCommand {
640 id: "12345".to_string(),
641 parent: "67890".to_string(),
642 title: "Copy".to_string(),
643 }),
644 instance: crate::cli::atlassian::InstanceArg::default(),
645 };
646 let _ = cmd.execute().await;
647
648 clear_credentials_env();
649 }
650
651 #[tokio::test]
653 #[allow(clippy::await_holding_lock)]
654 async fn confluence_command_execute_watcher_dispatch() {
655 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
656 .lock()
657 .unwrap_or_else(std::sync::PoisonError::into_inner);
658 set_dead_credentials();
659
660 let cmd = ConfluenceCommand {
661 command: ConfluenceSubcommands::Watcher(watcher::WatcherCommand {
662 command: watcher::WatcherSubcommands::Status(watcher::WatchArgs {
663 id: "12345".to_string(),
664 account_id: None,
665 }),
666 }),
667 instance: crate::cli::atlassian::InstanceArg::default(),
668 };
669 let _ = cmd.execute().await;
670
671 clear_credentials_env();
672 }
673
674 #[tokio::test]
676 #[allow(clippy::await_holding_lock)]
677 async fn confluence_command_execute_restriction_dispatch() {
678 let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
679 .lock()
680 .unwrap_or_else(std::sync::PoisonError::into_inner);
681 set_dead_credentials();
682
683 let cmd = ConfluenceCommand {
684 command: ConfluenceSubcommands::Restriction(restriction::RestrictionCommand {
685 command: restriction::RestrictionSubcommands::Get(restriction::GetCommand {
686 id: "12345".to_string(),
687 }),
688 }),
689 instance: crate::cli::atlassian::InstanceArg::default(),
690 };
691 let _ = cmd.execute().await;
692
693 clear_credentials_env();
694 }
695}