126 lines
4 KiB
Go
126 lines
4 KiB
Go
|
|
package schemaguard
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// repoRoot walks up from the package directory to the module root.
|
||
|
|
func repoRoot(t *testing.T) string {
|
||
|
|
t.Helper()
|
||
|
|
return filepath.Join("..", "..")
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestPublishedExamplesMatchTheirSchemas is the check approval-engine suggested
|
||
|
|
// after the same defect class hit three repositories: a published example that
|
||
|
|
// contradicts the schema it claims gets implemented in preference to the prose.
|
||
|
|
func TestPublishedExamplesMatchTheirSchemas(t *testing.T) {
|
||
|
|
root := repoRoot(t)
|
||
|
|
cases := []struct {
|
||
|
|
schema string
|
||
|
|
example string
|
||
|
|
}{
|
||
|
|
{"schemas/decision_envelope.schema.json", "examples/caring/decision_envelope.json"},
|
||
|
|
{"schemas/decision_envelope.schema.json", "examples/secrets-engine/replay/decision_rotate.json"},
|
||
|
|
{"schemas/decision_envelope.schema.json", "examples/secrets-engine/replay/decision_destroy_dual_control.json"},
|
||
|
|
{"schemas/action_authorization.schema.json", "examples/caring/action_authorization.json"},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.example, func(t *testing.T) {
|
||
|
|
s, err := Load(filepath.Join(root, tc.schema))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("load schema: %v", err)
|
||
|
|
}
|
||
|
|
problems, err := s.ValidateFile(filepath.Join(root, tc.example))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("read example: %v", err)
|
||
|
|
}
|
||
|
|
for _, p := range problems {
|
||
|
|
t.Errorf("%s: %s", tc.example, p)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestEveryAllowExampleStatesItsLifetime pins the §9.7.1 obligation that
|
||
|
|
// FLEX-WP-0019 closed. An allow without a lifetime is the gap G3 named, and an
|
||
|
|
// example missing it teaches consumers the pre-WP-0019 shape.
|
||
|
|
func TestEveryAllowExampleStatesItsLifetime(t *testing.T) {
|
||
|
|
root := repoRoot(t)
|
||
|
|
matches, err := filepath.Glob(filepath.Join(root, "examples", "*", "replay", "*.json"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
extra := filepath.Join(root, "examples", "caring", "decision_envelope.json")
|
||
|
|
if _, err := os.Stat(extra); err == nil {
|
||
|
|
matches = append(matches, extra)
|
||
|
|
}
|
||
|
|
if len(matches) == 0 {
|
||
|
|
t.Fatal("no decision examples found — this test would pass vacuously")
|
||
|
|
}
|
||
|
|
for _, path := range matches {
|
||
|
|
raw, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("%s: %v", path, err)
|
||
|
|
}
|
||
|
|
var doc map[string]any
|
||
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||
|
|
t.Fatalf("%s: %v", path, err)
|
||
|
|
}
|
||
|
|
if doc["effect"] != "allow" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, ok := doc["lifetime"].(map[string]any); !ok {
|
||
|
|
t.Errorf("%s: allow carries no lifetime (security-layer-model v0.7 §9.7.1)", path)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestApprovalClaimsInFixturesAreComplete validates the approval-claims embedded
|
||
|
|
// in this repo's check requests against approval-engine's published schema.
|
||
|
|
//
|
||
|
|
// It is skipped when the sibling repository is not checked out, because a
|
||
|
|
// cross-repo path is not a dependency this module can assert. It is worth
|
||
|
|
// running where it can: the destroy rule was first written against an invented
|
||
|
|
// claim shape, and a partial claim in a fixture is how a consumer learns one.
|
||
|
|
func TestApprovalClaimsInFixturesAreComplete(t *testing.T) {
|
||
|
|
schemaPath := filepath.Join(os.Getenv("HOME"), "approval-engine", "schemas", "approval_claim.schema.json")
|
||
|
|
if _, err := os.Stat(schemaPath); err != nil {
|
||
|
|
t.Skipf("approval-engine not checked out at %s", schemaPath)
|
||
|
|
}
|
||
|
|
s, err := Load(schemaPath)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("load claim schema: %v", err)
|
||
|
|
}
|
||
|
|
root := repoRoot(t)
|
||
|
|
matches, err := filepath.Glob(filepath.Join(root, "examples", "*", "check_request_*.json"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
checked := 0
|
||
|
|
for _, path := range matches {
|
||
|
|
raw, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("%s: %v", path, err)
|
||
|
|
}
|
||
|
|
var doc map[string]any
|
||
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||
|
|
t.Fatalf("%s: %v", path, err)
|
||
|
|
}
|
||
|
|
ctx, _ := doc["context"].(map[string]any)
|
||
|
|
claim, ok := ctx["approval"]
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
checked++
|
||
|
|
for _, p := range s.Validate(claim) {
|
||
|
|
t.Errorf("%s: context.approval: %s", path, p)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if checked == 0 {
|
||
|
|
t.Skip("no check request carries context.approval")
|
||
|
|
}
|
||
|
|
}
|