Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

125 lines
4.2 KiB
Go

package contracts
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
// TestSpringConfigValueBindingRelaxed proves the Spring config-key graph:
// application.yml leaf keys become value-redacted config-key nodes, a camelCase
// @Value reference binds to a kebab-case property key through relaxed
// canonicalization, and an @ConfigurationProperties(prefix) read fans out to
// every key under the prefix — all on the existing reads_config surface so
// "which beans read this property" is one traversal. Values are never stored.
func TestSpringConfigValueBindingRelaxed(t *testing.T) {
const ymlPath = "src/main/resources/application.yml"
yml := "app:\n" +
" my-prop: super-secret-token\n" +
" nested:\n" +
" timeout: 30\n" +
"db:\n" +
" url: jdbc:postgres://localhost\n" +
" pool-size: 10\n"
g := graph.New()
g.AddNode(&graph.Node{ID: ymlPath, Kind: graph.KindFile, Name: ymlPath, FilePath: ymlPath})
// A @Value bean reading the property in camelCase (`${app.myProp}`).
g.AddNode(&graph.Node{
ID: "AppConfig.java::AppConfig", Kind: graph.KindType, Name: "AppConfig",
FilePath: "AppConfig.java", StartLine: 5,
Meta: map[string]any{"spring_config_keys": []string{"app.myProp"}},
})
// A @ConfigurationProperties(prefix = "db") bean — prefix fanout.
g.AddNode(&graph.Node{
ID: "DbConfig.java::DbConfig", Kind: graph.KindType, Name: "DbConfig",
FilePath: "DbConfig.java", StartLine: 3,
Meta: map[string]any{"spring_config_keys": []string{"db.*"}},
})
srcFor := func(p string) []byte {
if p == ymlPath {
return []byte(yml)
}
return nil
}
if n := BindSpringConfig(g, srcFor); n == 0 {
t.Fatal("BindSpringConfig added nothing")
}
// Config-key nodes exist, canonicalized, value-redacted.
key := g.GetNode("cfg::spring::app.myprop")
if key == nil {
t.Fatal("missing config-key node cfg::spring::app.myprop")
}
if key.Kind != graph.KindConfigKey {
t.Errorf("app.my-prop node kind=%v, want config_key", key.Kind)
}
if r, _ := key.Meta["value_redacted"].(bool); !r {
t.Error("config-key node is not marked value_redacted")
}
if key.Name == "super-secret-token" || key.ID == "super-secret-token" {
t.Error("a config value leaked into the graph")
}
if key.Name != "app.my-prop" {
t.Errorf("config-key Name=%q, want the raw key app.my-prop", key.Name)
}
// Collect reads_config edges by (from, to).
type rc struct{ from, to string }
reads := map[rc]bool{}
for _, e := range g.AllEdges() {
if e.Kind == graph.EdgeReadsConfig {
reads[rc{e.From, e.To}] = true
}
}
// Relaxed binding: camelCase @Value `app.myProp` binds to kebab key app.my-prop.
if !reads[rc{"AppConfig.java::AppConfig", "cfg::spring::app.myprop"}] {
t.Error("relaxed @Value binding missing: AppConfig should read cfg::spring::app.myprop")
}
// Prefix fanout: @ConfigurationProperties(prefix=db) reads every db.* key.
for _, want := range []string{"cfg::spring::db.url", "cfg::spring::db.poolsize"} {
if !reads[rc{"DbConfig.java::DbConfig", want}] {
t.Errorf("prefix fanout missing: DbConfig should read %s", want)
}
}
}
// TestSpringConfigFileDetection covers the application(-profile)? file matcher
// and the .properties key parse path.
func TestSpringConfigFileDetection(t *testing.T) {
cases := []struct {
path string
wantOK bool
wantProfile string
}{
{"application.yml", true, ""},
{"src/main/resources/application.yaml", true, ""},
{`src\main\resources\application.yml`, true, ""},
{"application-prod.properties", true, "prod"},
{"config/application-staging.yml", true, "staging"},
{"other.yml", false, ""},
{"app/application.go", false, ""},
}
for _, c := range cases {
profile, ok := springConfigFile(c.path)
if ok != c.wantOK || profile != c.wantProfile {
t.Errorf("springConfigFile(%q) = (%q,%v), want (%q,%v)", c.path, profile, ok, c.wantProfile, c.wantOK)
}
}
keys := parsePropertiesKeys([]byte("# comment\napp.name=svc\nserver.port = 8080\n\n!bang=ignored\n"))
got := map[string]bool{}
for _, k := range keys {
got[k] = true
}
if !got["app.name"] || !got["server.port"] {
t.Errorf("parsePropertiesKeys missing keys, got %v", keys)
}
if got["!bang"] {
t.Error("parsePropertiesKeys did not skip a !-comment")
}
}