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
125 lines
4.2 KiB
Go
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")
|
|
}
|
|
}
|