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