test: add unit tests for config generation and form values
Cover the deploy-critical pure logic: - GenerateConfigTOML: round-trip decode of values + a guard asserting the config port is always containerAppPort (host port must not leak) - GenerateWireguardConfig: MTU emitted/commented, PROTO default UDP - WriteConfigFile / WriteWireguardConfigFile: numeric validation rejects bad input and does not write a file - FormStep.Values(): text/password/select resolution and select default fallback Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// fullConfigValues returns a ConfigValues with every field populated with valid data.
|
||||
func fullConfigValues() ConfigValues {
|
||||
return ConfigValues{
|
||||
Login: map[string]string{"user": "u", "password": "p"},
|
||||
Server: map[string]string{
|
||||
"port": "9090",
|
||||
"timeout": "45",
|
||||
"environment": "production",
|
||||
"seccomp_unconfined": "Não",
|
||||
},
|
||||
Database: map[string]string{
|
||||
"database_type": "postgres",
|
||||
"database_url": "postgres://user:pass@db:5432/app_dono_db",
|
||||
"max_conns": "20",
|
||||
"min_conns": "5",
|
||||
},
|
||||
Cert: map[string]string{"cert_dir_path": "/etc/app-dono/certs"},
|
||||
Application: map[string]string{
|
||||
"central_server_url": "https://central.example.com:8443",
|
||||
"enrollment_token": "tok-123",
|
||||
},
|
||||
Wireguard: map[string]string{
|
||||
"privkey": "PRIVKEY_VALUE",
|
||||
"vip": "127.0.0.1",
|
||||
"psk": "PSK_VALUE",
|
||||
"proxy_edps": "22:127.0.0.1:22",
|
||||
"mtu": "1380",
|
||||
"proto": "UDP",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateConfigTOML_RoundTrip(t *testing.T) {
|
||||
cv := fullConfigValues()
|
||||
|
||||
out, err := GenerateConfigTOML(cv)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateConfigTOML returned error: %v", err)
|
||||
}
|
||||
|
||||
var cfg AppConfig
|
||||
if _, err := toml.Decode(out, &cfg); err != nil {
|
||||
t.Fatalf("generated output is not valid TOML / failed to decode: %v\n---\n%s", err, out)
|
||||
}
|
||||
|
||||
if cfg.Server.Timeout != 45 {
|
||||
t.Errorf("timeout_seconds = %d, want 45", cfg.Server.Timeout)
|
||||
}
|
||||
if cfg.Server.Environment != "production" {
|
||||
t.Errorf("environment = %q, want %q", cfg.Server.Environment, "production")
|
||||
}
|
||||
if cfg.Database.Type != "postgres" {
|
||||
t.Errorf("database type = %q, want %q", cfg.Database.Type, "postgres")
|
||||
}
|
||||
if cfg.Database.URL != "postgres://user:pass@db:5432/app_dono_db" {
|
||||
t.Errorf("database url = %q", cfg.Database.URL)
|
||||
}
|
||||
if cfg.Database.MaxConns != 20 {
|
||||
t.Errorf("max_conns = %d, want 20", cfg.Database.MaxConns)
|
||||
}
|
||||
if cfg.Database.MinConns != 5 {
|
||||
t.Errorf("min_conns = %d, want 5", cfg.Database.MinConns)
|
||||
}
|
||||
if cfg.Certificates.DirPath != "/etc/app-dono/certs" {
|
||||
t.Errorf("mapped_dir = %q", cfg.Certificates.DirPath)
|
||||
}
|
||||
if cfg.Application.CentralServerURL != "https://central.example.com:8443" {
|
||||
t.Errorf("central_server_url = %q", cfg.Application.CentralServerURL)
|
||||
}
|
||||
if cfg.Application.EnrollmentToken != "tok-123" {
|
||||
t.Errorf("enrollment_token = %q", cfg.Application.EnrollmentToken)
|
||||
}
|
||||
}
|
||||
|
||||
// The container always listens on containerAppPort internally; the user-provided
|
||||
// "Porta (host)" value is only the host-side mapping and must never leak into the
|
||||
// generated config.toml. This guard fails loudly if that invariant is reverted.
|
||||
func TestGenerateConfigTOML_PortIsAlwaysContainerPort(t *testing.T) {
|
||||
cv := fullConfigValues()
|
||||
cv.Server["port"] = "9090" // host port, must NOT appear as the config port
|
||||
|
||||
out, err := GenerateConfigTOML(cv)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateConfigTOML returned error: %v", err)
|
||||
}
|
||||
|
||||
var cfg AppConfig
|
||||
if _, err := toml.Decode(out, &cfg); err != nil {
|
||||
t.Fatalf("failed to decode: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Server.Port != containerAppPort {
|
||||
t.Errorf("config port = %d, want fixed containerAppPort %d", cfg.Server.Port, containerAppPort)
|
||||
}
|
||||
if strings.Contains(out, "port = 9090") {
|
||||
t.Errorf("host port 9090 leaked into config.toml:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWireguardConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wireguard map[string]string
|
||||
wantContains []string
|
||||
wantNotContns []string
|
||||
}{
|
||||
{
|
||||
name: "all fields set",
|
||||
wireguard: map[string]string{
|
||||
"privkey": "PK", "vip": "10.0.0.1", "psk": "PSK",
|
||||
"proxy_edps": "22:127.0.0.1:22", "mtu": "1400", "proto": "TCP",
|
||||
},
|
||||
wantContains: []string{
|
||||
"PRIVKEY=PK", "VIP=10.0.0.1", "PSK=PSK",
|
||||
"PROXY_EDPS=22:127.0.0.1:22", "MTU=1400", "PROTO=TCP",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty mtu is commented out",
|
||||
wireguard: map[string]string{
|
||||
"privkey": "PK", "vip": "10.0.0.1", "psk": "PSK",
|
||||
"proxy_edps": "22:127.0.0.1:22", "mtu": "", "proto": "UDP",
|
||||
},
|
||||
wantContains: []string{"# MTU=1380", "PROTO=UDP"},
|
||||
wantNotContns: []string{"\nMTU="},
|
||||
},
|
||||
{
|
||||
name: "empty proto defaults to UDP",
|
||||
wireguard: map[string]string{
|
||||
"privkey": "PK", "vip": "10.0.0.1", "psk": "PSK",
|
||||
"proxy_edps": "22:127.0.0.1:22", "proto": "",
|
||||
},
|
||||
wantContains: []string{"PROTO=UDP"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
out, err := GenerateWireguardConfig(ConfigValues{Wireguard: tt.wireguard})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateWireguardConfig returned error: %v", err)
|
||||
}
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("output missing %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
for _, notWant := range tt.wantNotContns {
|
||||
if strings.Contains(out, notWant) {
|
||||
t.Errorf("output should not contain %q:\n%s", notWant, out)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteConfigFile_NumericValidation(t *testing.T) {
|
||||
numeric := []string{"port", "timeout", "max_conns", "min_conns"}
|
||||
|
||||
for _, field := range numeric {
|
||||
t.Run("invalid_"+field, func(t *testing.T) {
|
||||
cv := fullConfigValues()
|
||||
switch field {
|
||||
case "port", "timeout":
|
||||
cv.Server[field] = "abc"
|
||||
case "max_conns", "min_conns":
|
||||
cv.Database[field] = "abc"
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
err := WriteConfigFile(cv, path)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for non-numeric %q, got nil", field)
|
||||
}
|
||||
if !strings.Contains(err.Error(), field) {
|
||||
t.Errorf("error %q does not mention field %q", err.Error(), field)
|
||||
}
|
||||
if _, statErr := os.Stat(path); statErr == nil {
|
||||
t.Errorf("file should not have been written on validation failure")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteConfigFile_ValidWritesFile(t *testing.T) {
|
||||
cv := fullConfigValues()
|
||||
path := filepath.Join(t.TempDir(), "config.toml")
|
||||
|
||||
if err := WriteConfigFile(cv, path); err != nil {
|
||||
t.Fatalf("WriteConfigFile returned error: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("config file was not written: %v", err)
|
||||
}
|
||||
|
||||
var cfg AppConfig
|
||||
if _, err := toml.Decode(string(data), &cfg); err != nil {
|
||||
t.Fatalf("written file is not valid TOML: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteWireguardConfigFile_MTUValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mtu string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "valid mtu", mtu: "1380", wantErr: false},
|
||||
{name: "empty mtu allowed", mtu: "", wantErr: false},
|
||||
{name: "non-numeric mtu rejected", mtu: "big", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cv := fullConfigValues()
|
||||
cv.Wireguard["mtu"] = tt.mtu
|
||||
|
||||
path := filepath.Join(t.TempDir(), "envs")
|
||||
err := WriteWireguardConfigFile(cv, path)
|
||||
|
||||
if tt.wantErr && err == nil {
|
||||
t.Fatalf("expected error for mtu %q, got nil", tt.mtu)
|
||||
}
|
||||
if !tt.wantErr && err != nil {
|
||||
t.Fatalf("unexpected error for mtu %q: %v", tt.mtu, err)
|
||||
}
|
||||
if !tt.wantErr {
|
||||
if _, statErr := os.Stat(path); statErr != nil {
|
||||
t.Errorf("envs file should have been written: %v", statErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tui
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFormStepValues(t *testing.T) {
|
||||
f := NewFormStep("Test", []FormField{
|
||||
{
|
||||
Id: "name",
|
||||
Label: "Name",
|
||||
Default: "john",
|
||||
Type: FieldTypeText,
|
||||
},
|
||||
{
|
||||
Id: "secret",
|
||||
Label: "Secret",
|
||||
Default: "hunter2",
|
||||
Type: FieldTypePassword,
|
||||
},
|
||||
{
|
||||
Id: "proto",
|
||||
Label: "Protocol",
|
||||
Default: "TCP",
|
||||
Type: FieldTypeSelect,
|
||||
Options: []string{"UDP", "TCP"},
|
||||
},
|
||||
{
|
||||
Id: "mode",
|
||||
Label: "Mode",
|
||||
Type: FieldTypeSelect,
|
||||
Options: []string{"a", "b", "c"},
|
||||
},
|
||||
})
|
||||
|
||||
values := f.Values()
|
||||
|
||||
want := map[string]string{
|
||||
"name": "john", // text default
|
||||
"secret": "hunter2",
|
||||
"proto": "TCP", // select default resolves to the matching option
|
||||
"mode": "a", // select with no default falls back to first option
|
||||
}
|
||||
|
||||
for id, exp := range want {
|
||||
if got := values[id]; got != exp {
|
||||
t.Errorf("Values()[%q] = %q, want %q", id, got, exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormStepValues_SelectInvalidDefaultFallsBackToFirst(t *testing.T) {
|
||||
f := NewFormStep("Test", []FormField{
|
||||
{
|
||||
Id: "proto",
|
||||
Label: "Protocol",
|
||||
Default: "NOPE", // not among options
|
||||
Type: FieldTypeSelect,
|
||||
Options: []string{"UDP", "TCP"},
|
||||
},
|
||||
})
|
||||
|
||||
if got := f.Values()["proto"]; got != "UDP" {
|
||||
t.Errorf("Values()[proto] = %q, want first option %q", got, "UDP")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user