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", "APP_CORS_ORIGINS=*", "EINHERJAR_PG_HOST=localhost", "EINHERJAR_PG_USER=postgres", "EINHERJAR_PG_PASSWORD=postgres", "EINHERJAR_PG_NAME=myapp", // 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", // CORS lives on web.Config, which the scaffold does not compose — so it // must not be emitted (it would be a dead var). "EINHERJAR_SERVER_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) 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"}) 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"}) 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"}) 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"}); err == nil { t.Error("expected error for unknown module") } }