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}