Files
mcp/internal/tools/env_tools_test.go
T

211 lines
6.9 KiB
Go
Raw Normal View History

package tools
import (
"path/filepath"
"strings"
"testing"
"code.nochebuena.dev/einherjar/mcp/internal/index"
)
// loadRealIndex builds the index from the sibling framework checkout at test
// time, so the env tools are tested against the framework's actual struct tags.
// It deliberately does NOT read data/index.json: that file is only an empty
// placeholder in the repo (the real index is regenerated by cmd/indexer at
// image-build time). When the sibling modules aren't present — e.g. a CI that
// checks out only this module — the test skips rather than fails.
func loadRealIndex(t *testing.T) *index.Index {
t.Helper()
idx, err := index.Build(filepath.Join("..", "..", ".."))
if err != nil || idx == nil || len(idx.Modules) == 0 {
t.Skip("framework checkout not available; skipping index-derived test")
}
return idx
}
func TestRenderEnvExample(t *testing.T) {
idx := loadRealIndex(t)
got := renderEnvExample(idx, "myapp")
// Required, no default → uncommented with a runnable dev value.
mustContain := []string{
"APP_ENV=local",
// CORS now lives on server.Config (composed) → documented, commented, no default.
"# EINHERJAR_SERVER_CORS_ORIGINS=",
"EINHERJAR_PG_HOST=localhost",
"EINHERJAR_PG_USER=postgres",
"EINHERJAR_PG_PASSWORD=postgres",
"EINHERJAR_PG_NAME=myapp",
// launcher + health composed → their (defaulted) vars are documented, commented.
"# EINHERJAR_COMPONENT_STOP_TIMEOUT=15s",
"# EINHERJAR_HEALTH_CHECK_TIMEOUT=5s",
// logz is composed → its vars (both defaulted) are documented, commented.
"# EINHERJAR_LOG_LEVEL=INFO",
"# EINHERJAR_LOG_JSON=false",
// Has a framework default → commented line documenting it.
"# EINHERJAR_SERVER_HOST=0.0.0.0",
"# EINHERJAR_SERVER_PORT=8080",
"# EINHERJAR_PG_PORT=5432",
"# EINHERJAR_PG_SSL_MODE=disable",
}
for _, s := range mustContain {
if !strings.Contains(got, s) {
t.Errorf(".env.example missing line: %q\n---\n%s", s, got)
}
}
// The historical drift names must never appear.
mustNotContain := []string{
"EINHERJAR_PG_DATABASE",
"EINHERJAR_SERVER_ADDR",
// APP_CORS_ORIGINS was an invented var — the scaffold now uses the real
// framework var (EINHERJAR_SERVER_CORS_ORIGINS on server.Config), so no
// form of APP_CORS_ORIGINS may ever be emitted.
"APP_CORS_ORIGINS",
}
for _, s := range mustNotContain {
if strings.Contains(got, s) {
t.Errorf(".env.example must not contain %q\n---\n%s", s, got)
}
}
// A required var must never be silently emitted as a commented line.
for _, line := range strings.Split(got, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "# EINHERJAR_PG_NAME") {
t.Errorf("required EINHERJAR_PG_NAME emitted commented: %q", line)
}
}
}
func TestCheckEnvUnknownVar(t *testing.T) {
idx := loadRealIndex(t)
env := "EINHERJAR_PG_DATABASE=x\nEINHERJAR_SERVER_ADDR=:8080\nAPP_ENV=local\n"
findings, err := checkEnvVars(idx, env, nil, nil)
if err != nil {
t.Fatal(err)
}
unknown := map[string]bool{}
for _, f := range findings {
if f.Kind == "unknown-var" {
unknown[f.Var] = true
}
}
for _, want := range []string{"EINHERJAR_PG_DATABASE", "EINHERJAR_SERVER_ADDR"} {
if !unknown[want] {
t.Errorf("expected unknown-var finding for %s; got %+v", want, findings)
}
}
// APP_* must never be flagged.
for _, f := range findings {
if f.Var == "APP_ENV" {
t.Errorf("APP_ENV should never be flagged: %+v", f)
}
}
}
func TestCheckEnvMissingRequired(t *testing.T) {
idx := loadRealIndex(t)
// Compose db-postgres but set none of its required vars.
findings, err := checkEnvVars(idx, "APP_ENV=local\n", []string{"db-postgres"}, nil)
if err != nil {
t.Fatal(err)
}
missing := map[string]bool{}
for _, f := range findings {
if f.Kind == "missing-required" {
missing[f.Var] = true
}
}
for _, want := range []string{"EINHERJAR_PG_HOST", "EINHERJAR_PG_USER", "EINHERJAR_PG_PASSWORD", "EINHERJAR_PG_NAME"} {
if !missing[want] {
t.Errorf("expected missing-required for %s; got %+v", want, findings)
}
}
}
func TestCheckEnvNotComposed(t *testing.T) {
idx := loadRealIndex(t)
// A real MinIO var while only composing db-postgres → dead (not-composed).
env := "EINHERJAR_MINIO_ENDPOINT=x\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n"
findings, err := checkEnvVars(idx, env, []string{"db-postgres"}, nil)
if err != nil {
t.Fatal(err)
}
var got bool
for _, f := range findings {
if f.Kind == "not-composed" && f.Var == "EINHERJAR_MINIO_ENDPOINT" {
got = true
}
}
if !got {
t.Errorf("expected not-composed for EINHERJAR_MINIO_ENDPOINT; got %+v", findings)
}
}
func TestCheckEnvClean(t *testing.T) {
idx := loadRealIndex(t)
// The scaffold's own output, checked against the modules it composes, is clean.
env := renderEnvExample(idx, "myapp")
findings, err := checkEnvVars(idx, env, []string{"core", "web", "db-postgres"}, nil)
if err != nil {
t.Fatal(err)
}
for _, f := range findings {
if f.Severity == "error" {
t.Errorf("scaffold .env.example should have no errors, got %+v", f)
}
}
}
func TestCheckEnvUnknownModule(t *testing.T) {
idx := loadRealIndex(t)
if _, err := checkEnvVars(idx, "", []string{"nope"}, nil); err == nil {
t.Error("expected error for unknown module")
}
}
// TestCheckEnvStructLevel proves the struct-granularity path catches a dead var
// that module granularity cannot: EINHERJAR_HEALTH_CHECK_TIMEOUT lives on
// web/health.Config, so an app composing web/server/Config (not web/health/Config)
// never reads it — module "web" would hide that, struct selectors surface it.
func TestCheckEnvStructLevel(t *testing.T) {
idx := loadRealIndex(t)
env := "EINHERJAR_HEALTH_CHECK_TIMEOUT=5s\nEINHERJAR_PG_HOST=h\nEINHERJAR_PG_USER=u\nEINHERJAR_PG_PASSWORD=p\nEINHERJAR_PG_NAME=n\n"
// struct-level: server/Config has no health timeout → the var is flagged as dead.
structF, err := checkEnvVars(idx, env, nil, []string{"web/server/Config", "db-postgres/Config"})
if err != nil {
t.Fatal(err)
}
dead := false
for _, f := range structF {
if f.Kind == "not-composed" && f.Var == "EINHERJAR_HEALTH_CHECK_TIMEOUT" {
dead = true
}
}
if !dead {
t.Errorf("struct-level: expected EINHERJAR_HEALTH_CHECK_TIMEOUT flagged not-composed; got %+v", structF)
}
// module-level ["web"] keeps it silent (health lives in module web).
modF, err := checkEnvVars(idx, env, []string{"web", "db-postgres"}, nil)
if err != nil {
t.Fatal(err)
}
for _, f := range modF {
if f.Var == "EINHERJAR_HEALTH_CHECK_TIMEOUT" {
t.Errorf("module-level should not flag the health timeout (it lives in module web), got %+v", f)
}
}
}
func TestCheckEnvBadSelector(t *testing.T) {
idx := loadRealIndex(t)
if _, err := checkEnvVars(idx, "", nil, []string{"web"}); err == nil {
t.Error("expected error for a one-segment selector")
}
if _, err := checkEnvVars(idx, "", nil, []string{"web/server/Nope"}); err == nil {
t.Error("expected error for a non-existent struct selector")
}
}