Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 353 additions & 0 deletions app/cli/internal/policydevel/lint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
//
// Copyright 2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package policydevel

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/styrainc/regal/pkg/report"
)

func TestLookup(t *testing.T) {
tempDir := t.TempDir()

t.Run("non-existent file", func(t *testing.T) {
policy, err := Lookup(filepath.Join(tempDir, "nonexistent.yaml"), "", false)
assert.Error(t, err)
assert.Nil(t, policy)
assert.Contains(t, err.Error(), "unrecognized scheme")
})

t.Run("directory instead of file", func(t *testing.T) {
policy, err := Lookup(tempDir, "", false)
assert.Error(t, err)
assert.Nil(t, policy)
assert.Contains(t, err.Error(), "expected a file but got a directory")
})

t.Run("valid yaml file", func(t *testing.T) {
policy, err := Lookup("testdata/embedded-policy.yaml", "", false)
require.NoError(t, err)
assert.NotNil(t, policy)
assert.Contains(t, policy.Path, "testdata/embedded-policy.yaml")
assert.Len(t, policy.YAMLFiles, 1)
assert.Len(t, policy.RegoFiles, 0)
})

t.Run("valid rego file", func(t *testing.T) {
policy, err := Lookup("testdata/valid.rego", "", false)
require.NoError(t, err)
assert.NotNil(t, policy)
assert.Contains(t, policy.Path, "testdata/valid.rego")
assert.Len(t, policy.YAMLFiles, 0)
assert.Len(t, policy.RegoFiles, 1)
})

t.Run("unsupported file extension", func(t *testing.T) {
txtFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(txtFile, []byte("some content"), 0600)
require.NoError(t, err)

policy, err := Lookup(txtFile, "", false)
assert.Error(t, err)
assert.Nil(t, policy)
assert.Contains(t, err.Error(), "unsupported file extension .txt")
})

t.Run("yaml with referenced rego file", func(t *testing.T) {
policy, err := Lookup("testdata/policy.yaml", "", false)
require.NoError(t, err)
assert.NotNil(t, policy)
assert.Len(t, policy.YAMLFiles, 1)
assert.Len(t, policy.RegoFiles, 1)
})
}

func TestPolicyToLint_processFile(t *testing.T) {
tempDir := t.TempDir()
policy := &PolicyToLint{}

t.Run("process yaml file", func(t *testing.T) {
content := "test: yaml"
yamlFile := filepath.Join(tempDir, "test.yaml")
err := os.WriteFile(yamlFile, []byte(content), 0600)
require.NoError(t, err)

err = policy.processFile(yamlFile)
require.NoError(t, err)
assert.Len(t, policy.YAMLFiles, 1)
assert.Equal(t, yamlFile, policy.YAMLFiles[0].Path)
assert.Equal(t, []byte(content), policy.YAMLFiles[0].Content)
})

t.Run("process rego file", func(t *testing.T) {
content := "package main"
regoFile := filepath.Join(tempDir, "test.rego")
err := os.WriteFile(regoFile, []byte(content), 0600)
require.NoError(t, err)

err = policy.processFile(regoFile)
require.NoError(t, err)
assert.Len(t, policy.RegoFiles, 1)
assert.Equal(t, regoFile, policy.RegoFiles[0].Path)
assert.Equal(t, []byte(content), policy.RegoFiles[0].Content)
})

t.Run("unsupported file extension", func(t *testing.T) {
txtFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(txtFile, []byte("content"), 0600)
require.NoError(t, err)

err = policy.processFile(txtFile)
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported file extension .txt")
})
}

func TestPolicyToLint_checkResultStructure(t *testing.T) {
t.Run("valid result structure", func(t *testing.T) {
policy := &PolicyToLint{}
content, err := os.ReadFile("testdata/valid.rego")
require.NoError(t, err)
policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"})
assert.False(t, policy.HasErrors())
})

t.Run("missing result literal", func(t *testing.T) {
policy := &PolicyToLint{}
content := `package main

output := {
"violations": []
}`
policy.checkResultStructure(content, "test.rego", []string{"violations"})
assert.True(t, policy.HasErrors())
assert.Contains(t, policy.Errors[0].Message, "no result literal found")
})

t.Run("missing required keys", func(t *testing.T) {
policy := &PolicyToLint{}
content, err := os.ReadFile("testdata/missing-keys.rego")
require.NoError(t, err)
policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"})
assert.True(t, policy.HasErrors())
assert.Len(t, policy.Errors, 2)
assert.Contains(t, policy.Errors[0].Message, `missing "skip_reason" key`)
assert.Contains(t, policy.Errors[1].Message, `missing "skipped" key`)
})
}

func TestPolicyToLint_formatViolationError(t *testing.T) {
policy := &PolicyToLint{}

testCases := []struct {
name string
violation report.Violation
regoRuleMap map[int]string
expectedText string
}{
{
name: "violation with rule name",
violation: report.Violation{
Description: "Max rule length exceeded",
Location: report.Location{
Row: 5,
},
RelatedResources: []report.RelatedResource{
{Reference: "https://docs.styra.com/regal/rules/style/rule-length"},
},
},
regoRuleMap: map[int]string{5: "my_rule"},
expectedText: "[my_rule]: Max rule length exceeded - https://docs.styra.com/regal/rules/style/rule-length",
},
{
name: "violation without rule name",
violation: report.Violation{
Description: "General error",
Location: report.Location{
Row: 10,
},
RelatedResources: []report.RelatedResource{
{Reference: "https://example.com"},
},
},
regoRuleMap: map[int]string{},
expectedText: ": General error - https://example.com",
},
{
name: "violation with multiple resources",
violation: report.Violation{
Description: "Multiple issues found",
Location: report.Location{
Row: 3,
},
RelatedResources: []report.RelatedResource{
{Reference: "https://link1.com"},
{Reference: "https://link2.com"},
},
},
regoRuleMap: map[int]string{3: "test_rule"},
expectedText: "[test_rule]: Multiple issues found - https://link1.com, https://link2.com",
},
{
name: "violation with opa fmt reference",
violation: report.Violation{
Description: "Use `opa fmt` to format",
Location: report.Location{
Row: 1,
},
RelatedResources: []report.RelatedResource{
{Reference: "https://example.com"},
},
},
regoRuleMap: map[int]string{1: "format_rule"},
expectedText: "[format_rule]: Use `--format` to format - https://example.com",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := policy.formatViolationError(tc.violation, tc.regoRuleMap)
assert.Equal(t, tc.expectedText, result)
})
}
}

func TestPolicyToLint_buildRegoRuleMap(t *testing.T) {
policy := &PolicyToLint{}

testCases := []struct {
name string
regoFile string
expected map[int]string
}{
{
name: "single rule",
regoFile: "testdata/valid.rego",
expected: map[int]string{3: "result"},
},
{
name: "multiple rules",
regoFile: "testdata/multiple-rules.rego",
expected: map[int]string{
3: "allow",
5: "deny",
7: "result",
},
},
{
name: "empty rego",
regoFile: "",
expected: map[int]string{},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var content string
if tc.regoFile != "" {
contentBytes, err := os.ReadFile(tc.regoFile)
require.NoError(t, err)
content = string(contentBytes)
} else {
content = "invalid rego syntax {"
}
result := policy.buildRegoRuleMap(content)
assert.Equal(t, tc.expected, result)
})
}
}

func TestPolicyToLint_applyOPAFmt(t *testing.T) {
t.Run("format valid rego", func(t *testing.T) {
policy := &PolicyToLint{}
content, err := os.ReadFile("testdata/unformatted.rego")
require.NoError(t, err)

result := policy.applyOPAFmt(string(content), "test.rego")
assert.Contains(t, result, "result := {")
assert.False(t, policy.HasErrors())
})

t.Run("format invalid rego", func(t *testing.T) {
policy := &PolicyToLint{}
content := `invalid rego {`
result := policy.applyOPAFmt(content, "test.rego")
assert.Equal(t, content, result)
assert.True(t, policy.HasErrors())
assert.Contains(t, policy.Errors[0].Message, "auto-formatting failed")
})
}

func TestPolicyToLint_Validate(t *testing.T) {
tempDir := t.TempDir()

t.Run("validate rego files", func(t *testing.T) {
content, err := os.ReadFile("testdata/valid.rego")
require.NoError(t, err)

regoFile := filepath.Join(tempDir, "test.rego")
err = os.WriteFile(regoFile, content, 0600)
require.NoError(t, err)

policy := &PolicyToLint{
RegoFiles: []*File{
{
Path: regoFile,
Content: content,
},
},
}

policy.Validate()
assert.False(t, policy.HasErrors())
})

t.Run("validate and format rego files", func(t *testing.T) {
content, err := os.ReadFile("testdata/unformatted.rego")
require.NoError(t, err)

regoFile := filepath.Join(tempDir, "format_test.rego")
err = os.WriteFile(regoFile, content, 0600)
require.NoError(t, err)

policy := &PolicyToLint{
Format: true,
RegoFiles: []*File{
{
Path: regoFile,
Content: content,
},
},
}

policy.Validate()

formatted, err := os.ReadFile(regoFile)
require.NoError(t, err)
formattedStr := string(formatted)

expected, err := os.ReadFile("testdata/valid.rego")
require.NoError(t, err)
expectedStr := string(expected)

assert.Equal(t, expectedStr, formattedStr)
})
}
16 changes: 16 additions & 0 deletions app/cli/internal/policydevel/testdata/embedded-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: workflowcontract.chainloop.dev/v1
kind: Policy
metadata:
name: test-policy
description: Test validation policy
spec:
policies:
- embedded: |
package main

result := {
"violations": [],
"skip_reason": "",
"skipped": false
}
kind: ATTESTATION
5 changes: 5 additions & 0 deletions app/cli/internal/policydevel/testdata/missing-keys.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

result := {
"violations": []
}
9 changes: 9 additions & 0 deletions app/cli/internal/policydevel/testdata/multiple-rules.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

allow := true

deny := false

result := {
"violations": []
}
Loading
Loading