Files
Pulse/internal/api/system_settings_validate_test.go
2025-11-29 21:47:22 +00:00

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)
}
})
}
}