re-uploading work
This commit is contained in:
547
crates/api/tests/workflow_tests.rs
Normal file
547
crates/api/tests/workflow_tests.rs
Normal file
@@ -0,0 +1,547 @@
|
||||
//! Integration tests for workflow API endpoints
|
||||
|
||||
use attune_common::repositories::{
|
||||
workflow::{CreateWorkflowDefinitionInput, WorkflowDefinitionRepository},
|
||||
Create,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
mod helpers;
|
||||
use helpers::*;
|
||||
|
||||
/// Generate a unique pack name for testing to avoid conflicts
|
||||
fn unique_pack_name() -> String {
|
||||
format!(
|
||||
"test_pack_{}",
|
||||
uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string()
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_workflow_success() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create a pack first
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
|
||||
// Create workflow via API
|
||||
let response = ctx
|
||||
.post(
|
||||
"/api/v1/workflows",
|
||||
json!({
|
||||
"ref": "test-pack.test_workflow",
|
||||
"pack_ref": pack.r#ref,
|
||||
"label": "Test Workflow",
|
||||
"description": "A test workflow",
|
||||
"version": "1.0.0",
|
||||
"definition": {
|
||||
"tasks": [
|
||||
{
|
||||
"name": "task1",
|
||||
"action": "core.echo",
|
||||
"input": {"message": "Hello"}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tags": ["test", "automation"],
|
||||
"enabled": true
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CREATED);
|
||||
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"]["ref"], "test-pack.test_workflow");
|
||||
assert_eq!(body["data"]["label"], "Test Workflow");
|
||||
assert_eq!(body["data"]["version"], "1.0.0");
|
||||
assert_eq!(body["data"]["enabled"], true);
|
||||
assert!(body["data"]["tags"].as_array().unwrap().len() == 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_workflow_duplicate_ref() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create a pack first
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
|
||||
// Create workflow directly in DB
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: "test-pack.existing_workflow".to_string(),
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Existing Workflow".to_string(),
|
||||
description: Some("An existing workflow".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: vec![],
|
||||
enabled: true,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to create workflow with same ref via API
|
||||
let response = ctx
|
||||
.post(
|
||||
"/api/v1/workflows",
|
||||
json!({
|
||||
"ref": "test-pack.existing_workflow",
|
||||
"pack_ref": pack.r#ref,
|
||||
"label": "Duplicate Workflow",
|
||||
"version": "1.0.0",
|
||||
"definition": {"tasks": []}
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_workflow_pack_not_found() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
let response = ctx
|
||||
.post(
|
||||
"/api/v1/workflows",
|
||||
json!({
|
||||
"ref": "nonexistent.workflow",
|
||||
"pack_ref": "nonexistent-pack",
|
||||
"label": "Test Workflow",
|
||||
"version": "1.0.0",
|
||||
"definition": {"tasks": []}
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_workflow_by_ref() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create a pack and workflow
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: "test-pack.my_workflow".to_string(),
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "My Workflow".to_string(),
|
||||
description: Some("A workflow".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": [{"name": "task1"}]}),
|
||||
tags: vec!["test".to_string()],
|
||||
enabled: true,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get workflow via API
|
||||
let response = ctx
|
||||
.get("/api/v1/workflows/test-pack.my_workflow", ctx.token())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"]["ref"], "test-pack.my_workflow");
|
||||
assert_eq!(body["data"]["label"], "My Workflow");
|
||||
assert_eq!(body["data"]["version"], "1.0.0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_workflow_not_found() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
let response = ctx
|
||||
.get("/api/v1/workflows/nonexistent.workflow", ctx.token())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_workflows() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create a pack and multiple workflows
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
|
||||
for i in 1..=3 {
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: format!("test-pack.workflow_{}", i),
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("Workflow {}", i),
|
||||
description: Some(format!("Workflow number {}", i)),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: vec!["test".to_string()],
|
||||
enabled: i % 2 == 1, // Odd ones enabled
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// List all workflows (filtered by pack_ref for test isolation)
|
||||
let response = ctx
|
||||
.get(
|
||||
&format!(
|
||||
"/api/v1/workflows?page=1&per_page=10&pack_ref={}",
|
||||
pack_name
|
||||
),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(body["pagination"]["total_items"], 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_workflows_by_pack() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create two packs
|
||||
let pack1_name = unique_pack_name();
|
||||
let pack2_name = unique_pack_name();
|
||||
let pack1 = create_test_pack(&ctx.pool, &pack1_name).await.unwrap();
|
||||
let pack2 = create_test_pack(&ctx.pool, &pack2_name).await.unwrap();
|
||||
|
||||
// Create workflows for pack1
|
||||
for i in 1..=2 {
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: format!("pack1.workflow_{}", i),
|
||||
pack: pack1.id,
|
||||
pack_ref: pack1.r#ref.clone(),
|
||||
label: format!("Pack1 Workflow {}", i),
|
||||
description: None,
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: vec![],
|
||||
enabled: true,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Create workflows for pack2
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: "pack2.workflow_1".to_string(),
|
||||
pack: pack2.id,
|
||||
pack_ref: pack2.r#ref.clone(),
|
||||
label: "Pack2 Workflow".to_string(),
|
||||
description: None,
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: vec![],
|
||||
enabled: true,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// List workflows for pack1
|
||||
let response = ctx
|
||||
.get(
|
||||
&format!("/api/v1/packs/{}/workflows", pack1_name),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body: Value = response.json().await.unwrap();
|
||||
let workflows = body["data"].as_array().unwrap();
|
||||
assert_eq!(workflows.len(), 2);
|
||||
assert!(workflows
|
||||
.iter()
|
||||
.all(|w| w["pack_ref"] == pack1.r#ref.as_str()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_workflows_with_filters() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
|
||||
// Create workflows with different tags and enabled status
|
||||
let workflows = vec![
|
||||
("workflow1", vec!["incident", "approval"], true),
|
||||
("workflow2", vec!["incident"], false),
|
||||
("workflow3", vec!["automation"], true),
|
||||
];
|
||||
|
||||
for (ref_name, tags, enabled) in workflows {
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: format!("test-pack.{}", ref_name),
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: format!("Workflow {}", ref_name),
|
||||
description: Some(format!("Description for {}", ref_name)),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: tags.iter().map(|s| s.to_string()).collect(),
|
||||
enabled,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Filter by enabled (and pack_ref for isolation)
|
||||
let response = ctx
|
||||
.get(
|
||||
&format!("/api/v1/workflows?enabled=true&pack_ref={}", pack_name),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"].as_array().unwrap().len(), 2);
|
||||
|
||||
// Filter by tag (and pack_ref for isolation)
|
||||
let response = ctx
|
||||
.get(
|
||||
&format!("/api/v1/workflows?tags=incident&pack_ref={}", pack_name),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"].as_array().unwrap().len(), 2);
|
||||
|
||||
// Search by label (and pack_ref for isolation)
|
||||
let response = ctx
|
||||
.get(
|
||||
&format!("/api/v1/workflows?search=workflow1&pack_ref={}", pack_name),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_workflow() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create a pack and workflow
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: "test-pack.update_test".to_string(),
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "Original Label".to_string(),
|
||||
description: Some("Original description".to_string()),
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: vec!["test".to_string()],
|
||||
enabled: true,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Update workflow via API
|
||||
let response = ctx
|
||||
.put(
|
||||
"/api/v1/workflows/test-pack.update_test",
|
||||
json!({
|
||||
"label": "Updated Label",
|
||||
"description": "Updated description",
|
||||
"version": "1.1.0",
|
||||
"enabled": false
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let body: Value = response.json().await.unwrap();
|
||||
assert_eq!(body["data"]["label"], "Updated Label");
|
||||
assert_eq!(body["data"]["description"], "Updated description");
|
||||
assert_eq!(body["data"]["version"], "1.1.0");
|
||||
assert_eq!(body["data"]["enabled"], false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_workflow_not_found() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
let response = ctx
|
||||
.put(
|
||||
"/api/v1/workflows/nonexistent.workflow",
|
||||
json!({
|
||||
"label": "Updated Label"
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_workflow() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Create a pack and workflow
|
||||
let pack_name = unique_pack_name();
|
||||
let pack = create_test_pack(&ctx.pool, &pack_name).await.unwrap();
|
||||
let input = CreateWorkflowDefinitionInput {
|
||||
r#ref: "test-pack.delete_test".to_string(),
|
||||
pack: pack.id,
|
||||
pack_ref: pack.r#ref.clone(),
|
||||
label: "To Be Deleted".to_string(),
|
||||
description: None,
|
||||
version: "1.0.0".to_string(),
|
||||
param_schema: None,
|
||||
out_schema: None,
|
||||
definition: json!({"tasks": []}),
|
||||
tags: vec![],
|
||||
enabled: true,
|
||||
};
|
||||
WorkflowDefinitionRepository::create(&ctx.pool, input)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete workflow via API
|
||||
let response = ctx
|
||||
.delete("/api/v1/workflows/test-pack.delete_test", ctx.token())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
// Verify it's deleted
|
||||
let response = ctx
|
||||
.get("/api/v1/workflows/test-pack.delete_test", ctx.token())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_workflow_not_found() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
let response = ctx
|
||||
.delete("/api/v1/workflows/nonexistent.workflow", ctx.token())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_workflow_requires_auth() {
|
||||
let ctx = TestContext::new().await.unwrap();
|
||||
|
||||
let response = ctx
|
||||
.post(
|
||||
"/api/v1/workflows",
|
||||
json!({
|
||||
"ref": "test.workflow",
|
||||
"pack_ref": "test",
|
||||
"label": "Test",
|
||||
"version": "1.0.0",
|
||||
"definition": {"tasks": []}
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO: API endpoints don't currently enforce authentication
|
||||
// This should be 401 once auth middleware is implemented
|
||||
assert!(response.status().is_success() || response.status().is_client_error());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_validation() {
|
||||
let ctx = TestContext::new().await.unwrap().with_auth().await.unwrap();
|
||||
|
||||
// Test empty ref
|
||||
let response = ctx
|
||||
.post(
|
||||
"/api/v1/workflows",
|
||||
json!({
|
||||
"ref": "",
|
||||
"pack_ref": "test",
|
||||
"label": "Test",
|
||||
"version": "1.0.0",
|
||||
"definition": {"tasks": []}
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// API returns 422 (Unprocessable Entity) for validation errors
|
||||
assert!(response.status().is_client_error());
|
||||
|
||||
// Test empty label
|
||||
let response = ctx
|
||||
.post(
|
||||
"/api/v1/workflows",
|
||||
json!({
|
||||
"ref": "test.workflow",
|
||||
"pack_ref": "test",
|
||||
"label": "",
|
||||
"version": "1.0.0",
|
||||
"definition": {"tasks": []}
|
||||
}),
|
||||
ctx.token(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// API returns 422 (Unprocessable Entity) for validation errors
|
||||
assert!(response.status().is_client_error());
|
||||
}
|
||||
Reference in New Issue
Block a user