Files
releases/cmd/exec_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

1604 lines
36 KiB
Go

//go:build !go1.27
package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"maps"
"net/http"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/open-policy-agent/opa/cmd/internal/exec"
"github.com/open-policy-agent/opa/internal/file/archive"
loggingtest "github.com/open-policy-agent/opa/v1/logging/test"
"github.com/open-policy-agent/opa/v1/plugins"
"github.com/open-policy-agent/opa/v1/sdk"
sdk_test "github.com/open-policy-agent/opa/v1/sdk/test"
"github.com/open-policy-agent/opa/v1/util/test"
)
type execOutput struct {
Result []execResultItem `json:"result"`
}
type execResultItem struct {
DecisionID string `json:"decision_id,omitempty"`
Path string `json:"path"`
Error execResultItemError `json:"error"`
Result *any `json:"result,omitempty"`
}
type execResultItemError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (r execResultItemError) isEmpty() bool {
return r.Code == "" && r.Message == ""
}
func toAnyPtr(a any) *any {
return &a
}
func toStringSlice(a *any) []string {
switch a := (*a).(type) {
case []string:
return a
case []any:
strSlice := make([]string, len(a))
for i := range a {
strSlice[i] = a[i].(string)
}
return strSlice
}
return nil
}
func resultSliceEquals(t *testing.T, expected, output []execResultItem) {
t.Helper()
if len(expected) != len(output) {
t.Fatalf("Expected %d results but got %d", len(expected), len(output))
}
for i := range output {
if expected[i].Path != output[i].Path {
t.Fatalf("Expected path %v but got %v", expected[i].Path, output[i].Path)
}
if expected[i].Error.isEmpty() {
if !output[i].Error.isEmpty() {
t.Fatalf("Expected no error but got %v", output[i].Error)
}
if !slices.Equal(toStringSlice(expected[i].Result), toStringSlice(output[i].Result)) {
t.Fatalf("Expected result %v but got %v", expected[i].Result, output[i].Result)
}
if !uuidPattern.MatchString(output[i].DecisionID) {
t.Fatalf("Expected decision ID to be a UUID but got %v", output[i].DecisionID)
}
} else {
if expected[i].Error.Code != output[i].Error.Code {
t.Fatalf("Expected error code %v but got %v", expected[i].Error.Code, output[i].Error.Code)
}
if expected[i].Error.Message != output[i].Error.Message {
t.Fatalf("Expected error message %v but got %v", expected[i].Error.Message, output[i].Error.Message)
}
if output[i].DecisionID != "" {
t.Fatalf("Expected no decision ID but got %v", output[i].DecisionID)
}
}
}
}
var uuidPattern = regexp.MustCompile(`^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`)
func TestExecBasic(t *testing.T) {
files := map[string]string{
"test.json": `{"foo": 7}`,
"test2.yaml": `bar: 8`,
"test3.yml": `baz: 9`,
"ignore": `garbage`, // do not recognize this filetype
}
test.WithTempFS(files, func(dir string) {
s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{
"test.rego": `
package system
main contains "hello"
`,
}))
defer s.Stop()
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.ConfigOverrides = []string{
"services.test.url=" + s.URL(),
"bundles.test.resource=/bundles/bundle.tar.gz",
}
params.Paths = append(params.Paths, dir)
err := runExec(params)
if err != nil {
t.Fatal(err)
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, []execResultItem{
{
Path: "/test.json",
Result: toAnyPtr([]string{"hello"}),
},
{
Path: "/test2.yaml",
Result: toAnyPtr([]string{"hello"}),
},
{
Path: "/test3.yml",
Result: toAnyPtr([]string{"hello"}),
},
}, output.Result)
})
}
func TestExecDecisionOption(t *testing.T) {
files := map[string]string{
"test.json": `{"foo": 7}`,
}
test.WithTempFS(files, func(dir string) {
s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{
"test.rego": `
package foo
main contains "hello"
`,
}))
defer s.Stop()
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.Decision = "foo/main"
params.ConfigOverrides = []string{
"services.test.url=" + s.URL(),
"bundles.test.resource=/bundles/bundle.tar.gz",
}
params.Paths = append(params.Paths, dir)
err := runExec(params)
if err != nil {
t.Fatal(err)
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, []execResultItem{
{
Path: "/test.json",
Result: toAnyPtr([]string{"hello"}),
},
}, output.Result)
})
}
func TestExecBundleFlag(t *testing.T) {
files := map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
main contains "hello"`,
}
test.WithTempFS(files, func(dir string) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.BundlePaths = []string{dir + "/bundle/"}
params.Paths = append(params.Paths, dir+"/files/")
err := runExec(params)
if err != nil {
t.Fatal(err)
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, []execResultItem{
{
Path: "/files/test.json",
Result: toAnyPtr([]string{"hello"}),
},
}, output.Result)
})
}
func TestExec_DefaultRegoVersion(t *testing.T) {
tests := []struct {
note string
module string
expErrs []string
}{
{
note: "v0, module",
module: `package system
main["hello"] {
input.foo == "bar"
}`,
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 system
main contains "hello" if {
input.foo == "bar"
}`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.json": `{"foo": "bar"}`,
}
test.WithTempFS(files, func(dir string) {
s := sdk_test.MustNewServer(
sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{"test.rego": tc.module}),
sdk_test.RawBundles(true),
)
defer s.Stop()
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.ConfigOverrides = []string{
"services.test.url=" + s.URL(),
"bundles.test.resource=/bundles/bundle.tar.gz",
}
params.Paths = append(params.Paths, dir)
if len(tc.expErrs) > 0 {
testLogger := loggingtest.New()
params.Logger = testLogger
// Wait for bundle server to be ready
bundleURL := s.URL() + "/bundles/bundle.tar.gz"
test.EventuallyOrFatal(t, 1*time.Second, func() bool {
resp, err := http.Get(bundleURL)
if resp != nil {
defer resp.Body.Close()
}
return err == nil && resp.StatusCode == 200
})
_ = runExec(params)
// Check logged errors
for _, expErr := range tc.expErrs {
found := false
for _, e := range testLogger.Entries() {
if strings.Contains(e.Message, expErr) {
found = true
break
}
}
if !found {
t.Errorf("Could not find expected logged error: %s in %v", expErr, testLogger.Entries())
}
}
} else {
err := runExec(params)
if err != nil {
t.Fatal(err)
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, []execResultItem{
{
Path: "/test.json",
Result: toAnyPtr([]string{"hello"}),
},
}, output.Result)
}
})
})
}
}
func TestExecCompatibleFlags(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
v1Compatible bool
module string
expErrs []string
}{
{
note: "v0, no keywords used",
v0Compatible: true,
module: `package system
main["hello"] {
input.foo == "bar"
}`,
},
{
note: "v0, no keywords imported",
v0Compatible: true,
module: `package system
main contains "hello" if {
input.foo == "bar"
}`,
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: string cannot be used for rule name",
},
},
{
note: "v0, keywords imported",
v0Compatible: true,
module: `package system
import future.keywords
main contains "hello" if {
input.foo == "bar"
}`,
},
{
note: "v0, rego.v1 imported",
v0Compatible: true,
module: `package system
import rego.v1
main contains "hello" if {
input.foo == "bar"
}`,
},
{
note: "v1, no keywords used",
v1Compatible: true,
module: `package system
main["hello"] {
input.foo == "bar"
}`,
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 keywords imported",
v1Compatible: true,
module: `package system
main contains "hello" if {
input.foo == "bar"
}`,
},
{
note: "v1, keywords imported",
v1Compatible: true,
module: `package system
import future.keywords
main contains "hello" if {
input.foo == "bar"
}`,
},
{
note: "v1, rego.v1 imported",
v1Compatible: true,
module: `package system
import rego.v1
main contains "hello" if {
input.foo == "bar"
}`,
},
// v0 takes precedence over v1
{
note: "v0+v1, no keywords used",
v0Compatible: true,
v1Compatible: true,
module: `package system
main["hello"] {
input.foo == "bar"
}`,
},
{
note: "v0+v1, no keywords imported",
v0Compatible: true,
v1Compatible: true,
module: `package system
main contains "hello" if {
input.foo == "bar"
}`,
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: string cannot be used for rule name",
},
},
{
note: "v0+v1, keywords imported",
v0Compatible: true,
v1Compatible: true,
module: `package system
import future.keywords
main contains "hello" if {
input.foo == "bar"
}`,
},
{
note: "v0+v1, rego.v1 imported",
v0Compatible: true,
v1Compatible: true,
module: `package system
import rego.v1
main contains "hello" if {
input.foo == "bar"
}`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.json": `{"foo": "bar"}`,
}
test.WithTempFS(files, func(dir string) {
s := sdk_test.MustNewServer(
sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{"test.rego": tc.module}),
sdk_test.RawBundles(true),
)
defer s.Stop()
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.V0Compatible = tc.v0Compatible
params.V1Compatible = tc.v1Compatible
_ = params.OutputFormat.Set("json")
params.ConfigOverrides = []string{
"services.test.url=" + s.URL(),
"bundles.test.resource=/bundles/bundle.tar.gz",
}
params.Paths = append(params.Paths, dir)
if len(tc.expErrs) > 0 {
testLogger := loggingtest.New()
params.Logger = testLogger
// Wait for bundle server to be ready
bundleURL := s.URL() + "/bundles/bundle.tar.gz"
test.EventuallyOrFatal(t, 1*time.Second, func() bool {
resp, err := http.Get(bundleURL)
if resp != nil {
defer resp.Body.Close()
}
return err == nil && resp.StatusCode == 200
})
_ = runExec(params)
// Check logged errors
for _, expErr := range tc.expErrs {
found := false
for _, e := range testLogger.Entries() {
if strings.Contains(e.Message, expErr) {
found = true
break
}
}
if !found {
t.Errorf("Could not find expected logged error: %s in %v", expErr, testLogger.Entries())
}
}
} else {
err := runExec(params)
if err != nil {
t.Fatal(err)
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, []execResultItem{
{
Path: "/test.json",
Result: toAnyPtr([]string{"hello"}),
},
}, output.Result)
}
})
})
}
}
func TestExecWithBundleRegoVersion(t *testing.T) {
tests := []struct {
note string
files map[string]string
expErrs []string
}{
{
note: "v0.x bundle, no keywords used",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package system
main["hello"] {
input.foo == "bar"
}`,
},
},
{
note: "v0.x bundle, no keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package system
main contains "hello" if {
input.foo == "bar"
}`,
},
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: string cannot be used for rule name",
},
},
{
note: "v0.x bundle, keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package system
import future.keywords
main contains "hello" if {
input.foo == "bar"
}`,
},
},
{
note: "v0.x bundle, rego.v1 imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package system
import rego.v1
main contains "hello" if {
input.foo == "bar"
}`,
},
},
{
note: "v0 bundle, v1 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package system
p[42] {
input.foo == "bar"
}`,
"policy2.rego": `package system
main contains "hello" if {
42 in p
}`,
},
},
{
note: "v0 bundle, v1 per-file override (glob)",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"*/policy2.rego": 1
}
}`,
"policy1.rego": `package system
p[42] {
input.foo == "bar"
}`,
"policy2.rego": `package system
main contains "hello" if {
42 in p
}`,
},
},
{
note: "v0 bundle, v1 per-file override, incompatible",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package system
p[42] {
input.foo == "bar"
}`,
"policy2.rego": `package system
main["hello"] {
p[_] == 42
}`,
},
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 used",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package system
main["hello"] {
input.foo == "bar"
}`,
},
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 imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package system
main contains "hello" if {
input.foo == "bar"
}`,
},
},
{
note: "v1.0 bundle, keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package system
import future.keywords
main contains "hello" if {
input.foo == "bar"
}`,
},
},
{
note: "v1.0 bundle, rego.v1 imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package system
import rego.v1
main contains "hello" if {
input.foo == "bar"
}`,
},
},
{
note: "v1 bundle, v0 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package system
p[42] {
input.foo == "bar"
}`,
"policy2.rego": `package system
main contains "hello" if {
42 in p
}`,
},
},
{
note: "v1 bundle, v0 per-file override (glob)",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"*/policy1.rego": 0
}
}`,
"policy1.rego": `package system
p[42] {
input.foo == "bar"
}`,
"policy2.rego": `package system
main contains "hello" if {
42 in p
}`,
},
},
{
note: "v1 bundle, v0 per-file override, incompatible",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package system
p contains 42 {
input.foo == "bar"
}`,
"policy2.rego": `package system
main contains "hello" if {
42 in p
}`,
},
expErrs: []string{
"rego_parse_error: var cannot be used for rule name",
"rego_parse_error: number cannot be used for rule name",
"rego_parse_error: set 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{
"files/test.json": `{"foo": "bar"}`,
}
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)
}
}
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.Paths = append(params.Paths, root+"/files/")
params.BundlePaths = []string{p}
params.V1Compatible = v1CompatibleFlag.used
_ = params.OutputFormat.Set("json")
if len(tc.expErrs) > 0 {
testLogger := loggingtest.New()
params.Logger = testLogger
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
// when bundles fail to parse, OPA never signals the ready channel, causing
// runExec to hang indefinitely. WithContext allows us to cancel the context
// when we have the required errors logged.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err := runExecWithContext(ctx, params)
// we cancelled the context, so we expect that error
if err != nil && err.Error() != "context canceled" {
// make sure it isn't an expected error
for _, msg := range tc.expErrs {
if strings.Contains(err.Error(), msg) {
return
}
}
t.Error(err)
return
}
}()
test.EventuallyOrFatal(t, 5*time.Second, func() bool {
for _, expErr := range tc.expErrs {
found := false
for _, e := range testLogger.Entries() {
if strings.Contains(e.Message, expErr) {
found = true
break
}
}
if !found {
return false
}
}
return true
})
cancel()
wg.Wait()
} else {
err := runExec(params)
if err != nil {
t.Fatal(err)
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(root), nil), &output); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, []execResultItem{
{
Path: "/files/test.json",
Result: toAnyPtr([]string{"hello"}),
},
}, output.Result)
}
})
})
}
}
}
}
func TestInvalidConfig(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.Fail = true
params.FailDefined = true
err := exec.Exec(t.Context(), nil, params)
if err == nil || err.Error() != "specify --fail or --fail-defined but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail or --fail-defined but not both", err.Error())
}
}
func TestInvalidConfigAllThree(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.Fail = true
params.FailDefined = true
params.FailNonEmpty = true
err := exec.Exec(t.Context(), nil, params)
if err == nil || err.Error() != "specify --fail or --fail-defined but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail or --fail-defined but not both", err.Error())
}
}
func TestInvalidConfigNonEmptyAndFail(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.FailNonEmpty = true
params.Fail = true
err := exec.Exec(t.Context(), nil, params)
if err == nil || err.Error() != "specify --fail-non-empty or --fail but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail-non-empty or --fail but not both", err.Error())
}
}
func TestInvalidConfigNonEmptyAndFailDefined(t *testing.T) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.FailNonEmpty = true
params.FailDefined = true
err := exec.Exec(t.Context(), nil, params)
if err == nil || err.Error() != "specify --fail-non-empty or --fail-defined but not both" {
t.Fatalf("Expected error '%s' but got '%s'", "specify --fail-non-empty or --fail-defined but not both", err.Error())
}
}
func TestFailFlagCases(t *testing.T) {
tests := []struct {
description string
files map[string]string
decision string
expectError bool
expected []byte
fail bool
failDefined bool
failNonEmpty bool
}{
{
description: "--fail-defined with undefined result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
},
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"error": {
"code": "opa_undefined_error",
"message": "/system/main decision was undefined"
}
}]}`),
failDefined: true,
},
{
description: "--fail-defined with populated result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
main contains "hello"`,
},
decision: "",
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": ["hello"]
}]}`),
failDefined: true,
},
{
description: "--fail-defined with true boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.defined.flag
import rego.v1
some_function if {
input.foo == 7
}
default fail_test := false
fail_test if {
some_function
}`,
},
decision: "fail/defined/flag/fail_test",
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": true
}]}`),
failDefined: true,
},
{
description: "--fail-defined with false boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.defined.flag
import rego.v1
default fail_test := false
fail_test if {
false
}`,
},
decision: "fail/defined/flag/fail_test",
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": false
}]}`),
failDefined: true,
},
{
description: "--fail with undefined result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
},
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"error": {
"code": "opa_undefined_error",
"message": "/system/main decision was undefined"
}
}]}`),
fail: true,
},
{
description: "--fail with populated result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
main contains "hello"`,
},
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": ["hello"]
}]}`),
fail: true,
},
{
description: "--fail with true boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.defined.flag
import rego.v1
some_function if {
input.foo == 7
}
default fail_test := false
fail_test if {
some_function
}`,
},
decision: "fail/defined/flag/fail_test",
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": true
}]}`),
fail: true,
},
{
description: "--fail with false boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.defined.flag
import rego.v1
default fail_test := false
fail_test if {
false
}`,
},
decision: "fail/defined/flag/fail_test",
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": false
}]}`),
fail: true,
},
{
description: "--fail-non-empty with undefined result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
},
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"error": {
"code": "opa_undefined_error",
"message": "/system/main decision was undefined"
}
}]}`),
failNonEmpty: true,
},
{
description: "--fail-non-empty with populated result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
main contains "hello"`,
},
decision: "",
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": ["hello"]
}]}`),
failNonEmpty: true,
},
{
description: "--fail-non-empty with true boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
import rego.v1
some_function if {
input.foo == 7
}
default fail_test := false
fail_test if {
some_function
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": true
}]}`),
failNonEmpty: true,
},
{
description: "--fail-non-empty with false boolean result",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
import rego.v1
default fail_test := false
fail_test if {
false
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: true,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": false
}]}`),
failNonEmpty: true,
},
{
description: "--fail-non-empty with an empty array",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
import rego.v1
default fail_test := ["something", "hello"]
fail_test := [] if {
input.foo == 7
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": []
}]}`),
failNonEmpty: true,
},
{
description: "--fail-non-empty for an empty set coming from a partial rule",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package fail.non.empty.flag
import rego.v1
fail_test contains message if {
false
message := "not gonna happen"
}`,
},
decision: "fail/non/empty/flag/fail_test",
expectError: false,
expected: []byte(`{"result": [{
"path": "/files/test.json",
"result": []
}]}`),
failNonEmpty: true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
test.WithTempFS(tt.files, func(dir string) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.BundlePaths = []string{dir + "/bundle/"}
params.Paths = append(params.Paths, dir+"/files/")
if tt.decision != "" {
params.Decision = tt.decision
}
params.FailDefined = tt.failDefined
params.Fail = tt.fail
params.FailNonEmpty = tt.failNonEmpty
err := runExec(params)
if err != nil && !tt.expectError {
t.Fatal("unexpected error in test")
}
if err == nil && tt.expectError {
t.Fatal("expected error, but none occurred in test")
}
var output execOutput
if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil {
t.Fatal(err)
}
var expected execOutput
if err := json.Unmarshal(tt.expected, &expected); err != nil {
t.Fatal(err)
}
resultSliceEquals(t, expected.Result, output.Result)
})
})
}
}
func TestExecJSONOutputBytes(t *testing.T) {
files := map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
}
test.WithTempFS(files, func(dir string) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.BundlePaths = []string{dir + "/bundle/"}
params.Paths = append(params.Paths, dir+"/files/")
params.FailDefined = true
if err := runExec(params); err != nil {
t.Fatal("unexpected error in test:", err)
}
actual := bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil)
expected := `{
"result": [
{
"path": "/files/test.json",
"error": {
"code": "opa_undefined_error",
"message": "/system/main decision was undefined"
}
}
]
}
`
if diff := cmp.Diff(expected, string(actual)); diff != "" {
t.Errorf("unexpected result (-want, +got):\n%s", diff)
}
})
}
func TestExecWithInvalidInputOptions(t *testing.T) {
tests := []struct {
description string
files map[string]string
stdIn bool
input string
expectError bool
expected string
}{
{
description: "path passed in as arg should not raise error",
files: map[string]string{
"files/test.json": `{"foo": 7}`,
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
},
expectError: false,
expected: "",
},
{
description: "no paths passed in as args should raise error if --stdin-input flag not set",
files: map[string]string{
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
},
expectError: true,
expected: "requires at least 1 path arg, or the --stdin-input flag",
},
{
description: "should not raise error if --stdin-input flag is set when no paths passed in as args",
files: map[string]string{
"bundle/x.rego": `package system
import rego.v1
test_fun := x if {
x = false
x
}
undefined_test if {
test_fun
}`,
},
stdIn: true,
input: `{"foo": 7}`,
expectError: false,
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
test.WithTempFS(tt.files, func(dir string) {
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.BundlePaths = []string{dir + "/bundle/"}
if tt.stdIn {
params.StdIn = true
tempFile, err := os.CreateTemp(t.TempDir(), "test")
if err != nil {
t.Fatalf("unexpected error creating temp file: %q", err.Error())
}
if _, err := tempFile.WriteString(tt.input); err != nil {
t.Fatalf("unexpeced error when writing to temp file: %q", err.Error())
}
if _, err := tempFile.Seek(0, 0); err != nil {
t.Fatalf("unexpected error when rewinding temp file: %q", err.Error())
}
oldStdin := os.Stdin
defer func() {
os.Stdin = oldStdin
os.Remove(tempFile.Name())
}()
os.Stdin = tempFile
} else {
if _, ok := tt.files["files/test.json"]; ok {
params.Paths = append(params.Paths, dir+"/files/")
}
}
err := runExec(params)
if err != nil && !tt.expectError {
t.Fatalf("unexpected error in test: %q", err.Error())
}
if err == nil && tt.expectError {
t.Fatalf("expected error %q, but none occurred in test", tt.expected)
}
if err != nil && err.Error() != tt.expected {
t.Fatalf("expected error %q, but got %q", tt.expected, err.Error())
}
})
})
}
}
func TestExecMalformedRemoteBundle(t *testing.T) {
bundlePath := "/bundles/bundle.tar.gz"
// Note(philipc): We add the "raw bundles" flag so that we can stuff a
// malformed bundle into the mock bundle server. Otherwise, the server
// will just return 503 errors forever, because it won't be able to
// build the bundle on its end.
s := sdk_test.MustNewServer(
sdk_test.RawBundles(true),
sdk_test.MockBundle(bundlePath, map[string]string{
"example.rego": `
package example
p := bits.sand(42, 43) # typo of bits.and
`,
}))
defer s.Stop()
// Wait for the bundle server to be ready before running exec
bundleURL := s.URL() + bundlePath
test.EventuallyOrFatal(t, 1*time.Second, func() bool {
resp, err := http.Get(bundleURL)
if resp != nil {
defer resp.Body.Close()
}
return err == nil && resp.StatusCode == 200
})
var buf bytes.Buffer
params := exec.NewParams(&buf)
_ = params.OutputFormat.Set("json")
params.ConfigOverrides = []string{
"services.test.url=" + s.URL(),
"bundles.test.resource=" + bundlePath,
}
// Note(philipc): We can set this timeout almost arbitrarily high or
// low-- the test will time out before it ever succeeds, due to the
// faulty bundle.
params.Timeout = time.Millisecond * 50
params.Paths = append(params.Paths, t.TempDir())
err := runExec(params)
if err == nil {
t.Fatalf("Expected error, got nil instead.")
}
exp := "runtime error: Bundle name: test, Code: bundle_error, HTTPCode: -1, Message: 1 error occurred: /example.rego:4: rego_type_error: undefined function bits.sand"
if !strings.HasPrefix(err.Error(), exp) {
t.Fatalf("Expected error: %s, got %s", exp, err.Error())
}
}
func TestExecStopsPlugins(t *testing.T) {
fact := &factory{}
sdk.SetDefaultOptions(sdk.Options{
Plugins: map[string]plugins.Factory{
"test_plugin": fact,
},
})
s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{
"test.rego": `
package system
main contains "hello"
`,
}))
defer s.Stop()
cfg := filepath.Join(t.TempDir(), "opa.yaml")
if err := os.WriteFile(cfg, []byte(`
plugins:
test_plugin: {}
`), 0x777); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
params := exec.NewParams(&buf)
params.ConfigFile = cfg
_ = params.OutputFormat.Set("json")
params.ConfigOverrides = []string{
"services.test.url=" + s.URL(),
"bundles.test.resource=/bundles/bundle.tar.gz",
}
dir := t.TempDir()
params.Paths = append(params.Paths, dir)
err := runExec(params)
if err != nil {
t.Fatal(err)
}
if !fact.stopped {
t.Errorf("expected plugin to be stopped")
}
}
type factory struct {
stopped bool
m *plugins.Manager
}
func (f *factory) New(m *plugins.Manager, _ any) plugins.Plugin {
f.m = m
return f
}
func (*factory) Validate(*plugins.Manager, []byte) (any, error) {
return nil, nil
}
func (f *factory) Start(context.Context) error {
f.m.UpdatePluginStatus("test_plugin", &plugins.Status{State: plugins.StateOK})
return nil
}
func (f *factory) Stop(context.Context) {
f.stopped = true
}
func (*factory) Reconfigure(context.Context, any) {
}