ironflow_api/routes/
list_workflows.rs1use axum::extract::{Query, State};
4use axum::response::IntoResponse;
5use ironflow_auth::extractor::Authenticated;
6use serde::Deserialize;
7
8use crate::error::ApiError;
9use crate::response::ok;
10use crate::state::AppState;
11
12#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams, utoipa::ToSchema))]
14#[derive(Debug, Deserialize)]
15pub struct ListWorkflowsQuery {
16 pub name: Option<String>,
18}
19
20#[cfg_attr(
26 feature = "openapi",
27 utoipa::path(
28 get,
29 path = "/api/v1/workflows",
30 tags = ["workflows"],
31 params(ListWorkflowsQuery),
32 responses(
33 (status = 200, description = "List of workflow names"),
34 (status = 401, description = "Unauthorized")
35 ),
36 security(("Bearer" = []))
37 )
38)]
39pub async fn list_workflows(
40 _auth: Authenticated,
41 State(state): State<AppState>,
42 Query(params): Query<ListWorkflowsQuery>,
43) -> Result<impl IntoResponse, ApiError> {
44 let mut names: Vec<String> = state
45 .engine
46 .handler_names()
47 .into_iter()
48 .map(|s| s.to_string())
49 .collect();
50
51 if let Some(ref filter) = params.name {
52 let lower = filter.to_lowercase();
53 names.retain(|n| n.to_lowercase().contains(&lower));
54 }
55
56 Ok(ok(names))
57}
58
59#[cfg(test)]
60mod tests {
61 use axum::Router;
62 use axum::body::Body;
63 use axum::http::{Request, StatusCode};
64 use axum::routing::get;
65 use http_body_util::BodyExt;
66 use ironflow_auth::jwt::AccessToken;
67 use ironflow_core::providers::claude::ClaudeCodeProvider;
68 use ironflow_engine::context::WorkflowContext;
69 use ironflow_engine::engine::Engine;
70 use ironflow_engine::handler::{HandlerFuture, WorkflowHandler};
71 use ironflow_engine::notify::Event;
72 use ironflow_store::api_key_store::ApiKeyStore;
73 use ironflow_store::memory::InMemoryStore;
74 use serde_json::{Value as JsonValue, from_slice, from_value};
75 use std::sync::Arc;
76 use tokio::sync::broadcast;
77 use tower::ServiceExt;
78 use uuid::Uuid;
79
80 use super::*;
81
82 fn make_auth_header(state: &AppState) -> String {
83 let user_id = Uuid::now_v7();
84 let token = AccessToken::for_user(user_id, "testuser", false, &state.jwt_config).unwrap();
85 format!("Bearer {}", token.0)
86 }
87
88 struct TestWorkflow;
89
90 impl WorkflowHandler for TestWorkflow {
91 fn name(&self) -> &str {
92 "test-workflow"
93 }
94
95 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
96 Box::pin(async move { Ok(()) })
97 }
98 }
99
100 struct AnotherWorkflow;
101
102 impl WorkflowHandler for AnotherWorkflow {
103 fn name(&self) -> &str {
104 "another-workflow"
105 }
106
107 fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
108 Box::pin(async move { Ok(()) })
109 }
110 }
111
112 fn test_state() -> AppState {
113 let store = Arc::new(InMemoryStore::new());
114 let user_store: Arc<dyn ironflow_store::user_store::UserStore> =
115 Arc::new(InMemoryStore::new());
116 let api_key_store: Arc<dyn ApiKeyStore> = Arc::new(InMemoryStore::new());
117 let provider = Arc::new(ClaudeCodeProvider::new());
118 let mut engine = Engine::new(store.clone(), provider);
119 engine.register(TestWorkflow).unwrap();
120 engine.register(AnotherWorkflow).unwrap();
121 let jwt_config = Arc::new(ironflow_auth::jwt::JwtConfig {
122 secret: "test-secret".to_string(),
123 access_token_ttl_secs: 900,
124 refresh_token_ttl_secs: 604800,
125 cookie_domain: None,
126 cookie_secure: false,
127 });
128 let (event_sender, _) = broadcast::channel::<Event>(1);
129 AppState::new(
130 store,
131 user_store,
132 api_key_store,
133 Arc::new(engine),
134 jwt_config,
135 "test-worker-token".to_string(),
136 event_sender,
137 )
138 }
139
140 #[tokio::test]
141 async fn list_workflows_empty() {
142 let store = Arc::new(InMemoryStore::new());
143 let user_store: Arc<dyn ironflow_store::user_store::UserStore> =
144 Arc::new(InMemoryStore::new());
145 let api_key_store: Arc<dyn ApiKeyStore> = Arc::new(InMemoryStore::new());
146 let provider = Arc::new(ClaudeCodeProvider::new());
147 let engine = Arc::new(Engine::new(store.clone(), provider));
148 let jwt_config = Arc::new(ironflow_auth::jwt::JwtConfig {
149 secret: "test-secret".to_string(),
150 access_token_ttl_secs: 900,
151 refresh_token_ttl_secs: 604800,
152 cookie_domain: None,
153 cookie_secure: false,
154 });
155 let (event_sender, _) = broadcast::channel::<Event>(1);
156 let state = AppState::new(
157 store,
158 user_store,
159 api_key_store,
160 engine,
161 jwt_config,
162 "test-worker-token".to_string(),
163 event_sender,
164 );
165 let auth_header = make_auth_header(&state);
166
167 let app = Router::new()
168 .route("/", get(list_workflows))
169 .with_state(state);
170
171 let req = Request::builder()
172 .uri("/")
173 .header("authorization", auth_header)
174 .body(Body::empty())
175 .unwrap();
176
177 let resp = app.oneshot(req).await.unwrap();
178 assert_eq!(resp.status(), StatusCode::OK);
179
180 let body = resp.into_body().collect().await.unwrap().to_bytes();
181 let json_val: JsonValue = from_slice(&body).unwrap();
182 assert_eq!(json_val["data"].as_array().unwrap().len(), 0);
183 }
184
185 #[tokio::test]
186 async fn list_workflows_multiple() {
187 let state = test_state();
188 let auth_header = make_auth_header(&state);
189 let app = Router::new()
190 .route("/", get(list_workflows))
191 .with_state(state);
192
193 let req = Request::builder()
194 .uri("/")
195 .header("authorization", auth_header)
196 .body(Body::empty())
197 .unwrap();
198
199 let resp = app.oneshot(req).await.unwrap();
200 assert_eq!(resp.status(), StatusCode::OK);
201
202 let body = resp.into_body().collect().await.unwrap().to_bytes();
203 let json_val: JsonValue = from_slice(&body).unwrap();
204 let workflows: Vec<String> = from_value(json_val["data"].clone()).unwrap();
205 assert_eq!(workflows.len(), 2);
206 assert!(workflows.contains(&"test-workflow".to_string()));
207 assert!(workflows.contains(&"another-workflow".to_string()));
208 }
209
210 #[tokio::test]
211 async fn list_workflows_filtered_by_name() {
212 let state = test_state();
213 let auth_header = make_auth_header(&state);
214 let app = Router::new()
215 .route("/", get(list_workflows))
216 .with_state(state);
217
218 let req = Request::builder()
219 .uri("/?name=test")
220 .header("authorization", &auth_header)
221 .body(Body::empty())
222 .unwrap();
223
224 let resp = app.oneshot(req).await.unwrap();
225 assert_eq!(resp.status(), StatusCode::OK);
226
227 let body = resp.into_body().collect().await.unwrap().to_bytes();
228 let json_val: JsonValue = from_slice(&body).unwrap();
229 let workflows: Vec<String> = from_value(json_val["data"].clone()).unwrap();
230 assert_eq!(workflows.len(), 1);
231 assert_eq!(workflows[0], "test-workflow");
232 }
233
234 #[tokio::test]
235 async fn list_workflows_filter_case_insensitive() {
236 let state = test_state();
237 let auth_header = make_auth_header(&state);
238 let app = Router::new()
239 .route("/", get(list_workflows))
240 .with_state(state);
241
242 let req = Request::builder()
243 .uri("/?name=TEST")
244 .header("authorization", &auth_header)
245 .body(Body::empty())
246 .unwrap();
247
248 let resp = app.oneshot(req).await.unwrap();
249 let body = resp.into_body().collect().await.unwrap().to_bytes();
250 let json_val: JsonValue = from_slice(&body).unwrap();
251 let workflows: Vec<String> = from_value(json_val["data"].clone()).unwrap();
252 assert_eq!(workflows.len(), 1);
253 assert_eq!(workflows[0], "test-workflow");
254 }
255
256 #[tokio::test]
257 async fn list_workflows_filter_no_match() {
258 let state = test_state();
259 let auth_header = make_auth_header(&state);
260 let app = Router::new()
261 .route("/", get(list_workflows))
262 .with_state(state);
263
264 let req = Request::builder()
265 .uri("/?name=nonexistent")
266 .header("authorization", &auth_header)
267 .body(Body::empty())
268 .unwrap();
269
270 let resp = app.oneshot(req).await.unwrap();
271 let body = resp.into_body().collect().await.unwrap().to_bytes();
272 let json_val: JsonValue = from_slice(&body).unwrap();
273 let workflows: Vec<String> = from_value(json_val["data"].clone()).unwrap();
274 assert!(workflows.is_empty());
275 }
276}