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
19
20func TestMain(m *testing.M) {
21 ctx := cuecontext.New()
22
23 schemaDir, err := filepath.Abs("..")
24 if err != nil {
25 panic("failed to resolve schema directory: " + err.Error())
26 }
27
28 cfg := &load.Config{
29 Dir: schemaDir,
30 }
31 instances := load.Instances([]string{"."}, cfg)
32 if len(instances) != 1 {
33 panic("expected exactly one CUE instance")
34 }
35
36 schemaValue = ctx.BuildInstance(instances[0])
37 if schemaValue.Err() != nil {
38 panic("failed to build CUE schema: " + schemaValue.Err().Error())
39 }
40
41 os.Exit(m.Run())
42}
43
44func TestSchemaValidation(t *testing.T) {
45 tests := []struct {
46 name string
47 file string
48 definition string
49 wantErr bool
50 errContains string
51 }{
52 // ControlCatalog — positive
53 {"valid control catalog YAML", "./test-data/good-ccc.yaml", "#ControlCatalog", false, ""},
54 {"valid control catalog JSON", "./test-data/good-ccc.json", "#ControlCatalog", false, ""},
55 {"valid OSPS baseline", "./test-data/good-osps.yml", "#ControlCatalog", false, ""},
56 {"valid lifecycle catalog", "./test-data/good-lifecycle.yaml", "#ControlCatalog", false, ""},
57 {"valid nested control catalog", "./test-data/nested-good-ccc.yaml", "#ControlCatalog", false, ""},
58
59 // GuidanceCatalog — positive
60 {"valid AI governance framework", "./test-data/good-aigf.yaml", "#GuidanceCatalog", false, ""},
61
62 // VectorCatalog — positive
63 {"valid vector catalog", "./test-data/good-vector-catalog.yaml", "#VectorCatalog", false, ""},
64 {"threats with vectors", "./test-data/good-threat-catalog.yaml", "#ThreatCatalog", false, ""},
65 {"valid capability catalog", "./test-data/good-capability-catalog.yaml", "#CapabilityCatalog", false, ""},
66 {"vector mapping", "./test-data/good-vector-mitre-mapping.yaml", "#MappingDocument", false, ""},
67
68 // RiskCatalog — positive
69 {"valid risk catalog", "./test-data/good-risk-catalog.yaml", "#RiskCatalog", false, ""},
70
71 // Policy — positive
72 {"valid policy", "./test-data/good-policy.yaml", "#Policy", false, ""},
73 {"valid security policy", "./test-data/good-security-policy.yml", "#Policy", false, ""},
74
75 // ControlCatalog — negative
76 {"invalid YAML", "./test-data/bad.yaml", "#ControlCatalog", true, ""},
77 {"invalid JSON", "./test-data/bad.json", "#ControlCatalog", true, ""},
78 {"controls without groups", "./test-data/bad-no-groups.yaml", "#ControlCatalog", true, ""},
79
80 // MappingDocument — positive
81 {"valid mapping document", "./test-data/good-mapping-document.yaml", "#MappingDocument", false, ""},
82
83 // MappingDocument — negative
84 {"invalid mapping document without mapping-references", "./test-data/bad-mapping-document.yaml", "#MappingDocument", true, ""},
85 {"mapping missing target for non-no-match relationship", "./test-data/bad-mapping-no-target.yaml", "#MappingDocument", true, ""},
86
87 // GuidanceCatalog — negative
88 {"retired guideline with recommendations", "./test-data/bad-lifecycle.yaml", "#GuidanceCatalog", true, ""},
89
90 // EvaluationLog — positive
91 {"valid PVTR baseline scan", "./test-data/pvtr-baseline-scan.yaml", "#EvaluationLog", false, ""},
92
93 // EnforcementLog — positive
94 {"valid enforcement log", "./test-data/good-enforcement-log.yaml", "#EnforcementLog", false, ""},
95
96 // EnforcementLog — negative
97 {"enforcement action with invalid disposition", "./test-data/bad-enforcement-log.yaml", "#EnforcementLog", true, ""},
98 {"enforcement action missing log reference", "./test-data/bad-enforcement-missing-log.yaml", "#EnforcementLog", true, ""},
99 {"clear disposition with failed assessment", "./test-data/bad-enforcement-clear-failed.yaml", "#EnforcementLog", true, ""},
100
101 // ControlCatalog — edge cases
102 {"empty nested catalog", "./test-data/nested-empty.yaml", "#ControlCatalog", false, ""},
103 }
104
105 for _, tt := range tests {
106 t.Run(tt.name, func(t *testing.T) {
107 data, err := os.ReadFile(tt.file)
108 if err != nil {
109 t.Fatalf("read %s: %v", tt.file, err)
110 }
111
112 def := schemaValue.LookupPath(cue.ParsePath(tt.definition))
113 if def.Err() != nil {
114 t.Fatalf("lookup %s: %v", tt.definition, def.Err())
115 }
116
117 var validationErr error
118 switch {
119 case strings.HasSuffix(tt.file, ".json"):
120 validationErr = cuejson.Validate(data, def)
121 case strings.HasSuffix(tt.file, ".yaml"), strings.HasSuffix(tt.file, ".yml"):
122 validationErr = cueyaml.Validate(data, def)
123 default:
124 t.Fatalf("unsupported file extension: %s", tt.file)
125 }
126
127 if tt.wantErr && validationErr == nil {
128 t.Error("expected validation error, got nil")
129 }
130 if !tt.wantErr && validationErr != nil {
131 t.Errorf("unexpected validation error: %v", validationErr)
132 }
133 if tt.errContains != "" && validationErr != nil {
134 if !strings.Contains(validationErr.Error(), tt.errContains) {
135 t.Errorf("error %q does not contain %q", validationErr.Error(), tt.errContains)
136 }
137 }
138 })
139 }
140}