Implement the ux-first-run change: a state-derived setup checklist on /operator/setup with a landing region that recedes once required steps are done, and empty states that distinguish blocked from empty across operator and member surfaces. Also add production deployment and environment reference docs, plus a config-key completeness test.
184 lines
6.5 KiB
Go
184 lines
6.5 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// configKeyCallRE matches viper accessor call sites whose first argument is
|
|
// a string literal: viper.GetString("db-dsn"), viper.Set("port", v),
|
|
// viper.SetDefault("env", "development"), viper.IsSet("x"),
|
|
// viper.BindEnv("x"), and so on. It intentionally also matches a few Viper
|
|
// bootstrap calls (SetConfigType, SetConfigName, SetEnvPrefix) that are not
|
|
// application configuration keys at all — those are excluded below via
|
|
// excludedLiterals, one entry per call site, each commented with why.
|
|
//
|
|
// This is a lightweight regex scan, not a Go parser: it does not understand
|
|
// multi-line string concatenation or build tags, and a call site whose key
|
|
// argument is a variable or struct field (e.g. viper.GetString(pair.Name))
|
|
// is invisible to it by design — those keys are declared data-driven
|
|
// (internal/config.ConfigKey) and validated by other means
|
|
// (RequiredKeysUnresolved, ApplyOverlay), not by this literal scan.
|
|
var configKeyCallRE = regexp.MustCompile(`viper\.(?:Get\w*|Set\w*|IsSet|BindEnv|RegisterAlias)\(\s*"([^"]*)"`)
|
|
|
|
// excludedLiterals lists string literals configKeyCallRE matches that are
|
|
// not application configuration keys, with the reason each is not one.
|
|
// Keep this list small — a real new config key belongs in
|
|
// docs/environment-reference.md, not here.
|
|
var excludedLiterals = map[string]string{
|
|
"yaml": "viper.SetConfigType(\"yaml\") in cmd/root.go — the config file format, not a config key",
|
|
"mc-config": "viper.SetConfigName(\"mc-config\") in cmd/root.go — the config file's base name, not a config key",
|
|
"MC": "viper.SetEnvPrefix(\"MC\") in cmd/root.go — the env var prefix itself, not a config key",
|
|
}
|
|
|
|
// repoRootFromCaller locates the repository root from this test file's own
|
|
// path, so the test works regardless of the working directory it runs
|
|
// under (`go test ./...` from the repo root, `go test .` from this
|
|
// package's directory, an IDE runner, etc).
|
|
func repoRootFromCaller(t *testing.T) string {
|
|
t.Helper()
|
|
_, thisFile, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("runtime.Caller(0) failed; cannot locate repo root")
|
|
}
|
|
// This file lives at <repoRoot>/internal/config/config_reference_test.go.
|
|
root := filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", ".."))
|
|
return root
|
|
}
|
|
|
|
// extractConfigKeys walks dir (skipping _test.go files and non-.go files)
|
|
// and returns the sorted, de-duplicated set of string-literal keys found at
|
|
// viper accessor call sites, along with the first file:line each was found
|
|
// at (for failure messages).
|
|
func extractConfigKeys(t *testing.T, dir string) (keys []string, firstSeenAt map[string]string) {
|
|
t.Helper()
|
|
firstSeenAt = make(map[string]string)
|
|
seen := make(map[string]bool)
|
|
|
|
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, line := range strings.Split(string(content), "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
// Skip whole-line comments; this is a line-based scan (the
|
|
// codebase's viper call sites are all single-line), so a
|
|
// commented-out call on its own line never produces a false
|
|
// positive.
|
|
if strings.HasPrefix(trimmed, "//") {
|
|
continue
|
|
}
|
|
for _, m := range configKeyCallRE.FindAllStringSubmatch(line, -1) {
|
|
key := m[1]
|
|
if !seen[key] {
|
|
seen[key] = true
|
|
rel, relErr := filepath.Rel(dir, path)
|
|
if relErr != nil {
|
|
rel = path
|
|
}
|
|
firstSeenAt[key] = rel + ":" + strconv.Itoa(i+1)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walking %s: %v", dir, err)
|
|
}
|
|
|
|
for k := range seen {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys, firstSeenAt
|
|
}
|
|
|
|
// TestEnvironmentReferenceDocumentsEveryConfigKey scans cmd/ and internal/
|
|
// for every string-literal key read or written through Viper and asserts
|
|
// each one (other than the deliberately excluded bootstrap literals) is
|
|
// documented in docs/environment-reference.md. It is a completeness check,
|
|
// not a correctness check: it does not verify the documented purpose,
|
|
// default, or required/optional call is accurate, only that the key is not
|
|
// silently undocumented.
|
|
func TestEnvironmentReferenceDocumentsEveryConfigKey(t *testing.T) {
|
|
root := repoRootFromCaller(t)
|
|
docPath := filepath.Join(root, "docs", "environment-reference.md")
|
|
|
|
docBytes, err := os.ReadFile(docPath)
|
|
if err != nil {
|
|
t.Fatalf("reading %s: %v", docPath, err)
|
|
}
|
|
doc := string(docBytes)
|
|
|
|
var allKeys []string
|
|
allFirstSeen := make(map[string]string)
|
|
for _, sub := range []string{"cmd", "internal"} {
|
|
dir := filepath.Join(root, sub)
|
|
keys, firstSeen := extractConfigKeys(t, dir)
|
|
allKeys = append(allKeys, keys...)
|
|
for k, v := range firstSeen {
|
|
// Keep the first location found across both directories.
|
|
if _, ok := allFirstSeen[k]; !ok {
|
|
allFirstSeen[k] = filepath.Join(sub, v)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(allKeys)
|
|
|
|
var missing []string
|
|
for _, key := range allKeys {
|
|
if _, excluded := excludedLiterals[key]; excluded {
|
|
continue
|
|
}
|
|
if !strings.Contains(doc, key) {
|
|
missing = append(missing, key)
|
|
}
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
var b strings.Builder
|
|
b.WriteString("the following config keys are read via viper but not documented in docs/environment-reference.md:\n")
|
|
for _, key := range missing {
|
|
b.WriteString(" - " + key + " (first seen at " + allFirstSeen[key] + ")\n")
|
|
}
|
|
b.WriteString("add each to docs/environment-reference.md (or, if it is deliberately not an application config key, to excludedLiterals in internal/config/config_reference_test.go with a reason)")
|
|
t.Error(b.String())
|
|
}
|
|
}
|
|
|
|
// TestExcludedLiteralsStillMatchesRealCallSites guards against a stale
|
|
// allowlist entry: if a bootstrap call site is ever rewritten to no longer
|
|
// use one of these literals, the entry should be removed rather than left
|
|
// silently unused.
|
|
func TestExcludedLiteralsStillMatchesRealCallSites(t *testing.T) {
|
|
root := repoRootFromCaller(t)
|
|
var found = make(map[string]bool)
|
|
for _, sub := range []string{"cmd", "internal"} {
|
|
keys, _ := extractConfigKeys(t, filepath.Join(root, sub))
|
|
for _, k := range keys {
|
|
found[k] = true
|
|
}
|
|
}
|
|
for literal := range excludedLiterals {
|
|
if !found[literal] {
|
|
t.Errorf("excludedLiterals entry %q no longer matches any viper call site; remove it from internal/config/config_reference_test.go", literal)
|
|
}
|
|
}
|
|
}
|