Skip to main content

omni_dev/cli/atlassian/
confluence.rs

1//! Confluence CLI subcommands.
2
3pub(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/// Confluence page management, search, and more.
27#[derive(Parser)]
28pub struct ConfluenceCommand {
29    /// The Confluence subcommand to execute.
30    #[command(subcommand)]
31    pub command: ConfluenceSubcommands,
32}
33
34/// Confluence subcommands.
35#[derive(Subcommand)]
36pub enum ConfluenceSubcommands {
37    /// Manages comments on a Confluence page.
38    Comment(comment::CommentCommand),
39    /// Fetches a Confluence page and outputs it as JFM markdown or ADF JSON (mirrors the `confluence_read` MCP tool).
40    ///
41    /// The output carries `localId` attributes (and inline-comment anchor
42    /// spans) that anchor inline comments and other stateful nodes. Preserve
43    /// them verbatim when editing so a later `write` does not drop those
44    /// comments.
45    Read(read::ReadCommand),
46    /// Pushes content to a Confluence page (mirrors the `confluence_write` MCP tool).
47    ///
48    /// This fully replaces the page body, so it can silently lose data. Inline
49    /// comments (and task-item state) are anchored to the page through the
50    /// `localId` attributes that `read` emits; if the body you push omits them,
51    /// Confluence drops the inline comments tied to those anchors. Edit the JFM
52    /// produced by `read` and keep its `localId`s intact — do not hand-author a
53    /// fresh body or push content produced with local IDs stripped (see
54    /// `atlassian convert from-adf --strip-local-ids`).
55    Write(write::WriteCommand),
56    /// Interactive fetch-edit-push cycle for a Confluence page.
57    Edit(edit::EditCommand),
58    /// Searches Confluence pages using CQL (mirrors the `confluence_search` MCP tool).
59    Search(search::SearchCommand),
60    /// Creates a new Confluence page (mirrors the `confluence_create` MCP tool).
61    Create(create::CreateCommand),
62    /// Deletes a Confluence page (mirrors the `confluence_delete` MCP tool).
63    Delete(delete::DeleteCommand),
64    /// Moves or reparents a Confluence page (same-space only) (mirrors the `confluence_move` MCP tool).
65    Move(move_page::MoveCommand),
66    /// Copies a Confluence page under a destination parent (mirrors the `confluence_copy` MCP tool).
67    Copy(copy::CopyCommand),
68    /// Manages labels on Confluence pages.
69    Label(label::LabelCommand),
70    /// Manages attachments on Confluence pages.
71    Attachment(attachment::AttachmentCommand),
72    /// Recursively downloads a Confluence page tree (mirrors the `confluence_download` MCP tool).
73    Download(download::DownloadCommand),
74    /// Lists child pages of a Confluence page or top-level pages in a space (mirrors the `confluence_children` MCP tool).
75    Children(children::ChildrenCommand),
76    /// Lists version history (metadata) for a Confluence page (mirrors the `confluence_history` MCP tool).
77    History(history::HistoryCommand),
78    /// Compares two versions of a Confluence page (structural diff).
79    Compare(compare::CompareCommandGroup),
80    /// Confluence user operations.
81    User(user::UserCommand),
82    /// Confluence space operations.
83    Space(space::SpaceCommand),
84    /// Manages watchers on Confluence pages.
85    Watcher(watcher::WatcherCommand),
86    /// Manages read/update restrictions on Confluence pages.
87    Restriction(restriction::RestrictionCommand),
88}
89
90impl ConfluenceCommand {
91    /// Executes the Confluence command.
92    pub async fn execute(self) -> Result<()> {
93        match self.command {
94            ConfluenceSubcommands::Comment(cmd) => cmd.execute().await,
95            ConfluenceSubcommands::Read(cmd) => cmd.execute().await,
96            ConfluenceSubcommands::Write(cmd) => cmd.execute().await,
97            ConfluenceSubcommands::Edit(cmd) => cmd.execute().await,
98            ConfluenceSubcommands::Search(cmd) => cmd.execute().await,
99            ConfluenceSubcommands::Create(cmd) => cmd.execute().await,
100            ConfluenceSubcommands::Label(cmd) => cmd.execute().await,
101            ConfluenceSubcommands::Attachment(cmd) => cmd.execute().await,
102            ConfluenceSubcommands::Delete(cmd) => cmd.execute().await,
103            ConfluenceSubcommands::Move(cmd) => cmd.execute().await,
104            ConfluenceSubcommands::Copy(cmd) => cmd.execute().await,
105            ConfluenceSubcommands::Download(cmd) => cmd.execute().await,
106            ConfluenceSubcommands::Children(cmd) => cmd.execute().await,
107            ConfluenceSubcommands::History(cmd) => cmd.execute().await,
108            ConfluenceSubcommands::Compare(cmd) => cmd.execute().await,
109            ConfluenceSubcommands::User(cmd) => cmd.execute().await,
110            ConfluenceSubcommands::Space(cmd) => cmd.execute().await,
111            ConfluenceSubcommands::Watcher(cmd) => cmd.execute().await,
112            ConfluenceSubcommands::Restriction(cmd) => cmd.execute().await,
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::cli::atlassian::format::{ContentFormat, OutputFormat};
121
122    #[test]
123    fn confluence_subcommands_comment_variant() {
124        let cmd = ConfluenceCommand {
125            command: ConfluenceSubcommands::Comment(comment::CommentCommand {
126                command: comment::CommentSubcommands::List(comment::ListCommand {
127                    id: "12345".to_string(),
128                    kind: comment::CommentKindFilter::All,
129                    limit: 25,
130                    output: OutputFormat::Table,
131                }),
132            }),
133        };
134        assert!(matches!(cmd.command, ConfluenceSubcommands::Comment(_)));
135    }
136
137    #[test]
138    fn confluence_subcommands_read_variant() {
139        let cmd = ConfluenceCommand {
140            command: ConfluenceSubcommands::Read(read::ReadCommand {
141                id: "12345".to_string(),
142                out_file: None,
143                output: None,
144                format: ContentFormat::Jfm,
145                version: None,
146            }),
147        };
148        assert!(matches!(cmd.command, ConfluenceSubcommands::Read(_)));
149    }
150
151    #[test]
152    fn confluence_subcommands_write_variant() {
153        let cmd = ConfluenceCommand {
154            command: ConfluenceSubcommands::Write(write::WriteCommand {
155                id: "12345".to_string(),
156                file: None,
157                format: ContentFormat::Adf,
158                force: false,
159                dry_run: false,
160            }),
161        };
162        assert!(matches!(cmd.command, ConfluenceSubcommands::Write(_)));
163    }
164
165    #[test]
166    fn confluence_subcommands_edit_variant() {
167        let cmd = ConfluenceCommand {
168            command: ConfluenceSubcommands::Edit(edit::EditCommand {
169                id: "12345".to_string(),
170            }),
171        };
172        assert!(matches!(cmd.command, ConfluenceSubcommands::Edit(_)));
173    }
174
175    #[test]
176    fn confluence_subcommands_search_variant() {
177        let cmd = ConfluenceCommand {
178            command: ConfluenceSubcommands::Search(search::SearchCommand {
179                cql: Some("space = ENG".to_string()),
180                space: None,
181                title: None,
182                limit: 25,
183                output: OutputFormat::Table,
184            }),
185        };
186        assert!(matches!(cmd.command, ConfluenceSubcommands::Search(_)));
187    }
188
189    #[test]
190    fn confluence_subcommands_create_variant() {
191        let cmd = ConfluenceCommand {
192            command: ConfluenceSubcommands::Create(create::CreateCommand {
193                file: None,
194                format: ContentFormat::Jfm,
195                space: Some("ENG".to_string()),
196                title: Some("Test".to_string()),
197                parent: None,
198                dry_run: false,
199            }),
200        };
201        assert!(matches!(cmd.command, ConfluenceSubcommands::Create(_)));
202    }
203
204    #[test]
205    fn confluence_subcommands_label_variant() {
206        let cmd = ConfluenceCommand {
207            command: ConfluenceSubcommands::Label(label::LabelCommand {
208                command: label::LabelSubcommands::List(label::ListCommand {
209                    id: "12345".to_string(),
210                    output: OutputFormat::Table,
211                }),
212            }),
213        };
214        assert!(matches!(cmd.command, ConfluenceSubcommands::Label(_)));
215    }
216
217    #[test]
218    fn confluence_subcommands_attachment_variant() {
219        let cmd = ConfluenceCommand {
220            command: ConfluenceSubcommands::Attachment(attachment::AttachmentCommand {
221                command: attachment::AttachmentSubcommands::List(attachment::ListCommand {
222                    page_id: "12345".to_string(),
223                    cursor: None,
224                    limit: 25,
225                    output: OutputFormat::Table,
226                }),
227            }),
228        };
229        assert!(matches!(cmd.command, ConfluenceSubcommands::Attachment(_)));
230    }
231
232    #[test]
233    fn confluence_subcommands_delete_variant() {
234        let cmd = ConfluenceCommand {
235            command: ConfluenceSubcommands::Delete(delete::DeleteCommand {
236                id: "12345".to_string(),
237                force: true,
238                dry_run: false,
239                purge: false,
240            }),
241        };
242        assert!(matches!(cmd.command, ConfluenceSubcommands::Delete(_)));
243    }
244
245    #[test]
246    fn confluence_subcommands_move_variant() {
247        let cmd = ConfluenceCommand {
248            command: ConfluenceSubcommands::Move(move_page::MoveCommand {
249                id: "12345".to_string(),
250                target: "456".to_string(),
251                position: move_page::MovePosition::Append,
252            }),
253        };
254        assert!(matches!(cmd.command, ConfluenceSubcommands::Move(_)));
255    }
256
257    /// Exercises the `Move` dispatch arm in `ConfluenceCommand::execute` with
258    /// injected fake credentials so `create_client()` succeeds; the API call
259    /// is allowed to fail.
260    #[tokio::test]
261    #[allow(clippy::await_holding_lock)]
262    async fn confluence_command_execute_move_dispatch() {
263        // Serialise on the one canonical env mutex (issue #950).
264        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
265            .lock()
266            .unwrap_or_else(std::sync::PoisonError::into_inner);
267
268        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
269        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
270        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
271
272        let cmd = ConfluenceCommand {
273            command: ConfluenceSubcommands::Move(move_page::MoveCommand {
274                id: "12345".to_string(),
275                target: "456".to_string(),
276                position: move_page::MovePosition::Append,
277            }),
278        };
279        let _ = cmd.execute().await;
280
281        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
282        std::env::remove_var("ATLASSIAN_EMAIL");
283        std::env::remove_var("ATLASSIAN_API_TOKEN");
284    }
285
286    #[test]
287    fn confluence_subcommands_user_variant() {
288        let cmd = ConfluenceCommand {
289            command: ConfluenceSubcommands::User(user::UserCommand {
290                command: user::UserSubcommands::Search(user::UserSearchCommand {
291                    query: "alice".to_string(),
292                    limit: 25,
293                    output: OutputFormat::Table,
294                }),
295            }),
296        };
297        assert!(matches!(cmd.command, ConfluenceSubcommands::User(_)));
298    }
299
300    #[test]
301    fn confluence_subcommands_space_variant() {
302        let cmd = ConfluenceCommand {
303            command: ConfluenceSubcommands::Space(space::SpaceCommand {
304                command: space::SpaceSubcommands::List(space::ListCommand {
305                    keys: vec![],
306                    r#type: None,
307                    status: None,
308                    cursor: None,
309                    limit: 25,
310                    output: OutputFormat::Table,
311                }),
312            }),
313        };
314        assert!(matches!(cmd.command, ConfluenceSubcommands::Space(_)));
315    }
316
317    #[test]
318    fn confluence_subcommands_space_pages_variant() {
319        let pages = ConfluenceCommand {
320            command: ConfluenceSubcommands::Space(space::SpaceCommand {
321                command: space::SpaceSubcommands::Pages(space::PagesCommand {
322                    key: "ENG".to_string(),
323                    status: None,
324                    sort: None,
325                    cursor: None,
326                    limit: 25,
327                    output: OutputFormat::Table,
328                }),
329            }),
330        };
331        let other = ConfluenceCommand {
332            command: ConfluenceSubcommands::Read(read::ReadCommand {
333                id: "1".to_string(),
334                out_file: None,
335                output: None,
336                format: ContentFormat::Jfm,
337                version: None,
338            }),
339        };
340        // Single `matches!` site exercised against both a matching and
341        // non-matching variant so both arms are covered at the same source
342        // line (avoids the partial-branch noise of two separate sites).
343        for (expected, cmd) in [(true, pages), (false, other)] {
344            assert_eq!(
345                matches!(cmd.command, ConfluenceSubcommands::Space(_)),
346                expected
347            );
348        }
349    }
350
351    /// Exercises the `Space` dispatch arm in `ConfluenceCommand::execute`
352    /// with injected fake credentials so `create_client()` succeeds and the
353    /// downstream call is reached. The subsequent API call is allowed to
354    /// fail — we only care that the dispatch line runs.
355    #[tokio::test]
356    #[allow(clippy::await_holding_lock)]
357    async fn confluence_command_execute_space_dispatch() {
358        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
359            .lock()
360            .unwrap_or_else(std::sync::PoisonError::into_inner);
361
362        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
363        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
364        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
365
366        let cmd = ConfluenceCommand {
367            command: ConfluenceSubcommands::Space(space::SpaceCommand {
368                command: space::SpaceSubcommands::List(space::ListCommand {
369                    keys: vec![],
370                    r#type: None,
371                    status: None,
372                    cursor: None,
373                    limit: 25,
374                    output: OutputFormat::Table,
375                }),
376            }),
377        };
378        let _ = cmd.execute().await;
379
380        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
381        std::env::remove_var("ATLASSIAN_EMAIL");
382        std::env::remove_var("ATLASSIAN_API_TOKEN");
383    }
384
385    #[test]
386    fn confluence_subcommands_children_variant() {
387        let cmd = ConfluenceCommand {
388            command: ConfluenceSubcommands::Children(children::ChildrenCommand {
389                id: Some("12345".to_string()),
390                space: None,
391                recursive: false,
392                max_depth: 0,
393                output: OutputFormat::Table,
394            }),
395        };
396        assert!(matches!(cmd.command, ConfluenceSubcommands::Children(_)));
397    }
398
399    #[test]
400    fn confluence_subcommands_history_variant() {
401        let cmd = ConfluenceCommand {
402            command: ConfluenceSubcommands::History(history::HistoryCommand {
403                id: "12345".to_string(),
404                since: None,
405                limit: 20,
406                output: OutputFormat::Table,
407            }),
408        };
409        assert!(matches!(cmd.command, ConfluenceSubcommands::History(_)));
410    }
411
412    /// Exercises the `History` dispatch arm in `ConfluenceCommand::execute`
413    /// with injected fake credentials so `create_client()` succeeds and the
414    /// downstream call is reached. The subsequent API call is allowed to
415    /// fail — we only care that the dispatch line runs.
416    #[tokio::test]
417    #[allow(clippy::await_holding_lock)]
418    async fn confluence_command_execute_history_dispatch() {
419        // Serialise on the one canonical env mutex (issue #950).
420        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
421            .lock()
422            .unwrap_or_else(std::sync::PoisonError::into_inner);
423
424        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
425        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
426        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
427
428        let cmd = ConfluenceCommand {
429            command: ConfluenceSubcommands::History(history::HistoryCommand {
430                id: "12345".to_string(),
431                since: None,
432                limit: 20,
433                output: OutputFormat::Table,
434            }),
435        };
436        let _ = cmd.execute().await;
437
438        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
439        std::env::remove_var("ATLASSIAN_EMAIL");
440        std::env::remove_var("ATLASSIAN_API_TOKEN");
441    }
442
443    /// Exercises the `Children` dispatch arm in `ConfluenceCommand::execute`
444    /// with injected fake credentials so `create_client()` succeeds and the
445    /// downstream call is reached. The subsequent API call is allowed to
446    /// fail — we only care that the dispatch line runs.
447    #[tokio::test]
448    #[allow(clippy::await_holding_lock)]
449    async fn confluence_command_execute_children_dispatch() {
450        // Routes through the crate-wide `AUTH_ENV_MUTEX` so the env-var
451        // mutation doesn't race against other Atlassian-touching tests.
452        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
453            .lock()
454            .unwrap_or_else(std::sync::PoisonError::into_inner);
455
456        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
457        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
458        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
459
460        let cmd = ConfluenceCommand {
461            command: ConfluenceSubcommands::Children(children::ChildrenCommand {
462                id: Some("12345".to_string()),
463                space: None,
464                recursive: false,
465                max_depth: 0,
466                output: OutputFormat::Table,
467            }),
468        };
469        let _ = cmd.execute().await;
470
471        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
472        std::env::remove_var("ATLASSIAN_EMAIL");
473        std::env::remove_var("ATLASSIAN_API_TOKEN");
474    }
475
476    /// Exercises the `Attachment` dispatch arm in `ConfluenceCommand::execute`.
477    #[tokio::test]
478    #[allow(clippy::await_holding_lock)]
479    async fn confluence_command_execute_attachment_dispatch() {
480        // Serialise on the one canonical env mutex (issue #950).
481        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
482            .lock()
483            .unwrap_or_else(std::sync::PoisonError::into_inner);
484
485        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
486        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
487        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
488
489        let cmd = ConfluenceCommand {
490            command: ConfluenceSubcommands::Attachment(attachment::AttachmentCommand {
491                command: attachment::AttachmentSubcommands::List(attachment::ListCommand {
492                    page_id: "12345".to_string(),
493                    cursor: None,
494                    limit: 25,
495                    output: OutputFormat::Table,
496                }),
497            }),
498        };
499        let _ = cmd.execute().await;
500
501        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
502        std::env::remove_var("ATLASSIAN_EMAIL");
503        std::env::remove_var("ATLASSIAN_API_TOKEN");
504    }
505
506    #[test]
507    fn confluence_subcommands_download_variant() {
508        let cmd = ConfluenceCommand {
509            command: ConfluenceSubcommands::Download(download::DownloadCommand {
510                id: Some("12345".to_string()),
511                space: None,
512                output_dir: std::path::PathBuf::from("."),
513                format: ContentFormat::Jfm,
514                concurrency: 8,
515                max_depth: 0,
516                title_filter: None,
517                resume: false,
518                include_attachments: false,
519                on_conflict: download::OnConflict::Backup,
520            }),
521        };
522        assert!(matches!(cmd.command, ConfluenceSubcommands::Download(_)));
523    }
524
525    #[test]
526    fn confluence_subcommands_download_space_variant() {
527        let cmd = ConfluenceCommand {
528            command: ConfluenceSubcommands::Download(download::DownloadCommand {
529                id: None,
530                space: Some("AD".to_string()),
531                output_dir: std::path::PathBuf::from("./AD"),
532                format: ContentFormat::Jfm,
533                concurrency: 8,
534                max_depth: 0,
535                title_filter: Some("architecture".to_string()),
536                resume: false,
537                include_attachments: false,
538                on_conflict: download::OnConflict::Backup,
539            }),
540        };
541        assert!(matches!(cmd.command, ConfluenceSubcommands::Download(_)));
542    }
543
544    /// Exercises the `Compare` dispatch arm in `ConfluenceCommand::execute`
545    /// with injected fake credentials so `create_client()` succeeds and the
546    /// downstream call is reached. The subsequent API call is allowed to
547    /// fail — we only care that the dispatch line runs.
548    #[tokio::test]
549    #[allow(clippy::await_holding_lock)]
550    async fn confluence_command_execute_compare_dispatch() {
551        // Serialise on the one canonical env mutex (issue #950).
552        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
553            .lock()
554            .unwrap_or_else(std::sync::PoisonError::into_inner);
555
556        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
557        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
558        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
559
560        let cmd = ConfluenceCommand {
561            command: ConfluenceSubcommands::Compare(compare::CompareCommandGroup {
562                command: compare::CompareSubcommands::Run(compare::CompareCommand {
563                    id: "12345".to_string(),
564                    from: "previous".to_string(),
565                    to: "latest".to_string(),
566                    detail: compare::DetailArg::Outline,
567                    include: "body".to_string(),
568                    ignore_whitespace: true,
569                    min_change_chars: 0,
570                    filter_sections: Vec::new(),
571                    budget: 16384,
572                    output: OutputFormat::Yaml,
573                }),
574            }),
575        };
576        let _ = cmd.execute().await;
577
578        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
579        std::env::remove_var("ATLASSIAN_EMAIL");
580        std::env::remove_var("ATLASSIAN_API_TOKEN");
581    }
582
583    /// Points credentials at a dead address so `cmd.execute()` reaches the
584    /// dispatch arm and the subcommand's `create_client()` glue then fails on
585    /// transport — the success paths are covered by each subcommand's own
586    /// wiremock tests. Sync helpers (the caller holds the env mutex across the
587    /// await, so the future must not capture a guard here).
588    fn set_dead_credentials() {
589        std::env::set_var("ATLASSIAN_INSTANCE_URL", "http://127.0.0.1:1");
590        std::env::set_var("ATLASSIAN_EMAIL", "test@example.com");
591        std::env::set_var("ATLASSIAN_API_TOKEN", "fake-token");
592    }
593
594    fn clear_credentials_env() {
595        std::env::remove_var("ATLASSIAN_INSTANCE_URL");
596        std::env::remove_var("ATLASSIAN_EMAIL");
597        std::env::remove_var("ATLASSIAN_API_TOKEN");
598    }
599
600    /// Exercises the `Copy` dispatch arm in `ConfluenceCommand::execute`.
601    #[tokio::test]
602    #[allow(clippy::await_holding_lock)]
603    async fn confluence_command_execute_copy_dispatch() {
604        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
605            .lock()
606            .unwrap_or_else(std::sync::PoisonError::into_inner);
607        set_dead_credentials();
608
609        let cmd = ConfluenceCommand {
610            command: ConfluenceSubcommands::Copy(copy::CopyCommand {
611                id: "12345".to_string(),
612                parent: "67890".to_string(),
613                title: "Copy".to_string(),
614            }),
615        };
616        let _ = cmd.execute().await;
617
618        clear_credentials_env();
619    }
620
621    /// Exercises the `Watcher` dispatch arm in `ConfluenceCommand::execute`.
622    #[tokio::test]
623    #[allow(clippy::await_holding_lock)]
624    async fn confluence_command_execute_watcher_dispatch() {
625        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
626            .lock()
627            .unwrap_or_else(std::sync::PoisonError::into_inner);
628        set_dead_credentials();
629
630        let cmd = ConfluenceCommand {
631            command: ConfluenceSubcommands::Watcher(watcher::WatcherCommand {
632                command: watcher::WatcherSubcommands::Status(watcher::WatchArgs {
633                    id: "12345".to_string(),
634                    account_id: None,
635                }),
636            }),
637        };
638        let _ = cmd.execute().await;
639
640        clear_credentials_env();
641    }
642
643    /// Exercises the `Restriction` dispatch arm in `ConfluenceCommand::execute`.
644    #[tokio::test]
645    #[allow(clippy::await_holding_lock)]
646    async fn confluence_command_execute_restriction_dispatch() {
647        let _lock = crate::atlassian::auth::test_util::AUTH_ENV_MUTEX
648            .lock()
649            .unwrap_or_else(std::sync::PoisonError::into_inner);
650        set_dead_credentials();
651
652        let cmd = ConfluenceCommand {
653            command: ConfluenceSubcommands::Restriction(restriction::RestrictionCommand {
654                command: restriction::RestrictionSubcommands::Get(restriction::GetCommand {
655                    id: "12345".to_string(),
656                }),
657            }),
658        };
659        let _ = cmd.execute().await;
660
661        clear_credentials_env();
662    }
663}