211 lines
6.9 KiB
Go
211 lines
6.9 KiB
Go
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")
|
|
}
|
|
}
|