From 91e0660a7a8b29da902bd132dd3ee6d91cfa3183 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 16 Feb 2026 07:27:15 -0800 Subject: [PATCH] add request show/delete commands and cli integration tests --- Cargo.lock | 95 ++++++++++++++++ crates-cli/yaak-cli/Cargo.toml | 5 + crates-cli/yaak-cli/PLAN.md | 2 + crates-cli/yaak-cli/src/cli.rs | 16 +++ crates-cli/yaak-cli/src/commands/request.rs | 40 +++++++ crates-cli/yaak-cli/tests/request_cli.rs | 120 ++++++++++++++++++++ 6 files changed, 278 insertions(+) create mode 100644 crates-cli/yaak-cli/tests/request_cli.rs diff --git a/Cargo.lock b/Cargo.lock index 4e86ab19..6e0c8695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,21 @@ dependencies = [ "zbus", ] +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -630,6 +645,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.18.1" @@ -1357,6 +1383,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -1735,6 +1767,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -3419,6 +3460,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "notify" version = "8.0.0" @@ -4290,6 +4337,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -6288,6 +6365,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -7040,6 +7123,15 @@ dependencies = [ "libc", ] +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -8057,11 +8149,14 @@ dependencies = [ name = "yaak-cli" version = "0.1.0" dependencies = [ + "assert_cmd", "clap", "dirs", "env_logger", "log", + "predicates", "serde_json", + "tempfile", "tokio", "yaak-crypto", "yaak-http", diff --git a/crates-cli/yaak-cli/Cargo.toml b/crates-cli/yaak-cli/Cargo.toml index f3c3c2fe..0e61a8d8 100644 --- a/crates-cli/yaak-cli/Cargo.toml +++ b/crates-cli/yaak-cli/Cargo.toml @@ -20,3 +20,8 @@ yaak-http = { workspace = true } yaak-models = { workspace = true } yaak-plugins = { workspace = true } yaak-templates = { workspace = true } + +[dev-dependencies] +assert_cmd = "2" +predicates = "3" +tempfile = "3" diff --git a/crates-cli/yaak-cli/PLAN.md b/crates-cli/yaak-cli/PLAN.md index 38d7102c..22a7a6db 100644 --- a/crates-cli/yaak-cli/PLAN.md +++ b/crates-cli/yaak-cli/PLAN.md @@ -145,6 +145,8 @@ Existing behavior stays the same, just reorganized. Remove the `get` command. ### Phase 2: Add missing CRUD commands +Status: in progress (request `show` and `delete` implemented) + 1. `workspace show ` 2. `workspace create --name ` (and `--json`) 3. `workspace update --json` diff --git a/crates-cli/yaak-cli/src/cli.rs b/crates-cli/yaak-cli/src/cli.rs index 2e3de8f2..d9faf6cf 100644 --- a/crates-cli/yaak-cli/src/cli.rs +++ b/crates-cli/yaak-cli/src/cli.rs @@ -73,6 +73,12 @@ pub enum RequestCommands { workspace_id: String, }, + /// Show a request as JSON + Show { + /// Request ID + request_id: String, + }, + /// Send an HTTP request by ID Send { /// Request ID @@ -96,6 +102,16 @@ pub enum RequestCommands { #[arg(short, long)] url: String, }, + + /// Delete a request + Delete { + /// Request ID + request_id: String, + + /// Skip confirmation prompt + #[arg(short, long)] + yes: bool, + }, } #[derive(Args)] diff --git a/crates-cli/yaak-cli/src/commands/request.rs b/crates-cli/yaak-cli/src/commands/request.rs index 1dafbe57..3c2d4e66 100644 --- a/crates-cli/yaak-cli/src/commands/request.rs +++ b/crates-cli/yaak-cli/src/commands/request.rs @@ -3,6 +3,7 @@ use crate::context::CliContext; use log::info; use serde_json::Value; use std::collections::BTreeMap; +use std::io::{self, IsTerminal, Write}; use tokio::sync::mpsc; use yaak_http::path_placeholders::apply_path_placeholders; use yaak_http::sender::{HttpSender, ReqwestSender}; @@ -17,12 +18,14 @@ use yaak_templates::{RenderOptions, parse_and_render, render_json_value_raw}; pub async fn run(ctx: &CliContext, args: RequestArgs, environment: Option<&str>, verbose: bool) { match args.command { RequestCommands::List { workspace_id } => list(ctx, &workspace_id), + RequestCommands::Show { request_id } => show(ctx, &request_id), RequestCommands::Send { request_id } => { send_request_by_id(ctx, &request_id, environment, verbose).await; } RequestCommands::Create { workspace_id, name, method, url } => { create(ctx, workspace_id, name, method, url) } + RequestCommands::Delete { request_id, yes } => delete(ctx, &request_id, yes), } } @@ -57,6 +60,43 @@ fn create(ctx: &CliContext, workspace_id: String, name: String, method: String, println!("Created request: {}", created.id); } +fn show(ctx: &CliContext, request_id: &str) { + let request = ctx + .db() + .get_http_request(request_id) + .expect("Failed to get request"); + let output = serde_json::to_string_pretty(&request).expect("Failed to serialize request"); + println!("{output}"); +} + +fn delete(ctx: &CliContext, request_id: &str, yes: bool) { + if !yes && !confirm_delete_request(request_id) { + println!("Aborted"); + return; + } + + let deleted = ctx + .db() + .delete_http_request_by_id(request_id, &UpdateSource::Sync) + .expect("Failed to delete request"); + println!("Deleted request: {}", deleted.id); +} + +fn confirm_delete_request(request_id: &str) -> bool { + if !io::stdin().is_terminal() { + eprintln!("Refusing to delete in non-interactive mode without --yes"); + std::process::exit(1); + } + + print!("Delete request {request_id}? [y/N]: "); + io::stdout().flush().expect("Failed to flush stdout"); + + let mut input = String::new(); + io::stdin().read_line(&mut input).expect("Failed to read confirmation"); + + matches!(input.trim().to_lowercase().as_str(), "y" | "yes") +} + /// Send a request by ID and print response in the same format as legacy `send`. pub async fn send_request_by_id( ctx: &CliContext, diff --git a/crates-cli/yaak-cli/tests/request_cli.rs b/crates-cli/yaak-cli/tests/request_cli.rs new file mode 100644 index 00000000..c00c3d18 --- /dev/null +++ b/crates-cli/yaak-cli/tests/request_cli.rs @@ -0,0 +1,120 @@ +use assert_cmd::cargo::cargo_bin_cmd; +use assert_cmd::Command; +use predicates::str::contains; +use std::path::Path; +use tempfile::TempDir; +use yaak_models::models::{HttpRequest, Workspace}; +use yaak_models::util::UpdateSource; + +fn cli_cmd(data_dir: &Path) -> Command { + let mut cmd = cargo_bin_cmd!("yaakcli"); + cmd.arg("--data-dir").arg(data_dir); + cmd +} + +fn seed_workspace(data_dir: &Path, workspace_id: &str) { + let db_path = data_dir.join("db.sqlite"); + let blob_path = data_dir.join("blobs.sqlite"); + let (query_manager, _blob_manager, _rx) = + yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB"); + + let workspace = Workspace { + id: workspace_id.to_string(), + name: "Test Workspace".to_string(), + description: "Integration test workspace".to_string(), + ..Default::default() + }; + + query_manager + .connect() + .upsert_workspace(&workspace, &UpdateSource::Sync) + .expect("Failed to seed workspace"); +} + +fn seed_request(data_dir: &Path, workspace_id: &str, request_id: &str) { + let db_path = data_dir.join("db.sqlite"); + let blob_path = data_dir.join("blobs.sqlite"); + let (query_manager, _blob_manager, _rx) = + yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB"); + + let request = HttpRequest { + id: request_id.to_string(), + workspace_id: workspace_id.to_string(), + name: "Seeded Request".to_string(), + method: "GET".to_string(), + url: "https://example.com".to_string(), + ..Default::default() + }; + + query_manager + .connect() + .upsert_http_request(&request, &UpdateSource::Sync) + .expect("Failed to seed request"); +} + +#[test] +fn request_show_and_delete_yes_round_trip() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + seed_workspace(data_dir, "wk_test"); + + let create_assert = cli_cmd(data_dir) + .args([ + "request", + "create", + "wk_test", + "--name", + "Smoke Test", + "--url", + "https://example.com", + ]) + .assert() + .success(); + + let create_stdout = String::from_utf8_lossy(&create_assert.get_output().stdout).to_string(); + let request_id = create_stdout + .trim() + .split_once(": ") + .map(|(_, id)| id.to_string()) + .expect("Expected request id in create output"); + + cli_cmd(data_dir) + .args(["request", "show", &request_id]) + .assert() + .success() + .stdout(contains(format!("\"id\": \"{request_id}\""))) + .stdout(contains("\"workspaceId\": \"wk_test\"")); + + cli_cmd(data_dir) + .args(["request", "delete", &request_id, "--yes"]) + .assert() + .success() + .stdout(contains(format!("Deleted request: {request_id}"))); + + let db_path = data_dir.join("db.sqlite"); + let blob_path = data_dir.join("blobs.sqlite"); + let (query_manager, _blob_manager, _rx) = + yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB"); + assert!(query_manager.connect().get_http_request(&request_id).is_err()); +} + +#[test] +fn request_delete_without_yes_fails_in_non_interactive_mode() { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let data_dir = temp_dir.path(); + seed_workspace(data_dir, "wk_test"); + seed_request(data_dir, "wk_test", "rq_seed_delete_noninteractive"); + + cli_cmd(data_dir) + .args(["request", "delete", "rq_seed_delete_noninteractive"]) + .assert() + .failure() + .code(1) + .stderr(contains("Refusing to delete in non-interactive mode without --yes")); + + let db_path = data_dir.join("db.sqlite"); + let blob_path = data_dir.join("blobs.sqlite"); + let (query_manager, _blob_manager, _rx) = + yaak_models::init_standalone(&db_path, &blob_path).expect("Failed to initialize DB"); + assert!(query_manager.connect().get_http_request("rq_seed_delete_noninteractive").is_ok()); +}