Files
gateway/config_loader_test.go
2026-06-10 14:04:36 -07:00

146 lines
4.8 KiB
Go

// Package gateway — config loader tests.
//
// Validates that the koanf-based config parser used by `gateway run -c`
// accepts the operator-emitted JSON shape (port as int) and surfaces a
// usable error when the GATEWAY_-prefixed env override is not parseable
// as the underlying field type.
//
// Background: the runtime parser is `krakend-koanf` which loads the JSON
// file then merges every `GATEWAY_<KEY>` env var into the same key
// space (callback strips the prefix and lowercases). When the env value
// fails to convert, the error message points at the JSON file path, not
// at the env var, making misconfigurations look like a bug in the
// operator-managed ConfigMap. These tests pin the contract end-to-end so
// any future regression on the JSON-int -> Go-int path is caught at
// build time.
package gateway
import (
"os"
"path/filepath"
"strings"
"testing"
koanf "github.com/krakend/krakend-koanf"
)
// loadConfigPath is shared test setup — writes the given JSON body to a
// temp file under the test dir and returns the path.
func loadConfigPath(t *testing.T, body string) string {
t.Helper()
dir := t.TempDir()
p := filepath.Join(dir, "gateway.json")
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatalf("write temp config: %v", err)
}
return p
}
// minimalConfig is the smallest valid v3 config the parser will accept.
// Mirrors the operator-generated ConfigMap shape (port as JSON int).
const minimalConfig = `{
"version": 3,
"name": "test",
"port": 8080,
"timeout": "30s",
"endpoints": []
}`
// TestPortAsJSONInt — the canonical, no-env case the operator emits.
// Asserts that `"port": 8080` (int literal) round-trips to ServiceConfig
// without the parser tripping the StringToInt hook (which only fires
// when the value arrives as a string from the env provider).
func TestPortAsJSONInt(t *testing.T) {
path := loadConfigPath(t, minimalConfig)
cfg, err := koanf.New().Parse(path)
if err != nil {
t.Fatalf("Parse(%q) failed: %v", path, err)
}
if cfg.Port != 8080 {
t.Fatalf("Port = %d, want 8080", cfg.Port)
}
}
// TestPortFromEnvNumeric — operator deployments may also set
// GATEWAY_PORT=8080 (always a string at the OS layer). The koanf env
// provider strips the GATEWAY_ prefix, lowercases, and merges, so
// `port` ends up as the string "8080". WeaklyTypedInput + StringToInt
// hook must convert this cleanly. Regression guard for future koanf or
// mapstructure upgrades.
func TestPortFromEnvNumeric(t *testing.T) {
path := loadConfigPath(t, minimalConfig)
t.Setenv("GATEWAY_PORT", "9090")
cfg, err := koanf.New().Parse(path)
if err != nil {
t.Fatalf("Parse(%q) with GATEWAY_PORT=9090 failed: %v", path, err)
}
if cfg.Port != 9090 {
t.Fatalf("Port = %d, want 9090 (env override)", cfg.Port)
}
}
// TestPortFromEnvNonNumeric — documents the failure mode operators can
// trip when GATEWAY_PORT is set to a non-numeric string. The error
// message currently mentions the file path (because koanf attaches it
// during Parse) but the underlying mapstructure field is `'port'`. We
// pin both fragments so anyone reading a future error log can track it
// back to the env override, not to a corrupted ConfigMap.
func TestPortFromEnvNonNumeric(t *testing.T) {
path := loadConfigPath(t, minimalConfig)
t.Setenv("GATEWAY_PORT", "not-a-number")
_, err := koanf.New().Parse(path)
if err == nil {
t.Fatalf("Parse(%q) with GATEWAY_PORT=not-a-number unexpectedly succeeded", path)
}
msg := err.Error()
for _, want := range []string{"'port'", "invalid syntax"} {
if !strings.Contains(msg, want) {
t.Fatalf("error message %q missing fragment %q", msg, want)
}
}
}
// TestOperatorGeneratedConfig — exact shape of the ConfigMap the Rust
// operator renders for Gateway resources on devnet/testnet/mainnet.
// Port is an int literal, no GATEWAY_ env vars are projected onto the
// pod. Failure here means a v1.0.x bump broke the contract with the
// operator-generated config.
func TestOperatorGeneratedConfig(t *testing.T) {
body := `{
"$schema": "https://www.krakend.io/schema/v2.7/gateway.json",
"cache_ttl": "0s",
"endpoints": [],
"extra_config": {
"security/cors": {
"allow_headers": ["Content-Type", "Authorization", "X-Org-Id", "X-Request-ID"],
"allow_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
"allow_origins": ["https://exchange.test.example.com"],
"expose_headers": ["Content-Length"],
"max_age": "12h"
}
},
"name": "Gateway - default",
"port": 8080,
"timeout": "30s",
"version": 3
}`
path := loadConfigPath(t, body)
cfg, err := koanf.New().Parse(path)
if err != nil {
t.Fatalf("Parse operator-generated config: %v", err)
}
if cfg.Port != 8080 {
t.Fatalf("Port = %d, want 8080", cfg.Port)
}
if cfg.Version != 3 {
t.Fatalf("Version = %d, want 3", cfg.Version)
}
}