548 lines
16 KiB
Rust
548 lines
16 KiB
Rust
//! 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());
|
|
}
|