Skip to main content

stormchaser_cli/commands/
cron.rs

1use crate::utils::{handle_response, parse_key_val_list, require_token};
2use anyhow::Result;
3use clap::Subcommand;
4use serde_json::json;
5
6#[derive(Subcommand)]
7pub enum CronCommands {
8    /// List scheduled cron workflows
9    List,
10    /// Schedule a new cron workflow
11    Create {
12        name: String,
13        #[arg(long)]
14        cron: String,
15        #[arg(long)]
16        workflow: String,
17        #[arg(long)]
18        repo: String,
19        #[arg(long)]
20        path: String,
21        #[arg(long, default_value = "main")]
22        git_ref: String,
23        #[arg(long)]
24        description: Option<String>,
25        /// Input parameters in key=value format
26        #[arg(short, long)]
27        input: Vec<String>,
28    },
29    /// Delete a scheduled cron workflow
30    Delete {
31        id: stormchaser_model::CronWorkflowId,
32    },
33}
34
35pub async fn handle(
36    url: &str,
37    token: Option<&str>,
38    http_client: &reqwest_middleware::ClientWithMiddleware,
39    command: CronCommands,
40) -> Result<()> {
41    match command {
42        CronCommands::List => {
43            let token = require_token(token)?;
44            let res = http_client
45                .get(format!("{}/api/v1/cron-workflows", url))
46                .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
47                .send()
48                .await?;
49            handle_response(res).await?;
50        }
51        CronCommands::Create {
52            name,
53            cron,
54            workflow,
55            repo,
56            path,
57            git_ref,
58            description,
59            input,
60        } => {
61            let inputs = parse_key_val_list(input);
62            let token = require_token(token)?;
63            let res = http_client
64                .post(format!("{}/api/v1/cron-workflows", url))
65                .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
66                .json(&json!({
67                    "name": name,
68                    "cronspec": cron,
69                    "workflow_name": workflow,
70                    "repo_url": repo,
71                    "workflow_path": path,
72                    "git_ref": git_ref,
73                    "inputs": inputs,
74                    "description": description,
75                }))
76                .send()
77                .await?;
78            handle_response(res).await?;
79        }
80        CronCommands::Delete { id } => {
81            let token = require_token(token)?;
82            let res = http_client
83                .delete(format!("{}/api/v1/cron-workflows/{}", url, id))
84                .header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
85                .send()
86                .await?;
87            handle_response(res).await?;
88        }
89    }
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use reqwest_middleware::ClientBuilder;
97    use wiremock::matchers::{header, method, path};
98    use wiremock::{Mock, MockServer, ResponseTemplate};
99
100    #[tokio::test]
101    async fn test_cron_list() {
102        let server = MockServer::start().await;
103        Mock::given(method("GET"))
104            .and(path("/api/v1/cron-workflows"))
105            .and(header(reqwest::header::AUTHORIZATION, "Bearer test-token"))
106            .respond_with(ResponseTemplate::new(200).set_body_json(json!([])))
107            .mount(&server)
108            .await;
109
110        let client = ClientBuilder::new(reqwest::Client::new()).build();
111        let cmd = CronCommands::List;
112
113        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
114        assert!(result.is_ok());
115    }
116
117    #[tokio::test]
118    async fn test_cron_create() {
119        let server = MockServer::start().await;
120        Mock::given(method("POST"))
121            .and(path("/api/v1/cron-workflows"))
122            .and(header(reqwest::header::AUTHORIZATION, "Bearer test-token"))
123            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "created"})))
124            .mount(&server)
125            .await;
126
127        let client = ClientBuilder::new(reqwest::Client::new()).build();
128        let cmd = CronCommands::Create {
129            name: "test-cron".to_string(),
130            cron: "* * * * *".to_string(),
131            workflow: "my-wf".to_string(),
132            repo: "https://github.com/a/b".to_string(),
133            path: "wf.storm".to_string(),
134            git_ref: "main".to_string(),
135            description: Some("desc".to_string()),
136            input: vec!["key=value".to_string()],
137        };
138
139        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
140        assert!(result.is_ok());
141    }
142
143    #[tokio::test]
144    async fn test_cron_delete() {
145        let server = MockServer::start().await;
146        let id = stormchaser_model::CronWorkflowId::new_v4();
147        Mock::given(method("DELETE"))
148            .and(path(format!("/api/v1/cron-workflows/{}", id)))
149            .and(header(reqwest::header::AUTHORIZATION, "Bearer test-token"))
150            .respond_with(ResponseTemplate::new(200).set_body_json(json!({"status": "deleted"})))
151            .mount(&server)
152            .await;
153
154        let client = ClientBuilder::new(reqwest::Client::new()).build();
155        let cmd = CronCommands::Delete { id };
156
157        let result = handle(&server.uri(), Some("test-token"), &client, cmd).await;
158        assert!(result.is_ok());
159    }
160}