maidn-cli/internal/config/config_test.go

287 lines
13 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
func validConfig(t *testing.T) Config {
t.Helper()
return Config{
ClusterID: "test-cluster",
WorkspaceDir: t.TempDir(),
Git: GitConfig{Provider: "forgejo", BaseURL: "https://git.example.test", Username: "bot", Token: "token", Owner: "test-org"},
Flux: FluxConfig{RepoName: "cluster", Branch: "main", ClusterPath: "./clusters/test", ClusterDomain: "example.test", ManifestsRepo: "manifests"},
Templates: TemplateConfig{TalosRepoURL: "https://git.example.test/talos.git", TalosRepoRef: "main", CICDRepoURL: "https://git.example.test/template.git", CICDRepoRef: "main", ManifestsRepoURL: "https://git.example.test/manifests.git", ManifestsRepoRef: "main"},
Cilium: CiliumConfig{TrafficInterface: "eth1", LoadBalancerStart: "192.168.45.19", LoadBalancerEnd: "192.168.45.30"},
DemocraticCSI: DemocraticCSIConfig{TrueNASAPIKey: "api-key", TrueNASHost: "truenas.example.test", TargetPortal: "truenas.example.test:3260", ShareHost: "truenas.example.test", DatasetParentNFS: "pool/kubernetes/nfs/v", DatasetSnapshotsNFS: "pool/kubernetes/nfs/s", AllowedNetworks: "192.168.45.0/24", NameSuffix: "-test", PortalGroup: "1", InitiatorGroup: "1"},
Delivery: DeliveryConfig{AppName: "web-ui", AppRepoURL: "https://git.example.test/test-org/web-ui.git", AppRepoRef: "main", ProductionBranch: "production", ImageRepository: "registry.example.test/test-org/web-ui", BuildStrategy: "static", BuildOutputDirectory: "dist", BuildConfiguration: "production", WebhookHostname: "tekton.example.test", WebhookPath: "/"},
Talos: TalosConfig{
RepoDirName: "talos", TerraformDir: "terraform", GeneratedDir: "generated", ConfigFileName: "terraform.tfvars",
Proxmox: TalosProxmoxConfig{APIURL: "https://proxmox.example.test:8006", APITokenID: "id", APITokenSecret: "secret"},
Cluster: TalosClusterConfig{Name: "test-cluster", Domain: "example.test"},
Image: TalosImageConfig{TalosVersion: "v1.13.6", KubernetesVersion: "v1.33.4", SchematicID: "abcdefghijkl"},
Nodes: []TalosNode{{Name: "cp-01", VMID: 100, Role: "controlplane", Networks: []TalosNetwork{{IP: "192.168.45.3", CIDR: "192.168.45.0/28", Gateway: "192.168.45.1", VLANID: 45}, {IP: "192.168.45.18", CIDR: "192.168.45.16/28", VLANID: 451}}}},
},
}
}
func TestResolveDeliveryStates(t *testing.T) {
platformOnly := validConfig(t)
platformOnly.Delivery = DeliveryConfig{}
partialLegacy := platformOnly
partialLegacy.Delivery = DeliveryConfig{AppName: "legacy-app", AppRepoURL: "https://git.example.test/test-org/legacy-app.git", ProductionBranch: "production", ImageRepository: "registry.example.test/test-org/legacy-app"}
partialLegacy.Talos.AutoBootstrapFlux = true
for _, test := range []struct {
name string
cfg Config
configured bool
}{
{name: "absent", cfg: platformOnly},
{name: "partial legacy", cfg: partialLegacy},
{name: "complete", cfg: validConfig(t), configured: true},
} {
t.Run(test.name, func(t *testing.T) {
resolved, err := Resolve(test.cfg)
if err != nil {
t.Fatalf("Resolve() error = %v", err)
}
if resolved.Delivery.Configured() != test.configured {
t.Fatalf("Configured() = %t, want %t", resolved.Delivery.Configured(), test.configured)
}
if !test.configured && resolved.Delivery.AppRepoRef != "" {
t.Fatal("Resolve() defaulted delivery configuration treated as absent")
}
})
}
}
func TestResolveDeliveryAppliesDefaultsOnlyForExplicitAppOperations(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.AppRepoRef = ""
cfg.Delivery.BuildOutputDirectory = ""
cfg.Delivery.BuildConfiguration = ""
cfg.Delivery.BuildStrategy = ""
cfg.Delivery.WebhookHostname = ""
cfg.Delivery.WebhookPath = ""
platform, err := Resolve(cfg)
if err != nil {
t.Fatal(err)
}
if platform.Delivery.Configured() {
t.Fatal("platform resolution treated incomplete delivery as configured")
}
delivery, err := ResolveDelivery(platform)
if err != nil {
t.Fatal(err)
}
if !delivery.Delivery.Configured() || delivery.Delivery.BuildStrategy != "static" || delivery.Delivery.AppRepoRef != "main" || delivery.Delivery.BuildOutputDirectory != "dist" || delivery.Delivery.BuildConfiguration != "production" || delivery.Delivery.WebhookURL() != "https://tekton.example.test/" {
t.Fatalf("ResolveDelivery() did not apply the complete delivery contract: %#v", delivery.Delivery)
}
}
func TestValidateDeliveryBuildStrategy(t *testing.T) {
cfg := validConfig(t)
for _, strategy := range []string{"static", "runtime"} {
cfg.Delivery.BuildStrategy = strategy
if err := ValidateDelivery(cfg); err != nil {
t.Fatalf("ValidateDelivery() rejected %q: %v", strategy, err)
}
}
cfg.Delivery.BuildStrategy = "container"
if err := ValidateDelivery(cfg); err == nil || !strings.Contains(err.Error(), "buildStrategy") {
t.Fatalf("ValidateDelivery() accepted invalid build strategy: %v", err)
}
}
func TestValidateDeliveryRequiresCompleteConfig(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.ImageRepository = ""
if err := ValidateDelivery(cfg); err == nil || !strings.Contains(err.Error(), "imageRepository") {
t.Fatalf("ValidateDelivery() error = %v, want incomplete delivery error", err)
}
}
func TestValidateDeliveryRejectsUnsafeTektonDashboardURL(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.TektonDashboardURL = "https://token@example.test"
if err := ValidateDelivery(cfg); err == nil || !strings.Contains(err.Error(), "tektonDashboardUrl") {
t.Fatalf("ValidateDelivery() accepted credential-bearing dashboard URL: %v", err)
}
}
func TestResolveAppOnboardingAllowsOnlyCanonicalCrossOwnerSource(t *testing.T) {
cfg := validConfig(t)
cfg.Git.Owner = "test-org-2"
cfg.Delivery.AppRepoURL = "https://git.example.test/Maidn/maidn-e2e-web.git"
if _, err := ResolveAppOnboarding(cfg); err != nil {
t.Fatalf("ResolveAppOnboarding() rejected canonical source: %v", err)
}
cfg.Delivery.AppRepoURL = "https://git.example.test/test-org-2/web-ui.git"
if resolved, err := ResolveAppOnboarding(cfg); err != nil || resolved.Delivery.WebhookURL() != "https://tekton.example.test/" {
t.Fatalf("ResolveAppOnboarding() = %#v, %v", resolved.Delivery, err)
}
for _, appRepoURL := range []string{
"https://git.example.test/Maidn/web-ui.git",
"https://git.example.test/other-org/maidn-e2e-web.git",
} {
cfg.Delivery.AppRepoURL = appRepoURL
if _, err := ResolveAppOnboarding(cfg); err == nil || !strings.Contains(err.Error(), "canonical Maidn E2E fixture") {
t.Fatalf("ResolveAppOnboarding() accepted noncanonical cross-owner source %q: %v", appRepoURL, err)
}
}
}
func TestResolveAppOnboardingRequiresProductionBranch(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.ProductionBranch = "release"
if _, err := ResolveAppOnboarding(cfg); err == nil || !strings.Contains(err.Error(), "literal production") {
t.Fatalf("ResolveAppOnboarding() accepted nonstandard production branch: %v", err)
}
}
func TestValidateRejectsCredentialBearingDeliveryURLs(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.AppRepoURL = "https://reader:token@git.example.test/test-org/web-ui.git"
if _, err := Resolve(cfg); err == nil || !strings.Contains(err.Error(), "credentials") {
t.Fatalf("Resolve() error = %v, want credential-free app repository error", err)
}
}
func TestValidateSecretGrants(t *testing.T) {
if err := ValidateSecretGrants([]SecretGrant{{Application: "orders-api", Consumer: "runtime", Environment: "production", Secrets: []string{"database"}, Shared: []string{"rabbitmq"}}}); err != nil {
t.Fatal(err)
}
for _, grant := range []SecretGrant{
{Application: "orders-api", Consumer: "runtime", Environment: "preview"},
{Application: "orders-api", Consumer: "build", Environment: "staging"},
{Application: "orders-api", Consumer: "publish"},
{Application: "orders-api", Consumer: "publish", Shared: []string{"../platform"}},
{Application: "orders-api", Consumer: "publish", Secrets: []string{"database", "database"}},
} {
if err := ValidateSecretGrants([]SecretGrant{grant}); err == nil {
t.Fatalf("invalid secret grant accepted: %#v", grant)
}
}
}
func TestValidateRequiresDeliveryRepositoryOnForgejoOrigin(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.AppRepoURL = "https://attacker.example.test/test-org/web-ui.git"
if _, err := Resolve(cfg); err == nil || !strings.Contains(err.Error(), "configured Forgejo origin") {
t.Fatalf("Resolve() error = %v, want Forgejo origin error", err)
}
cfg.Delivery.AppRepoURL = "https://git.example.test/test-org/web-ui.git"
if _, err := Resolve(cfg); err != nil {
t.Fatalf("Resolve() rejected delivery repository on configured Forgejo origin: %v", err)
}
}
func TestValidateRequiresDistinctValidProductionBranch(t *testing.T) {
cfg := validConfig(t)
cfg.Delivery.ProductionBranch = cfg.Delivery.AppRepoRef
if _, err := Resolve(cfg); err == nil || !strings.Contains(err.Error(), "productionBranch") {
t.Fatalf("Resolve() error = %v, want distinct production branch error", err)
}
cfg.Delivery.ProductionBranch = "production/../unsafe"
if _, err := Resolve(cfg); err == nil || !strings.Contains(err.Error(), "productionBranch") {
t.Fatalf("Resolve() error = %v, want valid production branch error", err)
}
}
func TestResolveDefaultsWebhookEndpoint(t *testing.T) {
input := validConfig(t)
input.Delivery.WebhookHostname = ""
input.Delivery.WebhookPath = ""
platform, err := Resolve(input)
if err != nil {
t.Fatal(err)
}
cfg, err := ResolveDelivery(platform)
if err != nil {
t.Fatal(err)
}
if cfg.Delivery.WebhookURL() != "https://tekton.example.test/" {
t.Fatalf("WebhookURL() = %q", cfg.Delivery.WebhookURL())
}
if cfg.Templates.TektonCatalogRepoURL != "https://git.pingu.pw/Maidn/tekton-pipelines.git" || cfg.Templates.TektonCatalogRepoRef != "main" || cfg.Flux.TektonCatalogRepo != "tekton-pipelines" {
t.Fatalf("Tekton catalog defaults = %q@%q", cfg.Templates.TektonCatalogRepoURL, cfg.Templates.TektonCatalogRepoRef)
}
}
func TestResolveFreshBootstrapRequiresAnIsolatedExplicitWorkspace(t *testing.T) {
cfg := validConfig(t)
cfg.Git.Owner = ""
cfg.Git.CloneParent = filepath.Join(cfg.WorkspaceDir, "checkouts")
cfg.Flux.TektonCatalogRepo = "catalog"
cfg.Templates.TektonCatalogRepoURL = "https://git.example.test/templates/catalog.git"
cfg.Templates.TektonCatalogRepoRef = "main"
resolved, err := ResolveFreshBootstrap(cfg, "new-org", false)
if err != nil || resolved.Git.Owner != "new-org" {
t.Fatalf("ResolveFreshBootstrap() = (%#v, %v)", resolved.Git.Owner, err)
}
cfg.Git.CloneParent = cfg.WorkspaceDir
if _, err := ResolveFreshBootstrap(cfg, "new-org", false); err == nil || !strings.Contains(err.Error(), "cloneParent") {
t.Fatalf("ResolveFreshBootstrap() error = %v, want isolated clone parent error", err)
}
}
func TestResolveFreshBootstrapRejectsConflictingOwner(t *testing.T) {
cfg := validConfig(t)
cfg.Git.CloneParent = filepath.Join(cfg.WorkspaceDir, "checkouts")
if _, err := ResolveFreshBootstrap(cfg, "other-org", false); err == nil || !strings.Contains(err.Error(), "owner") {
t.Fatalf("ResolveFreshBootstrap() error = %v, want owner mismatch", err)
}
}
func TestLoadRejectsUnknownFields(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte("workspaceDir: test\nunknown: value\n"), 0600); err != nil {
t.Fatal(err)
}
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "field unknown") {
t.Fatalf("Load() error = %v, want unknown field error", err)
}
}
func TestLoadRawAllowsCompletionBeforeValidation(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(path, []byte("workspaceDir: test\n"), 0600); err != nil {
t.Fatal(err)
}
if _, err := LoadRaw(path); err != nil {
t.Fatalf("LoadRaw() error = %v", err)
}
}
func TestWriteRedactedOmitsCredentials(t *testing.T) {
cfg := validConfig(t)
cfg.Git.BaseURL = "https://reader:git-token@git.example.test"
cfg.Templates.CICDRepoURL = "https://reader:template-token@git.example.test/template.git?access_token=query-token"
cfg.Delivery.ImageRepository = "reader:registry-token@registry.example.test/team/app"
path := filepath.Join(t.TempDir(), "resolved.yaml")
if err := WriteRedacted(path, cfg); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), "token: token") || strings.Contains(string(data), "apiTokenSecret: secret") || strings.Contains(string(data), "truenasApiKey: api-key") || strings.Contains(string(data), "git-token") || strings.Contains(string(data), "template-token") || strings.Contains(string(data), "query-token") || strings.Contains(string(data), "registry-token") {
t.Fatal("redacted config contains credentials")
}
}
func TestValidateRejectsLoadBalancerAddressOnNode(t *testing.T) {
cfg := validConfig(t)
cfg.Talos.Nodes[0].Networks[1].IP = cfg.Cilium.LoadBalancerStart
if _, err := Resolve(cfg); err == nil {
t.Fatal("Resolve() succeeded with traffic node address in LoadBalancer range")
}
}