policy/v2: keep failing tests apart per entry

A refusal can then name each failing test's src.
This commit is contained in:
Kristoffer Dalby committed 2026-10-29 11:04:05 +01:00
1 parent 00f597aa6d
commit a336e1e7e6
2 files changed
+65 -8

No files matched your search

+17 -2
View File
@@ -92,6 +92,21 @@ func (r SSHPolicyTestResults) Errors() string {
return strings.Join(lines, "\n")
}
// failures is the breakdown apart per failing entry, so an API answer can
// name each entry's src.
func (r SSHPolicyTestResults) failures() []TestFailure {
var out []TestFailure
for _, res := range r.Results {
if !res.Passed {
one := SSHPolicyTestResults{Results: []SSHPolicyTestResult{res}}
out = append(out, TestFailure{Src: res.Src, Errors: strings.Split(one.Errors(), "\n")})
}
}
return out
}
func sortedUsers(m map[string][]string) []string {
return slices.Sorted(maps.Keys(m))
}
@@ -128,7 +143,7 @@ func (pm *PolicyManager) RunSSHTests() error {
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
results := runSSHPolicyTests(pm.pol, pm.users, pm.nodes, cache)
return wrapTestResult(errSSHPolicyTestsFailed, results.AllPassed, results.Errors)
return wrapTestResult(errSSHPolicyTestsFailed, results.AllPassed, results)
}
// evaluateSSHTests runs the block against pol without mutating live state.
@@ -144,7 +159,7 @@ func evaluateSSHTests(
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
results := runSSHPolicyTests(pol, users, nodes, cache)
return wrapTestResult(errSSHPolicyTestsFailed, results.AllPassed, results.Errors)
return wrapTestResult(errSSHPolicyTestsFailed, results.AllPassed, results)
}
// runSSHPolicyTests evaluates every sshTests entry. The cache is keyed
+48 -6
View File
@@ -196,15 +196,57 @@ func (r PolicyTestResults) Errors() string {
return strings.Join(lines, "\n")
}
// TestFailure is one failing tests or sshTests entry: its src as written and
// its lines of the failure breakdown.
type TestFailure struct {
Src string
Errors []string
}
// TestsFailedError is a failed tests or sshTests run. It reads as the whole
// breakdown; Failures holds it per entry.
type TestsFailedError struct {
err error
Failures []TestFailure
}
func (e *TestsFailedError) Error() string { return e.err.Error() }
func (e *TestsFailedError) Unwrap() error { return e.err }
// failures is the breakdown apart per failing entry, so an API answer can
// name each entry's src.
func (r PolicyTestResults) failures() []TestFailure {
var out []TestFailure
for _, res := range r.Results {
if !res.Passed {
one := PolicyTestResults{Results: []PolicyTestResult{res}}
out = append(out, TestFailure{Src: res.Src, Errors: strings.Split(one.Errors(), "\n")})
}
}
return out
}
// testReport is the outcome of a tests or sshTests run.
type testReport interface {
Errors() string
failures() []TestFailure
}
// wrapTestResult returns nil when every test passed, otherwise wraps the
// rendered failure breakdown in sentinel. errs is passed uncalled so it is
// only evaluated on the failure path.
func wrapTestResult(sentinel error, allPassed bool, errs func() string) error {
// rendered failure breakdown in sentinel. The report is rendered only on the
// failure path.
func wrapTestResult(sentinel error, allPassed bool, r testReport) error {
if allPassed {
return nil
}
return fmt.Errorf("%w:\n%s", sentinel, errs())
return &TestsFailedError{
err: fmt.Errorf("%w:\n%s", sentinel, r.Errors()),
Failures: r.failures(),
}
}
// RunTests evaluates the policy's own `tests` block against the live compiled
@@ -220,7 +262,7 @@ func (pm *PolicyManager) RunTests() error {
results := runPolicyTests(pm.pol, pm.filter, pm.users, pm.nodes)
return wrapTestResult(errPolicyTestsFailed, results.AllPassed, results.Errors)
return wrapTestResult(errPolicyTestsFailed, results.AllPassed, results)
}
// evaluateTests runs the `tests` block against a fresh compilation of pol.
@@ -242,7 +284,7 @@ func evaluateTests(pol *Policy, users []types.User, nodes views.Slice[types.Node
results := runPolicyTests(pol, filter, users, nodes)
return wrapTestResult(errPolicyTestsFailed, results.AllPassed, results.Errors)
return wrapTestResult(errPolicyTestsFailed, results.AllPassed, results)
}
// runPolicyTests is the pure evaluation function: given a policy, the