Files
headscale/hscontrol/scope/scope_property_test.go
Kristoffer Dalby ee95cf58d9 hscontrol: add the OAuth client and access-token model
Add the OAuth client type, its database storage, the scope grant package,
policy tag-ownership exposure, and the state operations backing the v2
OAuth client-credentials flow.
2026-06-26 09:18:06 +02:00

91 lines
2.6 KiB
Go

package scope
import (
"slices"
"testing"
"pgregory.net/rapid"
)
// scopeGen draws a scope: mostly from the real vocabulary, sometimes adversarial
// junk (including read-like junk such as "foo:read") so the rules are exercised
// against unknown input too.
func scopeGen() *rapid.Generator[Scope] {
known := Known()
return rapid.Custom(func(t *rapid.T) Scope {
if rapid.Float64().Draw(t, "junkP") < 0.2 {
return Scope(rapid.StringMatching(`[a-z_]{1,12}(:read)?`).Draw(t, "junk"))
}
return rapid.SampledFrom(known).Draw(t, "vocab")
})
}
// TestGrantsMatchesOracle fuzzes Grants against the independent oracle over random
// granted-sets and want-scopes (vocabulary + junk).
func TestGrantsMatchesOracle(t *testing.T) {
rapid.Check(t, func(rt *rapid.T) {
granted := rapid.SliceOfN(scopeGen(), 0, 6).Draw(rt, "granted")
want := scopeGen().Draw(rt, "want")
if got, exp := Grants(granted, want), oracleGrants(granted, want); got != exp {
rt.Fatalf("Grants(%v, %q) = %v, oracle = %v", granted, want, got, exp)
}
})
}
// TestGrantsInvariants asserts the algebraic properties of the grant relation hold
// for arbitrary inputs.
func TestGrantsInvariants(t *testing.T) {
rapid.Check(t, func(rt *rapid.T) {
granted := rapid.SliceOfN(scopeGen(), 0, 6).Draw(rt, "granted")
want := scopeGen().Draw(rt, "want")
// Reflexivity: a scope always grants itself.
if !Grants([]Scope{want}, want) {
rt.Fatalf("reflexivity: %q does not grant itself", want)
}
// The empty set grants nothing.
if Grants(nil, want) {
rt.Fatalf("empty grant satisfied %q", want)
}
// OR-semantics: a set grants iff some member does.
anyMember := slices.ContainsFunc(granted, func(g Scope) bool {
return Grants([]Scope{g}, want)
})
if Grants(granted, want) != anyMember {
rt.Fatalf("OR-semantics broken for %v / %q", granted, want)
}
// Monotonicity: adding a scope never withdraws a grant.
before := Grants(granted, want)
extra := scopeGen().Draw(rt, "extra")
after := Grants(append(slices.Clone(granted), extra), want)
if before && !after {
rt.Fatalf("monotonicity broken: adding %q withdrew the grant of %q", extra, want)
}
})
}
// TestSuperScopeProperties fuzzes the super-scope rules.
func TestSuperScopeProperties(t *testing.T) {
rapid.Check(t, func(rt *rapid.T) {
want := scopeGen().Draw(rt, "want")
// all grants everything.
if !Grants([]Scope{All}, want) {
rt.Fatalf("all did not grant %q", want)
}
// all:read grants exactly the read scopes.
if Grants([]Scope{AllRead}, want) != want.IsRead() {
rt.Fatalf("all:read grant of %q = %v, want IsRead = %v",
want, Grants([]Scope{AllRead}, want), want.IsRead())
}
})
}