Files
releases/cmd/check_test.go
T
Anders Eknert db035b09fc Add support for Go 1.27 & jsonv2 (#8947)
Makes OPA build and pass its tests on Go 1.27, while keeping Go 1.25 and
1.26 working. JSON output is unchanged on every supported version.

Go 1.27 json package honours `encoding.TextAppender`. Many v1 ast types
implement AppendText to build their Rego string cheaply, so on 1.27 they
would have marshalled as Rego text. Files built only with 1.27 now
implement MarshalJSONTo.

Library users should keep using `json.Marshal` etc. The MarshalJSONTo
methods are implementation details, are absent from 1.25 and 1.26
builds, and may change.

---------

Signed-off-by: Anders Eknert <anders.eknert@apple.com>
Signed-off-by: Charlie Egan <charlie_egan@apple.com>
Co-authored-by: Charlie Egan <charlie_egan@apple.com>
2026-07-30 17:41:28 +01:00

1387 lines
32 KiB
Go

//go:build !go1.27
// Copyright 2022 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 (
"bytes"
"encoding/json"
"fmt"
"maps"
"os"
"path"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/open-policy-agent/opa/internal/file/archive"
pr "github.com/open-policy-agent/opa/internal/presentation"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util/test"
)
func TestCheckRespectsCapabilities(t *testing.T) {
//nolint:prealloc // test slice is extended dynamically, initial values are clearer as slice literal
tests := []struct {
note string
caps string
policy string
err string
bundleMode bool // check with "-b" flag
}{
{
note: "builtin defined in caps",
caps: `{
"builtins": [
{
"name": "is_foo",
"decl": {
"args": [
{
"type": "string"
}
],
"result": {
"type": "boolean"
},
"type": "function"
}
}
]
}`,
policy: `package test
p { is_foo("bar") }`,
},
{
note: "future kw NOT defined in caps",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.FutureKeywords = []string{"in"}
c.Features = []string{}
j, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(j)
}(),
policy: `package test
import future.keywords.if
import future.keywords.in
p if "opa" in input.tools`,
err: "rego_parse_error: unexpected keyword, must be one of [in]",
},
{
note: "future kw NOT defined in caps, rego-v1 feature",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.FutureKeywords = []string{"in"}
c.Features = []string{ast.FeatureRegoV1}
j, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(j)
}(),
policy: `package test
import future.keywords.if
import future.keywords.in
p if "opa" in input.tools`,
},
{
note: "future kw are defined in caps",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.FutureKeywords = []string{"in", "if"}
j, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(j)
}(),
policy: `package test
import future.keywords.if
import future.keywords.in
p if "opa" in input.tools`,
},
{
note: "rego.v1 imported but NOT defined in capabilities",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.Features = []string{}
j, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(j)
}(),
policy: `package test
import rego.v1`,
err: "rego_parse_error: invalid import, `rego.v1` is not supported by current capabilities",
},
{
note: "rego.v1 imported AND defined in capabilities",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.Features = []string{ast.FeatureRegoV1Import}
j, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(j)
}(),
policy: `package test
import rego.v1`,
},
{
note: "rego.v1 imported AND rego-v1 in capabilities",
caps: func() string {
c := ast.CapabilitiesForThisVersion()
c.Features = []string{ast.FeatureRegoV1}
j, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(j)
}(),
policy: `package test
import rego.v1`,
},
}
// add same tests for bundle-mode == true:
for i := range tests {
tc := tests[i]
tc.bundleMode = true
tc.note += " (as bundle)"
tests = append(tests, tc)
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"capabilities.json": tc.caps,
"test.rego": tc.policy,
}
test.WithTempFS(files, func(root string) {
caps := newCapabilitiesFlag()
if err := caps.Set(path.Join(root, "capabilities.json")); err != nil {
t.Fatal(err)
}
params := newCheckParams()
params.capabilities = caps
params.bundleMode = tc.bundleMode
// Capabilities in test cases is pre v1
params.v0Compatible = true
err := checkModules(params, []string{root})
switch {
case err != nil && tc.err != "":
if !strings.Contains(err.Error(), tc.err) {
t.Fatalf("expected err %v, got %v", tc.err, err)
}
return // don't read back bundle below
case err != nil && tc.err == "":
t.Fatalf("unexpected error: %v", err)
case err == nil && tc.err != "":
t.Fatalf("expected error %v, got nil", tc.err)
}
})
})
}
}
func testCheckWithSchemasAnnotationButNoSchemaFlag(policy string) error {
files := map[string]string{
"test.rego": policy,
}
var err error
test.WithTempFS(files, func(path string) {
params := newCheckParams()
err = checkModules(params, []string{path})
})
return err
}
func TestCheckIgnoresNonRegoFiles(t *testing.T) {
files := map[string]string{
"test.rego": `package test`,
"test.json": `{"foo": "bar"}`,
"test.yaml": `foo: bar`,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
err := checkModules(params, []string{root})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
func TestCheckIgnoreBundleMode(t *testing.T) {
t.Parallel()
files := map[string]string{
"ignore.rego": `invalid rego`,
"include.rego": `package valid`,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
params.ignore = []string{"ignore.rego"}
params.bundleMode = true
err := checkModules(params, []string{root})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
func TestCheckBundleReportsPolicyVsDataConflict(t *testing.T) {
t.Parallel()
files := map[string]string{
"policy.rego": "package p\nallow := false\n",
"data.json": `{"p":{"allow":false}}`,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
// Bundle mode required as the check command *should* ignore data entirely otherwise
params.bundleMode = true
err := checkModules(params, []string{root})
if err == nil {
t.Fatal("expected error but received none")
}
exp := fmt.Sprintf(
"1 error occurred: %s:2: rego_compile_error: conflicting rule for data path p/allow found",
filepath.Join(root, "policy.rego"),
)
if err.Error() != exp {
t.Fatalf("expected error %q, got %q", exp, err.Error())
}
})
}
func TestCheckFailsOnInvalidRego(t *testing.T) {
files := map[string]string{
"test.rego": `package test
{}`,
"test.json": `{"foo": "bar"}`,
}
expectedError := "rego_parse_error: object cannot be used for rule name"
test.WithTempFS(files, func(root string) {
params := newCheckParams()
err := checkModules(params, []string{root})
if err == nil {
t.Fatalf("expected error %v but received none", expectedError)
}
if !strings.Contains(err.Error(), expectedError) {
t.Fatalf("expected error %v but received %v", expectedError, err)
}
})
}
func TestCheckJSONOutputBytes(t *testing.T) {
files := map[string]string{
"test.rego": `package test
{}`,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
checkErr := checkModules(params, []string{root})
if checkErr == nil {
t.Fatal("expected error but received none")
}
var buf bytes.Buffer
if err := pr.JSON(&buf, pr.Output{Errors: pr.NewOutputErrors(checkErr)}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := strings.ReplaceAll(`{
"errors": [
{
"message": "object cannot be used for rule name",
"code": "rego_parse_error",
"location": {
"file": "TEMPDIR/test.rego",
"row": 2,
"col": 1
}
}
]
}
`, "TEMPDIR", root)
if diff := cmp.Diff(expected, buf.String()); diff != "" {
t.Errorf("unexpected result (-want, +got):\n%s", diff)
}
})
}
// Assert that 'schemas' annotations with schema refs are only informing the type checker when the --schema flag is used
func TestCheckWithSchemasAnnotationButNoSchemaFlag(t *testing.T) {
policiesWithSchemaRef := []string{`
package test
import rego.v1
# METADATA
# schemas:
# - input: schema["input"]
p if {
rego.metadata.rule() # presence of rego.metadata.* calls must not trigger unwanted schema evaluation
input.foo == 42 # type mismatch with schema that should be ignored
}`,
`
package p
# METADATA
# schemas:
# - data.p.x: schema["nope"]
bug := data.p.x
`}
for i, pol := range policiesWithSchemaRef {
err := testCheckWithSchemasAnnotationButNoSchemaFlag(pol)
if err != nil {
t.Fatalf("unexpected error from eval policy %d with schema ref: %v", i, err)
}
}
policyWithInlinedSchema := `
package test
import rego.v1
# METADATA
# schemas:
# - input.foo: {"type": "boolean"}
p if {
rego.metadata.rule() # presence of rego.metadata.* calls must not trigger unwanted schema evaluation
input.foo == 42 # type mismatch with schema that should be ignored
}`
err := testCheckWithSchemasAnnotationButNoSchemaFlag(policyWithInlinedSchema)
// We expect an error here, as inlined schemas are always used for type checking
if !strings.Contains(err.Error(), "rego_type_error: match error") {
t.Fatalf("unexpected error from eval with inlined schema, got: %v", err)
}
}
func TestCheckRegoV1(t *testing.T) {
cases := []struct {
note string
policy string
expErrs []string
}{
{
note: "rego.v1 imported, v1 compliant",
policy: `package test
import rego.v1
p contains x if {
x := [1,2,3]
}`,
},
{
note: "rego.v1 imported, NOT v1 compliant (parser)",
policy: `package test
import rego.v1
p contains x {
x := [1,2,3]
}
q.r`,
expErrs: []string{
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "rego.v1 imported, NOT v1 compliant (compiler)",
policy: `package test
import rego.v1
import data.foo
import data.bar as foo
`,
expErrs: []string{
"test.rego:5: rego_compile_error: import must not shadow import data.foo",
},
},
{
note: "keywords imported, v1 compliant",
policy: `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
x := [1,2,3]
}`,
},
{
note: "keywords imported, NOT v1 compliant",
policy: `package test
import future.keywords.contains
p contains x {
x := [1,2,3]
}
q.r`,
expErrs: []string{
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "keywords imported, NOT v1 compliant (compiler)",
policy: `package test
import future.keywords.if
input := 1 if {
1 == 2
}`,
expErrs: []string{
"test.rego:4: rego_compile_error: rules must not shadow input (use a different rule name)",
},
},
{
note: "no imports, v1 compliant",
policy: `package test
p := 1
`,
},
{
note: "no imports, NOT v1 compliant but v0 compliant (compiler)",
policy: `package test
p.x`,
expErrs: []string{
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "no imports, v1 compliant but NOT v0 compliant",
policy: `package test
p contains x if {
x := [1,2,3]
}`,
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name", // This error actually appears three times: once for 'p'; once for 'contains'; and once for 'x'. All are interpreted as [invalid] rule declarations with no value and body.
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
},
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
params.regoV1 = true
err := checkModules(params, []string{root})
switch {
case err != nil && len(tc.expErrs) > 0:
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
}
}
return // don't read back bundle below
case err != nil && len(tc.expErrs) == 0:
t.Fatalf("unexpected error: %v", err)
case err == nil && len(tc.expErrs) > 0:
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
}
})
})
}
}
func TestCheck_DefaultRegoVersion(t *testing.T) {
cases := []struct {
note string
policy string
expErrs []string
}{
{
note: "v0 module",
policy: `package test
a[x] {
x := 42
}`,
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",
policy: `package test
a contains x if {
x := 42
}`,
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
err := checkModules(params, []string{root})
switch {
case err != nil && len(tc.expErrs) > 0:
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
}
}
return // don't read back bundle below
case err != nil && len(tc.expErrs) == 0:
t.Fatalf("unexpected error: %v", err)
case err == nil && len(tc.expErrs) > 0:
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
}
})
})
}
}
func TestCheckWithRegoV1Capability(t *testing.T) {
cases := []struct {
note string
v0Compatible bool
capabilities *ast.Capabilities
policy string
expErrs []string
}{
{
note: "v0 module, v0-compatible, no capabilities",
v0Compatible: true,
policy: `package test
a[x] {
x := 42
}`,
},
{
note: "v0 module, v0-compatible, v0 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
policy: `package test
a[x] {
x := 42
}`,
},
{
note: "v0 module, v0-compatible, v1 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
policy: `package test
a[x] {
x := 42
}`,
},
{
note: "v0 module, not v0-compatible, no capabilities",
policy: `package test
a[x] {
x := 42
}`,
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: "v0 module, not v0-compatible, v0 capabilities",
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
policy: `package test
a[x] {
x := 42
}`,
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: "v0 module, not v0-compatible, v0 capabilities without rego_v1 feature",
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
policy: `package test
a[x] {
x := 42
}`,
expErrs: []string{
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
},
},
{
note: "v0 module, not v0-compatible, v1 capabilities",
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
policy: `package test
a[x] {
x := 42
}`,
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, v0-compatible, no capabilities",
v0Compatible: true,
policy: `package test
a contains x if {
x := 42
}`,
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, v0-compatible, v0 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
policy: `package test
a contains x if {
x := 42
}`,
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, v0-compatible, v1 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
policy: `package test
a contains x if {
x := 42
}`,
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, not v0-compatible, no capabilities",
policy: `package test
a contains x if {
x := 42
}`,
},
{
note: "v1 module, not v0-compatible, v0 capabilities",
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
policy: `package test
a contains x if {
x := 42
}`,
},
{
note: "v1 module, not v0-compatible, v0 capabilities without rego_v1 feature",
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
policy: `package test
a contains x if {
x := 42
}`,
expErrs: []string{
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
},
},
{
note: "v1 module, not v0-compatible, v1 capabilities",
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
policy: `package test
a contains x if {
x := 42
}`,
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
params.v0Compatible = tc.v0Compatible
params.capabilities.C = tc.capabilities
err := checkModules(params, []string{root})
switch {
case err != nil && len(tc.expErrs) > 0:
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
}
}
return // don't read back bundle below
case err != nil && len(tc.expErrs) == 0:
t.Fatalf("unexpected error: %v", err)
case err == nil && len(tc.expErrs) > 0:
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
}
})
})
}
}
func TestCheckCompatibleFlags(t *testing.T) {
cases := []struct {
note string
v0Compatible bool
v1Compatible bool
policy string
expErrs []string
}{
{
note: "v0, no illegal keywords",
v0Compatible: true,
policy: `package test
p[x] {
x := [1,2,3]
}`,
},
{
note: "v0, illegal keywords",
v0Compatible: true,
policy: `package test
p contains x if {
x := [1,2,3]
}`,
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v0, future.keywords imported",
v0Compatible: true,
policy: `package test
import future.keywords
p contains x if {
x := [1,2,3]
}`,
},
{
note: "v0, rego.v1 imported",
v0Compatible: true,
policy: `package test
import rego.v1
p contains x if {
x := [1,2,3]
}`,
},
{
note: "v1, rego.v1 imported, v1 compliant",
v1Compatible: true,
policy: `package test
import rego.v1
p contains x if {
x := [1,2,3]
}`,
},
{
note: "v1, rego.v1 imported, NOT v1 compliant (parser)",
v1Compatible: true,
policy: `package test
import rego.v1
p contains x {
x := [1,2,3]
}
q.r`,
expErrs: []string{
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1, rego.v1 imported, NOT v1 compliant (compiler)",
v1Compatible: true,
policy: `package test
import rego.v1
import data.foo
import data.bar as foo
`,
expErrs: []string{
"test.rego:5: rego_compile_error: import must not shadow import data.foo",
},
},
{
note: "v1, keywords imported, v1 compliant",
v1Compatible: true,
policy: `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
x := [1,2,3]
}`,
},
{
note: "v1, keywords imported, NOT v1 compliant",
v1Compatible: true,
policy: `package test
import future.keywords.contains
p contains x {
x := [1,2,3]
}
q.r`,
expErrs: []string{
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
"test.rego:7: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1, keywords imported, NOT v1 compliant (compiler)",
v1Compatible: true,
policy: `package test
import future.keywords.if
input := 1 if {
1 == 2
}`,
expErrs: []string{
"test.rego:4: rego_compile_error: rules must not shadow input (use a different rule name)",
},
},
{
note: "v1, no imports, v1 compliant",
v1Compatible: true,
policy: `package test
p := 1
`,
},
{
note: "v1, no imports, NOT v1 compliant but v0 compliant (compiler)",
v1Compatible: true,
policy: `package test
p.x`,
expErrs: []string{
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1, no imports, v1 compliant but NOT v0 compliant",
v1Compatible: true,
policy: `package test
p contains x if {
x := [1,2,3]
}`,
},
// v0 takes precedence over v1
{
note: "v0+v1, no illegal keywords",
v0Compatible: true,
v1Compatible: true,
policy: `package test
p[x] {
x := [1,2,3]
}`,
},
{
note: "v0+v1, illegal keywords",
v0Compatible: true,
v1Compatible: true,
policy: `package test
p contains x if {
x := [1,2,3]
}`,
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v0+v1, future.keywords imported",
v0Compatible: true,
v1Compatible: true,
policy: `package test
import future.keywords
p contains x if {
x := [1,2,3]
}`,
},
{
note: "v0+v1, rego.v1 imported",
v0Compatible: true,
v1Compatible: true,
policy: `package test
import rego.v1
p contains x if {
x := [1,2,3]
}`,
},
}
for _, tc := range cases {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(root string) {
params := newCheckParams()
params.v0Compatible = tc.v0Compatible
params.v1Compatible = tc.v1Compatible
err := checkModules(params, []string{root})
switch {
case err != nil && len(tc.expErrs) > 0:
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
}
}
return // don't read back bundle below
case err != nil && len(tc.expErrs) == 0:
t.Fatalf("unexpected error: %v", err)
case err == nil && len(tc.expErrs) > 0:
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
}
})
})
}
}
func TestCheckWithBundleRegoVersion(t *testing.T) {
cases := []struct {
note string
files map[string]string
expErrs []string
}{
{
note: "v0.x bundle, illegal keywords",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
p contains x if {
x := [1,2,3]
}`,
},
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v0.x bundle, rego.v1 imported, v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import rego.v1
p contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v0.x bundle, rego.v1 imported, NOT v1 compliant (parser)",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import rego.v1
p contains x {
x := [1,2,3]
}
q.r`,
},
expErrs: []string{
"rego_parse_error: `if` keyword is required before rule body",
"rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v0.x bundle, rego.v1 imported, NOT v1 compliant (compiler)",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import rego.v1
import data.foo
import data.bar as foo
`,
},
expErrs: []string{
"rego_compile_error: import must not shadow import data.foo",
},
},
{
note: "v0.x bundle, keywords imported, v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v0.x bundle, no imports, v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
p := 1
`,
},
},
{
note: "v0 bundle, v1 per-file overrides, compliant",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[x] {
x := [1,2,3]
}`,
"policy2.rego": `package test
q contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v0 bundle, v1 per-file overrides (glob), compliant",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"*/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[x] {
x := [1,2,3]
}`,
"policy2.rego": `package test
q contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v0 bundle, v1 per-file overrides, incompliant",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[x] {
x := [1,2,3]
}`,
"policy2.rego": `package test
q[x] {
x := [1,2,3]
}`,
},
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, keywords used but not imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v1.0 bundle, rego.v1 imported, v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import rego.v1
p contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v1.0 bundle, rego.v1 imported, NOT v1 compliant (parser)",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import rego.v1
p contains x {
x := [1,2,3]
}
q.r`,
},
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, rego.v1 imported, NOT v1 compliant (compiler)",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import rego.v1
import data.foo
import data.bar as foo
`,
},
expErrs: []string{
"rego_compile_error: import must not shadow import data.foo",
},
},
{
note: "v1.0 bundle, keywords imported, v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import future.keywords.if
import future.keywords.contains
p contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v1.0 bundle, keywords imported, NOT v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import future.keywords.contains
p contains x {
x := [1,2,3]
}
q.r`,
},
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, keywords imported, NOT v1 compliant (compiler)",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import future.keywords.if
input := 1 if {
1 == 2
}`,
},
expErrs: []string{
"rego_compile_error: rules must not shadow input (use a different rule name)",
},
},
{
note: "v1.0 bundle, no imports, v1 compliant",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p := 1
`,
},
},
{
note: "v1.0 bundle, no imports, NOT v1 compliant but v0 compliant (compiler)",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p.x`,
},
expErrs: []string{
"rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1.0 bundle, no imports, v1 compliant but NOT v0 compliant",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
p contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v1 bundle, v0 per-file overrides, compliant",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package test
p[x] {
x := [1,2,3]
}`,
"policy2.rego": `package test
q contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v1 bundle, v0 per-file overrides (glob), compliant",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"*/policy1.rego": 0
}
}`,
"policy1.rego": `package test
p[x] {
x := [1,2,3]
}`,
"policy2.rego": `package test
q contains x if {
x := [1,2,3]
}`,
},
},
{
note: "v1 bundle, v0 per-file overrides, incompliant",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package test
p contains x if {
x := [1,2,3]
}`,
"policy2.rego": `package test
q contains x if {
x := [1,2,3]
}`,
},
expErrs: []string{
"rego_parse_error: var 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 cases {
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 := newCheckParams()
params.bundleMode = true
params.v1Compatible = v1CompatibleFlag.used
err := checkModules(params, []string{p})
switch {
case err != nil && len(tc.expErrs) > 0:
for _, expErr := range tc.expErrs {
if !strings.Contains(err.Error(), expErr) {
t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err)
}
}
return // don't read back bundle below
case err != nil && len(tc.expErrs) == 0:
t.Fatalf("unexpected error: %v", err)
case err == nil && len(tc.expErrs) > 0:
t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs)
}
})
})
}
}
}
}