541 lines
11 KiB
Go
541 lines
11 KiB
Go
package routing
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestValidateValidWorkflowSpec(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test-workflow",
|
||
|
|
Input: map[string]interface{}{
|
||
|
|
"repo": "https://github.com/test/repo",
|
||
|
|
},
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Clone",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "CloneRepoActivity",
|
||
|
|
Parameters: map[string]interface{}{
|
||
|
|
"repo": "${input.repo}",
|
||
|
|
},
|
||
|
|
Timeout: "5m",
|
||
|
|
Next: "Analyze",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Name: "Analyze",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "AnalyzeCodeActivity",
|
||
|
|
Parameters: map[string]interface{}{
|
||
|
|
"path": "${Clone.output.path}",
|
||
|
|
},
|
||
|
|
Timeout: "10m",
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if !result.Valid {
|
||
|
|
t.Errorf("Spec should be valid. Errors: %v", result.Errors)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateWorkflowSpecNilName(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Test",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "CloneRepoActivity",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Spec with empty name should be invalid")
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(result.Errors) == 0 {
|
||
|
|
t.Error("Should have validation errors")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateWorkflowSpecNoStates(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Spec with no states should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateInvalidStateTransition(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "A",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "CloneRepoActivity",
|
||
|
|
Next: "NonExistent",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Spec with invalid state transition should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateDuplicateStateName(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "A",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "CloneRepoActivity",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Name: "A", // Duplicate!
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "CloneRepoActivity",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Spec with duplicate state names should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateUnknownActivity(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "A",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "UnknownActivity",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Spec with unknown activity should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidatePassState(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Success",
|
||
|
|
Type: StateTypePass,
|
||
|
|
Result: map[string]interface{}{"status": "ok"},
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if !result.Valid {
|
||
|
|
t.Errorf("Pass state spec should be valid: %v", result.Errors)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidatePassStateNoResult(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Success",
|
||
|
|
Type: StateTypePass,
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Pass state without result should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateFailState(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Fail",
|
||
|
|
Type: StateTypeFail,
|
||
|
|
Error: "TestError",
|
||
|
|
Cause: "For testing",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if !result.Valid {
|
||
|
|
t.Errorf("Fail state spec should be valid: %v", result.Errors)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateInvalidDuration(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &WorkflowSpec{
|
||
|
|
Name: "test",
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "A",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "CloneRepoActivity",
|
||
|
|
Timeout: "invalid",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Spec with invalid timeout should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateCronWorkflowSpec(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &CronWorkflowSpec{
|
||
|
|
Name: "daily-scan",
|
||
|
|
Type: "CronWorkflow",
|
||
|
|
Schedule: "0 2 * * *",
|
||
|
|
Timezone: "UTC",
|
||
|
|
MaxConcurrent: 1,
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Scan",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "SecurityScanActivity",
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateCronWorkflowSpec(spec)
|
||
|
|
if !result.Valid {
|
||
|
|
t.Errorf("Valid cron spec should pass: %v", result.Errors)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateCronWorkflowSpecInvalidTimezone(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &CronWorkflowSpec{
|
||
|
|
Name: "daily-scan",
|
||
|
|
Type: "CronWorkflow",
|
||
|
|
Schedule: "0 2 * * *",
|
||
|
|
Timezone: "InvalidTimezone",
|
||
|
|
MaxConcurrent: 1,
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Scan",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "SecurityScanActivity",
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateCronWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Cron spec with invalid timezone should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateCronWorkflowSpecInvalidSchedule(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &CronWorkflowSpec{
|
||
|
|
Name: "daily-scan",
|
||
|
|
Type: "CronWorkflow",
|
||
|
|
Schedule: "invalid cron",
|
||
|
|
Timezone: "UTC",
|
||
|
|
MaxConcurrent: 1,
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Scan",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "SecurityScanActivity",
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateCronWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Cron spec with invalid schedule should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateCronWorkflowSpecInvalidType(t *testing.T) {
|
||
|
|
kbPath := getKBPath()
|
||
|
|
if kbPath == "" {
|
||
|
|
t.Skip("Knowledge base not found")
|
||
|
|
}
|
||
|
|
|
||
|
|
kb, err := LoadKnowledgeBase(kbPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Failed to load KB: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
validator := NewValidator(kb)
|
||
|
|
|
||
|
|
spec := &CronWorkflowSpec{
|
||
|
|
Name: "daily-scan",
|
||
|
|
Type: "WrongType",
|
||
|
|
Schedule: "0 2 * * *",
|
||
|
|
Timezone: "UTC",
|
||
|
|
MaxConcurrent: 1,
|
||
|
|
States: []State{
|
||
|
|
{
|
||
|
|
Name: "Scan",
|
||
|
|
Type: StateTypeTask,
|
||
|
|
Resource: "SecurityScanActivity",
|
||
|
|
End: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
result := validator.ValidateCronWorkflowSpec(spec)
|
||
|
|
if result.Valid {
|
||
|
|
t.Error("Cron spec with wrong type should be invalid")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateDuration(t *testing.T) {
|
||
|
|
validDurations := []string{"1s", "5m", "1h", "100ms"}
|
||
|
|
for _, dur := range validDurations {
|
||
|
|
if err := validateDuration(dur); err != nil {
|
||
|
|
t.Errorf("Duration %s should be valid: %v", dur, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
invalidDurations := []string{"invalid", "5x", ""}
|
||
|
|
for _, dur := range invalidDurations {
|
||
|
|
if err := validateDuration(dur); err == nil {
|
||
|
|
t.Errorf("Duration %s should be invalid", dur)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidateCronExpression(t *testing.T) {
|
||
|
|
validCrons := []string{
|
||
|
|
"0 2 * * *", // 2 AM daily
|
||
|
|
"*/5 * * * *", // Every 5 minutes
|
||
|
|
"0 0 1 * *", // First of month
|
||
|
|
"0 12 * * 1", // Noon on Mondays
|
||
|
|
"30 15 * * *", // 3:30 PM daily
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, cron := range validCrons {
|
||
|
|
if err := validateCronExpression(cron); err != nil {
|
||
|
|
t.Errorf("Cron %s should be valid: %v", cron, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
invalidCrons := []string{
|
||
|
|
"invalid", // Too few fields
|
||
|
|
"0 2 * * * *", // Too many fields
|
||
|
|
"60 * * * *", // Invalid minute
|
||
|
|
"* 25 * * *", // Invalid hour
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, cron := range invalidCrons {
|
||
|
|
if err := validateCronExpression(cron); err == nil {
|
||
|
|
t.Errorf("Cron %s should be invalid", cron)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestValidationResultString(t *testing.T) {
|
||
|
|
result := &ValidationResult{
|
||
|
|
Valid: true,
|
||
|
|
Errors: []ValidationError{},
|
||
|
|
}
|
||
|
|
|
||
|
|
str := result.String()
|
||
|
|
if str != "Valid ✓" {
|
||
|
|
t.Errorf("Valid result string should be 'Valid ✓', got: %s", str)
|
||
|
|
}
|
||
|
|
|
||
|
|
result.Valid = false
|
||
|
|
result.Errors = []ValidationError{
|
||
|
|
{
|
||
|
|
Path: "spec.name",
|
||
|
|
Message: "name is required",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
str = result.String()
|
||
|
|
if !contains(str, "Invalid") {
|
||
|
|
t.Error("Invalid result string should contain 'Invalid'")
|
||
|
|
}
|
||
|
|
}
|