github.com/gemaraproj/gemara@v1.3.0

test/schema_test.go raw

  1// SPDX-License-Identifier: Apache-2.0
  2
  3package schema_test
  4
  5import (
  6	"os"
  7	"path/filepath"
  8	"strings"
  9	"testing"
 10
 11	"cuelang.org/go/cue"
 12	"cuelang.org/go/cue/cuecontext"
 13	"cuelang.org/go/cue/load"
 14	cuejson "cuelang.org/go/encoding/json"
 15	cueyaml "cuelang.org/go/encoding/yaml"
 16)
 17
 18var schemaValue cue.Value
 19var schemaCtx *cue.Context
 20
 21func TestMain(m *testing.M) {
 22	schemaCtx = cuecontext.New()
 23	ctx := schemaCtx
 24
 25	schemaDir, err := filepath.Abs("..")
 26	if err != nil {
 27		panic("failed to resolve schema directory: " + err.Error())
 28	}
 29
 30	cfg := &load.Config{
 31		Dir: schemaDir,
 32	}
 33	instances := load.Instances([]string{"."}, cfg)
 34	if len(instances) != 1 {
 35		panic("expected exactly one CUE instance")
 36	}
 37
 38	schemaValue = ctx.BuildInstance(instances[0])
 39	if schemaValue.Err() != nil {
 40		panic("failed to build CUE schema: " + schemaValue.Err().Error())
 41	}
 42
 43	os.Exit(m.Run())
 44}
 45
 46func TestSchemaValidation(t *testing.T) {
 47	tests := []struct {
 48		name        string
 49		file        string
 50		definition  string
 51		wantErr     bool
 52		errContains string
 53	}{
 54		// ControlCatalog — positive
 55		{"valid control catalog YAML", "./test-data/good-ccc.yaml", "#ControlCatalog", false, ""},
 56		{"valid control catalog JSON", "./test-data/good-ccc.json", "#ControlCatalog", false, ""},
 57		{"valid OSPS baseline", "./test-data/good-osps.yml", "#ControlCatalog", false, ""},
 58		{"valid lifecycle catalog", "./test-data/good-lifecycle.yaml", "#ControlCatalog", false, ""},
 59		{"valid nested control catalog", "./test-data/nested-good-ccc.yaml", "#ControlCatalog", false, ""},
 60
 61		// GuidanceCatalog — positive
 62		{"valid AI governance framework", "./test-data/good-aigf.yaml", "#GuidanceCatalog", false, ""},
 63		// PrinciplesCatalog — positive
 64		{"valid AIGF principles catalog", "./test-data/good-aigf-principles.yaml", "#PrincipleCatalog", false, ""},
 65
 66		// VectorCatalog — positive
 67		{"valid AIGF vector catalog", "./test-data/good-aigf-vectors.yaml", "#VectorCatalog", false, ""},
 68		{"threats with vectors", "./test-data/good-threat-catalog.yaml", "#ThreatCatalog", false, ""},
 69		{"valid capability catalog", "./test-data/good-capability-catalog.yaml", "#CapabilityCatalog", false, ""},
 70		{"vector mapping", "./test-data/good-vector-owasp-mapping.yaml", "#MappingDocument", false, ""},
 71
 72		// RiskCatalog — positive
 73		{"valid risk catalog", "./test-data/good-risk-catalog.yaml", "#RiskCatalog", false, ""},
 74
 75		// RiskCatalog — negative
 76		{"risk catalog with duplicate rank", "./test-data/bad-risk-catalog-duplicate-rank.yaml", "#RiskCatalog", true, ""},
 77
 78		// Policy — positive
 79		{"valid policy", "./test-data/good-policy.yaml", "#Policy", false, ""},
 80		{"valid security policy", "./test-data/good-security-policy.yml", "#Policy", false, ""},
 81
 82		// ControlCatalog — negative
 83		{"invalid YAML", "./test-data/bad.yaml", "#ControlCatalog", true, ""},
 84		{"invalid JSON", "./test-data/bad.json", "#ControlCatalog", true, ""},
 85		{"controls without groups", "./test-data/bad-no-groups.yaml", "#ControlCatalog", true, ""},
 86
 87		// MappingDocument — positive
 88		{"valid mapping document", "./test-data/good-mapping-document.yaml", "#MappingDocument", false, ""},
 89		{"valid AIGF NIST 800-53 mapping", "./test-data/good-aigf-nist-mapping.yaml", "#MappingDocument", false, ""},
 90
 91		// MappingDocument — negative
 92		{"invalid mapping document without mapping-references", "./test-data/bad-mapping-document.yaml", "#MappingDocument", true, ""},
 93		{"mapping missing targets for non-no-match relationship", "./test-data/bad-mapping-no-target.yaml", "#MappingDocument", true, ""},
 94
 95		// Lexicon — positive
 96		{"valid lexicon", "./test-data/good-lexicon.yaml", "#Lexicon", false, ""},
 97
 98		// Lexicon — negative
 99		{"lexicon with duplicate term ids", "./test-data/bad-lexicon-duplicate-term-id.yaml", "#Lexicon", true, ""},
100
101		// GuidanceCatalog — negative
102		{"retired guideline with recommendations", "./test-data/bad-lifecycle.yaml", "#GuidanceCatalog", true, ""},
103
104		// EvaluationLog — positive
105		{"valid PVTR baseline scan", "./test-data/pvtr-baseline-scan.yaml", "#EvaluationLog", false, ""},
106
107		// EnforcementLog — positive
108		{"valid enforcement log", "./test-data/good-enforcement-log.yaml", "#EnforcementLog", false, ""},
109
110		// EnforcementLog — negative
111		{"enforcement action with invalid disposition", "./test-data/bad-enforcement-log.yaml", "#EnforcementLog", true, ""},
112		{"enforcement action missing log reference", "./test-data/bad-enforcement-missing-log.yaml", "#EnforcementLog", true, ""},
113		{"clear disposition with failed assessment", "./test-data/bad-enforcement-clear-failed.yaml", "#EnforcementLog", true, ""},
114
115		// AuditLog — positive
116		{"valid audit log", "./test-data/good-audit-log.yaml", "#AuditLog", false, ""},
117
118		// AuditLog — negative
119		{"audit log missing summary criteria and results", "./test-data/bad-audit-log.yaml", "#AuditLog", true, ""},
120
121		// CapabilityCatalog — negative
122		{"capability with invalid group", "./test-data/bad-capability-invalid-group.yaml", "#CapabilityCatalog", true, ""},
123
124		// ThreatCatalog — negative
125		{"threat with invalid group", "./test-data/bad-threat-invalid-group.yaml", "#ThreatCatalog", true, ""},
126
127		// PrincipleCatalog — negative
128		{"principle with invalid group", "./test-data/bad-principle-invalid-group.yaml", "#PrincipleCatalog", true, ""},
129
130		// ControlCatalog — negative (group validation)
131		{"control with invalid group", "./test-data/bad-control-invalid-group.yaml", "#ControlCatalog", true, ""},
132
133		// ControlCatalog — edge cases
134		{"empty nested catalog", "./test-data/nested-empty.yaml", "#ControlCatalog", false, ""},
135	}
136
137	for _, tt := range tests {
138		t.Run(tt.name, func(t *testing.T) {
139			data, err := os.ReadFile(tt.file)
140			if err != nil {
141				t.Fatalf("read %s: %v", tt.file, err)
142			}
143
144			def := schemaValue.LookupPath(cue.ParsePath(tt.definition))
145			if def.Err() != nil {
146				t.Fatalf("lookup %s: %v", tt.definition, def.Err())
147			}
148
149			var validationErr error
150			switch {
151			case strings.HasSuffix(tt.file, ".json"):
152				validationErr = cuejson.Validate(data, def)
153			case strings.HasSuffix(tt.file, ".yaml"), strings.HasSuffix(tt.file, ".yml"):
154				validationErr = cueyaml.Validate(data, def)
155			default:
156				t.Fatalf("unsupported file extension: %s", tt.file)
157			}
158
159			if tt.wantErr && validationErr == nil {
160				t.Error("expected validation error, got nil")
161			}
162			if !tt.wantErr && validationErr != nil {
163				t.Errorf("unexpected validation error: %v", validationErr)
164			}
165			if tt.errContains != "" && validationErr != nil {
166				if !strings.Contains(validationErr.Error(), tt.errContains) {
167					t.Errorf("error %q does not contain %q", validationErr.Error(), tt.errContains)
168				}
169			}
170		})
171	}
172}