mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
db035b09fc
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>
3919 lines
88 KiB
Go
3919 lines
88 KiB
Go
//go:build !go1.27
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/open-policy-agent/opa/cmd/formats"
|
|
"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: "object ref selection from local var",
|
|
files: map[string]string{
|
|
"/test.rego": `package test
|
|
|
|
test_foo if {
|
|
tc := {"data": false, "expected": {"data": true}}
|
|
tc.data == tc.expected.data
|
|
}`,
|
|
},
|
|
expected: `%ROOT%/test.rego:3:
|
|
data.test.test_foo: FAIL (%TIME%)
|
|
--------------------------------------------------------------------------------
|
|
FAILURES
|
|
--------------------------------------------------------------------------------
|
|
data.test.test_foo: FAIL (%TIME%)
|
|
|
|
%ROOT%/test.rego:5:
|
|
tc.data == tc.expected.data
|
|
| |
|
|
| true
|
|
tc.data: false
|
|
tc: {"data": false, "expected": {"data": true}}
|
|
|
|
--------------------------------------------------------------------------------
|
|
FAIL: 1/1
|
|
`,
|
|
},
|
|
{
|
|
note: "object ref selection alongside function call",
|
|
files: map[string]string{
|
|
"/test.rego": `package test
|
|
|
|
test_foo if {
|
|
tc := {"content": "big string value", "inp": {"foo": false}, "outp": {"foo": true}}
|
|
foo(tc.inp) == tc.outp
|
|
}
|
|
|
|
foo(inp) := {"foo": inp.foo}`,
|
|
},
|
|
expected: `%ROOT%/test.rego:3:
|
|
data.test.test_foo: FAIL (%TIME%)
|
|
--------------------------------------------------------------------------------
|
|
FAILURES
|
|
--------------------------------------------------------------------------------
|
|
data.test.test_foo: FAIL (%TIME%)
|
|
|
|
%ROOT%/test.rego:5:
|
|
foo(tc.inp) == tc.outp
|
|
| | |
|
|
| | {"foo": true}
|
|
| tc.inp: {"foo": false}
|
|
| tc: {"content": "big string value", "inp": {"foo": false}, "outp"...
|
|
{"foo": false}
|
|
|
|
--------------------------------------------------------------------------------
|
|
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"
|
|
|
|
var durationJSONRe = regexp.MustCompile(`"duration":\s*\d+`)
|
|
|
|
func TestOpaTestJSONOutputBytes(t *testing.T) {
|
|
files := map[string]string{
|
|
"test.rego": `package test
|
|
|
|
test_p if { true }
|
|
`,
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
var tempDirPath string
|
|
test.WithTempFS(files, func(root string) {
|
|
tempDirPath = root
|
|
testParams := newTestCommandParams()
|
|
testParams.count = 1
|
|
testParams.outputFormat = formats.Flag(formats.JSON, formats.Pretty)
|
|
testParams.output = &stdout
|
|
testParams.errOutput = io.Discard
|
|
|
|
if exitCode := opaTest([]string{root}, testParams); exitCode != 0 {
|
|
t.Fatalf("unexpected exit code: %d", exitCode)
|
|
}
|
|
})
|
|
|
|
normalized := durationJSONRe.ReplaceAll(stdout.Bytes(), []byte(`"duration":0`))
|
|
|
|
expectedOutput := strings.ReplaceAll(`[
|
|
{
|
|
"location": {
|
|
"file": "TEMPDIR/test.rego",
|
|
"row": 3,
|
|
"col": 1
|
|
},
|
|
"package": "data.test",
|
|
"name": "test_p",
|
|
"duration":0
|
|
}
|
|
]
|
|
`, "TEMPDIR", tempDirPath)
|
|
|
|
if diff := cmp.Diff(expectedOutput, string(normalized)); diff != "" {
|
|
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
|
}
|
|
}
|