commit 1fde769e6d5114e42f275be5a8214bce594457c3
parent bebee95eb806dd4b9c9a566e23c2a6668c89faf7
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Apr 2026 10:21:04 +0000
cli: normalize approval token validation
- trim approval tokens before required operation checks
- reuse the shared context guard across approval-gated mutations
- cover absent, empty, and whitespace tokens for required operations
- verify target cli and signer runtime mode integration tests
Diffstat:
6 files changed, 68 insertions(+), 18 deletions(-)
diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs
@@ -118,6 +118,16 @@ impl OperationContext {
});
context
}
+
+ pub fn requires_approval_token(&self) -> bool {
+ !self.dry_run && !self.has_approval_token()
+ }
+
+ pub fn has_approval_token(&self) -> bool {
+ self.approval_token
+ .as_deref()
+ .is_some_and(|token| !token.trim().is_empty())
+ }
}
pub type OperationData = Map<String, Value>;
diff --git a/src/operation_core.rs b/src/operation_core.rs
@@ -233,7 +233,7 @@ impl OperationService<AccountImportRequest> for CoreOperationService<'_> {
"account": account_summary_view(&account),
}));
}
- if request.context.approval_token.is_none() {
+ if request.context.requires_approval_token() {
return Err(OperationAdapterError::approval_required(
request.operation_id(),
));
@@ -321,7 +321,7 @@ impl OperationService<AccountRemoveRequest> for CoreOperationService<'_> {
"remaining_account_count": preview.remaining_account_count,
}));
}
- if request.context.approval_token.is_none() {
+ if request.context.requires_approval_token() {
return Err(OperationAdapterError::approval_required(
request.operation_id(),
));
diff --git a/src/operation_farm.rs b/src/operation_farm.rs
@@ -143,7 +143,7 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
print_job: bool_input(&request, "print_job").unwrap_or(false),
print_event: bool_input(&request, "print_event").unwrap_or(false),
};
- if !request.context.dry_run && request.context.approval_token.is_none() {
+ if request.context.requires_approval_token() {
return Err(OperationAdapterError::approval_required(
request.operation_id(),
));
diff --git a/src/operation_listing.rs b/src/operation_listing.rs
@@ -208,7 +208,7 @@ fn require_approval<P>(request: &OperationRequest<P>) -> Result<(), OperationAda
where
P: OperationRequestPayload + OperationRequestData,
{
- if request.context.approval_token.is_none() {
+ if request.context.requires_approval_token() {
return Err(OperationAdapterError::approval_required(
request.operation_id(),
));
diff --git a/src/operation_order.rs b/src/operation_order.rs
@@ -29,7 +29,7 @@ impl OperationService<OrderSubmitRequest> for OrderOperationService<'_> {
&self,
request: OperationRequest<OrderSubmitRequest>,
) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
- if !request.context.dry_run && request.context.approval_token.is_none() {
+ if request.context.requires_approval_token() {
return Err(OperationAdapterError::approval_required(
request.operation_id(),
));
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -6,8 +6,8 @@ use std::path::Path;
use serde_json::Value;
use support::{
- RadrootsCliSandbox, assert_no_removed_command_reference, create_listing_draft,
- make_listing_publishable, ndjson_from_stdout, radroots,
+ RadrootsCliSandbox, assert_no_removed_command_reference, create_listing_draft, identity_public,
+ make_listing_publishable, ndjson_from_stdout, radroots, write_public_identity_profile,
};
const LISTING_ADDR: &str =
@@ -542,18 +542,58 @@ fn store_backup_dry_run_preflights_initialized_store_without_writing_file() {
}
#[test]
-fn required_approval_missing_token_returns_structured_error() {
- let output = radroots()
- .args(["--format", "json", "order", "submit"])
- .output()
- .expect("run order submit");
-
- assert_eq!(output.status.code(), Some(6));
- let value: Value = serde_json::from_slice(&output.stdout).expect("json envelope");
+fn required_approval_token_rejects_absent_empty_and_whitespace_values() {
+ let sandbox = RadrootsCliSandbox::new();
+ let public_identity = identity_public(61);
+ let public_identity_file =
+ write_public_identity_profile(&sandbox, "approval-import", &public_identity);
+ let public_identity_path = public_identity_file.to_string_lossy();
+
+ assert_required_approval_token_rejected(
+ &sandbox,
+ "account.import",
+ &["account", "import", public_identity_path.as_ref()],
+ );
+ assert_required_approval_token_rejected(
+ &sandbox,
+ "account.remove",
+ &["account", "remove", "acct_missing"],
+ );
+ assert_required_approval_token_rejected(&sandbox, "farm.publish", &["farm", "publish"]);
+ assert_required_approval_token_rejected(
+ &sandbox,
+ "listing.publish",
+ &["listing", "publish", "missing-listing.toml"],
+ );
+ assert_required_approval_token_rejected(
+ &sandbox,
+ "listing.archive",
+ &["listing", "archive", "missing-listing.toml"],
+ );
+ assert_required_approval_token_rejected(&sandbox, "order.submit", &["order", "submit"]);
+}
- assert_eq!(value["operation_id"], "order.submit");
- assert_eq!(value["errors"][0]["code"], "approval_required");
- assert_eq!(value["errors"][0]["exit_code"], 6);
+fn assert_required_approval_token_rejected(
+ sandbox: &RadrootsCliSandbox,
+ operation_id: &str,
+ command_args: &[&str],
+) {
+ for token in [None, Some(""), Some(" \t ")] {
+ let mut args = vec!["--format", "json"];
+ if let Some(token) = token {
+ args.push("--approval-token");
+ args.push(token);
+ }
+ args.extend_from_slice(command_args);
+
+ let (output, value) = sandbox.json_output(&args);
+
+ assert_eq!(output.status.code(), Some(6), "`{args:?}` should fail");
+ assert_eq!(value["operation_id"], operation_id);
+ assert_eq!(value["errors"][0]["code"], "approval_required");
+ assert_eq!(value["errors"][0]["exit_code"], 6);
+ assert_no_removed_command_reference(&value, &args);
+ }
}
#[test]