diff --git a/README.md b/README.md index 5babc9b..a2c6a2f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ As part of building the import server, the following commands exercise each oper - [x] trello attachment get - includes download url - [x] trello attachment save - saves to disk - [x] nextcloud deck get - includes list of boards -- [ ] nextcloud board get (was stack list) +- [x] nextcloud board get - includes list of stacks - [ ] nextcloud stack get (was card list) - [x] nextcloud card create - [x] nextcloud card add-label diff --git a/src/nextcloud/board.rs b/src/nextcloud/board.rs new file mode 100644 index 0000000..6290be5 --- /dev/null +++ b/src/nextcloud/board.rs @@ -0,0 +1,38 @@ +// +use clap::Parser; + +use crate::execute::Execute; +use crate::nextcloud::model::NextcloudBoardId; +use crate::{p, FullCtx}; + +#[derive(Parser, Debug)] +pub enum NextcloudBoardCommand { + Get { + #[clap(long, action = clap::ArgAction::SetTrue)] + dump: bool, + board_id: i64, + }, +} + +impl Execute for NextcloudBoardCommand { + async fn execute(&self, ctx: &FullCtx) -> color_eyre::Result<()> { + match self { + Self::Get { dump, board_id } => { + let api_result = ctx + .deck_client() + .get_stacks(NextcloudBoardId::new(*board_id)) + .await; + if *dump { + p!(ctx.prt, "{}", api_result.text); + } else { + let mut stacks = api_result.result?; + stacks.sort_by(|a, b| a.title.cmp(&b.title)); + stacks.into_iter().for_each(|stack| { + p!(ctx.prt, "{}:{}", stack.id, stack.title); + }); + } + Ok(()) + } + } + } +} diff --git a/src/nextcloud/client.rs b/src/nextcloud/client.rs index 78b42e4..4a050d6 100644 --- a/src/nextcloud/client.rs +++ b/src/nextcloud/client.rs @@ -16,6 +16,8 @@ pub(crate) struct DeckClient<'ctx> { password: &'ctx NextcloudPassword, } +impl DeckClient<'_> {} + // Uses the API described here: https://deck.readthedocs.io/en/stable/API/#cards impl<'ctx> DeckClient<'ctx> { pub fn new(ctx: &'ctx FullCtx) -> Self { diff --git a/src/nextcloud/mod.rs b/src/nextcloud/mod.rs index b5016ea..09646e3 100644 --- a/src/nextcloud/mod.rs +++ b/src/nextcloud/mod.rs @@ -1,6 +1,7 @@ // use clap::Parser; +use crate::nextcloud::board::NextcloudBoardCommand; use crate::{ execute::Execute, nextcloud::{ @@ -12,6 +13,7 @@ use crate::{ FullCtx, }; +pub(crate) mod board; pub(crate) mod card; pub(crate) mod client; pub(crate) mod deck; @@ -33,6 +35,8 @@ pub enum NextcloudCommand { #[clap(subcommand)] Deck(NextcloudDeckCommand), #[clap(subcommand)] + Board(NextcloudBoardCommand), + #[clap(subcommand)] Stack(NextcloudStackCommand), #[clap(subcommand)] Card(NextcloudCardCommand), @@ -41,6 +45,7 @@ impl Execute for NextcloudCommand { async fn execute(&self, ctx: &FullCtx) -> color_eyre::Result<()> { match self { NextcloudCommand::Deck(cmd) => cmd.execute(ctx).await, + NextcloudCommand::Board(cmd) => cmd.execute(ctx).await, NextcloudCommand::Stack(cmd) => cmd.execute(ctx).await, NextcloudCommand::Card(cmd) => cmd.execute(ctx).await, } diff --git a/src/nextcloud/tests/board/get.rs b/src/nextcloud/tests/board/get.rs new file mode 100644 index 0000000..da2008c --- /dev/null +++ b/src/nextcloud/tests/board/get.rs @@ -0,0 +1,78 @@ +use crate::execute::Execute; +use crate::nextcloud::board::NextcloudBoardCommand; +use crate::nextcloud::card::NextcloudCardCommand; +use crate::nextcloud::stack::NextcloudStackCommand; +use crate::nextcloud::NextcloudCommand; +use crate::Command; +// +use super::*; + +#[rstest::fixture] +fn ctx() -> FullCtx { + let fs = given::a_filesystem(); + + let nextcloud_config = given::a_nextcloud_config(); + + let hostname = &nextcloud_config.hostname; + let board_id = nextcloud_config.board_id; + + let mock_net = given::a_network(); + mock_net + .on() + .get(crate::f!( + "https://{hostname}/index.php/apps/deck/api/v1.0/boards/{board_id}/stacks", + )) + .respond(StatusCode::OK) + .body(include_str!( + "../../../tests/responses/nextcloud-board-get.json" + )) + .expect("mock request"); + + given::a_full_context(fs, mock_net) +} + +#[rstest::rstest] +#[test_log::test(tokio::test)] +async fn dump(ctx: FullCtx) { + //given + let prt = ctx.prt.clone(); + let prt = prt.as_test().unwrap(); + let board_id = ctx.cfg.nextcloud.board_id; + + //when + Command::Nextcloud(NextcloudCommand::Board(NextcloudBoardCommand::Get { + dump: true, + board_id: board_id.into(), + })) + .execute(&ctx) + .await + .expect("execute"); + + //then + let output = prt.output(); + assert_eq!( + output.trim(), + include_str!("../../../tests/responses/nextcloud-board-get.json").trim() + ); +} + +#[rstest::rstest] +#[test_log::test(tokio::test)] +async fn no_dump(ctx: FullCtx) { + //given + let prt = ctx.prt.clone(); + let prt = prt.as_test().unwrap(); + + //when + Command::Nextcloud(NextcloudCommand::Board(NextcloudBoardCommand::Get { + dump: false, + board_id: ctx.cfg.nextcloud.board_id.into(), + })) + .execute(&ctx) + .await + .expect("execute"); + + //then + let output = prt.output(); + assert_eq!(output.trim(), ["2:Doing\n3:Done\n1:To do"].join("\n")); +} diff --git a/src/nextcloud/tests/board/mod.rs b/src/nextcloud/tests/board/mod.rs new file mode 100644 index 0000000..2563f63 --- /dev/null +++ b/src/nextcloud/tests/board/mod.rs @@ -0,0 +1,4 @@ +// +use super::*; + +mod get; diff --git a/src/nextcloud/tests/mod.rs b/src/nextcloud/tests/mod.rs index 5229654..888c314 100644 --- a/src/nextcloud/tests/mod.rs +++ b/src/nextcloud/tests/mod.rs @@ -29,6 +29,7 @@ use crate::{ AppConfig, FullCtx, }; +mod board; mod card; mod deck; mod stack; diff --git a/src/tests/responses/nextcloud-board-get.json b/src/tests/responses/nextcloud-board-get.json new file mode 100644 index 0000000..ff155d9 --- /dev/null +++ b/src/tests/responses/nextcloud-board-get.json @@ -0,0 +1,290 @@ +[ + { + "id": 3, + "title": "Done", + "boardId": 1, + "deletedAt": 0, + "lastModified": 1733515991, + "cards": [ + { + "id": 322, + "title": "Lunch: Soup & Toast", + "description": "", + "stackId": 3, + "type": "plain", + "lastModified": 1733515991, + "lastEditor": null, + "createdAt": 1733043472, + "labels": [ + { + "id": 4, + "title": "Later", + "color": "F1DB50", + "boardId": 1, + "cardId": 322, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + } + ], + "assignedUsers": [ + { + "id": 25, + "participant": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "cardId": 322, + "type": 0 + } + ], + "attachments": null, + "attachmentCount": 0, + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "order": 0, + "archived": false, + "done": null, + "duedate": null, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "ETag": "dda386b3b247d7b4bd8917e19d38c01b", + "overdue": 0 + } + ], + "order": 2, + "ETag": "dda386b3b247d7b4bd8917e19d38c01b" + }, + { + "id": 2, + "title": "Doing", + "boardId": 1, + "deletedAt": 0, + "lastModified": 1733695323, + "cards": [ + { + "id": 319, + "title": "That", + "description": "", + "stackId": 2, + "type": "plain", + "lastModified": 1733335979, + "lastEditor": null, + "createdAt": 1732610551, + "labels": [], + "assignedUsers": [], + "attachments": null, + "attachmentCount": 1, + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "order": 0, + "archived": false, + "done": null, + "duedate": null, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "ETag": "79aeb703494e67736cc66b35053d258d", + "overdue": 0 + }, + { + "id": 323, + "title": "Second lunch: Poached Egg & Toasted Muffin", + "description": "", + "stackId": 2, + "type": "plain", + "lastModified": 1733695323, + "lastEditor": null, + "createdAt": 1733043481, + "labels": [], + "assignedUsers": [ + { + "id": 26, + "participant": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "cardId": 323, + "type": 0 + } + ], + "attachments": null, + "attachmentCount": 0, + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "order": 1, + "archived": false, + "done": null, + "duedate": null, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "ETag": "9de45fc7d68507f1eaef462e90e6414c", + "overdue": 0 + } + ], + "order": 1, + "ETag": "9de45fc7d68507f1eaef462e90e6414c" + }, + { + "id": 1, + "title": "To do", + "boardId": 1, + "deletedAt": 0, + "lastModified": 1734115321, + "cards": [ + { + "id": 318, + "title": "This", + "description": "", + "stackId": 1, + "type": "plain", + "lastModified": 1733049748, + "lastEditor": null, + "createdAt": 1732610548, + "labels": [], + "assignedUsers": [], + "attachments": null, + "attachmentCount": 1, + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "order": 0, + "archived": false, + "done": null, + "duedate": null, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "ETag": "e5007451d88799e3e3d3581cbcb30210", + "overdue": 0 + }, + { + "id": 321, + "title": "Breakfast: Cereal", + "description": "", + "stackId": 1, + "type": "plain", + "lastModified": 1734115321, + "lastEditor": null, + "createdAt": 1733043461, + "labels": [ + { + "id": 1, + "title": "Finished", + "color": "31CC7C", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + }, + { + "id": 1, + "title": "Finished", + "color": "31CC7C", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + }, + { + "id": 1, + "title": "Finished", + "color": "31CC7C", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + }, + { + "id": 2, + "title": "To review", + "color": "317CCC", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + }, + { + "id": 2, + "title": "To review", + "color": "317CCC", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + }, + { + "id": 3, + "title": "Action needed", + "color": "FF7A66", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + }, + { + "id": 4, + "title": "Later", + "color": "F1DB50", + "boardId": 1, + "cardId": 321, + "lastModified": 1670965629, + "ETag": "983f87848dc9c18d0aee63e7ee0fc83f" + } + ], + "assignedUsers": [ + { + "id": 24, + "participant": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "cardId": 321, + "type": 0 + } + ], + "attachments": null, + "attachmentCount": 0, + "owner": { + "primaryKey": "pcampbell", + "uid": "pcampbell", + "displayname": "Paul Campbell", + "type": 0 + }, + "order": 1, + "archived": false, + "done": null, + "duedate": null, + "deletedAt": 0, + "commentsUnread": 0, + "commentsCount": 0, + "ETag": "9acdd42135c0347968891d2a073b87be", + "overdue": 0 + } + ], + "order": 0, + "ETag": "9acdd42135c0347968891d2a073b87be" + } +]