Skip to content

Commit 3f635ce

Browse files
committed
Add required-args pre-flight checks for four create/update commands
Closes the 'empty-body 400' gap audited across the create/update surface: where the user supplies zero (or trivially insufficient) flags, the server today returns a 400 with no useful body — the new Stage A renderer can't help. These commands now fail fast client-side with a CliError::Arg naming the missing flags. - endpoint create: require at least --chain or --network - endpoint update: require --label - endpoint security set-options: require at least one toggle - team set-endpoints: require at least one endpoint id webhook create's required fields are already enforced by clap. Four integration tests assert exit code 1 and zero HTTP traffic.
1 parent 896a2a7 commit 3f635ce

5 files changed

Lines changed: 80 additions & 0 deletions

File tree

src/commands/endpoint/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ async fn list(a: ListArgs, ctx: Ctx) -> Result<(), CliError> {
224224
}
225225

226226
async fn create(a: CreateArgs, ctx: Ctx) -> Result<(), CliError> {
227+
if a.chain.is_none() && a.network.is_none() {
228+
return Err(CliError::Arg(
229+
"'endpoint create' requires at least one of --chain or --network. \
230+
Run 'qn chain list' to see available chains."
231+
.into(),
232+
));
233+
}
227234
let req = CreateEndpointRequest {
228235
chain: a.chain,
229236
network: a.network,
@@ -243,6 +250,9 @@ async fn show(id: &str, ctx: Ctx) -> Result<(), CliError> {
243250
}
244251

245252
async fn update(a: UpdateArgs, ctx: Ctx) -> Result<(), CliError> {
253+
if a.label.is_none() {
254+
return Err(CliError::Arg("'endpoint update' requires --label.".into()));
255+
}
246256
let req = UpdateEndpointRequest { label: a.label };
247257
ctx.sdk.admin.update_endpoint(&a.id, &req).await?;
248258
ctx.out.note(&format!("✓ Updated endpoint {}", a.id));

src/commands/endpoint/security.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,23 @@ async fn options_show(id: &str, ctx: Ctx) -> Result<(), CliError> {
216216
}
217217

218218
async fn set_options(a: SetOptionsArgs, ctx: Ctx) -> Result<(), CliError> {
219+
if a.tokens.is_none()
220+
&& a.referrers.is_none()
221+
&& a.jwts.is_none()
222+
&& a.ips.is_none()
223+
&& a.domain_masks.is_none()
224+
&& a.hsts.is_none()
225+
&& a.cors.is_none()
226+
&& a.request_filters.is_none()
227+
&& a.ip_custom_header.is_none()
228+
{
229+
return Err(CliError::Arg(
230+
"'endpoint security set-options' requires at least one of: \
231+
--tokens, --referrers, --jwts, --ips, --domain-masks, --hsts, \
232+
--cors, --request-filters, --ip-custom-header."
233+
.into(),
234+
));
235+
}
219236
let options = SecurityOptionsUpdate {
220237
tokens: a.tokens.map(|t| t.as_str().to_string()),
221238
referrers: a.referrers.map(|t| t.as_str().to_string()),

src/commands/team.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ async fn endpoints(id: i64, ctx: Ctx) -> Result<(), CliError> {
165165
}
166166

167167
async fn set_endpoints(a: SetEndpointsArgs, ctx: Ctx) -> Result<(), CliError> {
168+
if a.endpoint_ids.is_empty() {
169+
return Err(CliError::Arg(
170+
"'team set-endpoints' requires at least one endpoint id.".into(),
171+
));
172+
}
168173
let req = UpdateTeamEndpointsRequest {
169174
endpoint_ids: a.endpoint_ids.clone(),
170175
};

tests/admin_extra.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,12 @@ async fn bulk_tag_add() {
237237
.await;
238238
assert_eq!(out.exit_code, 0, "stderr={}", out.stderr);
239239
}
240+
241+
#[tokio::test]
242+
async fn team_set_endpoints_no_ids_fails_before_api_call() {
243+
let server = MockServer::start().await;
244+
let out = run_qn(&server.uri(), &["team", "set-endpoints", "7"]).await;
245+
assert_eq!(out.exit_code, 1, "stderr={}", out.stderr);
246+
assert!(out.stderr.contains("endpoint id"), "stderr={}", out.stderr);
247+
assert_eq!(server.received_requests().await.unwrap().len(), 0);
248+
}

tests/endpoint.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,42 @@ async fn endpoint_ratelimit_set_omits_unset_fields() {
491491
.await;
492492
assert_eq!(out.exit_code, 0, "stderr={}", out.stderr);
493493
}
494+
495+
// ---- Stage B: required-args pre-flight checks ---- //
496+
497+
#[tokio::test]
498+
async fn endpoint_create_no_flags_fails_before_api_call() {
499+
// No mocks mounted; if the CLI tries to make a request, wiremock would 404.
500+
// We assert that the pre-flight check fires *before* any HTTP call.
501+
let server = MockServer::start().await;
502+
let out = run_qn(&server.uri(), &["endpoint", "create"]).await;
503+
assert_eq!(out.exit_code, 1, "stderr={}", out.stderr);
504+
assert!(
505+
out.stderr.contains("--chain") && out.stderr.contains("--network"),
506+
"stderr={}",
507+
out.stderr
508+
);
509+
assert_eq!(server.received_requests().await.unwrap().len(), 0);
510+
}
511+
512+
#[tokio::test]
513+
async fn endpoint_update_no_flags_fails_before_api_call() {
514+
let server = MockServer::start().await;
515+
let out = run_qn(&server.uri(), &["endpoint", "update", "ep-1"]).await;
516+
assert_eq!(out.exit_code, 1, "stderr={}", out.stderr);
517+
assert!(out.stderr.contains("--label"), "stderr={}", out.stderr);
518+
assert_eq!(server.received_requests().await.unwrap().len(), 0);
519+
}
520+
521+
#[tokio::test]
522+
async fn endpoint_security_set_options_no_flags_fails_before_api_call() {
523+
let server = MockServer::start().await;
524+
let out = run_qn(
525+
&server.uri(),
526+
&["endpoint", "security", "set-options", "ep-1"],
527+
)
528+
.await;
529+
assert_eq!(out.exit_code, 1, "stderr={}", out.stderr);
530+
assert!(out.stderr.contains("--tokens"), "stderr={}", out.stderr);
531+
assert_eq!(server.received_requests().await.unwrap().len(), 0);
532+
}

0 commit comments

Comments
 (0)