mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
1100 lines
37 KiB
Go
1100 lines
37 KiB
Go
package api
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
// TestValidateSystemSettings provides comprehensive coverage of all validation paths
|
|
// in the validateSystemSettings function using table-driven tests.
|
|
func TestValidateSystemSettings(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input map[string]interface{}
|
|
expectError bool
|
|
errorText string // key term to check with strings.Contains, empty means don't check
|
|
}{
|
|
// =================================================================
|
|
// pvePollingInterval validation
|
|
// =================================================================
|
|
{
|
|
name: "pvePollingInterval: valid minimum (10)",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(10)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pvePollingInterval: valid maximum (3600)",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(3600)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pvePollingInterval: valid middle value (60)",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(60)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pvePollingInterval: zero is invalid",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(0)},
|
|
expectError: true,
|
|
errorText: "positive",
|
|
},
|
|
{
|
|
name: "pvePollingInterval: negative is invalid",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(-1)},
|
|
expectError: true,
|
|
errorText: "positive",
|
|
},
|
|
{
|
|
name: "pvePollingInterval: below minimum (9)",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(9)},
|
|
expectError: true,
|
|
errorText: "10 seconds",
|
|
},
|
|
{
|
|
name: "pvePollingInterval: above maximum (3601)",
|
|
input: map[string]interface{}{"pvePollingInterval": float64(3601)},
|
|
expectError: true,
|
|
errorText: "3600",
|
|
},
|
|
{
|
|
name: "pvePollingInterval: string instead of number",
|
|
input: map[string]interface{}{"pvePollingInterval": "60"},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
{
|
|
name: "pvePollingInterval: boolean instead of number",
|
|
input: map[string]interface{}{"pvePollingInterval": true},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
|
|
// =================================================================
|
|
// pbsPollingInterval validation (same rules as PVE)
|
|
// =================================================================
|
|
{
|
|
name: "pbsPollingInterval: valid minimum (10)",
|
|
input: map[string]interface{}{"pbsPollingInterval": float64(10)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pbsPollingInterval: valid maximum (3600)",
|
|
input: map[string]interface{}{"pbsPollingInterval": float64(3600)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pbsPollingInterval: zero is invalid",
|
|
input: map[string]interface{}{"pbsPollingInterval": float64(0)},
|
|
expectError: true,
|
|
errorText: "positive",
|
|
},
|
|
{
|
|
name: "pbsPollingInterval: below minimum (9)",
|
|
input: map[string]interface{}{"pbsPollingInterval": float64(9)},
|
|
expectError: true,
|
|
errorText: "10 seconds",
|
|
},
|
|
{
|
|
name: "pbsPollingInterval: above maximum (3601)",
|
|
input: map[string]interface{}{"pbsPollingInterval": float64(3601)},
|
|
expectError: true,
|
|
errorText: "3600",
|
|
},
|
|
{
|
|
name: "pbsPollingInterval: string instead of number",
|
|
input: map[string]interface{}{"pbsPollingInterval": "60"},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
|
|
// =================================================================
|
|
// pmgPollingInterval validation (same rules as PVE)
|
|
// =================================================================
|
|
{
|
|
name: "pmgPollingInterval: valid minimum (10)",
|
|
input: map[string]interface{}{"pmgPollingInterval": float64(10)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pmgPollingInterval: valid maximum (3600)",
|
|
input: map[string]interface{}{"pmgPollingInterval": float64(3600)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "pmgPollingInterval: zero is invalid",
|
|
input: map[string]interface{}{"pmgPollingInterval": float64(0)},
|
|
expectError: true,
|
|
errorText: "positive",
|
|
},
|
|
{
|
|
name: "pmgPollingInterval: below minimum (9)",
|
|
input: map[string]interface{}{"pmgPollingInterval": float64(9)},
|
|
expectError: true,
|
|
errorText: "10 seconds",
|
|
},
|
|
{
|
|
name: "pmgPollingInterval: above maximum (3601)",
|
|
input: map[string]interface{}{"pmgPollingInterval": float64(3601)},
|
|
expectError: true,
|
|
errorText: "3600",
|
|
},
|
|
{
|
|
name: "pmgPollingInterval: string instead of number",
|
|
input: map[string]interface{}{"pmgPollingInterval": "60"},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
|
|
// =================================================================
|
|
// backupPollingInterval validation
|
|
// NOTE: backupPollingInterval allows 0 (to disable backup polling),
|
|
// unlike PVE/PBS/PMG intervals which must be positive.
|
|
// This is intentional - see lines 172-185 in system_settings.go.
|
|
// =================================================================
|
|
{
|
|
name: "backupPollingInterval: zero is valid (disables polling)",
|
|
input: map[string]interface{}{"backupPollingInterval": float64(0)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "backupPollingInterval: valid minimum positive (10)",
|
|
input: map[string]interface{}{"backupPollingInterval": float64(10)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "backupPollingInterval: valid maximum (604800)",
|
|
input: map[string]interface{}{"backupPollingInterval": float64(604800)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "backupPollingInterval: negative is invalid",
|
|
input: map[string]interface{}{"backupPollingInterval": float64(-1)},
|
|
expectError: true,
|
|
errorText: "negative",
|
|
},
|
|
{
|
|
name: "backupPollingInterval: below minimum if positive (9)",
|
|
input: map[string]interface{}{"backupPollingInterval": float64(9)},
|
|
expectError: true,
|
|
errorText: "10 seconds",
|
|
},
|
|
{
|
|
name: "backupPollingInterval: above maximum (604801)",
|
|
input: map[string]interface{}{"backupPollingInterval": float64(604801)},
|
|
expectError: true,
|
|
errorText: "604800",
|
|
},
|
|
{
|
|
name: "backupPollingInterval: string instead of number",
|
|
input: map[string]interface{}{"backupPollingInterval": "60"},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
|
|
// =================================================================
|
|
// Boolean field type validation
|
|
// =================================================================
|
|
{
|
|
name: "autoUpdateEnabled: true is valid",
|
|
input: map[string]interface{}{"autoUpdateEnabled": true},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "autoUpdateEnabled: false is valid",
|
|
input: map[string]interface{}{"autoUpdateEnabled": false},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "autoUpdateEnabled: string instead of bool",
|
|
input: map[string]interface{}{"autoUpdateEnabled": "true"},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "autoUpdateEnabled: number instead of bool",
|
|
input: map[string]interface{}{"autoUpdateEnabled": float64(1)},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "discoveryEnabled: true is valid",
|
|
input: map[string]interface{}{"discoveryEnabled": true},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryEnabled: false is valid",
|
|
input: map[string]interface{}{"discoveryEnabled": false},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryEnabled: string instead of bool",
|
|
input: map[string]interface{}{"discoveryEnabled": "true"},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "allowEmbedding: true is valid",
|
|
input: map[string]interface{}{"allowEmbedding": true},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "allowEmbedding: false is valid",
|
|
input: map[string]interface{}{"allowEmbedding": false},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "allowEmbedding: string instead of bool",
|
|
input: map[string]interface{}{"allowEmbedding": "false"},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "backupPollingEnabled: true is valid",
|
|
input: map[string]interface{}{"backupPollingEnabled": true},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "backupPollingEnabled: false is valid",
|
|
input: map[string]interface{}{"backupPollingEnabled": false},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "backupPollingEnabled: string instead of bool",
|
|
input: map[string]interface{}{"backupPollingEnabled": "true"},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "temperatureMonitoringEnabled: true is valid",
|
|
input: map[string]interface{}{"temperatureMonitoringEnabled": true},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "temperatureMonitoringEnabled: false is valid",
|
|
input: map[string]interface{}{"temperatureMonitoringEnabled": false},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "temperatureMonitoringEnabled: string instead of bool",
|
|
input: map[string]interface{}{"temperatureMonitoringEnabled": "true"},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
|
|
// =================================================================
|
|
// autoUpdateCheckInterval validation
|
|
// NOTE: Like backupPollingInterval, this allows 0 to disable.
|
|
// =================================================================
|
|
{
|
|
name: "autoUpdateCheckInterval: zero is valid (disables check)",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": float64(0)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "autoUpdateCheckInterval: valid minimum positive (1)",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": float64(1)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "autoUpdateCheckInterval: valid maximum (168)",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": float64(168)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "autoUpdateCheckInterval: negative is invalid",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": float64(-1)},
|
|
expectError: true,
|
|
errorText: "negative",
|
|
},
|
|
{
|
|
name: "autoUpdateCheckInterval: below minimum if positive (0.5)",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": float64(0.5)},
|
|
expectError: true,
|
|
errorText: "1 hour",
|
|
},
|
|
{
|
|
name: "autoUpdateCheckInterval: above maximum (169)",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": float64(169)},
|
|
expectError: true,
|
|
errorText: "168",
|
|
},
|
|
{
|
|
name: "autoUpdateCheckInterval: string instead of number",
|
|
input: map[string]interface{}{"autoUpdateCheckInterval": "24"},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig validation - field name variants (camelCase and snake_case)
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig: camelCase field name is valid",
|
|
input: map[string]interface{}{"discoveryConfig": map[string]interface{}{"maxHostsPerScan": float64(100)}},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig: snake_case field name is valid",
|
|
input: map[string]interface{}{"discovery_config": map[string]interface{}{"max_hosts_per_scan": float64(100)}},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig: nil/null is invalid (must be object)",
|
|
input: map[string]interface{}{"discoveryConfig": nil},
|
|
expectError: true,
|
|
errorText: "object",
|
|
},
|
|
{
|
|
name: "discoveryConfig: string instead of object",
|
|
input: map[string]interface{}{"discoveryConfig": "invalid"},
|
|
expectError: true,
|
|
errorText: "object",
|
|
},
|
|
{
|
|
name: "discoveryConfig: array instead of object",
|
|
input: map[string]interface{}{"discoveryConfig": []interface{}{}},
|
|
expectError: true,
|
|
errorText: "object",
|
|
},
|
|
{
|
|
name: "discoveryConfig: empty object is valid",
|
|
input: map[string]interface{}{"discoveryConfig": map[string]interface{}{}},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.environment_override validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.environment_override: empty string is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": ""},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: 'auto' is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "auto"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: 'native' is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "native"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: 'docker_host' is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "docker_host"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: 'docker_bridge' is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "docker_bridge"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: 'lxc_privileged' is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "lxc_privileged"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: 'lxc_unprivileged' is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "lxc_unprivileged"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: invalid value",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": "invalid_env"},
|
|
},
|
|
expectError: true,
|
|
errorText: "invalid discovery environment",
|
|
},
|
|
{
|
|
name: "discoveryConfig.environment_override: number instead of string",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environment_override": float64(123)},
|
|
},
|
|
expectError: true,
|
|
errorText: "string",
|
|
},
|
|
{
|
|
name: "discoveryConfig.environmentOverride: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"environmentOverride": "auto"},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.subnet_allowlist validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: empty array is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": []interface{}{}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: valid CIDR",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": []interface{}{"192.168.1.0/24"}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: multiple valid CIDRs",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": []interface{}{"192.168.1.0/24", "10.0.0.0/8"}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: invalid CIDR format",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": []interface{}{"192.168.1.0"}},
|
|
},
|
|
expectError: true,
|
|
errorText: "invalid CIDR",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: invalid CIDR in array",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": []interface{}{"192.168.1.0/24", "not-a-cidr"}},
|
|
},
|
|
expectError: true,
|
|
errorText: "invalid CIDR",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: number in array instead of string",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": []interface{}{float64(123)}},
|
|
},
|
|
expectError: true,
|
|
errorText: "string",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_allowlist: not an array",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_allowlist": "192.168.1.0/24"},
|
|
},
|
|
expectError: true,
|
|
errorText: "array",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnetAllowlist: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnetAllowlist": []interface{}{"192.168.1.0/24"}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.subnet_blocklist validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.subnet_blocklist: empty array is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_blocklist": []interface{}{}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_blocklist: valid CIDR",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_blocklist": []interface{}{"169.254.0.0/16"}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_blocklist: multiple valid CIDRs",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_blocklist": []interface{}{"169.254.0.0/16", "127.0.0.0/8"}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_blocklist: invalid CIDR format",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_blocklist": []interface{}{"169.254.0.0"}},
|
|
},
|
|
expectError: true,
|
|
errorText: "invalid CIDR",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_blocklist: number in array instead of string",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_blocklist": []interface{}{float64(456)}},
|
|
},
|
|
expectError: true,
|
|
errorText: "string",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnet_blocklist: not an array",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnet_blocklist": "169.254.0.0/16"},
|
|
},
|
|
expectError: true,
|
|
errorText: "array",
|
|
},
|
|
{
|
|
name: "discoveryConfig.subnetBlocklist: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"subnetBlocklist": []interface{}{"169.254.0.0/16"}},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.max_hosts_per_scan validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.max_hosts_per_scan: valid positive value",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_hosts_per_scan": float64(100)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_hosts_per_scan: large value",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_hosts_per_scan": float64(10000)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_hosts_per_scan: zero is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_hosts_per_scan": float64(0)},
|
|
},
|
|
expectError: true,
|
|
errorText: "greater than zero",
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_hosts_per_scan: negative is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_hosts_per_scan": float64(-1)},
|
|
},
|
|
expectError: true,
|
|
errorText: "greater than zero",
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_hosts_per_scan: string instead of number",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_hosts_per_scan": "100"},
|
|
},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
{
|
|
name: "discoveryConfig.maxHostsPerScan: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"maxHostsPerScan": float64(100)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.max_concurrent validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.max_concurrent: valid minimum (1)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": float64(1)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_concurrent: valid maximum (1000)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": float64(1000)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_concurrent: valid middle value (50)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": float64(50)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_concurrent: zero is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": float64(0)},
|
|
},
|
|
expectError: true,
|
|
errorText: "between 1 and 1000",
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_concurrent: negative is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": float64(-1)},
|
|
},
|
|
expectError: true,
|
|
errorText: "between 1 and 1000",
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_concurrent: above maximum (1001)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": float64(1001)},
|
|
},
|
|
expectError: true,
|
|
errorText: "between 1 and 1000",
|
|
},
|
|
{
|
|
name: "discoveryConfig.max_concurrent: string instead of number",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"max_concurrent": "50"},
|
|
},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
{
|
|
name: "discoveryConfig.maxConcurrent: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"maxConcurrent": float64(50)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.enable_reverse_dns validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.enable_reverse_dns: true is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"enable_reverse_dns": true},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.enable_reverse_dns: false is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"enable_reverse_dns": false},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.enable_reverse_dns: string instead of bool",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"enable_reverse_dns": "true"},
|
|
},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "discoveryConfig.enableReverseDns: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"enableReverseDns": true},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.scan_gateways validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.scan_gateways: true is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"scan_gateways": true},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.scan_gateways: false is valid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"scan_gateways": false},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.scan_gateways: string instead of bool",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"scan_gateways": "true"},
|
|
},
|
|
expectError: true,
|
|
errorText: "boolean",
|
|
},
|
|
{
|
|
name: "discoveryConfig.scanGateways: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"scanGateways": true},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.dial_timeout_ms validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.dial_timeout_ms: valid positive value (1000)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"dial_timeout_ms": float64(1000)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.dial_timeout_ms: minimum positive (1)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"dial_timeout_ms": float64(1)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.dial_timeout_ms: zero is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"dial_timeout_ms": float64(0)},
|
|
},
|
|
expectError: true,
|
|
errorText: "greater than zero",
|
|
},
|
|
{
|
|
name: "discoveryConfig.dial_timeout_ms: negative is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"dial_timeout_ms": float64(-1)},
|
|
},
|
|
expectError: true,
|
|
errorText: "greater than zero",
|
|
},
|
|
{
|
|
name: "discoveryConfig.dial_timeout_ms: string instead of number",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"dial_timeout_ms": "1000"},
|
|
},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
{
|
|
name: "discoveryConfig.dialTimeoutMs: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"dialTimeoutMs": float64(1000)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// discoveryConfig.http_timeout_ms validation
|
|
// =================================================================
|
|
{
|
|
name: "discoveryConfig.http_timeout_ms: valid positive value (2000)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"http_timeout_ms": float64(2000)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.http_timeout_ms: minimum positive (1)",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"http_timeout_ms": float64(1)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig.http_timeout_ms: zero is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"http_timeout_ms": float64(0)},
|
|
},
|
|
expectError: true,
|
|
errorText: "greater than zero",
|
|
},
|
|
{
|
|
name: "discoveryConfig.http_timeout_ms: negative is invalid",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"http_timeout_ms": float64(-1)},
|
|
},
|
|
expectError: true,
|
|
errorText: "greater than zero",
|
|
},
|
|
{
|
|
name: "discoveryConfig.http_timeout_ms: string instead of number",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"http_timeout_ms": "2000"},
|
|
},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
{
|
|
name: "discoveryConfig.httpTimeoutMs: camelCase variant works",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{"httpTimeoutMs": float64(2000)},
|
|
},
|
|
expectError: false,
|
|
},
|
|
|
|
// =================================================================
|
|
// connectionTimeout validation
|
|
// =================================================================
|
|
{
|
|
name: "connectionTimeout: zero is valid (disables timeout)",
|
|
input: map[string]interface{}{"connectionTimeout": float64(0)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "connectionTimeout: valid minimum positive (1)",
|
|
input: map[string]interface{}{"connectionTimeout": float64(1)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "connectionTimeout: valid maximum (300)",
|
|
input: map[string]interface{}{"connectionTimeout": float64(300)},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "connectionTimeout: negative is invalid",
|
|
input: map[string]interface{}{"connectionTimeout": float64(-1)},
|
|
expectError: true,
|
|
errorText: "negative",
|
|
},
|
|
{
|
|
name: "connectionTimeout: below minimum if positive (0.5)",
|
|
input: map[string]interface{}{"connectionTimeout": float64(0.5)},
|
|
expectError: true,
|
|
errorText: "1 second",
|
|
},
|
|
{
|
|
name: "connectionTimeout: above maximum (301)",
|
|
input: map[string]interface{}{"connectionTimeout": float64(301)},
|
|
expectError: true,
|
|
errorText: "300",
|
|
},
|
|
{
|
|
name: "connectionTimeout: string instead of number",
|
|
input: map[string]interface{}{"connectionTimeout": "30"},
|
|
expectError: true,
|
|
errorText: "number",
|
|
},
|
|
|
|
// =================================================================
|
|
// theme validation
|
|
// =================================================================
|
|
{
|
|
name: "theme: empty string is valid",
|
|
input: map[string]interface{}{"theme": ""},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "theme: 'light' is valid",
|
|
input: map[string]interface{}{"theme": "light"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "theme: 'dark' is valid",
|
|
input: map[string]interface{}{"theme": "dark"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "theme: invalid value",
|
|
input: map[string]interface{}{"theme": "purple"},
|
|
expectError: true,
|
|
errorText: "light", // error message contains valid options
|
|
},
|
|
{
|
|
name: "theme: number instead of string",
|
|
input: map[string]interface{}{"theme": float64(1)},
|
|
expectError: true,
|
|
errorText: "string",
|
|
},
|
|
{
|
|
name: "theme: boolean instead of string",
|
|
input: map[string]interface{}{"theme": true},
|
|
expectError: true,
|
|
errorText: "string",
|
|
},
|
|
|
|
// =================================================================
|
|
// updateChannel validation
|
|
// =================================================================
|
|
{
|
|
name: "updateChannel: empty string is valid",
|
|
input: map[string]interface{}{"updateChannel": ""},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "updateChannel: 'stable' is valid",
|
|
input: map[string]interface{}{"updateChannel": "stable"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "updateChannel: 'rc' is valid",
|
|
input: map[string]interface{}{"updateChannel": "rc"},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "updateChannel: invalid value",
|
|
input: map[string]interface{}{"updateChannel": "beta"},
|
|
expectError: true,
|
|
errorText: "stable", // error message contains valid options
|
|
},
|
|
{
|
|
name: "updateChannel: number instead of string",
|
|
input: map[string]interface{}{"updateChannel": float64(1)},
|
|
expectError: true,
|
|
errorText: "string",
|
|
},
|
|
|
|
// =================================================================
|
|
// Complex/edge case scenarios
|
|
// =================================================================
|
|
{
|
|
name: "multiple valid fields together",
|
|
input: map[string]interface{}{
|
|
"pvePollingInterval": float64(60),
|
|
"pbsPollingInterval": float64(120),
|
|
"autoUpdateEnabled": true,
|
|
"theme": "dark",
|
|
"connectionTimeout": float64(30),
|
|
"backupPollingInterval": float64(3600),
|
|
"backupPollingEnabled": true,
|
|
"autoUpdateCheckInterval": float64(24),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "complex discoveryConfig with multiple valid fields",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{
|
|
"environment_override": "docker_host",
|
|
"subnet_allowlist": []interface{}{"192.168.1.0/24", "10.0.0.0/8"},
|
|
"subnet_blocklist": []interface{}{"169.254.0.0/16"},
|
|
"max_hosts_per_scan": float64(500),
|
|
"max_concurrent": float64(100),
|
|
"enable_reverse_dns": true,
|
|
"scan_gateways": false,
|
|
"dial_timeout_ms": float64(1500),
|
|
"http_timeout_ms": float64(3000),
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "discoveryConfig: mixing camelCase and snake_case fields",
|
|
input: map[string]interface{}{
|
|
"discoveryConfig": map[string]interface{}{
|
|
"environment_override": "native",
|
|
"maxHostsPerScan": float64(1024),
|
|
"subnet_allowlist": []interface{}{"192.168.0.0/16"},
|
|
"enableReverseDns": true,
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "no fields provided (empty request)",
|
|
input: map[string]interface{}{},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateSystemSettings(&config.SystemSettings{}, tt.input)
|
|
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error but got nil")
|
|
} else if tt.errorText != "" && !strings.Contains(err.Error(), tt.errorText) {
|
|
t.Errorf("expected error containing %q, got: %v", tt.errorText, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("expected no error but got: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateSystemSettings_DiscoveryConfigMissing tests the behavior when
|
|
// discoveryConfig is completely missing from the request (not provided at all).
|
|
func TestValidateSystemSettings_DiscoveryConfigMissing(t *testing.T) {
|
|
input := map[string]interface{}{
|
|
"pvePollingInterval": float64(60),
|
|
// discoveryConfig is not provided at all
|
|
}
|
|
|
|
err := validateSystemSettings(&config.SystemSettings{}, input)
|
|
if err != nil {
|
|
t.Errorf("expected no error when discoveryConfig is missing, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestValidateSystemSettings_BoundaryConditions tests exact boundary conditions
|
|
// to ensure off-by-one errors are caught.
|
|
func TestValidateSystemSettings_BoundaryConditions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input map[string]interface{}
|
|
expectError bool
|
|
}{
|
|
// PVE interval boundaries
|
|
{name: "pvePollingInterval: 9.999", input: map[string]interface{}{"pvePollingInterval": 9.999}, expectError: true},
|
|
{name: "pvePollingInterval: 10.001", input: map[string]interface{}{"pvePollingInterval": 10.001}, expectError: false},
|
|
{name: "pvePollingInterval: 3599.999", input: map[string]interface{}{"pvePollingInterval": 3599.999}, expectError: false},
|
|
{name: "pvePollingInterval: 3600.001", input: map[string]interface{}{"pvePollingInterval": 3600.001}, expectError: true},
|
|
|
|
// Backup interval boundaries
|
|
{name: "backupPollingInterval: -0.001", input: map[string]interface{}{"backupPollingInterval": -0.001}, expectError: true},
|
|
{name: "backupPollingInterval: 0.001", input: map[string]interface{}{"backupPollingInterval": 0.001}, expectError: true},
|
|
{name: "backupPollingInterval: 9.999", input: map[string]interface{}{"backupPollingInterval": 9.999}, expectError: true},
|
|
{name: "backupPollingInterval: 10.001", input: map[string]interface{}{"backupPollingInterval": 10.001}, expectError: false},
|
|
{name: "backupPollingInterval: 604799.999", input: map[string]interface{}{"backupPollingInterval": 604799.999}, expectError: false},
|
|
{name: "backupPollingInterval: 604800.001", input: map[string]interface{}{"backupPollingInterval": 604800.001}, expectError: true},
|
|
|
|
// Auto-update check interval boundaries
|
|
{name: "autoUpdateCheckInterval: 0.999", input: map[string]interface{}{"autoUpdateCheckInterval": 0.999}, expectError: true},
|
|
{name: "autoUpdateCheckInterval: 1.001", input: map[string]interface{}{"autoUpdateCheckInterval": 1.001}, expectError: false},
|
|
{name: "autoUpdateCheckInterval: 167.999", input: map[string]interface{}{"autoUpdateCheckInterval": 167.999}, expectError: false},
|
|
{name: "autoUpdateCheckInterval: 168.001", input: map[string]interface{}{"autoUpdateCheckInterval": 168.001}, expectError: true},
|
|
|
|
// Connection timeout boundaries
|
|
{name: "connectionTimeout: 0.999", input: map[string]interface{}{"connectionTimeout": 0.999}, expectError: true},
|
|
{name: "connectionTimeout: 1.001", input: map[string]interface{}{"connectionTimeout": 1.001}, expectError: false},
|
|
{name: "connectionTimeout: 299.999", input: map[string]interface{}{"connectionTimeout": 299.999}, expectError: false},
|
|
{name: "connectionTimeout: 300.001", input: map[string]interface{}{"connectionTimeout": 300.001}, expectError: true},
|
|
|
|
// Max concurrent boundaries - fractional values are rejected since it's a goroutine count
|
|
{
|
|
name: "max_concurrent: 0.999 fractional rejected",
|
|
input: map[string]interface{}{"discoveryConfig": map[string]interface{}{"max_concurrent": 0.999}},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "max_concurrent: 1000.001 fractional rejected",
|
|
input: map[string]interface{}{"discoveryConfig": map[string]interface{}{"max_concurrent": 1000.001}},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "max_concurrent: 5.5 fractional rejected",
|
|
input: map[string]interface{}{"discoveryConfig": map[string]interface{}{"max_concurrent": 5.5}},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateSystemSettings(&config.SystemSettings{}, tt.input)
|
|
if tt.expectError && err == nil {
|
|
t.Error("expected error but got nil")
|
|
} else if !tt.expectError && err != nil {
|
|
t.Errorf("expected no error but got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|