diff --git a/backend/tests/apps.rs b/backend/tests/apps.rs new file mode 100644 index 0000000000..08b29d71fa --- /dev/null +++ b/backend/tests/apps.rs @@ -0,0 +1,161 @@ +use serde_json::json; +use sqlx::{Pool, Postgres}; + +mod common; +use common::*; + +fn app_url(port: u16, endpoint: &str, path: &str) -> String { + format!("http://localhost:{port}/api/w/test-workspace/apps/{endpoint}/{path}") +} + +fn client() -> reqwest::Client { + reqwest::Client::new() +} + +fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + builder.header("Authorization", "Bearer SECRET_TOKEN") +} + +async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response { + authed(client().get(app_url(port, endpoint, path))) + .send() + .await + .unwrap() +} + +fn new_app(path: &str, summary: &str) -> serde_json::Value { + json!({ + "path": path, + "summary": summary, + "value": { + "type": "rawapp", + "inline_script": null + }, + "policy": { + "execution_mode": "anonymous", + "triggerables": {}, + "on_behalf_of": null, + "on_behalf_of_email": null + } + }) +} + +#[sqlx::test(fixtures("base"))] +async fn test_app_endpoints(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace/apps"); + + // --- create --- + let resp = authed(client().post(format!("{base}/create"))) + .json(&new_app("u/test-user/test_app", "Test app")) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "create: {}", resp.text().await?); + + // create second app + let resp = authed(client().post(format!("{base}/create"))) + .json(&new_app("u/test-user/another_app", "Another app")) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "create another: {}", resp.text().await?); + + // --- exists --- + let resp = authed_get(port, "exists", "u/test-user/test_app").await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.json::().await?, true); + + let resp = authed_get(port, "exists", "u/test-user/nonexistent").await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.json::().await?, false); + + // --- get by path --- + let resp = authed_get(port, "get/p", "u/test-user/test_app").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_app"); + assert_eq!(body["summary"], "Test app"); + + // get not found + let resp = authed_get(port, "get/p", "u/test-user/nonexistent").await; + assert_eq!(resp.status(), 404); + + // --- get draft --- + let resp = authed_get(port, "get/draft", "u/test-user/test_app").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_app"); + + // --- get lite --- + let resp = authed_get(port, "get/lite", "u/test-user/test_app").await; + assert_eq!(resp.status(), 200); + + // --- list --- + let resp = authed(client().get(format!("{base}/list"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert!( + list.len() >= 2, + "expected at least 2 apps, got {}", + list.len() + ); + assert!(list.iter().any(|a| a["path"] == "u/test-user/test_app")); + + // --- list_search --- + let resp = authed(client().get(format!("{base}/list_search"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert!(!list.is_empty()); + + // --- history --- + let resp = authed_get(port, "history/p", "u/test-user/test_app").await; + assert_eq!(resp.status(), 200); + let history = resp.json::>().await?; + assert!(!history.is_empty()); + + // --- get_latest_version --- + let resp = authed_get(port, "get_latest_version", "u/test-user/test_app").await; + assert_eq!(resp.status(), 200); + + // --- update --- + let resp = authed(client().post(app_url(port, "update", "u/test-user/test_app"))) + .json(&json!({ + "summary": "Updated app", + "policy": { + "execution_mode": "anonymous", + "triggerables": {}, + "on_behalf_of": null, + "on_behalf_of_email": null + } + })) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200, "update: {}", resp.text().await?); + + // verify update + let resp = authed_get(port, "get/p", "u/test-user/test_app").await; + let body = resp.json::().await?; + assert_eq!(body["summary"], "Updated app"); + + // --- delete --- + let resp = authed(client().delete(app_url(port, "delete", "u/test-user/another_app"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + + let resp = authed_get(port, "exists", "u/test-user/another_app").await; + assert_eq!(resp.json::().await?, false); + + Ok(()) +} diff --git a/backend/tests/flows.rs b/backend/tests/flows.rs new file mode 100644 index 0000000000..d1eb1f9e21 --- /dev/null +++ b/backend/tests/flows.rs @@ -0,0 +1,185 @@ +use serde_json::json; +use sqlx::{Pool, Postgres}; + +mod common; +use common::*; + +fn flow_url(port: u16, endpoint: &str, path: &str) -> String { + format!("http://localhost:{port}/api/w/test-workspace/flows/{endpoint}/{path}") +} + +fn client() -> reqwest::Client { + reqwest::Client::new() +} + +fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + builder.header("Authorization", "Bearer SECRET_TOKEN") +} + +async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response { + authed(client().get(flow_url(port, endpoint, path))) + .send() + .await + .unwrap() +} + +fn new_flow(path: &str, summary: &str) -> serde_json::Value { + json!({ + "path": path, + "summary": summary, + "description": "", + "value": { + "modules": [] + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "required": [] + } + }) +} + +#[sqlx::test(fixtures("base"))] +async fn test_flow_endpoints(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace/flows"); + + // --- create --- + let resp = authed(client().post(format!("{base}/create"))) + .json(&new_flow("u/test-user/test_flow", "Test flow")) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "create: {}", resp.text().await?); + + // create second flow + let resp = authed(client().post(format!("{base}/create"))) + .json(&new_flow("u/test-user/another_flow", "Another flow")) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "create another: {}", resp.text().await?); + + // --- exists --- + let resp = authed_get(port, "exists", "u/test-user/test_flow").await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.json::().await?, true); + + let resp = authed_get(port, "exists", "u/test-user/nonexistent").await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.json::().await?, false); + + // --- get by path --- + let resp = authed_get(port, "get", "u/test-user/test_flow").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_flow"); + assert_eq!(body["summary"], "Test flow"); + + // get not found + let resp = authed_get(port, "get", "u/test-user/nonexistent").await; + assert_eq!(resp.status(), 404); + + // --- get draft --- + let resp = authed_get(port, "get/draft", "u/test-user/test_flow").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_flow"); + + // --- list --- + let resp = authed(client().get(format!("{base}/list"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert!( + list.len() >= 2, + "expected at least 2 flows, got {}", + list.len() + ); + assert!(list.iter().any(|f| f["path"] == "u/test-user/test_flow")); + + // --- list_search --- + let resp = authed(client().get(format!("{base}/list_search"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert!(!list.is_empty()); + + // --- list_paths --- + let resp = authed(client().get(format!("{base}/list_paths"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let paths = resp.json::>().await?; + assert!(paths.contains(&"u/test-user/test_flow".to_string())); + + // --- history --- + let resp = authed_get(port, "history/p", "u/test-user/test_flow").await; + assert_eq!(resp.status(), 200); + let history = resp.json::>().await?; + assert!(!history.is_empty()); + + // --- get_latest_version --- + let resp = authed_get(port, "get_latest_version", "u/test-user/test_flow").await; + assert_eq!(resp.status(), 200); + + // --- deployment_status --- + let resp = authed_get(port, "deployment_status/p", "u/test-user/test_flow").await; + assert_eq!(resp.status(), 200); + + // --- update --- + let resp = authed(client().post(flow_url(port, "update", "u/test-user/test_flow"))) + .json(&new_flow("u/test-user/test_flow", "Updated flow")) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200, "update: {}", resp.text().await?); + + // verify update + let resp = authed_get(port, "get", "u/test-user/test_flow").await; + let body = resp.json::().await?; + assert_eq!(body["summary"], "Updated flow"); + + // history should have 2 entries + let resp = authed_get(port, "history/p", "u/test-user/test_flow").await; + let history = resp.json::>().await?; + assert!( + history.len() >= 2, + "expected at least 2 history entries, got {}", + history.len() + ); + + // --- archive --- + let resp = authed(client().post(flow_url(port, "archive", "u/test-user/another_flow"))) + .json(&json!({})) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + + // archived flow should still be gettable + let resp = authed_get(port, "get", "u/test-user/another_flow").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["archived"], true); + + // --- delete --- + let resp = authed(client().delete(flow_url(port, "delete", "u/test-user/another_flow"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + + let resp = authed_get(port, "exists", "u/test-user/another_flow").await; + assert_eq!(resp.json::().await?, false); + + Ok(()) +} diff --git a/backend/tests/scripts.rs b/backend/tests/scripts.rs new file mode 100644 index 0000000000..873320d089 --- /dev/null +++ b/backend/tests/scripts.rs @@ -0,0 +1,240 @@ +use serde_json::json; +use sqlx::{Pool, Postgres}; + +mod common; +use common::*; + +fn script_url(port: u16, endpoint: &str, path: &str) -> String { + format!("http://localhost:{port}/api/w/test-workspace/scripts/{endpoint}/{path}") +} + +fn client() -> reqwest::Client { + reqwest::Client::new() +} + +fn authed(builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + builder.header("Authorization", "Bearer SECRET_TOKEN") +} + +async fn authed_get(port: u16, endpoint: &str, path: &str) -> reqwest::Response { + authed(client().get(script_url(port, endpoint, path))) + .send() + .await + .unwrap() +} + +fn new_script(path: &str, summary: &str, content: &str) -> serde_json::Value { + json!({ + "path": path, + "summary": summary, + "description": "", + "content": content, + "language": "deno", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "required": [] + } + }) +} + +#[sqlx::test(fixtures("base"))] +async fn test_script_endpoints(db: Pool) -> anyhow::Result<()> { + initialize_tracing().await; + let server = ApiServer::start(db.clone()).await?; + let port = server.addr.port(); + let base = format!("http://localhost:{port}/api/w/test-workspace/scripts"); + + // --- create --- + let resp = authed(client().post(format!("{base}/create"))) + .json(&new_script( + "u/test-user/test_script", + "Test script", + "export async function main() { return 42; }", + )) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "create: {}", resp.text().await?); + + // create second script + let resp = authed(client().post(format!("{base}/create"))) + .json(&new_script( + "u/test-user/another_script", + "Another script", + "export async function main() { return 'hello'; }", + )) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "create another: {}", resp.text().await?); + + // --- exists --- + let resp = authed_get(port, "exists/p", "u/test-user/test_script").await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.json::().await?, true); + + let resp = authed_get(port, "exists/p", "u/test-user/nonexistent").await; + assert_eq!(resp.status(), 200); + assert_eq!(resp.json::().await?, false); + + // --- get by path --- + let resp = authed_get(port, "get/p", "u/test-user/test_script").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_script"); + assert_eq!(body["summary"], "Test script"); + assert_eq!(body["language"], "deno"); + assert!(body["hash"].is_string(), "expected hash to be a hex string"); + let hash = body["hash"].as_str().unwrap().to_string(); + + // get not found + let resp = authed_get(port, "get/p", "u/test-user/nonexistent").await; + assert_eq!(resp.status(), 404); + + // --- get by hash --- + let resp = authed_get(port, "get/h", &hash).await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_script"); + + // --- get draft --- + let resp = authed_get(port, "get/draft", "u/test-user/test_script").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["path"], "u/test-user/test_script"); + + // --- raw by path (requires language extension) --- + let resp = authed_get(port, "raw/p", "u/test-user/test_script.ts").await; + assert_eq!(resp.status(), 200); + let body = resp.text().await?; + assert!(body.contains("return 42"), "expected script content, got: {body}"); + + // --- raw by hash (requires .ts suffix) --- + let resp = authed_get(port, "raw/h", &format!("{hash}.ts")).await; + assert_eq!(resp.status(), 200); + let body = resp.text().await?; + assert!(body.contains("return 42")); + + // --- list --- + let resp = authed(client().get(format!("{base}/list"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert!( + list.len() >= 2, + "expected at least 2 scripts, got {}", + list.len() + ); + assert!(list.iter().any(|s| s["path"] == "u/test-user/test_script")); + + // list with path_start filter + let resp = authed(client().get(format!( + "{base}/list?path_start=u/test-user/another" + ))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert_eq!(list.len(), 1); + assert_eq!(list[0]["path"], "u/test-user/another_script"); + + // --- list_search --- + let resp = authed(client().get(format!("{base}/list_search"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let list = resp.json::>().await?; + assert!(!list.is_empty()); + + // --- list_paths --- + let resp = authed(client().get(format!("{base}/list_paths"))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let paths = resp.json::>().await?; + assert!(paths.contains(&"u/test-user/test_script".to_string())); + + // --- history --- + let resp = authed_get(port, "history/p", "u/test-user/test_script").await; + assert_eq!(resp.status(), 200); + let history = resp.json::>().await?; + assert!(!history.is_empty()); + + // --- get_latest_version --- + let resp = authed_get(port, "get_latest_version", "u/test-user/test_script").await; + assert_eq!(resp.status(), 200); + + // --- deployment_status --- + let resp = authed_get(port, "deployment_status/h", &hash).await; + assert_eq!(resp.status(), 200); + + // --- update script (create new version) --- + let mut updated = new_script( + "u/test-user/test_script", + "Updated test script", + "export async function main() { return 99; }", + ); + updated["parent_hash"] = json!(&hash); + let resp = authed(client().post(format!("{base}/create"))) + .json(&updated) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 201, "update: {}", resp.text().await?); + + // verify new version + let resp = authed_get(port, "get/p", "u/test-user/test_script").await; + let body = resp.json::().await?; + assert_eq!(body["summary"], "Updated test script"); + let new_hash = body["hash"].as_str().unwrap(); + assert_ne!(new_hash, hash, "hash should change on update"); + + // history should have 2 entries now + let resp = authed_get(port, "history/p", "u/test-user/test_script").await; + let history = resp.json::>().await?; + assert!( + history.len() >= 2, + "expected at least 2 history entries, got {}", + history.len() + ); + + // --- archive --- + let resp = authed(client().post(script_url( + port, + "archive/p", + "u/test-user/another_script", + ))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + + // archived script should still be gettable + let resp = authed_get(port, "get/p", "u/test-user/another_script").await; + assert_eq!(resp.status(), 200); + let body = resp.json::().await?; + assert_eq!(body["archived"], true); + + // --- delete --- + let resp = authed(client().post(script_url( + port, + "delete/p", + "u/test-user/another_script", + ))) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + + let resp = authed_get(port, "exists/p", "u/test-user/another_script").await; + assert_eq!(resp.json::().await?, false); + + Ok(()) +}