Skip to content

Commit 7af13be

Browse files
committed
add tests
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
1 parent 3b55df9 commit 7af13be

2 files changed

Lines changed: 262 additions & 3 deletions

File tree

app/cli/documentation/cli-reference.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2729,6 +2729,7 @@ Options inherited from parent commands
27292729
### chainloop policy develop
27302730

27312731
Tools for policy development
2732+
Refer to https://docs.chainloop.dev/guides/custom-policies
27322733

27332734
Options
27342735

@@ -2797,7 +2798,7 @@ Initialize a new policy by creating template policy files in the specified direc
27972798
By default, it creates chainloop-policy.yaml and chainloop-policy.rego files.
27982799

27992800
```
2800-
chainloop policy develop init [directory] [flags]
2801+
chainloop policy develop init [flags]
28012802
```
28022803

28032804
Examples
@@ -2807,14 +2808,15 @@ Examples
28072808
Initialize in current directory with separate files
28082809
chainloop policy develop init
28092810
2810-
Initialize in specific directory with embedded format
2811-
chainloop policy develop init ./policies --embedded
2811+
Initialize in specific directory with embedded format and policy name
2812+
chainloop policy develop init --directory ./policies --embedded --name mypolicy
28122813
```
28132814

28142815
Options
28152816

28162817
```
28172818
--description string description of the policy
2819+
--directory string directory for policy
28182820
--embedded initialize an embedded policy (single YAML file)
28192821
-f, --force overwrite existing files
28202822
-h, --help help for init
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright 2025 The Chainloop Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package policy
16+
17+
import (
18+
"os"
19+
"path/filepath"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestInitialize(t *testing.T) {
27+
tempDir := t.TempDir()
28+
29+
t.Run("embedded rego", func(t *testing.T) {
30+
opts := &InitOptions{
31+
Directory: tempDir,
32+
Embedded: true,
33+
Name: "test-policy",
34+
Description: "test description",
35+
}
36+
37+
err := Initialize(opts)
38+
require.NoError(t, err)
39+
40+
policyPath := filepath.Join(tempDir, "test-policy.yaml")
41+
assert.FileExists(t, policyPath)
42+
})
43+
44+
t.Run("standalone rego file", func(t *testing.T) {
45+
opts := &InitOptions{
46+
Directory: tempDir,
47+
Embedded: false,
48+
Name: "standalone-rego",
49+
}
50+
51+
err := Initialize(opts)
52+
require.NoError(t, err)
53+
54+
assert.FileExists(t, filepath.Join(tempDir, "standalone-rego.yaml"))
55+
assert.FileExists(t, filepath.Join(tempDir, "standalone-rego.rego"))
56+
})
57+
58+
t.Run("file exists and no force", func(t *testing.T) {
59+
opts := &InitOptions{
60+
Directory: tempDir,
61+
Name: "duplicate",
62+
}
63+
64+
// First time should succeed
65+
err := Initialize(opts)
66+
require.NoError(t, err)
67+
68+
// Second time should fail
69+
err = Initialize(opts)
70+
require.Error(t, err)
71+
assert.Contains(t, err.Error(), "already exists")
72+
73+
// With force it should succeed
74+
opts.Force = true
75+
err = Initialize(opts)
76+
require.NoError(t, err)
77+
})
78+
79+
t.Run("name and description are properly set", func(t *testing.T) {
80+
customName := "custom-policy-name"
81+
customDesc := "This is a custom policy description"
82+
83+
opts := &InitOptions{
84+
Directory: tempDir,
85+
Name: customName,
86+
Description: customDesc,
87+
Embedded: true,
88+
}
89+
90+
err := Initialize(opts)
91+
require.NoError(t, err)
92+
93+
policyPath := filepath.Join(tempDir, customName+".yaml")
94+
assert.FileExists(t, policyPath)
95+
96+
content, err := os.ReadFile(policyPath)
97+
require.NoError(t, err)
98+
99+
policyContent := string(content)
100+
101+
assert.Contains(t, policyContent, "name: "+customName)
102+
103+
assert.Contains(t, policyContent, "description: "+customDesc)
104+
105+
assert.FileExists(t, filepath.Join(tempDir, customName+".yaml"))
106+
})
107+
}
108+
109+
func TestLoadAndProcessTemplates(t *testing.T) {
110+
t.Run("embedded rego", func(t *testing.T) {
111+
opts := &InitOptions{
112+
Embedded: true,
113+
Name: "embedded-test",
114+
}
115+
116+
content, err := loadAndProcessTemplates(opts)
117+
require.NoError(t, err)
118+
assert.NotEmpty(t, content.YAML)
119+
assert.Empty(t, content.Rego) // Rego file should be empty for embedded
120+
})
121+
122+
t.Run("separate rego file", func(t *testing.T) {
123+
opts := &InitOptions{
124+
Embedded: false,
125+
Name: "separate-rego-test",
126+
}
127+
128+
content, err := loadAndProcessTemplates(opts)
129+
require.NoError(t, err)
130+
assert.NotEmpty(t, content.YAML)
131+
assert.NotEmpty(t, content.Rego)
132+
})
133+
}
134+
135+
func TestExecuteTemplate(t *testing.T) {
136+
testCases := []struct {
137+
name string
138+
template string
139+
data *TemplateData
140+
expected string
141+
}{
142+
{
143+
name: "basic interpolation",
144+
template: "Hello {{.Name}}!",
145+
data: &TemplateData{Name: "world"},
146+
expected: "Hello world!",
147+
},
148+
{
149+
name: "sanitize function",
150+
template: "{{.Name | sanitize}}",
151+
data: &TemplateData{Name: "My Policy"},
152+
expected: "my-policy",
153+
},
154+
{
155+
name: "indent function",
156+
template: "{{indent 2 \"hello\"}}",
157+
expected: " hello",
158+
},
159+
{
160+
name: "multiple fields interpolation",
161+
template: "Name: {{.Name}}, Desc: {{.Description}}",
162+
data: &TemplateData{Name: "test", Description: "description"},
163+
expected: "Name: test, Desc: description",
164+
},
165+
{
166+
name: "trimSpace function",
167+
template: "{{.Name | trimSpace}}",
168+
data: &TemplateData{Name: " spaced "},
169+
expected: "spaced",
170+
},
171+
{
172+
name: "combined functions",
173+
template: "{{.Name | trimSpace | sanitize}}",
174+
data: &TemplateData{Name: " My Policy 123 "},
175+
expected: "my-policy-123",
176+
},
177+
{
178+
name: "empty template",
179+
template: "",
180+
data: &TemplateData{Name: "test"},
181+
expected: "",
182+
},
183+
{
184+
name: "embedded rego flag",
185+
template: "Embedded: {{.Embedded}}",
186+
data: &TemplateData{Embedded: true},
187+
expected: "Embedded: true",
188+
},
189+
{
190+
name: "material kind",
191+
template: "Material: {{.MaterialKind}}",
192+
data: &TemplateData{MaterialKind: "SBOM_CYCLONEDX_JSON"},
193+
expected: "Material: SBOM_CYCLONEDX_JSON",
194+
},
195+
}
196+
197+
for _, tc := range testCases {
198+
t.Run(tc.name, func(t *testing.T) {
199+
result, err := executeTemplate(tc.template, tc.data)
200+
require.NoError(t, err)
201+
assert.Equal(t, tc.expected, result)
202+
})
203+
}
204+
205+
errorCases := []struct {
206+
name string
207+
template string
208+
data *TemplateData
209+
errMsg string
210+
}{
211+
{
212+
name: "invalid template syntax",
213+
template: "{{.Name",
214+
data: &TemplateData{Name: "test"},
215+
errMsg: "template parsing error",
216+
},
217+
{
218+
name: "missing field",
219+
template: "{{.MissingField}}",
220+
data: &TemplateData{Name: "test"},
221+
errMsg: "template execution error",
222+
},
223+
{
224+
name: "invalid function",
225+
template: "{{.Name | invalidFunc}}",
226+
data: &TemplateData{Name: "test"},
227+
errMsg: "template parsing error",
228+
},
229+
}
230+
231+
for _, tc := range errorCases {
232+
t.Run(tc.name, func(t *testing.T) {
233+
_, err := executeTemplate(tc.template, tc.data)
234+
require.Error(t, err)
235+
assert.Contains(t, err.Error(), tc.errMsg)
236+
})
237+
}
238+
}
239+
240+
func TestSanitizeName(t *testing.T) {
241+
testCases := []struct {
242+
input string
243+
expected string
244+
}{
245+
{"My Policy", "my-policy"},
246+
{" Trim Spaces ", "trim-spaces"},
247+
{"UPPER CASE", "upper-case"},
248+
{"Special!@#Chars", "special!@#chars"},
249+
{"", ""},
250+
}
251+
252+
for _, tc := range testCases {
253+
t.Run(tc.input, func(t *testing.T) {
254+
assert.Equal(t, tc.expected, sanitizeName(tc.input))
255+
})
256+
}
257+
}

0 commit comments

Comments
 (0)