Files
releases/cmd/deps_test.go
T
Anders Eknert d2a415e25d opa check --bundle report virtual/base doc conflicts (#7701)
A tiny first step to have more tooling correctly report
virtual and base document conflicts, as detailed in #7694.

This PR fixes the `opa check` command to report conflicts
of this type when the `-b`/`--bundle` flag is provided. The
bundle flag is required as without that, `opa check` should
only verify policies and not load data at all.

While I was in the `cmd` directory, I got annoyed with how
many of these commands store the same constants for their
`--format` flag, so I decided to fix that too, even if it
wasn't related to what I originally planned to do. I hope
it's not too distracting.

Signed-off-by: Anders Eknert <anders@styra.com>
2025-06-16 12:24:42 +02:00

574 lines
12 KiB
Go

// Copyright 2024 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"io"
"maps"
"os"
"path/filepath"
"strings"
"testing"
"github.com/open-policy-agent/opa/cmd/formats"
"github.com/open-policy-agent/opa/internal/file/archive"
"github.com/open-policy-agent/opa/v1/util/test"
)
func TestDeps_DefaultRegoVersion(t *testing.T) {
tests := []struct {
note string
module string
query string
expErrs []string
}{
{
note: "v0 module",
module: `package test
a[x] {
x := 42
}`,
query: `data.test.p`,
expErrs: []string{
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1 module",
module: `package test
a contains x if {
x := 42
}`,
query: `data.test.a`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.module,
}
test.WithTempFS(files, func(rootPath string) {
params := newDepsCommandParams()
_ = params.outputFormat.Set(formats.Pretty)
for f := range files {
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
}
err := deps([]string{tc.query}, params, io.Discard)
if len(tc.expErrs) > 0 {
if err == nil {
t.Fatalf("Expected error but got nil")
}
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
}
}
} else if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
})
}
}
func TestDepsCompatibleFlags(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
v1Compatible bool
module string
query string
expErrs []string
}{
{
note: "v0, no keywords",
v0Compatible: true,
module: `package test
p[3] {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v0, keywords not imported, but used",
v0Compatible: true,
module: `package test
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: number cannot be used for rule name",
},
},
{
note: "v0, keywords imported",
v0Compatible: true,
module: `package test
import future.keywords
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v0, rego.v1 imported",
v0Compatible: true,
module: `package test
import rego.v1
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v1, no keywords",
v1Compatible: true,
module: `package test
p[3] {
input.x = 1
}`,
query: `data.test.p`,
expErrs: []string{
"rego_parse_error: `if` keyword is required before rule body",
"rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1, no keyword imports",
v1Compatible: true,
module: `package test
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v1, keywords imported",
v1Compatible: true,
module: `package test
import future.keywords
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v1, rego.v1 imported",
v1Compatible: true,
module: `package test
import rego.v1
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
// v0 takes precedence over v1
{
note: "v0+v1, no keywords",
v0Compatible: true,
v1Compatible: true,
module: `package test
p[3] {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v0+v1, keywords not imported, but used",
v0Compatible: true,
v1Compatible: true,
module: `package test
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: number cannot be used for rule name",
},
},
{
note: "v0+v1, keywords imported",
v0Compatible: true,
v1Compatible: true,
module: `package test
import future.keywords
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
{
note: "v0+v1, rego.v1 imported",
v0Compatible: true,
v1Compatible: true,
module: `package test
import rego.v1
p contains 3 if {
input.x = 1
}`,
query: `data.test.p`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.module,
}
test.WithTempFS(files, func(rootPath string) {
params := newDepsCommandParams()
params.v0Compatible = tc.v0Compatible
params.v1Compatible = tc.v1Compatible
_ = params.outputFormat.Set(formats.Pretty)
for f := range files {
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
}
err := deps([]string{tc.query}, params, io.Discard)
if len(tc.expErrs) > 0 {
if err == nil {
t.Fatalf("Expected error but got nil")
}
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
}
}
} else if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
})
}
}
func TestDepsV1WithBundleRegoVersion(t *testing.T) {
tests := []struct {
note string
files map[string]string
query string
expErrs []string
}{
{
note: "v0.x bundle, no keywords",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
p[3] {
input.x = 1
}`,
},
query: `data.test.p`,
},
{
note: "v0.x bundle, keywords not imported, but used",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
p contains 3 if {
input.x = 1
}`,
},
query: `data.test.p`,
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: number cannot be used for rule name",
},
},
{
note: "v0.x bundle, keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import future.keywords
p contains 3 if {
input.x = 1
}`,
},
query: `data.test.p`,
},
{
note: "v0.x bundle, rego.v1 imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import rego.v1
p contains 3 if {
input.x = 1
}`,
},
query: `data.test.p`,
},
{
note: "v0 bundle, v1 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[3] {
input.x = 1
}`,
"policy2.rego": `package test
p contains 4 if {
input.x = 1
}`,
},
},
{
note: "v0 bundle, v1 per-file override (glob)",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/bar/*.rego": 1
}
}`,
"foo/policy1.rego": `package test
p[3] {
input.x = 1
}`,
"bar/policy2.rego": `package test
p contains 4 if {
input.x = 1
}`,
},
},
{
note: "v0 bundle, v1 per-file override, incompliant",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[3] {
input.x = 1
}`,
"policy2.rego": `package test
p[4] {
input.x = 1
}`,
},
expErrs: []string{
"rego_parse_error: `if` keyword is required before rule body",
"rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1.0 bundle, no keywords",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p[3] {
input.x = 1
}`,
},
query: `data.test.p`,
expErrs: []string{
"rego_parse_error: `if` keyword is required before rule body",
"rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1.0 bundle, no keyword imports",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p contains 3 if {
input.x = 1
}`,
},
query: `data.test.p`,
},
{
note: "v1.0 bundle, keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import future.keywords
p contains 3 if {
input.x = 1
}`,
},
query: `data.test.p`,
},
{
note: "v1.0 bundle, rego.v1 imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import rego.v1
p contains 3 if {
input.x = 1
}`,
},
query: `data.test.p`,
},
{
note: "v1 bundle, v0 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package test
p[3] {
input.x = 1
}`,
"policy2.rego": `package test
p contains 4 if {
input.x = 1
}`,
},
},
{
note: "v1 bundle, v0 per-file override (glob)",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/foo/*.rego": 0
}
}`,
"foo/policy1.rego": `package test
p[3] {
input.x = 1
}`,
"bar/policy2.rego": `package test
p contains 4 if {
input.x = 1
}`,
},
},
{
note: "v1 bundle, v0 per-file override, incompliant",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package test
p contains 3 if {
input.x = 1
}`,
"policy2.rego": `package test
p contains 4 if {
input.x = 1
}`,
},
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: number cannot be used for rule name",
},
},
}
bundleTypeCases := []struct {
note string
tar bool
}{
{
"bundle dir", false,
},
{
"bundle tar", true,
},
}
v1CompatibleFlagCases := []struct {
note string
used bool
}{
{
"no --v1-compatible", false,
},
{
"--v1-compatible", true,
},
}
for _, bundleType := range bundleTypeCases {
for _, v1CompatibleFlag := range v1CompatibleFlagCases {
for _, tc := range tests {
t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v1CompatibleFlag.note, tc.note), func(t *testing.T) {
files := map[string]string{}
if bundleType.tar {
files["bundle.tar.gz"] = ""
} else {
maps.Copy(files, tc.files)
}
test.WithTempFS(files, func(root string) {
p := root
if bundleType.tar {
p = filepath.Join(root, "bundle.tar.gz")
files := make([][2]string, 0, len(tc.files))
for k, v := range tc.files {
files = append(files, [2]string{k, v})
}
buf := archive.MustWriteTarGz(files)
bf, err := os.Create(p)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
_, err = bf.Write(buf.Bytes())
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
params := newDepsCommandParams()
if err := params.bundlePaths.Set(p); err != nil {
t.Fatalf("Unexpected error: %s", err)
}
params.v1Compatible = v1CompatibleFlag.used
_ = params.outputFormat.Set(formats.Pretty)
err := deps([]string{tc.query}, params, io.Discard)
if len(tc.expErrs) > 0 {
if err == nil {
t.Fatalf("Expected error but got nil")
}
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
}
}
} else if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
})
})
}
}
}
}