Skip to content

Commit ac3f473

Browse files
authored
feat(policy-devel): add lint tests (#2401)
Signed-off-by: Sylwester Piskozub <[email protected]>
1 parent 247b0c5 commit ac3f473

File tree

7 files changed

+406
-0
lines changed

7 files changed

+406
-0
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package policydevel
17+
18+
import (
19+
"os"
20+
"path/filepath"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
"github.com/styrainc/regal/pkg/report"
26+
)
27+
28+
func TestLookup(t *testing.T) {
29+
tempDir := t.TempDir()
30+
31+
t.Run("non-existent file", func(t *testing.T) {
32+
policy, err := Lookup(filepath.Join(tempDir, "nonexistent.yaml"), "", false)
33+
assert.Error(t, err)
34+
assert.Nil(t, policy)
35+
assert.Contains(t, err.Error(), "unrecognized scheme")
36+
})
37+
38+
t.Run("directory instead of file", func(t *testing.T) {
39+
policy, err := Lookup(tempDir, "", false)
40+
assert.Error(t, err)
41+
assert.Nil(t, policy)
42+
assert.Contains(t, err.Error(), "expected a file but got a directory")
43+
})
44+
45+
t.Run("valid yaml file", func(t *testing.T) {
46+
policy, err := Lookup("testdata/embedded-policy.yaml", "", false)
47+
require.NoError(t, err)
48+
assert.NotNil(t, policy)
49+
assert.Contains(t, policy.Path, "testdata/embedded-policy.yaml")
50+
assert.Len(t, policy.YAMLFiles, 1)
51+
assert.Len(t, policy.RegoFiles, 0)
52+
})
53+
54+
t.Run("valid rego file", func(t *testing.T) {
55+
policy, err := Lookup("testdata/valid.rego", "", false)
56+
require.NoError(t, err)
57+
assert.NotNil(t, policy)
58+
assert.Contains(t, policy.Path, "testdata/valid.rego")
59+
assert.Len(t, policy.YAMLFiles, 0)
60+
assert.Len(t, policy.RegoFiles, 1)
61+
})
62+
63+
t.Run("unsupported file extension", func(t *testing.T) {
64+
txtFile := filepath.Join(tempDir, "test.txt")
65+
err := os.WriteFile(txtFile, []byte("some content"), 0600)
66+
require.NoError(t, err)
67+
68+
policy, err := Lookup(txtFile, "", false)
69+
assert.Error(t, err)
70+
assert.Nil(t, policy)
71+
assert.Contains(t, err.Error(), "unsupported file extension .txt")
72+
})
73+
74+
t.Run("yaml with referenced rego file", func(t *testing.T) {
75+
policy, err := Lookup("testdata/policy.yaml", "", false)
76+
require.NoError(t, err)
77+
assert.NotNil(t, policy)
78+
assert.Len(t, policy.YAMLFiles, 1)
79+
assert.Len(t, policy.RegoFiles, 1)
80+
})
81+
}
82+
83+
func TestPolicyToLint_processFile(t *testing.T) {
84+
tempDir := t.TempDir()
85+
policy := &PolicyToLint{}
86+
87+
t.Run("process yaml file", func(t *testing.T) {
88+
content := "test: yaml"
89+
yamlFile := filepath.Join(tempDir, "test.yaml")
90+
err := os.WriteFile(yamlFile, []byte(content), 0600)
91+
require.NoError(t, err)
92+
93+
err = policy.processFile(yamlFile)
94+
require.NoError(t, err)
95+
assert.Len(t, policy.YAMLFiles, 1)
96+
assert.Equal(t, yamlFile, policy.YAMLFiles[0].Path)
97+
assert.Equal(t, []byte(content), policy.YAMLFiles[0].Content)
98+
})
99+
100+
t.Run("process rego file", func(t *testing.T) {
101+
content := "package main"
102+
regoFile := filepath.Join(tempDir, "test.rego")
103+
err := os.WriteFile(regoFile, []byte(content), 0600)
104+
require.NoError(t, err)
105+
106+
err = policy.processFile(regoFile)
107+
require.NoError(t, err)
108+
assert.Len(t, policy.RegoFiles, 1)
109+
assert.Equal(t, regoFile, policy.RegoFiles[0].Path)
110+
assert.Equal(t, []byte(content), policy.RegoFiles[0].Content)
111+
})
112+
113+
t.Run("unsupported file extension", func(t *testing.T) {
114+
txtFile := filepath.Join(tempDir, "test.txt")
115+
err := os.WriteFile(txtFile, []byte("content"), 0600)
116+
require.NoError(t, err)
117+
118+
err = policy.processFile(txtFile)
119+
assert.Error(t, err)
120+
assert.Contains(t, err.Error(), "unsupported file extension .txt")
121+
})
122+
}
123+
124+
func TestPolicyToLint_checkResultStructure(t *testing.T) {
125+
t.Run("valid result structure", func(t *testing.T) {
126+
policy := &PolicyToLint{}
127+
content, err := os.ReadFile("testdata/valid.rego")
128+
require.NoError(t, err)
129+
policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"})
130+
assert.False(t, policy.HasErrors())
131+
})
132+
133+
t.Run("missing result literal", func(t *testing.T) {
134+
policy := &PolicyToLint{}
135+
content := `package main
136+
137+
output := {
138+
"violations": []
139+
}`
140+
policy.checkResultStructure(content, "test.rego", []string{"violations"})
141+
assert.True(t, policy.HasErrors())
142+
assert.Contains(t, policy.Errors[0].Message, "no result literal found")
143+
})
144+
145+
t.Run("missing required keys", func(t *testing.T) {
146+
policy := &PolicyToLint{}
147+
content, err := os.ReadFile("testdata/missing-keys.rego")
148+
require.NoError(t, err)
149+
policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"})
150+
assert.True(t, policy.HasErrors())
151+
assert.Len(t, policy.Errors, 2)
152+
assert.Contains(t, policy.Errors[0].Message, `missing "skip_reason" key`)
153+
assert.Contains(t, policy.Errors[1].Message, `missing "skipped" key`)
154+
})
155+
}
156+
157+
func TestPolicyToLint_formatViolationError(t *testing.T) {
158+
policy := &PolicyToLint{}
159+
160+
testCases := []struct {
161+
name string
162+
violation report.Violation
163+
regoRuleMap map[int]string
164+
expectedText string
165+
}{
166+
{
167+
name: "violation with rule name",
168+
violation: report.Violation{
169+
Description: "Max rule length exceeded",
170+
Location: report.Location{
171+
Row: 5,
172+
},
173+
RelatedResources: []report.RelatedResource{
174+
{Reference: "https://docs.styra.com/regal/rules/style/rule-length"},
175+
},
176+
},
177+
regoRuleMap: map[int]string{5: "my_rule"},
178+
expectedText: "[my_rule]: Max rule length exceeded - https://docs.styra.com/regal/rules/style/rule-length",
179+
},
180+
{
181+
name: "violation without rule name",
182+
violation: report.Violation{
183+
Description: "General error",
184+
Location: report.Location{
185+
Row: 10,
186+
},
187+
RelatedResources: []report.RelatedResource{
188+
{Reference: "https://example.com"},
189+
},
190+
},
191+
regoRuleMap: map[int]string{},
192+
expectedText: ": General error - https://example.com",
193+
},
194+
{
195+
name: "violation with multiple resources",
196+
violation: report.Violation{
197+
Description: "Multiple issues found",
198+
Location: report.Location{
199+
Row: 3,
200+
},
201+
RelatedResources: []report.RelatedResource{
202+
{Reference: "https://link1.com"},
203+
{Reference: "https://link2.com"},
204+
},
205+
},
206+
regoRuleMap: map[int]string{3: "test_rule"},
207+
expectedText: "[test_rule]: Multiple issues found - https://link1.com, https://link2.com",
208+
},
209+
{
210+
name: "violation with opa fmt reference",
211+
violation: report.Violation{
212+
Description: "Use `opa fmt` to format",
213+
Location: report.Location{
214+
Row: 1,
215+
},
216+
RelatedResources: []report.RelatedResource{
217+
{Reference: "https://example.com"},
218+
},
219+
},
220+
regoRuleMap: map[int]string{1: "format_rule"},
221+
expectedText: "[format_rule]: Use `--format` to format - https://example.com",
222+
},
223+
}
224+
225+
for _, tc := range testCases {
226+
t.Run(tc.name, func(t *testing.T) {
227+
result := policy.formatViolationError(tc.violation, tc.regoRuleMap)
228+
assert.Equal(t, tc.expectedText, result)
229+
})
230+
}
231+
}
232+
233+
func TestPolicyToLint_buildRegoRuleMap(t *testing.T) {
234+
policy := &PolicyToLint{}
235+
236+
testCases := []struct {
237+
name string
238+
regoFile string
239+
expected map[int]string
240+
}{
241+
{
242+
name: "single rule",
243+
regoFile: "testdata/valid.rego",
244+
expected: map[int]string{3: "result"},
245+
},
246+
{
247+
name: "multiple rules",
248+
regoFile: "testdata/multiple-rules.rego",
249+
expected: map[int]string{
250+
3: "allow",
251+
5: "deny",
252+
7: "result",
253+
},
254+
},
255+
{
256+
name: "empty rego",
257+
regoFile: "",
258+
expected: map[int]string{},
259+
},
260+
}
261+
262+
for _, tc := range testCases {
263+
t.Run(tc.name, func(t *testing.T) {
264+
var content string
265+
if tc.regoFile != "" {
266+
contentBytes, err := os.ReadFile(tc.regoFile)
267+
require.NoError(t, err)
268+
content = string(contentBytes)
269+
} else {
270+
content = "invalid rego syntax {"
271+
}
272+
result := policy.buildRegoRuleMap(content)
273+
assert.Equal(t, tc.expected, result)
274+
})
275+
}
276+
}
277+
278+
func TestPolicyToLint_applyOPAFmt(t *testing.T) {
279+
t.Run("format valid rego", func(t *testing.T) {
280+
policy := &PolicyToLint{}
281+
content, err := os.ReadFile("testdata/unformatted.rego")
282+
require.NoError(t, err)
283+
284+
result := policy.applyOPAFmt(string(content), "test.rego")
285+
assert.Contains(t, result, "result := {")
286+
assert.False(t, policy.HasErrors())
287+
})
288+
289+
t.Run("format invalid rego", func(t *testing.T) {
290+
policy := &PolicyToLint{}
291+
content := `invalid rego {`
292+
result := policy.applyOPAFmt(content, "test.rego")
293+
assert.Equal(t, content, result)
294+
assert.True(t, policy.HasErrors())
295+
assert.Contains(t, policy.Errors[0].Message, "auto-formatting failed")
296+
})
297+
}
298+
299+
func TestPolicyToLint_Validate(t *testing.T) {
300+
tempDir := t.TempDir()
301+
302+
t.Run("validate rego files", func(t *testing.T) {
303+
content, err := os.ReadFile("testdata/valid.rego")
304+
require.NoError(t, err)
305+
306+
regoFile := filepath.Join(tempDir, "test.rego")
307+
err = os.WriteFile(regoFile, content, 0600)
308+
require.NoError(t, err)
309+
310+
policy := &PolicyToLint{
311+
RegoFiles: []*File{
312+
{
313+
Path: regoFile,
314+
Content: content,
315+
},
316+
},
317+
}
318+
319+
policy.Validate()
320+
assert.False(t, policy.HasErrors())
321+
})
322+
323+
t.Run("validate and format rego files", func(t *testing.T) {
324+
content, err := os.ReadFile("testdata/unformatted.rego")
325+
require.NoError(t, err)
326+
327+
regoFile := filepath.Join(tempDir, "format_test.rego")
328+
err = os.WriteFile(regoFile, content, 0600)
329+
require.NoError(t, err)
330+
331+
policy := &PolicyToLint{
332+
Format: true,
333+
RegoFiles: []*File{
334+
{
335+
Path: regoFile,
336+
Content: content,
337+
},
338+
},
339+
}
340+
341+
policy.Validate()
342+
343+
formatted, err := os.ReadFile(regoFile)
344+
require.NoError(t, err)
345+
formattedStr := string(formatted)
346+
347+
expected, err := os.ReadFile("testdata/valid.rego")
348+
require.NoError(t, err)
349+
expectedStr := string(expected)
350+
351+
assert.Equal(t, expectedStr, formattedStr)
352+
})
353+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: test-policy
5+
description: Test validation policy
6+
spec:
7+
policies:
8+
- embedded: |
9+
package main
10+
11+
result := {
12+
"violations": [],
13+
"skip_reason": "",
14+
"skipped": false
15+
}
16+
kind: ATTESTATION
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package main
2+
3+
result := {
4+
"violations": []
5+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package main
2+
3+
allow := true
4+
5+
deny := false
6+
7+
result := {
8+
"violations": []
9+
}

0 commit comments

Comments
 (0)