1use axum::Json;
6use axum::extract::{Path, State};
7use axum::response::IntoResponse;
8use chrono::Utc;
9use ironflow_auth::extractor::Authenticated;
10use ironflow_engine::config::HUMAN_INPUT_SCHEMA_KEY;
11use ironflow_engine::engine::ExecutionMode;
12use ironflow_store::models::{
13 Run, RunStatus, Step, StepApproval, StepKind, StepStatus, StepUpdate,
14};
15use jsonschema::validator_for;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use tokio::spawn;
19use tracing::error;
20use uuid::Uuid;
21
22use crate::entities::RunResponse;
23use crate::error::ApiError;
24use crate::response::ok;
25use crate::routes::approve_run::{authorize_gate, awaits_gate, resume_paused_run_later};
26use crate::state::AppState;
27
28#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
30#[derive(Debug, Default, Deserialize, Serialize)]
31pub struct RejectHumanInputRequest {
32 #[serde(default)]
34 pub reason: Option<String>,
35}
36
37#[cfg_attr(
53 feature = "openapi",
54 utoipa::path(
55 post,
56 path = "/api/v1/runs/{id}/steps/{step_id}/input",
57 tags = ["runs"],
58 params(
59 ("id" = Uuid, Path, description = "Run ID"),
60 ("step_id" = Uuid, Path, description = "Step ID")
61 ),
62 request_body(content = Value, content_type = "application/json", description = "Answer matching the JSON schema stored on the step"),
63 responses(
64 (status = 200, description = "Answer recorded, the run resumes", body = RunResponse),
65 (status = 400, description = "Step is not a human input awaiting input"),
66 (status = 401, description = "Unauthorized"),
67 (status = 403, description = "Forbidden"),
68 (status = 404, description = "Run or step not found"),
69 (status = 409, description = "Input already answered or rejected"),
70 (status = 422, description = "Answer does not match the schema")
71 ),
72 security(("Bearer" = []))
73 )
74)]
75pub async fn submit_human_input(
76 auth: Authenticated,
77 State(state): State<AppState>,
78 Path((id, step_id)): Path<(Uuid, Uuid)>,
79 Json(answer): Json<Value>,
80) -> Result<impl IntoResponse, ApiError> {
81 let (run, step) = open_input_step(&state, id, step_id).await?;
82 let actor = authorize_gate(&auth, &state, &run, &step).await?;
83 validate_answer(step.input.as_ref(), &answer)?;
84
85 let now = Utc::now();
89 state
90 .store
91 .record_step_approval(
92 step.id,
93 StepApproval {
94 user_id: auth.user_id,
95 approved_by: actor,
96 at: now,
97 },
98 )
99 .await?;
100 state
101 .store
102 .update_step(
103 step.id,
104 StepUpdate {
105 status: Some(StepStatus::Completed),
106 output: Some(answer),
107 completed_at: Some(now),
108 clear_approval_deadline: true,
109 ..StepUpdate::default()
110 },
111 )
112 .await?;
113 release_run(&state, &run).await?;
114
115 Ok(ok(RunResponse::from(state.get_run_or_404(id).await?)))
116}
117
118#[cfg_attr(
132 feature = "openapi",
133 utoipa::path(
134 post,
135 path = "/api/v1/runs/{id}/steps/{step_id}/reject",
136 tags = ["runs"],
137 params(
138 ("id" = Uuid, Path, description = "Run ID"),
139 ("step_id" = Uuid, Path, description = "Step ID")
140 ),
141 request_body(content = RejectHumanInputRequest, description = "Optional rejection reason"),
142 responses(
143 (status = 200, description = "Input rejected, the run resumes", body = RunResponse),
144 (status = 400, description = "Step is not a human input awaiting input"),
145 (status = 401, description = "Unauthorized"),
146 (status = 403, description = "Forbidden"),
147 (status = 404, description = "Run or step not found"),
148 (status = 409, description = "Input already answered or rejected")
149 ),
150 security(("Bearer" = []))
151 )
152)]
153pub async fn reject_human_input(
154 auth: Authenticated,
155 State(state): State<AppState>,
156 Path((id, step_id)): Path<(Uuid, Uuid)>,
157 body: Option<Json<RejectHumanInputRequest>>,
158) -> Result<impl IntoResponse, ApiError> {
159 let (run, step) = open_input_step(&state, id, step_id).await?;
160 let actor = authorize_gate(&auth, &state, &run, &step).await?;
161
162 let reason = body
163 .and_then(|Json(b)| b.reason)
164 .filter(|r| !r.trim().is_empty())
165 .unwrap_or_else(|| format!("input rejected by {actor}"));
166
167 state
168 .store
169 .update_step(
170 step.id,
171 StepUpdate {
172 status: Some(StepStatus::Rejected),
173 error: Some(reason),
174 completed_at: Some(Utc::now()),
175 clear_approval_deadline: true,
176 ..StepUpdate::default()
177 },
178 )
179 .await?;
180 release_run(&state, &run).await?;
181
182 Ok(ok(RunResponse::from(state.get_run_or_404(id).await?)))
183}
184
185async fn open_input_step(
190 state: &AppState,
191 run_id: Uuid,
192 step_id: Uuid,
193) -> Result<(Run, Step), ApiError> {
194 let run = state.get_run_or_404(run_id).await?;
195 let step = state
196 .store
197 .get_step(step_id)
198 .await?
199 .filter(|s| s.run_id == run_id)
200 .ok_or(ApiError::StepNotFound(step_id))?;
201
202 if step.kind != StepKind::HumanInput {
203 return Err(ApiError::BadRequest(
204 "step does not wait for input".to_string(),
205 ));
206 }
207
208 match step.status.state {
209 StepStatus::Completed => {
210 return Err(ApiError::Conflict("input already provided".to_string()));
211 }
212 StepStatus::Rejected => {
213 return Err(ApiError::Conflict("input already rejected".to_string()));
214 }
215 _ => {}
216 }
217
218 if step.status.state != StepStatus::AwaitingApproval || !awaits_gate(&run) {
219 return Err(ApiError::BadRequest(
220 "step is not awaiting input".to_string(),
221 ));
222 }
223
224 Ok((run, step))
225}
226
227fn validate_answer(input: Option<&Value>, answer: &Value) -> Result<(), ApiError> {
229 let schema = input
230 .and_then(|i| i.get(HUMAN_INPUT_SCHEMA_KEY))
231 .ok_or_else(|| ApiError::Internal("input step has no stored schema".to_string()))?;
232 let validator = validator_for(schema)
233 .map_err(|e| ApiError::Internal(format!("input step has an invalid schema: {e}")))?;
234
235 let errors: Vec<String> = validator
236 .iter_errors(answer)
237 .map(|e| e.to_string())
238 .collect();
239 if errors.is_empty() {
240 Ok(())
241 } else {
242 Err(ApiError::InvalidInput(errors))
243 }
244}
245
246async fn release_run(state: &AppState, run: &Run) -> Result<(), ApiError> {
251 if run.status.state == RunStatus::Paused {
252 return resume_paused_run_later(state, run.id).await;
253 }
254 state
255 .store
256 .update_run_status(run.id, resume_status(state))
257 .await?;
258 resume_in_background(state, run.id);
259 Ok(())
260}
261
262fn resume_status(state: &AppState) -> RunStatus {
265 match state.engine.execution_mode() {
266 ExecutionMode::Local => RunStatus::Running,
267 ExecutionMode::Workers => RunStatus::Pending,
268 }
269}
270
271fn resume_in_background(state: &AppState, id: Uuid) {
275 if !matches!(state.engine.execution_mode(), ExecutionMode::Local) {
276 return;
277 }
278 let engine = state.engine.clone();
279 spawn(async move {
280 if let Err(err) = engine.resume_run(id).await {
281 error!(run_id = %id, error = %err, "failed to resume run after human input");
282 }
283 });
284}
285
286#[cfg(test)]
287mod tests {
288 use std::collections::HashMap;
289 use std::sync::Arc;
290
291 use axum::Router;
292 use axum::body::Body;
293 use axum::http::{Request, StatusCode};
294 use axum::routing::post;
295 use http_body_util::BodyExt;
296 use ironflow_auth::jwt::{AccessToken, JwtConfig};
297 use ironflow_auth::password;
298 use ironflow_core::providers::claude::ClaudeCodeProvider;
299 use ironflow_engine::engine::Engine;
300 use ironflow_engine::handler::input_schema_for;
301 use ironflow_engine::notify::Event;
302 use ironflow_store::memory::InMemoryStore;
303 use ironflow_store::models::{
304 Assignee, NewRun, NewStep, NewUser, TriggerKind, User, step_trace_id,
305 };
306 use ironflow_store::store::RunStore;
307 use ironflow_store::user_store::UserStore;
308 use schemars::JsonSchema;
309 use serde_json::{from_slice, json};
310 use tokio::sync::broadcast;
311 use tower::ServiceExt;
312
313 use super::*;
314 use crate::routes::test_helpers::create_user_auth_header;
315
316 #[allow(dead_code)]
318 #[derive(Deserialize, JsonSchema)]
319 struct Answers {
320 answers: Vec<String>,
321 }
322
323 #[allow(dead_code)]
325 #[derive(Deserialize, JsonSchema)]
326 struct Nested {
327 target: Target,
328 }
329
330 #[allow(dead_code)]
331 #[derive(Deserialize, JsonSchema)]
332 struct Target {
333 env: String,
334 }
335
336 fn test_state(store: Arc<InMemoryStore>) -> AppState {
337 let provider = Arc::new(ClaudeCodeProvider::new());
338 let engine = Arc::new(Engine::new(store.clone(), provider));
339 let jwt_config = Arc::new(JwtConfig {
340 secret: "test-secret".to_string(),
341 access_token_ttl_secs: 900,
342 refresh_token_ttl_secs: 604800,
343 cookie_domain: None,
344 cookie_secure: false,
345 });
346 let (event_sender, _) = broadcast::channel::<Event>(1);
347 AppState::new(
348 store,
349 engine,
350 jwt_config,
351 "test-worker-token".to_string(),
352 event_sender,
353 )
354 }
355
356 async fn member(store: &Arc<InMemoryStore>, username: &str) -> User {
357 let password_hash = password::hash("password123").expect("hash");
358 store
359 .create_user(NewUser {
360 email: format!("{username}@example.com"),
361 username: username.to_string(),
362 password_hash,
363 is_admin: Some(false),
365 })
366 .await
367 .expect("create user")
368 }
369
370 fn member_header(user: &User, state: &AppState) -> String {
371 let token = AccessToken::for_user(user.id, &user.username, false, &state.jwt_config)
372 .expect("token");
373 format!("Bearer {}", token.0)
374 }
375
376 async fn awaiting_run(store: &Arc<InMemoryStore>) -> Uuid {
377 let run = store
378 .create_run(NewRun {
379 created_by: None,
380 workflow_name: "test".to_string(),
381 trigger: TriggerKind::Manual,
382 payload: json!({}),
383 max_retries: 0,
384 handler_version: None,
385 labels: HashMap::new(),
386 scheduled_at: None,
387 idempotency_key: None,
388 concurrency_key: None,
389 priority: 0,
390 concurrency_limits: Vec::new(),
391 max_cost_usd: None,
392 worker_tags: Vec::new(),
393 })
394 .await
395 .unwrap()
396 .into_run();
397 store
398 .update_run_status(run.id, RunStatus::Running)
399 .await
400 .unwrap();
401 store
402 .update_run_status(run.id, RunStatus::AwaitingApproval)
403 .await
404 .unwrap();
405 run.id
406 }
407
408 async fn awaiting_step(
410 store: &Arc<InMemoryStore>,
411 run_id: Uuid,
412 kind: StepKind,
413 assignee: Option<Assignee>,
414 ) -> Uuid {
415 let step = store
416 .create_step(NewStep {
417 run_id,
418 trace_id: step_trace_id(run_id, "clarify", 0),
419 name: "clarify".to_string(),
420 kind,
421 position: 0,
422 input: Some(json!({
423 "message": "Answer the questions",
424 HUMAN_INPUT_SCHEMA_KEY: input_schema_for::<Answers>(),
425 })),
426 is_error_handler: false,
427 })
428 .await
429 .unwrap();
430 store
431 .update_step(
432 step.id,
433 StepUpdate {
434 status: Some(StepStatus::Running),
435 ..StepUpdate::default()
436 },
437 )
438 .await
439 .unwrap();
440 store
441 .update_step(
442 step.id,
443 StepUpdate {
444 status: Some(StepStatus::AwaitingApproval),
445 approval_assignee: assignee,
446 ..StepUpdate::default()
447 },
448 )
449 .await
450 .unwrap();
451 step.id
452 }
453
454 async fn setup() -> (Arc<InMemoryStore>, AppState, Uuid, Uuid) {
456 let store = Arc::new(InMemoryStore::new());
457 let run_id = awaiting_run(&store).await;
458 let step_id = awaiting_step(&store, run_id, StepKind::HumanInput, None).await;
459 let state = test_state(store.clone());
460 (store, state, run_id, step_id)
461 }
462
463 fn app(state: AppState) -> Router {
464 Router::new()
465 .route("/{id}/steps/{step_id}/input", post(submit_human_input))
466 .route("/{id}/steps/{step_id}/reject", post(reject_human_input))
467 .with_state(state)
468 }
469
470 async fn call(
472 state: &AppState,
473 auth: &str,
474 run_id: Uuid,
475 step_id: Uuid,
476 verb: &str,
477 body: Option<Value>,
478 ) -> (StatusCode, Value) {
479 let builder = Request::builder()
480 .method("POST")
481 .uri(format!("/{run_id}/steps/{step_id}/{verb}"))
482 .header("authorization", auth);
483 let req = match body {
484 Some(body) => builder
485 .header("content-type", "application/json")
486 .body(Body::from(body.to_string()))
487 .unwrap(),
488 None => builder.body(Body::empty()).unwrap(),
489 };
490
491 let resp = app(state.clone()).oneshot(req).await.unwrap();
492 let status = resp.status();
493 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
494 let json_val = if bytes.is_empty() {
495 Value::Null
496 } else {
497 from_slice(&bytes).unwrap()
498 };
499 (status, json_val)
500 }
501
502 #[tokio::test]
503 async fn human_input_submit_valid_answer_resumes_the_run() {
504 let (store, state, run_id, step_id) = setup().await;
505 let auth = create_user_auth_header(&state, "admin", true).await;
506 let answer = json!({"answers": ["staging"]});
507
508 let (status, body) = call(
509 &state,
510 &auth,
511 run_id,
512 step_id,
513 "input",
514 Some(answer.clone()),
515 )
516 .await;
517
518 assert_eq!(status, StatusCode::OK);
519 assert_eq!(body["data"]["status"], "running");
520 let step = store.get_step(step_id).await.unwrap().unwrap();
521 assert_eq!(step.status.state, StepStatus::Completed);
522 assert_eq!(step.output, Some(answer));
523 assert_eq!(step.approvals.len(), 1);
524 assert_eq!(step.approvals[0].approved_by, "admin");
525 }
526
527 #[tokio::test]
528 async fn human_input_submit_invalid_answer_returns_422() {
529 let (store, state, run_id, step_id) = setup().await;
530 let auth = create_user_auth_header(&state, "admin", true).await;
531
532 for answer in [json!({"answers": 3}), json!({})] {
533 let (status, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
534
535 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
536 assert_eq!(body["error"]["code"], "INVALID_INPUT");
537 let errors = body["error"]["details"]["errors"]
538 .as_array()
539 .expect("errors array");
540 assert!(!errors.is_empty());
541 }
542
543 let step = store.get_step(step_id).await.unwrap().unwrap();
544 assert_eq!(step.status.state, StepStatus::AwaitingApproval);
545 let run = store.get_run(run_id).await.unwrap().unwrap();
546 assert_eq!(run.status.state, RunStatus::AwaitingApproval);
547 }
548
549 #[tokio::test]
550 async fn human_input_submit_twice_returns_409() {
551 let (_store, state, run_id, step_id) = setup().await;
552 let auth = create_user_auth_header(&state, "admin", true).await;
553 let answer = json!({"answers": ["yes"]});
554
555 let (first, _) = call(
556 &state,
557 &auth,
558 run_id,
559 step_id,
560 "input",
561 Some(answer.clone()),
562 )
563 .await;
564 assert_eq!(first, StatusCode::OK);
565
566 let (second, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
567 assert_eq!(second, StatusCode::CONFLICT);
568 assert_eq!(body["error"]["message"], "input already provided");
569 }
570
571 #[tokio::test]
572 async fn human_input_submit_on_an_approval_step_returns_400() {
573 let store = Arc::new(InMemoryStore::new());
574 let run_id = awaiting_run(&store).await;
575 let step_id = awaiting_step(&store, run_id, StepKind::Approval, None).await;
576 let state = test_state(store);
577 let auth = create_user_auth_header(&state, "admin", true).await;
578
579 let answer = json!({"answers": []});
580 let (status, _) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
581 assert_eq!(status, StatusCode::BAD_REQUEST);
582 }
583
584 #[tokio::test]
585 async fn human_input_submit_on_a_run_not_awaiting_returns_400() {
586 let (store, state, run_id, step_id) = setup().await;
587 store
588 .update_run_status(run_id, RunStatus::Running)
589 .await
590 .unwrap();
591 let auth = create_user_auth_header(&state, "admin", true).await;
592
593 let answer = json!({"answers": []});
594 let (status, _) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
595 assert_eq!(status, StatusCode::BAD_REQUEST);
596 }
597
598 #[tokio::test]
599 async fn human_input_unknown_step_or_step_of_another_run_returns_404() {
600 let (store, state, run_id, step_id) = setup().await;
601 let auth = create_user_auth_header(&state, "admin", true).await;
602 let answer = json!({"answers": []});
603
604 let unknown = Uuid::now_v7();
605 let (status, body) = call(
606 &state,
607 &auth,
608 run_id,
609 unknown,
610 "input",
611 Some(answer.clone()),
612 )
613 .await;
614 assert_eq!(status, StatusCode::NOT_FOUND);
615 assert_eq!(body["error"]["code"], "STEP_NOT_FOUND");
616
617 let other_run = awaiting_run(&store).await;
618 let (status, _) = call(&state, &auth, other_run, step_id, "input", Some(answer)).await;
619 assert_eq!(status, StatusCode::NOT_FOUND);
620 }
621
622 #[tokio::test]
623 async fn human_input_unknown_run_returns_404() {
624 let (_store, state, _run_id, step_id) = setup().await;
625 let auth = create_user_auth_header(&state, "admin", true).await;
626
627 let answer = json!({"answers": []});
628 let (status, body) = call(
629 &state,
630 &auth,
631 Uuid::now_v7(),
632 step_id,
633 "input",
634 Some(answer),
635 )
636 .await;
637 assert_eq!(status, StatusCode::NOT_FOUND);
638 assert_eq!(body["error"]["code"], "RUN_NOT_FOUND");
639 }
640
641 #[tokio::test]
642 async fn human_input_non_admin_non_assignee_is_forbidden() {
643 let store = Arc::new(InMemoryStore::new());
644 let alice = member(&store, "alice").await;
645 let bob = member(&store, "bob").await;
646 let run_id = awaiting_run(&store).await;
647 let assignee = Some(Assignee::user(&alice.username));
648 let step_id = awaiting_step(&store, run_id, StepKind::HumanInput, assignee).await;
649 let state = test_state(store.clone());
650
651 let answer = json!({"answers": ["yes"]});
652 let bob_auth = member_header(&bob, &state);
653 let (status, _) = call(
654 &state,
655 &bob_auth,
656 run_id,
657 step_id,
658 "input",
659 Some(answer.clone()),
660 )
661 .await;
662 assert_eq!(status, StatusCode::FORBIDDEN);
663
664 let alice_auth = member_header(&alice, &state);
665 let (status, _) = call(&state, &alice_auth, run_id, step_id, "input", Some(answer)).await;
666 assert_eq!(status, StatusCode::OK);
667 let step = store.get_step(step_id).await.unwrap().unwrap();
668 assert_eq!(step.approvals[0].user_id, alice.id);
669 }
670
671 #[tokio::test]
672 async fn human_input_submit_on_a_paused_run_keeps_it_paused() {
673 let (store, state, run_id, step_id) = setup().await;
674 store
675 .update_run_status(run_id, RunStatus::Paused)
676 .await
677 .unwrap();
678 let auth = create_user_auth_header(&state, "admin", true).await;
679 let answer = json!({"answers": ["staging"]});
680
681 let (status, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
682
683 assert_eq!(status, StatusCode::OK);
684 assert_eq!(body["data"]["status"], "paused");
685 assert_eq!(body["data"]["resume_status"], "pending");
686 let step = store.get_step(step_id).await.unwrap().unwrap();
687 assert_eq!(step.status.state, StepStatus::Completed);
688 let run = store.get_run(run_id).await.unwrap().unwrap();
689 assert_eq!(run.status.state, RunStatus::Paused);
690 assert_eq!(run.resume_status, Some(RunStatus::Pending));
691 }
692
693 #[tokio::test]
694 async fn human_input_reject_marks_the_step_and_resumes_the_run() {
695 let (store, state, run_id, step_id) = setup().await;
696 let auth = create_user_auth_header(&state, "admin", true).await;
697
698 let body = json!({"reason": "out of scope"});
699 let (status, resp) = call(&state, &auth, run_id, step_id, "reject", Some(body)).await;
700
701 assert_eq!(status, StatusCode::OK);
702 assert_eq!(resp["data"]["status"], "running");
703 let step = store.get_step(step_id).await.unwrap().unwrap();
704 assert_eq!(step.status.state, StepStatus::Rejected);
705 assert_eq!(step.error.as_deref(), Some("out of scope"));
706 assert!(step.approval_deadline_at.is_none());
707 }
708
709 async fn workers_setup() -> (Arc<InMemoryStore>, AppState, Uuid, Uuid, String) {
712 let store = Arc::new(InMemoryStore::new());
713 let alice = member(&store, "alice").await;
714 let run_id = awaiting_run(&store).await;
715 let assignee = Some(Assignee::user(&alice.username));
716 let step_id = awaiting_step(&store, run_id, StepKind::HumanInput, assignee).await;
717 let provider = Arc::new(ClaudeCodeProvider::new());
718 let engine =
719 Engine::new(store.clone(), provider).with_execution_mode(ExecutionMode::Workers);
720 let jwt_config = Arc::new(JwtConfig {
721 secret: "test-secret".to_string(),
722 access_token_ttl_secs: 900,
723 refresh_token_ttl_secs: 604800,
724 cookie_domain: None,
725 cookie_secure: false,
726 });
727 let (event_sender, _) = broadcast::channel::<Event>(1);
728 let state = AppState::new(
729 store.clone(),
730 Arc::new(engine),
731 jwt_config,
732 "test-worker-token".to_string(),
733 event_sender,
734 );
735 let auth = member_header(&alice, &state);
736 (store, state, run_id, step_id, auth)
737 }
738
739 #[tokio::test]
740 async fn human_input_submit_in_workers_mode_requeues_the_run() {
741 let (store, state, run_id, step_id, auth) = workers_setup().await;
742
743 let answer = json!({"answers": ["ok"]});
744 let (status, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
745
746 assert_eq!(status, StatusCode::OK);
747 assert_eq!(body["data"]["status"], "pending");
748 let run = store.get_run(run_id).await.unwrap().unwrap();
749 assert_eq!(run.status.state, RunStatus::Pending);
750 let step = store.get_step(step_id).await.unwrap().unwrap();
751 assert_eq!(step.status.state, StepStatus::Completed);
752 }
753
754 #[tokio::test]
755 async fn human_input_reject_in_workers_mode_requeues_the_run() {
756 let (store, state, run_id, step_id, auth) = workers_setup().await;
757
758 let body = json!({"reason": "out of scope"});
759 let (status, resp) = call(&state, &auth, run_id, step_id, "reject", Some(body)).await;
760
761 assert_eq!(status, StatusCode::OK);
762 assert_eq!(resp["data"]["status"], "pending");
763 let run = store.get_run(run_id).await.unwrap().unwrap();
764 assert_eq!(run.status.state, RunStatus::Pending);
765 let step = store.get_step(step_id).await.unwrap().unwrap();
766 assert_eq!(step.status.state, StepStatus::Rejected);
767 }
768
769 #[tokio::test]
770 async fn human_input_reject_without_body_uses_the_default_reason() {
771 let (store, state, run_id, step_id) = setup().await;
772 let auth = create_user_auth_header(&state, "admin", true).await;
773
774 let (status, _) = call(&state, &auth, run_id, step_id, "reject", None).await;
775
776 assert_eq!(status, StatusCode::OK);
777 let step = store.get_step(step_id).await.unwrap().unwrap();
778 assert_eq!(step.error.as_deref(), Some("input rejected by admin"));
779 }
780
781 #[tokio::test]
782 async fn human_input_reject_with_a_blank_reason_uses_the_default_reason() {
783 let (store, state, run_id, step_id) = setup().await;
784 let auth = create_user_auth_header(&state, "admin", true).await;
785
786 let body = json!({"reason": " "});
787 let (status, _) = call(&state, &auth, run_id, step_id, "reject", Some(body)).await;
788
789 assert_eq!(status, StatusCode::OK);
790 let step = store.get_step(step_id).await.unwrap().unwrap();
791 assert_eq!(step.error.as_deref(), Some("input rejected by admin"));
792 }
793
794 #[tokio::test]
795 async fn human_input_reject_after_submit_returns_409() {
796 let (_store, state, run_id, step_id) = setup().await;
797 let auth = create_user_auth_header(&state, "admin", true).await;
798
799 let answer = json!({"answers": ["yes"]});
800 let (first, _) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
801 assert_eq!(first, StatusCode::OK);
802
803 let (second, _) = call(&state, &auth, run_id, step_id, "reject", None).await;
804 assert_eq!(second, StatusCode::CONFLICT);
805 }
806
807 #[tokio::test]
808 async fn human_input_submit_after_reject_returns_409() {
809 let (_store, state, run_id, step_id) = setup().await;
810 let auth = create_user_auth_header(&state, "admin", true).await;
811
812 let (first, _) = call(&state, &auth, run_id, step_id, "reject", None).await;
813 assert_eq!(first, StatusCode::OK);
814
815 let answer = json!({"answers": ["yes"]});
816 let (second, body) = call(&state, &auth, run_id, step_id, "input", Some(answer)).await;
817 assert_eq!(second, StatusCode::CONFLICT);
818 assert_eq!(body["error"]["message"], "input already rejected");
819 }
820
821 #[test]
822 fn human_input_validate_answer_without_a_schema_is_internal() {
823 let err = validate_answer(Some(&json!({"message": "Answer?"})), &json!({}))
824 .expect_err("no schema");
825 assert!(matches!(err, ApiError::Internal(_)), "got {err:?}");
826
827 let err = validate_answer(None, &json!({})).expect_err("no input");
828 assert!(matches!(err, ApiError::Internal(_)), "got {err:?}");
829 }
830
831 #[test]
832 fn human_input_validate_answer_resolves_defs_references() {
833 let schema = input_schema_for::<Nested>();
834 assert!(schema.get("$defs").is_some(), "schema: {schema}");
835 let input = json!({ HUMAN_INPUT_SCHEMA_KEY: schema });
836
837 let valid = validate_answer(Some(&input), &json!({"target": {"env": "prod"}}));
838 assert!(valid.is_ok(), "got {valid:?}");
839
840 let err = validate_answer(Some(&input), &json!({"target": {"env": 1}}))
841 .expect_err("wrong nested type");
842 match err {
843 ApiError::InvalidInput(errors) => assert_eq!(errors.len(), 1, "got {errors:?}"),
844 other => panic!("expected InvalidInput, got {other:?}"),
845 }
846 }
847}