github.com/gemaraproj/gemara@v0.23.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
 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}