Files
releases/cmd/test_test.go
T
Sebastian Spaink 81987eebe4 fix: populate modules from bundles for coverage reporting in bundle mode (#8496)
resolve: https://github.com/open-policy-agent/opa/issues/3324

When running `opa test -c -b`, the coverage reporter received nil
modules because cmd/test.go only populated the modules variable in the
file-loading branch. This caused cover.Report() to skip the NotCovered
walk, resulting in 100% coverage regardless of actual test coverage.

Extract modules from bundles via ParsedModules() when both bundle mode
and coverage are enabled, so the coverage reporter can correctly
identify uncovered lines.

Tested the change locally as well with the steps in the issue.

Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
2026-05-08 09:45:58 -05:00

3809 lines
85 KiB
Go

package cmd
import (
"bytes"
"context"
"fmt"
"io"
"maps"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"syscall"
"testing"
"time"
"github.com/open-policy-agent/opa/internal/file/archive"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/ir"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/repl"
"github.com/open-policy-agent/opa/v1/storage/inmem"
"github.com/open-policy-agent/opa/v1/topdown"
"github.com/open-policy-agent/opa/v1/util/test"
)
func TestFilterTraceDefault(t *testing.T) {
p := newTestCommandParams()
p.verbose = false
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Enter data.testing.q
| | | | Enter data.testing.r
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, &p, expected)
}
func TestFilterTraceVerbose(t *testing.T) {
p := newTestCommandParams()
p.verbose = true
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Note "test test"
| | | Enter data.testing.q
| | | | Note "got this far"
| | | | Enter data.testing.r
| | | | | Note "got this far2"
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, &p, expected)
}
func TestFilterTraceExplainFails(t *testing.T) {
p := newTestCommandParams()
err := p.explain.Set(explainModeFails)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Enter data.testing.q
| | | | Enter data.testing.r
| | | | | Fail x = data.x
| | | | Fail data.testing.r[x]
| | | Fail data.testing.q.foo
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, &p, expected)
}
func TestFilterTraceExplainNotes(t *testing.T) {
p := newTestCommandParams()
err := p.explain.Set(explainModeNotes)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
expected := `Enter data.testing.test_p = _
| Enter data.testing.test_p
| | Enter data.testing.p
| | | Note "test test"
| | | Enter data.testing.q
| | | | Note "got this far"
| | | | Enter data.testing.r
| | | | | Note "got this far2"
`
verifyFilteredTrace(t, &p, expected)
}
func TestFilterTraceExplainFull(t *testing.T) {
p := newTestCommandParams()
err := p.explain.Set(explainModeFull)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
expected := `Enter data.testing.test_p = _
| Eval data.testing.test_p = _
| Index data.testing.test_p (matched 1 rule, early exit)
| Enter data.testing.test_p
| | Eval data.testing.p with data.x as "bar"
| | Index data.testing.p (matched 1 rule, early exit)
| | Enter data.testing.p
| | | Eval data.testing.x
| | | Index data.testing.x (matched 1 rule, early exit)
| | | Enter data.testing.x
| | | | Eval data.testing.y
| | | | Index data.testing.y (matched 1 rule, early exit)
| | | | Enter data.testing.y
| | | | | Eval true
| | | | | Exit data.testing.y early
| | | | Exit data.testing.x early
| | | Eval trace("test test")
| | | Note "test test"
| | | Eval data.testing.q.foo
| | | Index data.testing.q (matched 1 rule)
| | | Enter data.testing.q
| | | | Eval trace("got this far")
| | | | Note "got this far"
| | | | Eval data.testing.r[x]
| | | | Index data.testing.r (matched 1 rule)
| | | | Enter data.testing.r
| | | | | Eval trace("got this far2")
| | | | | Note "got this far2"
| | | | | Eval x = data.x
| | | | | Fail x = data.x
| | | | | Redo trace("got this far2")
| | | | Fail data.testing.r[x]
| | | | Redo trace("got this far")
| | | Fail data.testing.q.foo
| | | Redo trace("test test")
| | | Redo data.testing.x
| | | Redo data.testing.x
| | | | Redo data.testing.y
| | | | | Redo true
| | Fail data.testing.p with data.x as "bar"
| Fail data.testing.test_p = _
`
verifyFilteredTrace(t, &p, expected)
}
func TestThresholdRange(t *testing.T) {
thresholds := []float64{-1, 101}
for _, threshold := range thresholds {
if isThresholdValid(threshold) {
t.Fatalf("invalid threshold %2f shoul be reported", threshold)
}
}
}
func verifyFilteredTrace(t *testing.T, params *testCommandParams, expected string) {
filtered := filterTrace(params, failTrace(t))
var buff bytes.Buffer
topdown.PrettyTrace(&buff, filtered)
actual := buff.String()
if actual != expected {
t.Fatalf("Expected:\n\n%s\n\nGot:\n\n%s\n\n", expected, actual)
}
}
func failTrace(t *testing.T) []*topdown.Event {
t.Helper()
mod := `
package testing
p if {
x # Always true
trace("test test")
q["foo"]
}
x if {
y
}
y if {
true
}
q contains x if {
some x
trace("got this far")
r[x]
trace("got this far1")
}
r contains x if {
trace("got this far2")
x := data.x
}
test_p if {
p with data.x as "bar"
}
`
tracer := topdown.NewBufferTracer()
_, err := rego.New(
rego.Module("test.rego", mod),
rego.Trace(true),
rego.QueryTracer(tracer),
rego.Query("data.testing.test_p"),
).Eval(t.Context())
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
return *tracer
}
func TestPrettyTraceWithLocalVars(t *testing.T) {
tests := []struct {
note string
includeVars bool
files map[string]string
expected string
}{
{
note: "without vars",
includeVars: false,
files: map[string]string{
"test.rego": `package test
test_p if {
x := 1
y := 2
z := 3
x == z + y
}
`,
},
expected: `%.*%/test.rego:3:
data.test.test_p: FAIL (%.*%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%.*%)
query:1 %.*% Enter data.test.test_p = _
query:1 %.*% | Eval data.test.test_p = _
query:1 %.*% | Index data.test.test_p (matched 1 rule, early exit)
%.*%/test.rego:3 | Enter data.test.test_p
%.*%/test.rego:4 | | Eval x = 1
%.*%/test.rego:5 | | Eval y = 2
%.*%/test.rego:6 | | Eval z = 3
%.*%/test.rego:7 | | Eval plus(z, y, __local3__)
%.*%/test.rego:7 | | Eval x = __local3__
%.*%/test.rego:7 | | Fail x = __local3__
%.*%/test.rego:7 | | Redo plus(z, y, __local3__)
%.*%/test.rego:6 | | Redo z = 3
%.*%/test.rego:5 | | Redo y = 2
%.*%/test.rego:4 | | Redo x = 1
query:1 %.*% | Fail data.test.test_p = _
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "with vars",
includeVars: true,
files: map[string]string{
"test.rego": `package test
test_p if {
x := 1
y := 2
z := 3
x == z + y
}
`,
},
expected: `%.*%/test.rego:3:
data.test.test_p: FAIL (%.*%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%.*%)
query:1 %.*% Enter data.test.test_p = _ {}
query:1 %.*% | Eval data.test.test_p = _ {}
query:1 %.*% | Index data.test.test_p (matched 1 rule, early exit) {}
%.*%/test.rego:3 | Enter data.test.test_p {}
%.*%/test.rego:4 | | Eval x = 1 {}
%.*%/test.rego:5 | | Eval y = 2 {}
%.*%/test.rego:6 | | Eval z = 3 {}
%.*%/test.rego:7 | | Eval plus(z, y, __local3__) {y: 2, z: 3}
%.*%/test.rego:7 | | Eval x = __local3__ {__local3__: 5, x: 1}
%.*%/test.rego:7 | | Fail x = __local3__ {__local3__: 5, x: 1}
%.*%/test.rego:7 | | Redo plus(z, y, __local3__) {__local3__: 5, y: 2, z: 3}
%.*%/test.rego:6 | | Redo z = 3 {z: 3}
%.*%/test.rego:5 | | Redo y = 2 {y: 2}
%.*%/test.rego:4 | | Redo x = 1 {x: 1}
query:1 %.*% | Fail data.test.test_p = _ {}
%.*%/test.rego:7:
x == z + y
| | |
| | 2
| z + y: 5
| z: 3
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(tc.files, func(root string) {
buf := new(bytes.Buffer)
testParams := newTestCommandParams()
testParams.count = 1
testParams.output = buf
testParams.errOutput = io.Discard
testParams.bundleMode = true
testParams.verbose = true
testParams.varValues = tc.includeVars
_ = testParams.explain.Set(explainModeFull)
errorCode := opaTest([]string{root}, testParams)
if errorCode != 2 {
t.Fatalf("Unexpected error code: %d", errorCode)
}
actual := buf.String()
if !stringsMatch(t, tc.expected, actual) {
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", tc.expected, actual)
}
})
})
}
}
func TestFailVarValues(t *testing.T) {
tests := []struct {
note string
files map[string]string
expected string
}{
{
note: "simple",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := 2
z := 3
x == y + z
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
x == y + z
| | |
| | 3
| y + z: 5
| y: 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "simple (not)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 5
y := 2
z := 3
not x == y + z
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
not x == y + z
| | |
| | 3
| y + z: 5
| y: 2
5
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "array",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := [1, 2, 3]
z := 3
x == y[2] + z
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
x == y[2] + z
| | |
| | 3
| y[2] + z: 6
| y[2]: 3
| y: [1, 2, 3]
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "array, var key",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := [1, 2, 3]
z := 3
i := 2
x == y[i] + z
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:8:
x == y[i] + z
| | | |
| | | 3
| | 2
| y[i] + z: 6
| y[i]: 3
| y: [1, 2, 3]
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "array containing vars",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := 2
z := 3
[x, y, z] == [4, 5, 6]
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
[x, y, z] == [4, 5, 6]
| | |
| | 3
| 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "array containing refs",
files: map[string]string{
"/test.rego": `package test
a := 1
b := 2
test_foo if {
[a, data.test.b, data.c] == [4, 5, 6]
}
`,
"data.json": `{"c": 3}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:8:
[a, data.test.b, data.c] == [4, 5, 6]
| | |
| | 3
| 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "array containing refs, undefined",
files: map[string]string{
"/test.rego": `package test
a := 1
b := data.b
test_foo if {
[a, b, data.c] == [4, 5, 6]
}
`,
"data.json": `{"c": 3}`,
},
// Note: each dynamic array element is broken out into a separate "co-expression" by the compiler.
// Since we failed on the 2nd element (b), we don't have value for the 3rd element (data.c).
expected: `%ROOT%/test.rego:7:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:8:
[a, b, data.c] == [4, 5, 6]
| |
| undefined
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "nested collections containing vars",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := 2
z := 3
[x, {y, {"a": z}}] == [4, {5, {"a": 6}}]
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
[x, {y, {"a": z}}] == [4, {5, {"a": 6}}]
| | |
| | 3
| 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "single line expression containing tabs",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := 2
z := 3
x == y + z
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
x == y + z
| | |
| | 3
| y + z: 5
| y: 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "single line expression containing tabs #2",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := 2
z := 3
x == y + z
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
x == y + z
| | |
| | 3
| y + z: 5
| y: 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "multi-line expression containing tabs",
files: map[string]string{
"/test.rego": `package test
test_foo if {
x := 1
y := 2
z := 3
obj := {
"foo_": 1,
"bar__": 42,
"baz": 3,
}
obj == {
"foo_": x,
"bar__": y,
"baz": z,
}
}
`,
},
// We can't deal with tabs in a consistent manner when they occur on multiple lines
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:12:
obj == {
"foo_": x,
"bar__": y,
"baz": z,
}
Where:
obj: {"bar__": 42, "baz": 3, "foo_": 1}
x: 1
y: 2
z: 3
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "composite rule",
files: map[string]string{
"/test.rego": `package test
p contains v if {
some v in numbers.range(1, 3)
}
test_p if {
p == {4, 5, 6}
}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:8:
p == {4, 5, 6}
|
{1, 2, 3}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "composite rule with ref-head",
files: map[string]string{
"/test.rego": `package test
p.q contains v if {
some v in numbers.range(1, 3)
}
test_p if {
p.q == {4, 5, 6}
}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:8:
p.q == {4, 5, 6}
|
{1, 2, 3}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "composite rule with ref-head, partial ref",
files: map[string]string{
"/test.rego": `package test
p.q contains v if {
some v in numbers.range(1, 3)
}
test_p if {
p == {
"q": {4, 5, 6}
}
}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:8:
p == {
"q": {4, 5, 6}
}
|
{"q": {1, 2, 3}}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "composite rules with ref-head, composite value",
files: map[string]string{
"/test.rego": `package test
p.q contains v if {
some v in numbers.range(1, 3)
}
p.r := "foo"
test_p if {
p == {
"q": {4, 5, 6},
"r": "bar"
}
}`,
},
expected: `%ROOT%/test.rego:9:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:10:
p == {
"q": {4, 5, 6},
"r": "bar"
}
|
{"q": {1, 2, 3}, "r": "foo"}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "refs in different compiled sub-expressions",
files: map[string]string{
"/test.rego": `package test
a := 1
b := 2
c := 3
test_p if {
# This expression is split into multiple final expressions by the compiler, each containing a rule ref
a == b + c
}
`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:9:
a == b + c
| | |
| | 3
| b + c: 5
| b: 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "rule not defined",
files: map[string]string{
"/test.rego": `package test
p if {
input.x == 1
}
test_p if {
p with input.x as 2
}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:8:
p with input.x as 2
|
undefined
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "rule defined (not)",
files: map[string]string{
"/test.rego": `package test
p if {
input.x == 1
}
test_p if {
not p with input.x as 1
}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:8:
not p with input.x as 1
|
true
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "data ref",
files: map[string]string{
"/test.rego": `package test
test_foo if {
y := 1
data.x == y
}
`,
"data.json": `{"x": 2}`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:5:
data.x == y
| |
| 1
2
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "data + virtual extent ref",
files: map[string]string{
"/test.rego": `package test
foo.x := 1
test_foo if {
y := {"x": 1, "y": 42}
foo == y
}
`,
"data.json": `{"test": {"foo": {"y": 2}}}`,
},
expected: `%ROOT%/test.rego:5:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
foo == y
| |
| {"x": 1, "y": 42}
{"x": 1, "y": 2}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "in (array)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := ["a", "b", "c"]
x := "q"
x in l
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:6:
x in l
| |
| ["a", "b", "c"]
"q"
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "in (set)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := {"a", "b", "c"}
x := "q"
x in l
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:6:
x in l
| |
| {"a", "b", "c"}
"q"
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "comprehension (array)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := ["a", "b", "c"]
[x | x := l[_]] == ["d", "e", "f"]
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:5:
[x | x := l[_]] == ["d", "e", "f"]
|
["a", "b", "c"]
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "comprehension (set)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := ["a"]
{x | x := l[_]} == {"b"}
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:5:
{x | x := l[_]} == {"b"}
|
{"a"}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "comprehension (object)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := ["a", "b", "c"]
{k: x | x := l[k]} == {3: "d", 4: "e", 5: "f"}
}
`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:5:
{k: x | x := l[k]} == {3: "d", 4: "e", 5: "f"}
|
{0: "a", 1: "b", 2: "c"}
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "every",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := [1, 2, 3]
every x in l {
x == 1
}
}`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:6:
x == 1
|
2
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "comprehension inside every",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := [1, 2, 3]
every x in l {
[v | v := x] == [42]
}
}`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:6:
[v | v := x] == [42]
|
[1]
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "nested every",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := [[1, 2], [3, 4], [5, 6]]
every x in l {
every y in x {
y < 4
}
}
}`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
y < 4
|
4
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "nested every with comprehension",
files: map[string]string{
"/test.rego": `package test
test_foo if {
l := [[1, 2], [3, 4], [5, 6]]
every x in l {
every y in x {
[v | v := y] == [42]
}
}
}`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
[v | v := y] == [42]
|
[1]
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "ref equality",
files: map[string]string{
"/test.rego": `package test
a := 1
b := 2
test_foo if {
a == b
}`,
},
expected: `%ROOT%/test.rego:6:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:7:
a == b
| |
| 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "ref equality (data)",
files: map[string]string{
"/test.rego": `package test
test_foo if {
data.a == data.b
}`,
"data.json": `{"a": 1, "b": 2}`,
},
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:4:
data.a == data.b
| |
| 2
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "with, containing local vars",
files: map[string]string{
"/test.rego": `package test
p := input.x
test_p if {
a := 1
p == 2 with input.x as a
}`,
},
expected: `%ROOT%/test.rego:5:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:7:
p == 2 with input.x as a
| |
| 1
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "with, containing ref",
files: map[string]string{
"/test.rego": `package test
p := input.x
testInput := {"x": 1}
test_p if {
p == 2 with input as testInput
}`,
},
expected: `%ROOT%/test.rego:7:
data.test.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_p: FAIL (%TIME%)
%ROOT%/test.rego:8:
p == 2 with input as testInput
| |
| {"x": 1}
1
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "negated rule ref",
files: map[string]string{
"/test.rego": `package test
a if {true}
test_foo if {
not a
}`,
"data.json": `{"a": true}`,
},
expected: `%ROOT%/test.rego:5:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:6:
not a
|
true
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
{
note: "negated data ref",
files: map[string]string{
"/test.rego": `package test
test_foo if {
not data.a
}`,
"data.json": `{"a": true}`,
},
// Because of the negated expr, the compiler will have opted out of rewriting the expression to
// capture the value of data.a in a local variable, and since data.a isn't in the local bindings
// or in the virtual cache, we don't know if it's undefined or unknown, and therefore can't report
// on a value.
expected: `%ROOT%/test.rego:3:
data.test.test_foo: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAILURES
--------------------------------------------------------------------------------
data.test.test_foo: FAIL (%TIME%)
%ROOT%/test.rego:4:
not data.a
--------------------------------------------------------------------------------
FAIL: 1/1
`,
},
}
r := regexp.MustCompile(`FAIL \(.*s\)`)
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(tc.files, func(root string) {
buf := new(bytes.Buffer)
testParams := newTestCommandParams()
testParams.count = 1
testParams.output = buf
testParams.errOutput = io.Discard
testParams.bundleMode = true
testParams.varValues = true
_ = testParams.explain.Set(explainModeFull)
exitCode := opaTest([]string{root}, testParams)
if exitCode != 2 {
t.Fatalf("Unexpected error code: %d", exitCode)
}
actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
expected := strings.ReplaceAll(tc.expected, "%ROOT%", root)
if !stringsMatch(t, expected, actual) {
t.Fatalf("Expected output to be:\n\n%s\n\ngot:\n\n%s", expected, actual)
}
})
})
}
}
// Assert that ignore flag is correctly used when the bundle flag is activated
func TestIgnoreFlag(t *testing.T) {
files := map[string]string{
"/test.rego": `package test
p := input.foo == 42
test_p if {
p with input.foo as 42
}`,
"/broken.rego": "package foo\n bar {",
}
var exitCode int
test.WithTempFS(files, func(root string) {
testParams := newTestCommandParams()
testParams.count = 1
testParams.errOutput = io.Discard
testParams.bundleMode = false
testParams.ignore = []string{"broken.rego"}
exitCode = opaTest([]string{root}, testParams)
})
if exitCode > 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
}
// Assert that ignore flag is correctly used when the bundle flag is activated
func TestIgnoreFlagWithBundleFlag(t *testing.T) {
files := map[string]string{
"/test.rego": `package test
p := input.foo == 42
test_p if {
p with input.foo as 42
}`,
"/broken.rego": "package foo\n bar {",
}
var exitCode int
test.WithTempFS(files, func(root string) {
testParams := newTestCommandParams()
testParams.count = 1
testParams.errOutput = io.Discard
testParams.bundleMode = true
testParams.ignore = []string{"broken.rego"}
exitCode = opaTest([]string{root}, testParams)
})
if exitCode > 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
}
func testSchemasAnnotation(rego string) (int, []byte) {
files := map[string]string{
"test.rego": rego,
}
var exitCode int
var buf bytes.Buffer
test.WithTempFS(files, func(path string) {
regoFilePath := filepath.Join(path, "test.rego")
testParams := newTestCommandParams()
testParams.count = 1
testParams.errOutput = &buf
exitCode = opaTest([]string{regoFilePath}, testParams)
})
return exitCode, buf.Bytes()
}
// Assert that 'schemas' annotations with schema ref are ignored, but not inlined schemas
func TestSchemasAnnotation(t *testing.T) {
policyWithSchemaRef := `
package test
# 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
}
test_p if {
p with input.foo as 42
}`
exitCode, _ := testSchemasAnnotation(policyWithSchemaRef)
if exitCode > 0 {
t.Fatalf("unexpected error when schema ref is present")
}
}
func TestSchemasAnnotationInline(t *testing.T) {
policyWithInlinedSchema := `
package test
# METADATA
# schemas:
# - input.foo: {"type": "boolean"}
p if {
input.foo == 42 # type mismatch with schema that should NOT be ignored since it is an inlined schema format
}
test_p if {
p with input.foo as 42
}`
exitCode, errOutput := testSchemasAnnotation(policyWithInlinedSchema)
// We expect an error here, as inlined schemas are always used for type checking
if exitCode == 0 {
t.Fatalf("didn't get expected error when inlined schema is present")
}
if !strings.Contains(string(errOutput), "rego_type_error: match error") {
t.Fatalf("didn't get expected %s error when inlined schema is present; got: %v", ast.TypeErr, string(errOutput))
}
}
func testSchemasAnnotationWithJSONFile(rego string, schema string) (int, []byte) {
files := map[string]string{
"test.rego": rego,
"demo_schema.json": schema,
}
var exitCode int
var buf bytes.Buffer
test.WithTempFS(files, func(path string) {
regoFilePath := filepath.Join(path, "test.rego")
testParams := newTestCommandParams()
testParams.count = 1
testParams.schema.path = path
testParams.errOutput = &buf
exitCode = opaTest([]string{regoFilePath}, testParams)
})
return exitCode, buf.Bytes()
}
func TestJSONSchemaSuccess(t *testing.T) {
regoContents := `package test
# METADATA
# schemas:
# - input: schema.demo_schema
p if {
input.foo == 42
}
test_p if {
p with input.foo as 42
}`
schema := `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "schema",
"type": "object",
"description": "The root schema comprises the entire JSON document.",
"required": [
"foo"
],
"properties": {
"foo": {
"$id": "#/properties/foo",
"type": "number",
"description": "foo"
}
},
"additionalProperties": false
}`
errorCode, _ := testSchemasAnnotationWithJSONFile(regoContents, schema)
if errorCode != 0 {
t.Fatalf("unexpected error code: %d", errorCode)
}
}
func TestJSONSchemaFail(t *testing.T) {
regoContents := `package test
# METADATA
# schemas:
# - input: schema.demo_schema
p if {
input.foo == 42
}
test_p if {
p with input.foo as 42
}`
schema := `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "schema",
"type": "object",
"description": "The root schema comprises the entire JSON document.",
"required": [
"foo"
],
"properties": {
"foo": {
"$id": "#/properties/foo",
"type": "boolean",
"description": "foo"
}
},
"additionalProperties": false
}`
exitCode, errOutput := testSchemasAnnotationWithJSONFile(regoContents, schema)
if exitCode == 0 {
t.Fatalf("didn't get expected error when schema is present and is defining a different type than being used.")
}
if !strings.Contains(string(errOutput), "rego_type_error: match error") {
t.Fatalf("didn't get expected %s error when schema is defining a different type than being used; got: %v", ast.TypeErr, string(errOutput))
}
}
func TestWatchMode(t *testing.T) {
files := map[string]string{
"/policy.rego": `package foo
p := 1`,
"/policy_test.rego": `package foo
test_p if {
p == 1
}`,
}
test.WithTempFS(files, func(root string) {
buf := test.BlockingWriter{}
testParams := newTestCommandParams()
testParams.output = &buf
testParams.watch = true
testParams.count = 1
done := make(chan struct{})
go func() {
_ = opaTest([]string{root}, testParams)
<-done
}()
expected := "Watching for changes ..."
if !test.Eventually(t, 2*time.Second, func() bool {
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update the test
f, _ := os.OpenFile(path.Join(root, "policy_test.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err := f.WriteString("package foo\n test_p if { p == 2 }")
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
r := regexp.MustCompile(`FAIL \(.*s\)`)
expected = `%ROOT%/policy_test.rego:2:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(actual, expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update policy so test passes
f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err = f.WriteString("package foo\n p := 2")
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// add new policy and test
if err := os.WriteFile(path.Join(root, "policy2.rego"), []byte("package bar\n q := \"hello\""), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path.Join(root, "policy2_test.rego"), []byte("package bar\n test_q if { q == \"hello\" }"), 0644); err != nil {
t.Fatal(err)
}
expected = `PASS: 2/2
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
testParams.stopChan <- syscall.SIGINT
done <- struct{}{}
})
}
func TestWatchMode_v0(t *testing.T) {
files := map[string]string{
"/policy.rego": `package foo
p := 1`,
"/policy_test.rego": `package foo
test_p {
p == 1
}`,
}
test.WithTempFS(files, func(root string) {
buf := test.BlockingWriter{}
testParams := newTestCommandParams()
testParams.output = &buf
testParams.watch = true
testParams.count = 1
testParams.v0Compatible = true
done := make(chan struct{})
go func() {
_ = opaTest([]string{root}, testParams)
<-done
}()
expected := "Watching for changes ..."
if !test.Eventually(t, 2*time.Second, func() bool {
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update the test
f, _ := os.OpenFile(path.Join(root, "policy_test.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err := f.WriteString("package foo\n test_p { p == 2 }")
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
r := regexp.MustCompile(`FAIL \(.*s\)`)
expected = `%ROOT%/policy_test.rego:2:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(actual, expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update policy so test passes
f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err = f.WriteString("package foo\n p := 2")
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// add new policy and test
if err := os.WriteFile(path.Join(root, "policy2.rego"), []byte("package bar\n q := \"hello\""), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path.Join(root, "policy2_test.rego"), []byte("package bar\n test_q { q == \"hello\" }"), 0644); err != nil {
t.Fatal(err)
}
expected = `PASS: 2/2
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
testParams.stopChan <- syscall.SIGINT
done <- struct{}{}
})
}
func TestWatchModeWithDataFile(t *testing.T) {
files := map[string]string{
"/policy.rego": `package foo
test_p if {
data.y == 1
}`,
"/data.json": `{"y": 1}`,
}
test.WithTempFS(files, func(root string) {
buf := test.BlockingWriter{}
testParams := newTestCommandParams()
testParams.output = &buf
testParams.watch = true
testParams.count = 1
done := make(chan struct{})
go func() {
_ = opaTest([]string{root}, testParams)
<-done
}()
expected := "Watching for changes ..."
if !test.Eventually(t, 2*time.Second, func() bool {
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update the data
f, _ := os.OpenFile(path.Join(root, "data.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err := f.WriteString(`{"y": 2}`)
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
r := regexp.MustCompile(`FAIL \(.*s\)`)
expected = `%ROOT%/policy.rego:3:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(actual, expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update policy so test passes
f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err = f.WriteString("package foo\n test_p if { data.y == 2 }")
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
testParams.stopChan <- syscall.SIGINT
done <- struct{}{}
})
}
func TestWatchModeWhenDataFileRemoved(t *testing.T) {
files := map[string]string{
"/policy.rego": `package foo
test_p if {
data.y == 1
}`,
"/data.json": `{"y": 1}`,
}
test.WithTempFS(files, func(root string) {
buf := test.BlockingWriter{}
testParams := newTestCommandParams()
testParams.output = &buf
testParams.watch = true
testParams.count = 1
done := make(chan struct{})
go func() {
_ = opaTest([]string{root}, testParams)
<-done
}()
expected := "Watching for changes ..."
if !test.Eventually(t, 2*time.Second, func() bool {
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update the data
f, _ := os.OpenFile(path.Join(root, "data.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err := f.WriteString(`{"y": 2}`)
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
r := regexp.MustCompile(`FAIL \(.*s\)`)
expected = `%ROOT%/policy.rego:3:
data.foo.test_p: FAIL (%TIME%)
--------------------------------------------------------------------------------
FAIL: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)")
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(actual, expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// update the data back to the original state, so the opa test passes
f, _ = os.OpenFile(path.Join(root, "data.json"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err = f.WriteString(`{"y": 1}`)
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
expected = `PASS: 1/1
********************************************************************************
Watching for changes ...
`
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// remove the data file, check that test fails afterward
err = os.Remove(path.Join(root, "data.json"))
if err != nil {
t.Fatal(err)
}
time.Sleep(500 * time.Millisecond)
testParams.stopChan <- syscall.SIGINT
done <- struct{}{}
})
}
func TestWatchModeBrokenFileRecovery(t *testing.T) {
tests := []struct {
note string
fileName string
brokenFile string
fixedFile string
expectedOutput string
}{
{
note: "empty data file (EOF read by watcher)",
fileName: "data.json",
fixedFile: `{"foo": "bar"}`,
expectedOutput: `1 error occurred during loading: %ROOT%/data.json: EOF
********************************************************************************
Watching for changes ...`,
},
{
note: "broken policy",
fileName: "broken_policy.rego",
brokenFile: "package foo\n bar {",
fixedFile: "package foo\n bar if {true}",
expectedOutput: `1 error occurred during loading: %ROOT%/broken_policy.rego:2: rego_parse_error: unexpected eof token
bar {
^
********************************************************************************
Watching for changes ...`,
},
}
files := map[string]string{
"/policy.rego": `package foo
p := 1`,
"/policy_test.rego": `package foo
test_p if {
p == 1
}`,
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(files, func(root string) {
buf := test.BlockingWriter{}
testParams := newTestCommandParams()
testParams.output = &buf
testParams.watch = true
testParams.count = 1
testParams.errOutput = io.Discard
done := make(chan struct{})
go func() {
_ = opaTest([]string{root}, testParams)
<-done
}()
expected := "Watching for changes ..."
if !test.Eventually(t, 2*time.Second, func() bool {
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
// create broken (possibly empty) file
f, _ := os.OpenFile(path.Join(root, tc.fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if len(tc.brokenFile) > 0 {
_, err := f.WriteString(tc.brokenFile)
if err != nil {
t.Fatal(err)
}
}
err := f.Close()
if err != nil {
t.Fatal(err)
}
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(tc.expectedOutput, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", tc.expectedOutput, buf.String())
}
buf.Reset()
// write data to empty file
f, _ = os.OpenFile(path.Join(root, tc.fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
_, err = f.WriteString(tc.fixedFile)
if err != nil {
t.Fatal(err)
}
err = f.Close()
if err != nil {
t.Fatal(err)
}
expected = "Watching for changes ..."
if !test.Eventually(t, 2*time.Second, func() bool {
expected := strings.ReplaceAll(expected, "%ROOT%", root)
return strings.Contains(buf.String(), expected)
}) {
t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String())
}
buf.Reset()
testParams.stopChan <- syscall.SIGINT
done <- struct{}{}
})
})
}
}
func testExitCode(rego string, skipExitZero bool) int {
files := map[string]string{
"test.rego": rego,
}
var exitCode int
test.WithTempFS(files, func(path string) {
regoFilePath := filepath.Join(path, "test.rego")
testParams := newTestCommandParams()
testParams.count = 1
testParams.skipExitZero = skipExitZero
testParams.errOutput = io.Discard
testParams.output = io.Discard
exitCode = opaTest([]string{regoFilePath}, testParams)
})
return exitCode
}
func testExitCodeWithFailOnEmpty(rego string, failOnEmpty bool) int {
files := map[string]string{
"test.rego": rego,
}
var exitCode int
test.WithTempFS(files, func(path string) {
regoFilePath := filepath.Join(path, "test.rego")
testParams := newTestCommandParams()
testParams.count = 1
testParams.failOnEmpty = failOnEmpty
testParams.errOutput = io.Discard
testParams.output = io.Discard
exitCode = opaTest([]string{regoFilePath}, testParams)
})
return exitCode
}
func TestExitCode(t *testing.T) {
testCases := map[string]struct {
Test string
ExitZeroOnSkipped bool
ExpectedExitCode int
}{
"pass when no failed or skipped tests": {
Test: `package foo
test_pass if { true }
`,
ExitZeroOnSkipped: false,
ExpectedExitCode: 0,
},
"fail when failed tests": {
Test: `package foo
test_pass if { true }
test_fail if { false }
`,
ExitZeroOnSkipped: false,
ExpectedExitCode: 2,
},
"fail when skipped tests": {
Test: `package foo
test_pass if { true }
todo_test_skip if { true }
`,
ExitZeroOnSkipped: false,
ExpectedExitCode: 2,
},
"fail when failed tests and skipped tests": {
Test: `package foo
test_pass if { true }
test_fail if { false }
todo_test_skip if { true }
`,
ExitZeroOnSkipped: false,
ExpectedExitCode: 2,
},
"pass when skipped tests and exit zero on skipped": {
Test: `package foo
test_pass if { true }
todo_test_skip if { true }
`,
ExitZeroOnSkipped: true,
ExpectedExitCode: 0,
},
"fail when failed tests and exit zero on skipped": {
Test: `package foo
test_pass if { true }
test_fail if { false }
`,
ExitZeroOnSkipped: true,
ExpectedExitCode: 2,
},
"fail when failed tests, skipped tests and exit zero on skipped": {
Test: `package foo
test_pass if { true }
test_fail if { false }
todo_test_skip if { true }
`,
ExitZeroOnSkipped: true,
ExpectedExitCode: 2,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
exitCode := testExitCode(tc.Test, tc.ExitZeroOnSkipped)
if exitCode != tc.ExpectedExitCode {
t.Errorf("Expected exit code to be %d but got %d", tc.ExpectedExitCode, exitCode)
}
})
}
}
func TestCoverageThreshold(t *testing.T) {
testCases := []struct {
note string
modules map[string]string
threshold float64
verbose bool
expectedErrOutput string
expectedExitCode int
}{
{
note: "coverage threshold met",
modules: map[string]string{
"test.rego": `package test
p := 1
test_p if { p == 1 }`,
},
expectedExitCode: 0,
},
{
note: "coverage threshold not met",
modules: map[string]string{
"test.rego": `package test
p := 1 if {
1 == 1
}
q := 2
r := 3
test_q if { q == 2 }`,
},
threshold: 100,
expectedExitCode: 2,
expectedErrOutput: "Code coverage threshold not met: got 40.00 instead of 100.00\n",
},
{
note: "coverage threshold not met (verbose)",
modules: map[string]string{
"test.rego": `package test
p := 1 if {
1 == 1
}
q := 2
r := 3
test_q if { q == 2 }`,
},
threshold: 100,
expectedExitCode: 2,
verbose: true,
expectedErrOutput: `Code coverage threshold not met: got 40.00 instead of 100.00
Lines not covered:
%ROOT%/test.rego:3-4
%ROOT%/test.rego:7
`,
},
{
note: "coverage threshold not met (verbose, multiple files)",
modules: map[string]string{
"policy1.rego": `package test
p := 1 if {
1 == 1
}
q := 2
r := 3`,
"policy2.rego": `package test
s := 4 if {
1 == 1
2 == 2
}
t := 5
u := 6
v := 7`,
"test.rego": `package test
test_q if { q == 2 }
test_t if { t == 5 }`,
},
threshold: 100,
expectedExitCode: 2,
verbose: true,
expectedErrOutput: `Code coverage threshold not met: got 33.33 instead of 100.00
Lines not covered:
%ROOT%/policy1.rego:3-4
%ROOT%/policy1.rego:7
%ROOT%/policy2.rego:3-5
%ROOT%/policy2.rego:8-9
`,
},
}
for _, tc := range testCases {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(tc.modules, func(root string) {
var buf bytes.Buffer
testParams := newTestCommandParams()
testParams.threshold = tc.threshold
testParams.verbose = tc.verbose
testParams.count = 1
testParams.errOutput = &buf
exitCode := opaTest([]string{root}, testParams)
if exitCode != tc.expectedExitCode {
t.Fatalf("unexpected exit code: %d", exitCode)
}
if len(tc.expectedErrOutput) == 0 && buf.Len() > 0 {
t.Fatalf("expected no error output but got:\n\n%q", buf.String())
}
expectedErrOutput := strings.ReplaceAll(tc.expectedErrOutput, "%ROOT%", root)
if buf.String() != expectedErrOutput {
t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expectedErrOutput, buf.String())
}
})
})
}
}
func TestCoverageThresholdWithBundles(t *testing.T) {
modules := map[string]string{
"authz/policy.rego": `package authz
import rego.v1
allow if {
input.path == ["users"]
input.method == "POST"
}
allow if {
some profile_id
input.path = ["users", profile_id]
input.method == "GET"
profile_id == input.user_id
}
`,
"authz/policy_test.rego": `package authz
import rego.v1
test_post_allowed if {
allow with input as {"path": ["users"], "method": "POST"}
}
test_get_anonymous_denied if {
not allow with input as {"path": ["users"], "method": "GET"}
}
test_get_another_user_denied if {
not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
}
`,
}
for _, loadType := range []loadType{loadBundle, loadTarball} {
t.Run(loadType.String(), func(t *testing.T) {
var files map[string]string
if loadType != loadTarball {
files = modules
}
test.WithTempFS(files, func(root string) {
if loadType == loadTarball {
f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
if err != nil {
t.Fatal(err)
}
testBundle := bundle.Bundle{
Data: map[string]any{},
}
for k, v := range modules {
testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
Path: k,
Raw: []byte(v),
})
}
if err := bundle.Write(f, testBundle); err != nil {
t.Fatal(err)
}
}
var buf bytes.Buffer
testParams := newTestCommandParams()
testParams.threshold = 100
testParams.verbose = true
testParams.count = 1
testParams.bundleMode = true
testParams.errOutput = &buf
testParams.coverage = true
var paths []string
if loadType == loadTarball {
paths = []string{filepath.Join(root, "bundle.tar.gz")}
} else {
paths = []string{root}
}
exitCode := opaTest(paths, testParams)
// Coverage should NOT be 100% since the second allow rule is never exercised
if exitCode != 2 {
t.Fatalf("expected exit code 2 (threshold not met), got %d\noutput: %s", exitCode, buf.String())
}
if !strings.Contains(buf.String(), "Code coverage threshold not met: got 92.31 instead of 100.00") {
t.Fatalf("expected coverage of 92.31, got:\n\n%q", buf.String())
}
})
})
}
}
type loadType int
const (
loadFile loadType = iota
loadBundle
loadTarball
)
func (t loadType) String() string {
return [...]string{"file", "bundle", "bundle tarball"}[t]
}
func TestRun_DefaultRegoVersion(t *testing.T) {
tests := []struct {
note string
files map[string]string
expErrs []string
}{
{
note: "v0 module",
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v1 module",
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
}
loadTypes := []loadType{loadFile, loadBundle, loadTarball}
for _, tc := range tests {
for _, loadType := range loadTypes {
t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
var files map[string]string
if loadType != loadTarball {
files = tc.files
}
test.WithTempFS(files, func(root string) {
if loadType == loadTarball {
f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
if err != nil {
t.Fatal(err)
}
testBundle := bundle.Bundle{
Data: map[string]any{},
}
for k, v := range tc.files {
testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
Path: k,
Raw: []byte(v),
})
}
if err := bundle.Write(f, testBundle); err != nil {
t.Fatal(err)
}
}
var buf bytes.Buffer
var errBuf bytes.Buffer
testParams := newTestCommandParams()
testParams.bundleMode = loadType == loadBundle
testParams.count = 1
testParams.output = &buf
testParams.errOutput = &errBuf
var paths []string
if loadType == loadTarball {
paths = []string{filepath.Join(root, "bundle.tar.gz")}
} else {
paths = []string{root}
}
exitCode := opaTest(paths, testParams)
if len(tc.expErrs) > 0 {
if exitCode == 0 {
t.Fatalf("expected non-zero exit code")
}
for _, expErr := range tc.expErrs {
if actual := errBuf.String(); !strings.Contains(actual, expErr) {
t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expErr, actual)
}
}
} else {
if exitCode != 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
if errBuf.Len() > 0 {
t.Fatalf("expected no error output but got:\n\n%q", buf.String())
}
expected := "PASS: 1/1"
if actual := buf.String(); !strings.Contains(actual, expected) {
t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
}
}
})
})
}
}
}
func TestRunWithRegoV1Capability(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
capabilities *ast.Capabilities
files map[string]string
expErrs []string
}{
{
note: "v0 module, v0-compatible, no capabilities",
v0Compatible: true,
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
},
{
note: "v0 module, v0-compatible, v0 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
},
{
note: "v0 module, v0-compatible, v1 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
},
{
note: "v0 module, not v0-compatible, no capabilities",
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v0 module, not v0-compatible, v0 capabilities",
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v0 module, not v0-compatible, v0 capabilities without rego_v1 feature",
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
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)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}
test_l {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: rego_parse_error: `if` keyword is required before rule body",
"test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules",
"test.rego:8: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v1 module, v0-compatible, no capabilities",
v0Compatible: true,
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: 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)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: 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)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
expErrs: []string{
"test.rego:4: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, not v0-compatible, no capabilities",
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1 module, not v0-compatible, v0 capabilities",
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1 module, not v0-compatible, v0 capabilities without rego_v1 feature",
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
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)),
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
}
loadTypes := []loadType{loadFile, loadBundle, loadTarball}
for _, tc := range tests {
for _, loadType := range loadTypes {
t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
var files map[string]string
if loadType != loadTarball {
files = tc.files
}
test.WithTempFS(files, func(root string) {
if loadType == loadTarball {
f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
if err != nil {
t.Fatal(err)
}
testBundle := bundle.Bundle{
Data: map[string]any{},
}
for k, v := range tc.files {
testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
Path: k,
Raw: []byte(v),
})
}
if err := bundle.Write(f, testBundle); err != nil {
t.Fatal(err)
}
}
var buf bytes.Buffer
var errBuf bytes.Buffer
testParams := newTestCommandParams()
testParams.bundleMode = loadType == loadBundle
testParams.count = 1
testParams.output = &buf
testParams.errOutput = &errBuf
testParams.v0Compatible = tc.v0Compatible
testParams.capabilities.C = tc.capabilities
var paths []string
if loadType == loadTarball {
paths = []string{filepath.Join(root, "bundle.tar.gz")}
} else {
paths = []string{root}
}
exitCode := opaTest(paths, testParams)
if len(tc.expErrs) > 0 {
if exitCode == 0 {
t.Fatalf("expected non-zero exit code")
}
for _, expErr := range tc.expErrs {
if actual := errBuf.String(); !strings.Contains(actual, expErr) {
t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expErr, actual)
}
}
} else {
if exitCode != 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
if errBuf.Len() > 0 {
t.Fatalf("expected no error output but got:\n\n%q", buf.String())
}
expected := "PASS: 1/1"
if actual := buf.String(); !strings.Contains(actual, expected) {
t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
}
}
})
})
}
}
}
func TestRun_CompatibleFlags(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
v1Compatible bool
files map[string]string
expErr string
}{
{
note: "v0 module, no imports",
v0Compatible: true,
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
expErr: "rego_parse_error",
},
{
note: "v0 module, rego.v1 imported",
v0Compatible: true,
files: map[string]string{
"/test.rego": `package test
import rego.v1
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v0 module, future.keywords imported",
v0Compatible: true,
files: map[string]string{
"/test.rego": `package test
import future.keywords
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1 compatible module, no imports",
v1Compatible: true,
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1 compatible module, rego.v1 imported",
v1Compatible: true,
files: map[string]string{
"/test.rego": `package test
import rego.v1
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1 compatible module, future.keywords imported",
v1Compatible: true,
files: map[string]string{
"/test.rego": `package test
import future.keywords
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
// v0 takes precedence over v1
{
note: "v0+v1 module, no imports",
v0Compatible: true,
v1Compatible: true,
files: map[string]string{
"/test.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
expErr: "rego_parse_error",
},
{
note: "v0+v1 module, rego.v1 imported",
v0Compatible: true,
v1Compatible: true,
files: map[string]string{
"/test.rego": `package test
import rego.v1
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v0+v1 module, future.keywords imported",
v0Compatible: true,
v1Compatible: true,
files: map[string]string{
"/test.rego": `package test
import future.keywords
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
}
loadTypes := []loadType{loadFile, loadBundle, loadTarball}
for _, tc := range tests {
for _, loadType := range loadTypes {
t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) {
var files map[string]string
if loadType != loadTarball {
files = tc.files
}
test.WithTempFS(files, func(root string) {
if loadType == loadTarball {
f, err := os.Create(filepath.Join(root, "bundle.tar.gz"))
if err != nil {
t.Fatal(err)
}
testBundle := bundle.Bundle{
Data: map[string]any{},
}
for k, v := range tc.files {
testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{
Path: k,
Raw: []byte(v),
})
}
if err := bundle.Write(f, testBundle); err != nil {
t.Fatal(err)
}
}
var buf bytes.Buffer
var errBuf bytes.Buffer
testParams := newTestCommandParams()
testParams.v0Compatible = tc.v0Compatible
testParams.v1Compatible = tc.v1Compatible
testParams.bundleMode = loadType == loadBundle
testParams.count = 1
testParams.output = &buf
testParams.errOutput = &errBuf
var paths []string
if loadType == loadTarball {
paths = []string{filepath.Join(root, "bundle.tar.gz")}
} else {
paths = []string{root}
}
exitCode := opaTest(paths, testParams)
if tc.expErr != "" {
if exitCode == 0 {
t.Fatalf("expected non-zero exit code")
}
if actual := errBuf.String(); !strings.Contains(actual, tc.expErr) {
t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", tc.expErr, actual)
}
} else {
if exitCode != 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
if errBuf.Len() > 0 {
t.Fatalf("expected no error output but got:\n\n%q", buf.String())
}
expected := "PASS: 1/1"
if actual := buf.String(); !strings.Contains(actual, expected) {
t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
}
}
})
})
}
}
}
func TestWithBundleRegoVersion(t *testing.T) {
tests := []struct {
note string
files map[string]string
expErr string
}{
{
note: "v0.x bundle, no imports",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
expErr: "rego_parse_error",
},
{
note: "v0.x bundle, rego.v1 imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import rego.v1
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v0.x bundle, future.keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
import future.keywords
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
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
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}`,
"policy2.rego": `package test
test_l if {
l1 == l2
}`,
},
},
{
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 test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}`,
"policy2.rego": `package test
test_l if {
l1 == l2
}`,
},
},
{
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 test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}`,
"policy2.rego": `package test
test_l {
l1 == l2
}`,
},
expErr: "rego_parse_error",
},
{
note: "v1.0 bundle, no imports",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1.0 bundle, rego.v1 imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import rego.v1
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
note: "v1.0 bundle, future.keywords imported",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import future.keywords
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}
test_l if {
l1 == l2
}`,
},
},
{
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
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}`,
"policy2.rego": `package test
test_l if {
l1 == l2
}`,
},
},
{
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 test
l1 := {1, 3, 5}
l2[v] {
v := l1[_]
}`,
"policy2.rego": `package test
test_l if {
l1 == l2
}`,
},
},
{
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 test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}`,
"policy2.rego": `package test
test_l if {
l1 == l2
}`,
},
expErr: "rego_parse_error",
},
}
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)
}
}
var buf bytes.Buffer
var errBuf bytes.Buffer
testParams := newTestCommandParams()
testParams.v1Compatible = v1CompatibleFlag.used
testParams.bundleMode = true
testParams.count = 1
testParams.output = &buf
testParams.errOutput = &errBuf
exitCode := opaTest([]string{p}, testParams)
if tc.expErr != "" {
if exitCode == 0 {
t.Fatalf("expected non-zero exit code")
}
if actual := errBuf.String(); !strings.Contains(actual, tc.expErr) {
t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", tc.expErr, actual)
}
} else {
if exitCode != 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
if errBuf.Len() > 0 {
t.Fatalf("expected no error output but got:\n\n%q", buf.String())
}
expected := "PASS: 1/1"
if actual := buf.String(); !strings.Contains(actual, expected) {
t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual)
}
}
})
})
}
}
}
}
// Assert that a failing test doesn't cause a panic.
// https://github.com/open-policy-agent/opa/issues/7205
func TestTestBenchFailingTest(t *testing.T) {
files := map[string]string{
"test.rego": `package test
test_fail if false`,
}
test.WithTempFS(files, func(path string) {
fp := filepath.Join(path, "test.rego")
tp := newTestCommandParams()
tp.benchmark = true
tp.count = 1
exitCode := opaTest([]string{fp}, tp)
if exitCode == 0 {
t.Fatalf("Expected exit code != 0, got %d", exitCode)
}
})
}
func TestTestRunParallel(t *testing.T) {
tests := []struct {
note string
parallel int
}{
{
note: "default workers",
parallel: 0,
},
{
note: "1 workers",
parallel: 1,
},
{
note: "2 workers",
parallel: 2,
},
{
note: "100 workers",
parallel: 100,
},
}
for _, tc := range tests {
testParams := newTestCommandParams()
testParams.parallel = tc.parallel
files := map[string]string{
"policy1.rego": `package test
l1 := {1, 3, 5}
l2 contains v if {
v := l1[_]
}`,
"policy2.rego": `package test
test_l if {
l1 == l2
}`}
var exitCode int
test.WithTempFS(files, func(root string) {
exitCode = opaTest([]string{root}, testParams)
})
if exitCode > 0 {
t.Fatalf("unexpected exit code: %d", exitCode)
}
}
}
func TestWithDefaultRegoPlugin(t *testing.T) {
// We're injecting a default rego plugin that always returns true.
// If it's picked as a default (as intended), the tests run here will also
// yield true. If it wasn't picked, we'd use topdown, and would see a failing
// test.
tp := &testPlugin{}
rego.RegisterPlugin(targetPlugin, tp)
t.Cleanup(func() { tp.target = targetPlugin })
t.Run("test", func(t *testing.T) {
test.WithTempFS(map[string]string{"test.rego": "package test\ntest_true if false"}, func(path string) {
fp := filepath.Join(path, "test.rego")
tp := newTestCommandParams()
tp.output = io.Discard
tp.count = 1
exitCode := opaTest([]string{fp}, tp)
if exitCode != 0 {
t.Fatalf("Expected exit code 0, got %d", exitCode)
}
})
})
t.Run("eval", func(t *testing.T) {
params := newEvalCommandParams()
params.fail = true
query := "2+2 = 5" // unification will fail ("2+2 == 5" would be false, but defined)
defined, err := eval([]string{query}, params, io.Discard, nil)
if err != nil {
t.Fatal("unexpected error", err)
}
if !defined {
t.Errorf("expected defined result")
}
})
t.Run("repl", func(t *testing.T) {
ctx := t.Context()
store := inmem.New()
var buffer bytes.Buffer
repl := repl.New(store, "", &buffer, "", 0, "")
if err := repl.OneShot(ctx, "2+2==5"); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
result := buffer.String()
if result != "true\n" {
t.Errorf("Expected result to be false but got: %v", result)
}
})
}
func TestFailOnEmpty(t *testing.T) {
testCases := map[string]struct {
Test string
FailOnEmpty bool
ExpectedExitCode int
}{
"pass when no tests and fail-on-empty disabled": {
Test: `package foo
p := 1
`,
FailOnEmpty: false,
ExpectedExitCode: 0,
},
"fail when no tests and fail-on-empty enabled": {
Test: `package foo
p := 1
`,
FailOnEmpty: true,
ExpectedExitCode: 1,
},
"pass when tests exist and fail-on-empty enabled": {
Test: `package foo
test_pass if { true }
`,
FailOnEmpty: true,
ExpectedExitCode: 0,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
exitCode := testExitCodeWithFailOnEmpty(tc.Test, tc.FailOnEmpty)
if exitCode != tc.ExpectedExitCode {
t.Errorf("Expected exit code to be %d but got %d", tc.ExpectedExitCode, exitCode)
}
})
}
}
type testPlugin struct {
target string
}
func (t *testPlugin) IsTarget(tgt string) bool {
return tgt == t.target // t == "" makes it the global default
}
func (*testPlugin) PrepareForEval(context.Context, *ir.Policy, ...rego.PrepareOption) (rego.TargetPluginEval, error) {
return &testPlugin{}, nil
}
func (*testPlugin) Eval(context.Context, *rego.EvalContext, ast.Value) (ast.Value, error) {
return ast.NewSet(ast.NewTerm(ast.NewObject([2]*ast.Term{ast.StringTerm("^term1"), ast.BooleanTerm(true)}))), nil
}
const targetPlugin = "rego_test_default_plugin"