Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion internal/config/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,37 @@ func (e EnvironmentConfig) Build(logger *zap.Logger) (*datagen.Environment, erro
DomainAdminsCount: e.Counts.DomainAdmins,
Logger: logger,
}
return datagen.GenerateEnvironment(seeds, opts)

env, err := genEnvironment(seeds, opts)
if err != nil {
return nil, err
}

// Gate the resolved environment on the network CIDR contract: an invalid
// CIDR fails the whole load rather than silently defaulting (PIPE-1002).
// Generated networks are always valid today; this is the enforcement point
// for when user-supplied CIDRs flow through.
if err := validateNetworks(env.Networks); err != nil {
return nil, fmt.Errorf("environment: %w", err)
}
return env, nil
}

// genEnvironment is the environment-composition seam. Build calls through it so
// tests can inject an environment (e.g. one carrying an invalid network CIDR)
// and assert Build's validation behavior; production always uses the real
// datagen.GenerateEnvironment.
var genEnvironment = datagen.GenerateEnvironment

// validateNetworks rejects any network whose CIDR fails datagen's blitz-network
// contract (see datagen.ValidateCIDR): unparseable, non-IPv4, or a prefix
// longer than /29. The first failure fails the entire environment load — the
// contract is "refuse to start", not "fall back to a default".
func validateNetworks(networks []*datagen.NetworkIdentity) error {
for _, n := range networks {
if err := n.Validate(); err != nil {
return err
}
}
return nil
}
71 changes: 71 additions & 0 deletions internal/config/environment_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,84 @@
package config

import (
"errors"
"strings"
"testing"

"github.com/observiq/blitz/internal/datagen"
"go.uber.org/zap"
)

func i64(v int64) *int64 { return &v }

// TestValidateNetworks covers the config-load CIDR gate directly: valid IPv4
// networks pass, while a network whose CIDR is unparseable, non-IPv4, or has a
// prefix longer than /29 fails and the error names the offending CIDR.
func TestValidateNetworks(t *testing.T) {
valid := []*datagen.NetworkIdentity{
{ID: "n1", CIDR: "10.10.1.0/24"},
{ID: "n2", CIDR: "192.168.0.0/29"},
{ID: "n3", CIDR: "10.0.0.0/8"},
}
if err := validateNetworks(valid); err != nil {
t.Errorf("validateNetworks(valid) = %v, want nil", err)
}

for _, bad := range []string{"10.10.1.0/30", "10.10.1.0/31", "10.10.1.0/32", "not-a-cidr", "2001:db8::/64"} {
nets := []*datagen.NetworkIdentity{
{ID: "ok", CIDR: "10.10.1.0/24"},
{ID: "bad", CIDR: bad},
}
err := validateNetworks(nets)
if err == nil {
t.Errorf("validateNetworks with CIDR %q = nil, want error", bad)
continue
}
if !strings.Contains(err.Error(), bad) {
t.Errorf("error for CIDR %q = %q, want it to name the offending CIDR", bad, err.Error())
}
}
}

// TestEnvironmentConfig_Build_RejectsInvalidNetworkCIDR confirms the gate is
// wired into the config-load path: when the environment resolves a network with
// an invalid CIDR, Build fails (rather than silently defaulting) and surfaces
// the offending CIDR. The genEnvironment seam injects the invalid network,
// since normal generation only ever produces valid CIDRs.
func TestEnvironmentConfig_Build_RejectsInvalidNetworkCIDR(t *testing.T) {
orig := genEnvironment
defer func() { genEnvironment = orig }()
genEnvironment = func(_ *datagen.SeedConfig, _ *datagen.EnvironmentOpts) (*datagen.Environment, error) {
return &datagen.Environment{
Networks: []*datagen.NetworkIdentity{{ID: "bad", Name: "Bad", CIDR: "10.10.1.0/30"}},
}, nil
}

_, err := EnvironmentConfig{}.Build(zap.NewNop())
if err == nil {
t.Fatal("Build with an invalid network CIDR should fail, got nil")
}
if !strings.Contains(err.Error(), "10.10.1.0/30") {
t.Errorf("Build error = %q, want it to name the offending CIDR", err.Error())
}
}

// TestEnvironmentConfig_Build_PropagatesGenerateError confirms Build forwards an
// environment-composition error unchanged (before it reaches the network gate).
func TestEnvironmentConfig_Build_PropagatesGenerateError(t *testing.T) {
orig := genEnvironment
defer func() { genEnvironment = orig }()
sentinel := errors.New("boom")
genEnvironment = func(_ *datagen.SeedConfig, _ *datagen.EnvironmentOpts) (*datagen.Environment, error) {
return nil, sentinel
}

_, err := EnvironmentConfig{}.Build(zap.NewNop())
if !errors.Is(err, sentinel) {
t.Errorf("Build error = %v, want the sentinel generation error", err)
}
}

func TestEnvironmentConfig_Build_Counts(t *testing.T) {
cfg := EnvironmentConfig{
SeedConfig: EnvironmentSeedConfig{Shared: i64(42)},
Expand Down
Loading