Files
DDI/agent/core/config_test.go
T
beug.k@beugsiud.deandClaude Opus 5.5 3065175673 feat(agent): enrollment, server connection and apply pipeline
- CLI: enroll, run, status, version, help (core.Main with a role
  factory); the agent binaries use a placeholder factory until the
  BIND and Kea roles are wired in.
- Enrollment: local ECDSA P-256 key and CSR, TLS 1.3 to the agent API
  with the server chain pinned to the CA fingerprint.
- ServerSource: mTLS gRPC with keepalive, Connect stream (Hello,
  Welcome, heartbeats, apply and renew commands), reconnect with
  exponential backoff and jitter, GetDesiredState polling as fallback,
  certificate renewal after half of the lifetime.
- Apply pipeline: fetch, bundle verification with replay protection,
  store, Validate, Activate, Health with retries, rollback to the last
  good revision; outcomes succeeded/rejected/rolled_back/failed are
  queued in state.json and reported; failed revisions are retried only
  by a newer revision or an explicit push.
- Lease reporting with change detection, persistent sequence and full
  snapshots at start, per connection and periodically.
- Tests with fake roles, a fake source and an in-process fake
  AgentService over real mTLS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:06:05 +02:00

129 lines
3.9 KiB
Go

// SPDX-License-Identifier: Apache-2.0
package core
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestParseConfigDefaults(t *testing.T) {
for _, data := range []string{"", " \n", "# only a comment\n"} {
cfg, err := ParseConfig([]byte(data), KindDNS)
if err != nil {
t.Fatalf("%q: %v", data, err)
}
if cfg.StateDir != DefaultStateDir(KindDNS) || cfg.ApplyTimeout != DefaultApplyTimeout ||
cfg.HealthTimeout != 30*time.Second || cfg.LeaseInterval != 30*time.Second || cfg.LeaseFullSync != time.Hour {
t.Errorf("defaults = %+v", cfg)
}
var role struct{ X int }
role.X = 7
if err := cfg.DecodeRole(&role); err != nil || role.X != 7 {
t.Errorf("absent role section: %v, %+v", err, role)
}
}
if !strings.HasSuffix(filepath.ToSlash(DefaultConfigPath(KindDHCP)), "/agent-dhcp.yaml") {
t.Errorf("DefaultConfigPath = %s", DefaultConfigPath(KindDHCP))
}
}
func TestParseConfigFull(t *testing.T) {
data := `
state_dir: /tmp/agent
log:
level: debug
format: text
apply_timeout: 2m
health_timeout: 45s
lease_interval: 10s
lease_full_sync: 30m
dhcp:
config_file: /etc/kea/kea-dhcp4.conf
control_socket: /run/kea/ctrl.sock
`
cfg, err := ParseConfig([]byte(data), KindDHCP)
if err != nil {
t.Fatal(err)
}
if cfg.StateDir != "/tmp/agent" || cfg.Log.Level != "debug" || cfg.Log.Format != "text" ||
cfg.ApplyTimeout != 2*time.Minute || cfg.HealthTimeout != 45*time.Second ||
cfg.LeaseInterval != 10*time.Second || cfg.LeaseFullSync != 30*time.Minute {
t.Errorf("config = %+v", cfg)
}
var role struct {
ConfigFile string `yaml:"config_file"`
ControlSocket string `yaml:"control_socket"`
}
if err := cfg.DecodeRole(&role); err != nil {
t.Fatal(err)
}
if role.ConfigFile != "/etc/kea/kea-dhcp4.conf" || role.ControlSocket != "/run/kea/ctrl.sock" {
t.Errorf("role = %+v", role)
}
// Strict decoding of the role section.
var narrow struct {
ConfigFile string `yaml:"config_file"`
}
if err := cfg.DecodeRole(&narrow); err == nil {
t.Error("unknown role key accepted")
}
}
func TestParseConfigErrors(t *testing.T) {
cases := map[string]string{
"unknown key": "flavor: blue\n",
"other role": "dhcp:\n config_file: x\n",
"short timeout": "health_timeout: 10ms\n",
"full sync": "lease_interval: 2m\nlease_full_sync: 1m\n",
"bad level": "log:\n level: loud\n",
"bad duration": "apply_timeout: soon\n",
"not a mapping": "- a\n- b\n",
"unknown log key": "log:\n flavor: x\n",
"negative timeout": "apply_timeout: -5s\n",
}
for name, data := range cases {
if _, err := ParseConfig([]byte(data), KindDNS); err == nil {
t.Errorf("%s: accepted", name)
}
}
if _, err := ParseConfig(nil, Kind("ntp")); err == nil {
t.Error("unknown role accepted")
}
if _, err := ParseConfig([]byte("dns:\n zone_dir: x\n"), KindDHCP); err == nil {
t.Error("dns section accepted by the DHCP agent")
}
// A null section is fine.
if _, err := ParseConfig([]byte("dhcp:\n"), KindDNS); err != nil {
t.Errorf("null section: %v", err)
}
}
func TestLoadConfig(t *testing.T) {
dir := t.TempDir()
cfg, err := LoadConfig(filepath.Join(dir, "missing.yaml"), KindDNS)
if err != nil || cfg.StateDir != DefaultStateDir(KindDNS) {
t.Fatalf("missing file: %+v, %v", cfg, err)
}
path := filepath.Join(dir, "agent-dns.yaml")
if err := os.WriteFile(path, []byte("state_dir: "+filepath.ToSlash(dir)+"\n"), 0o600); err != nil {
t.Fatal(err)
}
cfg, err = LoadConfig(path, KindDNS)
if err != nil || cfg.StateDir != filepath.ToSlash(dir) {
t.Fatalf("file: %+v, %v", cfg, err)
}
if err := os.WriteFile(path, []byte("bogus: 1\n"), 0o600); err != nil {
t.Fatal(err)
}
if _, err := LoadConfig(path, KindDNS); err == nil || !strings.Contains(err.Error(), path) {
t.Errorf("error does not name the file: %v", err)
}
if _, err := LoadConfig(dir, KindDNS); err == nil {
t.Error("directory accepted as configuration file")
}
}