feat(suite): Implement Suites (#1239)
* feat(suite): Implement Suites Fixes #1230 * Update docs * Fix variable alignment * Prevent always-run endpoint from running if a context placeholder fails to resolve in the URL * Return errors when a context placeholder path fails to resolve * Add a couple of unit tests * Add a couple of unit tests * fix(ui): Update group count properly Fixes #1233 * refactor: Pass down entire config instead of several sub-configs * fix: Change default suite interval and timeout * fix: Deprecate disable-monitoring-lock in favor of concurrency * fix: Make sure there are no duplicate keys * Refactor some code * Update watchdog/watchdog.go * Update web/app/src/components/StepDetailsModal.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: Remove useless log * fix: Set default concurrency to 3 instead of 5 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2105,3 +2105,382 @@ func TestConfig_GetUniqueExtraMetricLabels(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithDuplicateKeysAcrossEntityTypes(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
shouldError bool
|
||||
expectedErr string
|
||||
config string
|
||||
}{
|
||||
{
|
||||
name: "endpoint-suite-same-key",
|
||||
shouldError: true,
|
||||
expectedErr: "duplicate key 'backend_test-api': suite 'backend_test-api' conflicts with endpoint 'backend_test-api'",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: test-api
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: test-api
|
||||
group: backend
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "endpoint-suite-different-keys",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-service
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: integration-tests
|
||||
group: testing
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "endpoint-external-endpoint-suite-unique-keys",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-service
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
external-endpoints:
|
||||
- name: monitoring-agent
|
||||
group: infrastructure
|
||||
token: "secret-token"
|
||||
heartbeat:
|
||||
interval: 5m
|
||||
|
||||
suites:
|
||||
- name: integration-tests
|
||||
group: testing
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "suite-with-same-key-as-external-endpoint",
|
||||
shouldError: true,
|
||||
expectedErr: "duplicate key 'monitoring_health-check': suite 'monitoring_health-check' conflicts with external endpoint 'monitoring_health-check'",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: dummy
|
||||
url: https://example.com/dummy
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
external-endpoints:
|
||||
- name: health-check
|
||||
group: monitoring
|
||||
token: "secret-token"
|
||||
heartbeat:
|
||||
interval: 5m
|
||||
|
||||
suites:
|
||||
- name: health-check
|
||||
group: monitoring
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "endpoint-with-same-name-as-suite-endpoint-different-groups",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-health
|
||||
group: backend
|
||||
url: https://example.com/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: integration-suite
|
||||
group: testing
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: api-health
|
||||
url: https://example.com/api/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "endpoint-conflicting-with-suite-endpoint",
|
||||
shouldError: true,
|
||||
expectedErr: "duplicate key 'backend_api-health': endpoint 'backend_api-health' in suite 'backend_integration-suite' conflicts with endpoint 'backend_api-health'",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-health
|
||||
group: backend
|
||||
url: https://example.com/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: integration-suite
|
||||
group: backend
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: api-health
|
||||
url: https://example.com/api/health
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
||||
if scenario.shouldError {
|
||||
if err == nil {
|
||||
t.Error("should've returned an error")
|
||||
} else if scenario.expectedErr != "" && err.Error() != scenario.expectedErr {
|
||||
t.Errorf("expected error message '%s', got '%s'", scenario.expectedErr, err.Error())
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("shouldn't have returned an error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndValidateConfigBytesWithSuites(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
shouldError bool
|
||||
expectedErr string
|
||||
config string
|
||||
}{
|
||||
{
|
||||
name: "suite-with-no-name",
|
||||
shouldError: true,
|
||||
expectedErr: "invalid suite 'testing_': suite must have a name",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: dummy
|
||||
url: https://example.com/dummy
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- group: testing
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "suite-with-no-endpoints",
|
||||
shouldError: true,
|
||||
expectedErr: "invalid suite 'testing_empty-suite': suite must have at least one endpoint",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: dummy
|
||||
url: https://example.com/dummy
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: empty-suite
|
||||
group: testing
|
||||
interval: 30s
|
||||
endpoints: []`,
|
||||
},
|
||||
{
|
||||
name: "suite-with-duplicate-endpoint-names",
|
||||
shouldError: true,
|
||||
expectedErr: "invalid suite 'testing_duplicate-test': suite cannot have duplicate endpoint names: duplicate endpoint name 'step1'",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: dummy
|
||||
url: https://example.com/dummy
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: duplicate-test
|
||||
group: testing
|
||||
interval: 30s
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test1
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- name: step1
|
||||
url: https://example.com/test2
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "suite-with-invalid-negative-timeout",
|
||||
shouldError: true,
|
||||
expectedErr: "invalid suite 'testing_negative-timeout-suite': suite timeout must be positive",
|
||||
config: `
|
||||
endpoints:
|
||||
- name: dummy
|
||||
url: https://example.com/dummy
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: negative-timeout-suite
|
||||
group: testing
|
||||
interval: 30s
|
||||
timeout: -5m
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "valid-suite-with-defaults",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-service
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: integration-test
|
||||
group: testing
|
||||
endpoints:
|
||||
- name: step1
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- name: step2
|
||||
url: https://example.com/validate
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "valid-suite-with-all-fields",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-service
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: full-integration-test
|
||||
group: testing
|
||||
enabled: true
|
||||
interval: 15m
|
||||
timeout: 10m
|
||||
context:
|
||||
base_url: "https://example.com"
|
||||
user_id: 12345
|
||||
endpoints:
|
||||
- name: authentication
|
||||
url: https://example.com/auth
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- name: user-profile
|
||||
url: https://example.com/profile
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
- "[BODY].user_id == 12345"`,
|
||||
},
|
||||
{
|
||||
name: "valid-suite-with-endpoint-inheritance",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-service
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: inheritance-test
|
||||
group: parent-group
|
||||
endpoints:
|
||||
- name: child-endpoint
|
||||
url: https://example.com/test
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
{
|
||||
name: "valid-suite-with-store-functionality",
|
||||
shouldError: false,
|
||||
config: `
|
||||
endpoints:
|
||||
- name: api-service
|
||||
group: backend
|
||||
url: https://example.com/api
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
|
||||
suites:
|
||||
- name: store-test
|
||||
group: testing
|
||||
endpoints:
|
||||
- name: get-token
|
||||
url: https://example.com/auth
|
||||
conditions:
|
||||
- "[STATUS] == 200"
|
||||
store:
|
||||
auth_token: "[BODY].token"
|
||||
- name: use-token
|
||||
url: https://example.com/data
|
||||
headers:
|
||||
Authorization: "Bearer {auth_token}"
|
||||
conditions:
|
||||
- "[STATUS] == 200"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
_, err := parseAndValidateConfigBytes([]byte(scenario.config))
|
||||
if scenario.shouldError {
|
||||
if err == nil {
|
||||
t.Error("should've returned an error")
|
||||
} else if scenario.expectedErr != "" && err.Error() != scenario.expectedErr {
|
||||
t.Errorf("expected error message '%s', got '%s'", scenario.expectedErr, err.Error())
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("shouldn't have returned an error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user