add request show/delete commands and cli integration tests

This commit is contained in:
Gregory Schier
2026-02-16 07:27:15 -08:00
parent 26e145942a
commit 91e0660a7a
6 changed files with 278 additions and 0 deletions

95
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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 <id>`
2. `workspace create --name <name>` (and `--json`)
3. `workspace update --json`

View File

@@ -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)]

View File

@@ -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,

View File

@@ -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());
}