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

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

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

---------

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

3986 lines
107 KiB
Go

//go:build go1.27
// Copyright 2018 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// nolint: goconst // string duplication is for test readability.
package cmd
import (
"bufio"
"bytes"
"errors"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"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/internal/presentation"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/loader"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/topdown"
"github.com/open-policy-agent/opa/v1/util"
"github.com/open-policy-agent/opa/v1/util/test"
)
func TestEvalWithIllegalUnknownArgs(t *testing.T) {
tests := []struct {
name string
unknowns string
expectedErr error
}{
{
name: "happy path: passing input ref as unknown",
unknowns: "input",
expectedErr: nil,
},
{
name: "happy path: passing input.users ref as unknown",
unknowns: "input.users",
expectedErr: nil,
},
{
name: "passing multiple refs with ; separated",
unknowns: "input;input.users",
expectedErr: errors.New("expected exactly one term but got: input; input.users"),
},
{
name: "passing array as unknown",
unknowns: "[input, data.posts]",
expectedErr: errIllegalUnknownsArg,
},
{
name: "passing set as unknown",
unknowns: "{input, data.posts}",
expectedErr: errIllegalUnknownsArg,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := newEvalCommandParams()
params.unknowns = []string{tt.unknowns}
params.partial = true
err := validateEvalParams(&params, []string{"data"})
if tt.expectedErr != nil && !strings.EqualFold(err.Error(), tt.expectedErr.Error()) {
t.Errorf("expected %s; got %s", errIllegalUnknownsArg.Error(), err.Error())
}
})
}
}
func TestEvalExitCode(t *testing.T) {
params := newEvalCommandParams()
params.fail = true
tests := []struct {
note string
query string
wantDefined bool
wantErr bool
}{
{"defined result", "true=true", true, false},
{"undefined result", "true = false", false, false},
{"on error", `{k: v | k = ["a", "a"][_]; v = [0,1][_]}`, false, true},
}
var b bytes.Buffer
writer := bufio.NewWriter(&b)
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
defined, err := eval([]string{tc.query}, params, writer, nil)
if tc.wantErr && err == nil {
t.Fatal("wanted error but got success")
} else if !tc.wantErr && err != nil {
t.Fatal("wanted success but got error:", err)
} else if (tc.wantDefined && !defined) || (!tc.wantDefined && defined) {
t.Fatalf("wanted defined %v but got defined %v", tc.wantDefined, defined)
}
})
}
}
func TestEvalWithShowBuiltinErrors(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p if {
1/0
}
q if {
1/0
}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.showBuiltinErrors = true
params.dataPaths = newrepeatedStringFlag([]string{path})
var buf bytes.Buffer
defined, err := eval([]string{"data.x"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("unexpected undefined or error: %v", err)
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if len(output.Errors) != 2 {
t.Fatalf("Expected 2 errors in result, got:%v", len(output.Errors))
}
expectedCode := "eval_builtin_error"
expectedMessage := "div: divide by zero"
if code := output.Errors[0].Code; code != expectedCode {
t.Fatalf("expected code '%v', got '%v'", expectedCode, code)
}
if msg := output.Errors[0].Message; msg != expectedMessage {
t.Fatalf("expected message '%v', got '%v'", expectedMessage, msg)
}
if code := output.Errors[1].Code; code != expectedCode {
t.Fatalf("expected code '%v', got '%v'", expectedCode, code)
}
if msg := output.Errors[1].Message; msg != expectedMessage {
t.Fatalf("expected message '%v', got '%v'", expectedMessage, msg)
}
loc1 := output.Errors[0].Location
if loc1 == nil {
t.Fatal("unexpected nil location")
}
loc2 := output.Errors[1].Location
if loc2 == nil {
t.Fatal("unexpected nil location")
}
if loc1.Row == loc2.Row {
t.Fatal("expected 2 distinct error occurrences in policy")
}
})
}
func TestEvalWithProfiler(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p if {
a := 1
b := 2
c := 3
x = a + b * c
}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.profile = true
params.profileCriteria = newrepeatedStringFlag([]string{"line"})
params.dataPaths = newrepeatedStringFlag([]string{path})
var buf bytes.Buffer
defined, err := eval([]string{"data"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if len(output.Profile) == 0 {
t.Fatal("Expected profile output to be non-empty")
}
expectedNumEval := []int{3, 1, 1, 1, 1}
expectedNumRedo := []int{3, 1, 1, 1, 1}
expectedRow := []int{7, 6, 5, 4, 1}
expectedNumGenExpr := []int{3, 1, 1, 1, 1}
for idx, actualExprStat := range output.Profile {
if actualExprStat.NumEval != expectedNumEval[idx] {
t.Fatalf("Index %v: Expected number of evals %v but got %v", idx, expectedNumEval[idx], actualExprStat.NumEval)
}
if actualExprStat.NumRedo != expectedNumRedo[idx] {
t.Fatalf("Index %v: Expected number of redos %v but got %v", idx, expectedNumRedo[idx], actualExprStat.NumRedo)
}
if actualExprStat.Location.Row != expectedRow[idx] {
t.Fatalf("Index %v: Expected row %v but got %v", idx, expectedRow[idx], actualExprStat.Location.Row)
}
if actualExprStat.NumGenExpr != expectedNumGenExpr[idx] {
t.Fatalf("Index %v: Expected number of generated expressions %v but got %v", idx, expectedNumGenExpr[idx], actualExprStat.NumGenExpr)
}
}
})
}
func TestEvalWithCoverage(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p = 1`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.coverage = true
params.dataPaths = newrepeatedStringFlag([]string{path})
var buf bytes.Buffer
defined, err := eval([]string{"data"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if output.Coverage == nil || output.Coverage.Coverage != 100.0 {
t.Fatalf("Expected coverage in output but got: %v", buf.String())
}
})
}
func TestEvalWithOptimizeErrors(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p = 1`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
err := validateEvalParams(&params, []string{"data"})
if err == nil {
t.Fatal("Expected error but got nil")
}
expected := "specify either --data or --bundle flag with optimization level greater than 0"
if err.Error() != expected {
t.Fatalf("Expected error %v but got %v", expected, err.Error())
}
params = newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
var buf bytes.Buffer
_, err = eval([]string{"data.test"}, params, &buf, nil)
if err == nil {
t.Fatal("Expected error but got nil")
}
expected = "bundle optimizations require at least one entrypoint"
if err.Error() != expected {
t.Fatalf("Expected error %v but got %v", expected, err.Error())
}
})
}
func TestEvalWithOptimize(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p = false
p if { q }
q if { input.x = data.foo }`,
"data.json": `
{"foo": 1}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"test/p"})
var buf bytes.Buffer
defined, err := eval([]string{"data.test.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
// Ensure that entrypoint annotations don't cause panics when using
// higher levels of optimization.
// Reference: https://github.com/open-policy-agent/opa/issues/5368
func TestEvalIssue5368(t *testing.T) {
files := map[string]string{
"test.rego": `
package system
object_key_exists(object, key) if {
_ = object[key]
}
default main = false
# METADATA
# entrypoint: true
main := results if {
object_key_exists(input, "queries")
results := {key: result |
result := input.queries[key]
}
}`,
"input.json": `{}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 2
params.dataPaths = newrepeatedStringFlag([]string{path})
params.inputPath = filepath.Join(path, "input.json")
var buf bytes.Buffer
defined, err := eval([]string{"data.system.main"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func TestEvalWithOptimizeBundleData(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p = false
p if { q }
q if { input.x = data.foo }`,
"data.json": `
{"foo": 1}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
params.entrypoints = newrepeatedStringFlag([]string{"test/p"})
var buf bytes.Buffer
defined, err := eval([]string{"data.test.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func testEvalWithInputFile(t *testing.T, input string, query string, params evalCommandParams) error {
files := map[string]string{
"input.json": input,
}
var err error
test.WithTempFS(files, func(path string) {
params.inputPath = filepath.Join(path, "input.json")
var buf bytes.Buffer
var defined bool
defined, err = eval([]string{query}, params, &buf, nil)
if !defined || err != nil {
err = fmt.Errorf("Unexpected error or undefined from evaluation: %v", err)
return
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
rs := output.Result
if exp, act := true, rs.Allowed(); exp != act {
t.Errorf("expected %v, got %v", exp, act)
}
})
return err
}
func TestEvalWithInvalidInputFile(t *testing.T) {
input := `{badjson`
query := "input.b[0].a == 1"
err := testEvalWithInputFile(t, input, query, newEvalCommandParams())
if err == nil {
t.Fatalf("expected error but err == nil")
}
}
func testEvalWithSchemaFile(t *testing.T, input string, query string, schema string, policy string, expTypeErr bool) error {
files := map[string]string{
"input.json": input,
"schema.json": schema,
}
policyFilePresent := policy != ""
if policyFilePresent {
files["policy.rego"] = policy
}
var err error
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
if policyFilePresent {
params.dataPaths = newrepeatedStringFlag([]string{path})
}
params.schema = &schemaFlags{path: filepath.Join(path, "schema.json")}
var buf bytes.Buffer
defined, evalErr := eval([]string{query}, params, &buf, nil)
if !expTypeErr && (!defined || evalErr != nil) {
err = fmt.Errorf("unexpected error or undefined from evaluation: %v", evalErr)
return
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if expTypeErr {
if len(output.Errors) != 1 || output.Errors[0].Code != "rego_type_error" {
err = fmt.Errorf("expected type conflict, got %v", output.Errors)
}
return
}
rs := output.Result
if exp, act := true, rs.Allowed(); exp != act {
t.Errorf("expected %v, got %v", exp, act)
}
})
return err
}
func testEvalWithInvalidSchemaFile(input string, query string, schema string) error {
files := map[string]string{
"input.json": input,
"schema.json": schema,
}
var err error
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.schema = &schemaFlags{path: filepath.Join(path, "schemaBad.json")}
var buf bytes.Buffer
var defined bool
defined, err = eval([]string{query}, params, &buf, nil)
if !defined || err != nil {
err = fmt.Errorf("Unexpected error or undefined from evaluation: %v", err)
return
}
})
return err
}
func testEvalWithSchemasAnnotationButNoSchemaFlag(policy string) error {
query := "data.test.p"
files := map[string]string{
"input.json": `{
"foo": 42
}`,
"test.rego": policy,
}
var err error
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.dataPaths = newrepeatedStringFlag([]string{path})
var buf bytes.Buffer
var defined bool
defined, err = eval([]string{query}, params, &buf, nil)
if !defined || err != nil {
err = errors.New(buf.String())
}
})
return err
}
// Assert that 'schemas' annotations with schema refs are only informing the type checker when the --schema flag is used
func TestEvalWithSchemasAnnotationButNoSchemaFlag(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
}`
err := testEvalWithSchemasAnnotationButNoSchemaFlag(policyWithSchemaRef)
if err != nil {
t.Fatalf("unexpected error from eval with schema ref: %v", err)
}
policyWithInlinedSchema := `
package test
# METADATA
# schemas:
# - input.foo: {"type": "boolean"}
p if {
rego.metadata.rule() # presence of rego.metadata.* calls must not trigger unwanted schema evaluation
input.foo == 42 # type mismatch with schema that should NOT be ignored since it is an inlined schema format
}`
err = testEvalWithSchemasAnnotationButNoSchemaFlag(policyWithInlinedSchema)
// We expect an error here, as inlined schemas are always used for type checking
if !strings.Contains(err.Error(), `"code": "rego_type_error"`) {
t.Fatalf("unexpected error from eval with inlined schema, got: %v", err)
}
}
func testReadParamWithSchemaDir(input string, inputSchema string) error {
files := map[string]string{
"input.json": input,
"schemas/input.json": inputSchema,
"schemas/kubernetes/data-schema.json": inputSchema,
}
var err error
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.schema = &schemaFlags{path: filepath.Join(path, "schemas")}
// Don't assign over "err" or "err =" does nothing.
schemaSet, errSchema := loader.Schemas(params.schema.path)
if errSchema != nil {
err = fmt.Errorf("Unexpected error or undefined from evaluation: %v", errSchema)
return
}
if schemaSet == nil {
err = errors.New("Schema set is empty")
return
}
if schemaSet.Get(ast.MustParseRef("schema.input")) == nil {
err = errors.New("Expected schema for input in schemaSet but got none")
return
}
if schemaSet.Get(ast.MustParseRef(`schema.kubernetes["data-schema"]`)) == nil {
err = errors.New("Expected schemas for data in schemaSet but got none")
return
}
})
return err
}
func TestEvalWithRecursiveJSONSchema(t *testing.T) {
tests := []struct {
note string
input string
query string
schema string
policy string
expTypeErr bool
}{
{
note: "recursive object ref - valid usage",
input: `{"foo": {"foo": {}}}`,
query: "data.p.allow",
schema: `{
"$ref": "#/$defs/foo",
"$defs": {
"foo": {
"type": "object",
"properties": {
"foo": {
"$ref": "#/$defs/foo"
}
}
}
}
}`,
policy: `package p
allow if input.foo`,
},
{
note: "recursive object ref - type mismatch at top level",
input: `{"foo": {"foo": {}}}`,
query: "data.test.p",
schema: `{
"$ref": "#/$defs/foo",
"$defs": {
"foo": {
"type": "object",
"properties": {
"foo": {
"$ref": "#/$defs/foo"
}
}
}
}
}`,
policy: `
package test
# METADATA
# schemas:
# - input: schema
p if {
input.foo == 42
}`,
expTypeErr: true,
},
{
note: "recursive object ref - nested type mismatch",
input: `{"foo": {"foo": {}}}`,
query: "data.test.p",
schema: `{
"$ref": "#/$defs/foo",
"$defs": {
"foo": {
"type": "object",
"properties": {
"foo": {
"$ref": "#/$defs/foo"
}
}
}
}
}`,
policy: `
package test
# METADATA
# schemas:
# - input: schema
p if {
input.foo.foo == "hello"
}`,
expTypeErr: true,
},
{
note: "recursive object ref - inlined schema type mismatch",
input: `{"foo": {"foo": {}}}`,
query: "data.test.p",
schema: `{
"$ref": "#/$defs/foo",
"$defs": {
"foo": {
"type": "object",
"properties": {
"foo": {
"$ref": "#/$defs/foo"
}
}
}
}
}`,
policy: `
package test
# METADATA
# schemas:
# - input.foo: {"type": "boolean"}
p if {
input.foo == 42
}`,
expTypeErr: true,
},
{
note: "recursive object ref - valid with schema annotation",
input: `{"foo": {"foo": {}}}`,
query: "data.test.p",
schema: `{
"$ref": "#/$defs/foo",
"$defs": {
"foo": {
"type": "object",
"properties": {
"foo": {
"$ref": "#/$defs/foo"
}
}
}
}
}`,
policy: `
package test
# METADATA
# schemas:
# - input: schema
p if {
input.foo
}`,
},
{
note: "recursive array ref - valid usage",
input: `{"tree": [[[]]]}`,
query: "data.p.allow",
schema: `{
"type": "object",
"properties": {
"tree": {
"$ref": "#/$defs/tree"
}
},
"$defs": {
"tree": {
"type": "array",
"items": {
"$ref": "#/$defs/tree"
}
}
}
}`,
policy: `package p
allow if input.tree`,
},
{
note: "recursive array ref - type mismatch on element",
input: `{"tree": [[[]]]}`,
query: "data.test.p",
schema: `{
"type": "object",
"properties": {
"tree": {
"$ref": "#/$defs/tree"
}
},
"$defs": {
"tree": {
"type": "array",
"items": {
"$ref": "#/$defs/tree"
}
}
}
}`,
policy: `
package test
# METADATA
# schemas:
# - input: schema
p if {
input.tree[0] == "hello"
}`,
expTypeErr: true,
},
{
note: "recursive anyOf ref - valid usage",
input: `{"node": ["hello", ["world"]]}`,
query: "data.p.allow",
schema: `{
"type": "object",
"properties": {
"node": {
"$ref": "#/$defs/node"
}
},
"$defs": {
"node": {
"anyOf": [
{ "type": "string" },
{
"type": "array",
"items": { "$ref": "#/$defs/node" }
}
]
}
}
}`,
policy: `package p
allow if input.node`,
},
{
note: "non-recursive ref - valid usage",
input: `{"addr": {"street": "Main St", "city": "Springfield"}}`,
query: "data.p.allow",
schema: `{
"type": "object",
"properties": {
"addr": {
"$ref": "#/$defs/address"
}
},
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
}
}
}
}`,
policy: `package p
allow if input.addr.street`,
},
{
note: "non-recursive ref - type mismatch",
input: `{"addr": {"street": "Main St", "city": "Springfield"}}`,
query: "data.test.p",
schema: `{
"type": "object",
"properties": {
"addr": {
"$ref": "#/$defs/address"
}
},
"$defs": {
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
}
}
}
}`,
policy: `
package test
# METADATA
# schemas:
# - input: schema
p if {
input.addr.street == 42
}`,
expTypeErr: true,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
err := testEvalWithSchemaFile(t, tc.input, tc.query, tc.schema, tc.policy, tc.expTypeErr)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
})
}
}
func TestEvalWithJSONSchema(t *testing.T) {
input := `{
"foo": "a",
"b": [
{
"a": 1,
"b": [1, 2, 3],
"c": null
}
]
}`
schema := `{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "http://example.com/example.json",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"required": [
"foo",
"b"
],
"properties": {
"foo": {
"$id": "#/properties/foo",
"type": "string",
"title": "The foo schema",
"description": "An explanation about the purpose of this instance."
},
"b": {
"$id": "#/properties/b",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"additionalItems": false,
"items": {
"$id": "#/properties/b/items",
"type": "object",
"title": "The items schema",
"description": "An explanation about the purpose of this instance.",
"required": [
"a",
"b",
"c"
],
"properties": {
"a": {
"$id": "#/properties/b/items/properties/a",
"type": "integer",
"title": "The a schema",
"description": "An explanation about the purpose of this instance."
},
"b": {
"$id": "#/properties/b/items/properties/b",
"type": "array",
"title": "The b schema",
"description": "An explanation about the purpose of this instance.",
"additionalItems": false,
"items": {
"$id": "#/properties/b/items/properties/b/items",
"type": "integer",
"title": "The items schema",
"description": "An explanation about the purpose of this instance."
}
},
"c": {
"$id": "#/properties/b/items/properties/c",
"type": "null",
"title": "The c schema",
"description": "An explanation about the purpose of this instance."
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}`
query := "input.b[0].a == 1"
err := testEvalWithSchemaFile(t, input, query, schema, "", false)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
policyWithSchemasAnnotation := `
package test
# METADATA
# schemas:
# - input: schema
p if {
input.foo == 42 # type mismatch
}`
err = testEvalWithSchemaFile(t, input, query, schema, policyWithSchemasAnnotation, true)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
policyWithInlinedSchemasAnnotation := `
package test
# METADATA
# schemas:
# - input.foo: {"type": "boolean"}
p if {
input.foo == 42 # type mismatch
}`
err = testEvalWithSchemaFile(t, input, query, schema, policyWithInlinedSchemasAnnotation, true)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
err = testReadParamWithSchemaDir(input, schema)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
}
func TestEvalWithInvalidSchemaFile(t *testing.T) {
input := `{
"foo": "a",
"b": [
{
"a": 1,
"b": [1, 2, 3],
"c": null
}
]
}`
schema := `{badjson`
query := "input.b[0].a == 1"
err := testEvalWithSchemaFile(t, input, query, schema, "", false)
if err == nil {
t.Fatalf("expected error but err == nil")
}
err = testEvalWithInvalidSchemaFile(input, query, schema)
if err == nil {
t.Fatalf("expected error but err == nil")
}
}
func TestEvalWithSchemaFileWithRemoteRef(t *testing.T) {
input := `{"metadata": {"clusterName": "NAME"}}`
schemaFmt := `{
"type": "object",
"properties": {
"metadata": {
"$ref": "%s/v1.14.0/_definitions.json#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
"description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata"
}
}
}`
ts := kubeSchemaServer(t)
t.Cleanup(ts.Close)
query := "data.p.r"
files := map[string]string{
"input.json": input,
"schema.json": fmt.Sprintf(schemaFmt, ts.URL),
"p.rego": `package p
r if {
input.metadata.clusterName == "NAME"
}`,
}
t.Run("all remote refs disabled", func(t *testing.T) {
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.schema = &schemaFlags{path: filepath.Join(path, "schema.json")}
params.capabilities.C = ast.CapabilitiesForThisVersion()
params.capabilities.C.AllowNet = []string{}
_ = params.dataPaths.Set(filepath.Join(path, "p.rego"))
var buf bytes.Buffer
_, err := eval([]string{query}, params, &buf, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if exp, act := 1, len(output.Errors); exp != act {
t.Fatalf("expected %d errors, got %d", exp, act)
}
if exp, act := "rego_type_error", output.Errors[0].Code; exp != act {
t.Errorf("expected code %v, got %v", exp, act)
}
})
})
t.Run("all remote refs enabled", func(t *testing.T) {
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.schema = &schemaFlags{path: filepath.Join(path, "schema.json")}
_ = params.dataPaths.Set(filepath.Join(path, "p.rego"))
var buf bytes.Buffer
defined, err := eval([]string{query}, params, &buf, nil)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if exp, act := true, defined; exp != act {
t.Errorf("expected defined %v, got %v", exp, act)
}
})
})
t.Run("required remote ref host not enabled", func(t *testing.T) {
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.schema = &schemaFlags{path: filepath.Join(path, "schema.json")}
params.capabilities.C = ast.CapabilitiesForThisVersion()
params.capabilities.C.AllowNet = []string{"something.else"}
_ = params.dataPaths.Set(filepath.Join(path, "p.rego"))
var buf bytes.Buffer
_, err := eval([]string{query}, params, &buf, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if exp, act := 1, len(output.Errors); exp != act {
t.Fatalf("expected %d errors, got %d", exp, act)
}
if exp, act := "rego_type_error", output.Errors[0].Code; exp != act {
t.Errorf("expected code %v, got %v", exp, act)
}
})
})
t.Run("only required remote ref host enabled", func(t *testing.T) {
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.inputPath = filepath.Join(path, "input.json")
params.schema = &schemaFlags{path: filepath.Join(path, "schema.json")}
params.capabilities.C = ast.CapabilitiesForThisVersion()
params.capabilities.C.AllowNet = []string{"127.0.0.1"}
_ = params.dataPaths.Set(filepath.Join(path, "p.rego"))
var buf bytes.Buffer
defined, err := eval([]string{query}, params, &buf, nil)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if exp, act := true, defined; exp != act {
t.Errorf("expected defined %v, got %v", exp, act)
}
})
})
}
func TestBuiltinsCapabilities(t *testing.T) {
tests := []struct {
note string
policy string
query string
ruleName string
expectedCode string
expectedMessage string
}{
{
note: "rego.metadata.chain() not allowed",
policy: "package p\n r := rego.metadata.chain()",
query: "data.p",
ruleName: "rego.metadata.chain",
expectedCode: "rego_type_error",
expectedMessage: "undefined function rego.metadata.chain",
},
{
note: "rego.metadata.rule() not allowed",
policy: "package p\n r := rego.metadata.rule()",
query: "data.p",
ruleName: "rego.metadata.rule",
expectedCode: "rego_type_error",
expectedMessage: "undefined function rego.metadata.rule",
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"p.rego": tc.policy,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.capabilities.C = ast.CapabilitiesForThisVersion()
params.capabilities.C.Builtins = removeBuiltin(params.capabilities.C.Builtins, tc.ruleName)
_ = params.dataPaths.Set(filepath.Join(path, "p.rego"))
var buf bytes.Buffer
_, err := eval([]string{tc.query}, params, &buf, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if exp, act := 1, len(output.Errors); exp != act {
t.Fatalf("expected %d errors, got %d", exp, act)
}
if code := output.Errors[0].Code; code != tc.expectedCode {
t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code)
}
if msg := output.Errors[0].Message; msg != tc.expectedMessage {
t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg)
}
})
})
}
}
func removeBuiltin(builtins []*ast.Builtin, name string) []*ast.Builtin {
var cpy []*ast.Builtin
for _, builtin := range builtins {
if builtin.Name != name {
cpy = append(cpy, builtin)
}
}
return cpy
}
// Nearly identical to TestEvalWithOptimizeBundleData, but uses
// Rego entrypoint annotations instead of explicitly providing
// the entrypoints as CLI arguments.
func TestEvalWithRegoEntrypointAnnotations(t *testing.T) {
files := map[string]string{
"test.rego": `
package test
default p = false
# METADATA
# entrypoint: true
p if { q }
q if { input.x = data.foo }`,
"data.json": `
{"foo": 1}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
defined, err := eval([]string{"data.test.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func TestEvalReturnsRegoError(t *testing.T) {
buf := new(bytes.Buffer)
_, err := eval([]string{`{k: v | k = ["a", "a"][_]; v = [0,1][_]}`}, newEvalCommandParams(), buf, nil)
if _, ok := err.(regoError); !ok {
t.Fatal("expected regoError but got:", err)
}
}
func TestEvalBundlePathWithIgnoreFlag(t *testing.T) {
files := map[string]string{
"good_policy.rego": `
package example
p1 if { data.foo }`,
"bad_policy.rego": `
package example
var `,
"data.json": `
{"foo": true, "bar": false}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
if err := params.bundlePaths.Set(path); err != nil {
t.Fatalf("Unable to set bundle path: %v", err)
}
params.ignore = []string{"bad_policy.rego"}
var buf bytes.Buffer
// Evaluate policies
defined, err := eval([]string{"data.example.p1"}, params, &buf, &buf)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error for p1: %v", err)
}
})
}
func TestEvalWithBundleData(t *testing.T) {
files := map[string]string{
"x/x.rego": "package x\np = 1",
"x/data.json": `{"b": "bar"}`,
"other/not-data.json": `{"ignored": "data"}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
defined, err := eval([]string{"data"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
assertResultSet(t, output.Result, `[[{"x": {"p": 1, "b": "bar"}}]]`)
})
}
func TestEvalWithBundleDuplicateFileNames(t *testing.T) {
files := map[string]string{
// bundle a
"a/policy.rego": "package a\np = 1",
"a/.manifest": `{"roots":["a"]}`,
// bundle b
"b/policy.rego": "package b\nq = 1",
"b/.manifest": `{"roots":["b"]}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
if err := params.bundlePaths.Set(filepath.Join(path, "a")); err != nil {
t.Fatal(err)
}
if err := params.bundlePaths.Set(filepath.Join(path, "b")); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
defined, err := eval([]string{"data"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
assertResultSet(t, output.Result, `[[{"a":{"p":1},"b":{"q":1}}]]`)
})
}
func TestEvalWithReadASTValuesFromStore(t *testing.T) {
// Note: This test is a bit of a hack. It's difficult to discern whether AST values were actually read from the store.
// This just ensures that we don't get any unexpected errors when enabling the flag.
tests := []struct {
note string
readAst bool
}{
{
note: "read raw data from store",
readAst: false,
},
{
note: "read AST values from store",
readAst: true,
},
}
files := map[string]string{
"test.rego": `
package test
p = 1`,
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.dataPaths = newrepeatedStringFlag([]string{path})
params.ReadAstValuesFromStore = tc.readAst
var buf bytes.Buffer
defined, err := eval([]string{"data.test.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
})
}
}
func TestEvalWithStrictBuiltinErrors(t *testing.T) {
params := newEvalCommandParams()
params.strictBuiltinErrors = true
var buf bytes.Buffer
_, err := eval([]string{"1/0"}, params, &buf, nil)
if err == nil {
t.Fatal("expected error")
}
params.strictBuiltinErrors = false
buf.Reset()
_, err = eval([]string{"1/0"}, params, &buf, nil)
if err != nil {
t.Fatal("unexpected error:", err)
}
if buf.String() != "{}\n" {
t.Fatal("expected undefined output but got:", buf.String())
}
}
func assertResultSet(t *testing.T, rs rego.ResultSet, expected string) {
t.Helper()
result := make([]any, 0, len(rs))
for i := range rs {
values := make([]any, 0, len(rs[i].Expressions))
for j := range rs[i].Expressions {
values = append(values, rs[i].Expressions[j].Value)
}
result = append(result, values)
}
parsedExpected := util.MustUnmarshalJSON([]byte(expected))
if !reflect.DeepEqual(result, parsedExpected) {
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", parsedExpected, result)
}
}
func TestEvalErrorJSONOutput(t *testing.T) {
params := newEvalCommandParams()
err := params.outputFormat.Set(formats.JSON)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
var buf bytes.Buffer
defined, err := eval([]string{"{1,2,3} == {1,x,3}"}, params, &buf, nil)
if defined && err == nil {
t.Fatalf("Expected an error")
}
// Only check that it *can* be loaded as valid JSON, and that the errors
// are populated.
var output map[string]any
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if output["errors"] == nil {
t.Fatalf("Expected error to be non-nil")
}
}
func TestEvalDebugTraceJSONOutput(t *testing.T) {
params := newEvalCommandParams()
err := params.outputFormat.Set(formats.JSON)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
err = params.explain.Set(explainModeFull)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
params.disableIndexing = true
mod := `package x
p contains a if {
a := input.z
a == 1
}
p contains b if {
b := input.y
b == 1
}
`
input := `{"z": 1}`
files := map[string]string{
"policy.rego": mod,
"input.json": input,
}
var buf bytes.Buffer
var policyFile string
test.WithTempFS(files, func(path string) {
params.inputPath = filepath.Join(path, "input.json")
policyFile = filepath.Join(path, "policy.rego")
err := params.dataPaths.Set(policyFile)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
_, err = eval([]string{"data.x.p"}, params, &buf, nil)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
})
var output struct {
Explanation []struct {
Op string `json:"Op"`
Node any `json:"Node"`
Location *ast.Location `json:"Location"`
Locals []map[string]any `json:"Locals"`
LocalMetadata map[string]struct {
Name string `json:"name"`
} `json:"LocalMetadata"`
}
}
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if len(output.Explanation) == 0 {
t.Fatalf("Expected explanations to be non-nil")
}
type locationAndVars struct {
location *ast.Location
varBindings map[string]string
}
var evals []locationAndVars
for _, e := range output.Explanation {
if e.Op == string(topdown.EvalOp) {
bindings := map[string]string{}
for k, v := range e.LocalMetadata {
bindings[k] = v.Name
}
evals = append(evals, locationAndVars{location: e.Location, varBindings: bindings})
}
}
expectedEvalLocationsAndVars := []locationAndVars{
{
location: ast.NewLocation(nil, policyFile, 4, 3), // a := input.z
varBindings: map[string]string{"__local0__": "a"},
},
{
location: ast.NewLocation(nil, policyFile, 5, 3), // a == 1
varBindings: map[string]string{"__local0__": "a"},
},
{
location: ast.NewLocation(nil, policyFile, 9, 3), // b := input.y
varBindings: map[string]string{"__local1__": "b"},
},
}
for _, expected := range expectedEvalLocationsAndVars {
found := false
for _, actual := range evals {
if expected.location.Compare(actual.location) == 0 {
found = true
if !maps.Equal(expected.varBindings, actual.varBindings) {
t.Errorf("Expected var bindings:\n\n\t%+v\n\nGot\n\n\t%+v\n\n", expected.varBindings, actual.varBindings)
}
}
}
if !found {
t.Fatalf("Missing expected eval node in trace: %+v\nGot: %+v\n", expected, evals)
}
}
}
func TestEvalPrettyTrace(t *testing.T) {
tests := []struct {
note string
query string
includeVars bool
files map[string]string
expected string
}{
{
note: "simple without vars",
query: "data.test.p",
includeVars: false,
files: map[string]string{
"test.rego": `package test
import rego.v1
p if {
x := 1
y := 2
z := 3
x == z - y
}
`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _
query:1 %.*% | Eval data.test.p = _
query:1 %.*% | Index data.test.p (matched 1 rule, early exit)
%.*%/test.rego:4 | Enter data.test.p
%.*%/test.rego:5 | | Eval x = 1
%.*%/test.rego:6 | | Eval y = 2
%.*%/test.rego:7 | | Eval z = 3
%.*%/test.rego:8 | | Eval minus(z, y, __local3__)
%.*%/test.rego:8 | | Eval x = __local3__
%.*%/test.rego:4 | | Exit data.test.p early
query:1 %.*% | Exit data.test.p = _
query:1 %.*% Redo data.test.p = _
query:1 %.*% | Redo data.test.p = _
%.*%/test.rego:4 | Redo data.test.p
%.*%/test.rego:8 | | Redo x = __local3__
%.*%/test.rego:8 | | Redo minus(z, y, __local3__)
%.*%/test.rego:7 | | Redo z = 3
%.*%/test.rego:6 | | Redo y = 2
%.*%/test.rego:5 | | Redo x = 1
true
`,
},
{
note: "simple with vars",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
p if {
x := 1
y := 2
z := 3
x == z - y
}
`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:4 | Enter data.test.p {}
%.*%/test.rego:5 | | Eval x = 1 {}
%.*%/test.rego:6 | | Eval y = 2 {}
%.*%/test.rego:7 | | Eval z = 3 {}
%.*%/test.rego:8 | | Eval minus(z, y, __local3__) {y: 2, z: 3}
%.*%/test.rego:8 | | Eval x = __local3__ {__local3__: 1, x: 1}
%.*%/test.rego:4 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:4 | Redo data.test.p {}
%.*%/test.rego:8 | | Redo x = __local3__ {__local3__: 1, x: 1}
%.*%/test.rego:8 | | Redo minus(z, y, __local3__) {__local3__: 1, y: 2, z: 3}
%.*%/test.rego:7 | | Redo z = 3 {z: 3}
%.*%/test.rego:6 | | Redo y = 2 {y: 2}
%.*%/test.rego:5 | | Redo x = 1 {x: 1}
true
`,
},
{
note: "large var",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
v := {
"foo": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
"bar": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
"baz": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
"qux": ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
}
p if {
x := v
x.foo[_] == "a"
}
`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:11 | Enter data.test.p {}
%.*%/test.rego:12 | | Eval x = data.test.v {}
%.*%/test.rego:12 | | Index data.test.v (matched 1 rule, early exit) {}
%.*%/test.rego:4 | | Enter data.test.v {}
%.*%/test.rego:4 | | | Eval true {}
%.*%/test.rego:4 | | | Exit data.test.v early {}
%.*%/test.rego:14 | | Eval x.foo[_] = "a" {x: {"bar": ["a", "b", "c", "d", ...}
%.*%/test.rego:11 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:11 | Redo data.test.p {}
%.*%/test.rego:14 | | Redo x.foo[_] = "a" {_: 0, x: {"bar": ["a", "b", "c", "d", ...}
%.*%/test.rego:12 | | Redo x = data.test.v {data.test.v: {"bar": ["a", "b", "c", "d", ..., x: {"bar": ["a", "b", "c", "d", ...}
%.*%/test.rego:4 | | | Redo true {}
true
`,
},
{
note: "func call",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
p if {
x := 1
y := 2
z := 3
z == f(x, y)
}
f(a, b) := c if {
c := a + b
}
`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:4 | Enter data.test.p {}
%.*%/test.rego:5 | | Eval x = 1 {}
%.*%/test.rego:6 | | Eval y = 2 {}
%.*%/test.rego:7 | | Eval z = 3 {}
%.*%/test.rego:8 | | Eval data.test.f(x, y, __local6__) {x: 1, y: 2}
%.*%/test.rego:8 | | Index data.test.f (matched 1 rule) {x: 1, y: 2}
%.*%/test.rego:11 | | Enter data.test.f {}
%.*%/test.rego:12 | | | Eval plus(a, b, __local7__) {a: 1, b: 2}
%.*%/test.rego:12 | | | Eval c = __local7__ {__local7__: 3}
%.*%/test.rego:11 | | | Exit data.test.f {a: 1, b: 2, c: 3}
%.*%/test.rego:8 | | Eval z = __local6__ {__local6__: 3, z: 3}
%.*%/test.rego:4 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:4 | Redo data.test.p {}
%.*%/test.rego:8 | | Redo z = __local6__ {__local6__: 3, z: 3}
%.*%/test.rego:8 | | Redo data.test.f(x, y, __local6__) {__local6__: 3, x: 1, y: 2}
%.*%/test.rego:12 | | | Redo c = __local7__ {__local7__: 3, c: 3}
%.*%/test.rego:12 | | | Redo plus(a, b, __local7__) {__local7__: 3, a: 1, b: 2}
%.*%/test.rego:7 | | Redo z = 3 {z: 3}
%.*%/test.rego:6 | | Redo y = 2 {y: 2}
%.*%/test.rego:5 | | Redo x = 1 {x: 1}
true
`,
},
{
note: "every",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
p if {
l := ["a", "b", "c"]
every x in l {
count(x) == 1
}
}
f(a, b) := c if {
c := a + b
}
`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:4 | Enter data.test.p {}
%.*%/test.rego:5 | | Eval l = ["a", "b", "c"] {}
%.*%/test.rego:6 | | Eval __local6__ = l {l: ["a", "b", "c"]}
%.*%/test.rego:6 | | Eval every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local6__: ["a", "b", "c"]}
%.*%/test.rego:6 | | Enter every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local6__: ["a", "b", "c"]}
%.*%/test.rego:6 | | | Eval __local6__[__local1__] = x {__local6__: ["a", "b", "c"]}
%.*%/test.rego:7 | | | Enter count(x, __local7__); __local7__ = 1 {x: "a"}
%.*%/test.rego:7 | | | | Eval count(x, __local7__) {x: "a"}
%.*%/test.rego:7 | | | | Eval __local7__ = 1 {__local7__: 1}
%.*%/test.rego:7 | | | | Exit count(x, __local7__); __local7__ = 1 early {__local7__: 1, x: "a"}
%.*%/test.rego:7 | | | Redo count(x, __local7__); __local7__ = 1 {__local7__: 1, x: "a"}
%.*%/test.rego:7 | | | | Redo __local7__ = 1 {__local7__: 1}
%.*%/test.rego:7 | | | | Redo count(x, __local7__) {__local7__: 1, x: "a"}
%.*%/test.rego:6 | | | Redo every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local1__: 0, __local6__: ["a", "b", "c"], x: "a"}
%.*%/test.rego:6 | | | Redo __local6__[__local1__] = x {__local1__: 0, __local6__: ["a", "b", "c"], x: "a"}
%.*%/test.rego:7 | | | Enter count(x, __local7__); __local7__ = 1 {x: "b"}
%.*%/test.rego:7 | | | | Eval count(x, __local7__) {x: "b"}
%.*%/test.rego:7 | | | | Eval __local7__ = 1 {__local7__: 1}
%.*%/test.rego:7 | | | | Exit count(x, __local7__); __local7__ = 1 early {__local7__: 1, x: "b"}
%.*%/test.rego:7 | | | Redo count(x, __local7__); __local7__ = 1 {__local7__: 1, x: "b"}
%.*%/test.rego:7 | | | | Redo __local7__ = 1 {__local7__: 1}
%.*%/test.rego:7 | | | | Redo count(x, __local7__) {__local7__: 1, x: "b"}
%.*%/test.rego:6 | | | Redo every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local1__: 1, __local6__: ["a", "b", "c"], x: "b"}
%.*%/test.rego:6 | | | Redo __local6__[__local1__] = x {__local1__: 1, __local6__: ["a", "b", "c"], x: "b"}
%.*%/test.rego:7 | | | Enter count(x, __local7__); __local7__ = 1 {x: "c"}
%.*%/test.rego:7 | | | | Eval count(x, __local7__) {x: "c"}
%.*%/test.rego:7 | | | | Eval __local7__ = 1 {__local7__: 1}
%.*%/test.rego:7 | | | | Exit count(x, __local7__); __local7__ = 1 early {__local7__: 1, x: "c"}
%.*%/test.rego:7 | | | Redo count(x, __local7__); __local7__ = 1 {__local7__: 1, x: "c"}
%.*%/test.rego:7 | | | | Redo __local7__ = 1 {__local7__: 1}
%.*%/test.rego:7 | | | | Redo count(x, __local7__) {__local7__: 1, x: "c"}
%.*%/test.rego:6 | | | Redo every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local1__: 2, __local6__: ["a", "b", "c"], x: "c"}
%.*%/test.rego:6 | | | Redo __local6__[__local1__] = x {__local1__: 2, __local6__: ["a", "b", "c"], x: "c"}
%.*%/test.rego:4 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:4 | Redo data.test.p {}
%.*%/test.rego:6 | | Redo every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local6__: ["a", "b", "c"]}
%.*%/test.rego:6 | | | Exit every x in __local6__ { count(x, __local7__); __local7__ = 1 } {__local6__: ["a", "b", "c"]}
%.*%/test.rego:6 | | Redo __local6__ = l {__local6__: ["a", "b", "c"], l: ["a", "b", "c"]}
%.*%/test.rego:5 | | Redo l = ["a", "b", "c"] {l: ["a", "b", "c"]}
true
`,
},
{
note: "rule value",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
a := 1
p if {
a + 1 == 2
a + 2 == 3
}
`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:6 | Enter data.test.p {}
%.*%/test.rego:7 | | Eval __local2__ = data.test.a {}
%.*%/test.rego:7 | | Index data.test.a (matched 1 rule, early exit) {}
%.*%/test.rego:4 | | Enter data.test.a {}
%.*%/test.rego:4 | | | Eval true {}
%.*%/test.rego:4 | | | Exit data.test.a early {}
%.*%/test.rego:7 | | Eval plus(__local2__, 1, __local0__) {__local2__: 1}
%.*%/test.rego:7 | | Eval __local0__ = 2 {__local0__: 2}
%.*%/test.rego:8 | | Eval __local3__ = data.test.a {data.test.a: 1}
%.*%/test.rego:8 | | Index data.test.a (matched 1 rule, early exit) {data.test.a: 1}
%.*%/test.rego:8 | | Eval plus(__local3__, 2, __local1__) {__local3__: 1}
%.*%/test.rego:8 | | Eval __local1__ = 3 {__local1__: 3}
%.*%/test.rego:6 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:6 | Redo data.test.p {}
%.*%/test.rego:8 | | Redo __local1__ = 3 {__local1__: 3}
%.*%/test.rego:8 | | Redo plus(__local3__, 2, __local1__) {__local1__: 3, __local3__: 1}
%.*%/test.rego:8 | | Redo __local3__ = data.test.a {__local3__: 1, data.test.a: 1}
%.*%/test.rego:7 | | Redo __local0__ = 2 {__local0__: 2}
%.*%/test.rego:7 | | Redo plus(__local2__, 1, __local0__) {__local0__: 2, __local2__: 1}
%.*%/test.rego:7 | | Redo __local2__ = data.test.a {__local2__: 1, data.test.a: 1}
%.*%/test.rego:4 | | | Redo true {}
true
`,
},
{
note: "input values",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
p if {
input.x == 1
input.x + input.y == input.z
}
`,
"input.json": `{
"x": 1,
"y": 2,
"z": 3
}`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:4 | Enter data.test.p {}
%.*%/test.rego:5 | | Eval input.x = 1 {}
%.*%/test.rego:6 | | Eval __local1__ = input.x {}
%.*%/test.rego:6 | | Eval __local2__ = input.y {}
%.*%/test.rego:6 | | Eval plus(__local1__, __local2__, __local0__) {__local1__: 1, __local2__: 2}
%.*%/test.rego:6 | | Eval __local0__ = input.z {__local0__: 3}
%.*%/test.rego:4 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:4 | Redo data.test.p {}
%.*%/test.rego:6 | | Redo __local0__ = input.z {__local0__: 3}
%.*%/test.rego:6 | | Redo plus(__local1__, __local2__, __local0__) {__local0__: 3, __local1__: 1, __local2__: 2}
%.*%/test.rego:6 | | Redo __local2__ = input.y {__local2__: 2}
%.*%/test.rego:6 | | Redo __local1__ = input.x {__local1__: 1}
%.*%/test.rego:5 | | Redo input.x = 1 {}
true
`,
},
{
note: "data values",
query: "data.test.p",
includeVars: true,
files: map[string]string{
"test.rego": `package test
import rego.v1
p if {
data.x == 1
data.x + data.y == data.z
}
`,
"data.json": `{
"x": 1,
"y": 2,
"z": 3
}`,
},
expected: `%SKIP_LINE%
query:1 %.*% Enter data.test.p = _ {}
query:1 %.*% | Eval data.test.p = _ {}
query:1 %.*% | Index data.test.p (matched 1 rule, early exit) {}
%.*%/test.rego:4 | Enter data.test.p {}
%.*%/test.rego:5 | | Eval data.x = 1 {}
%.*%/test.rego:6 | | Eval __local1__ = data.x {}
%.*%/test.rego:6 | | Eval __local2__ = data.y {}
%.*%/test.rego:6 | | Eval plus(__local1__, __local2__, __local0__) {__local1__: 1, __local2__: 2}
%.*%/test.rego:6 | | Eval __local0__ = data.z {__local0__: 3}
%.*%/test.rego:4 | | Exit data.test.p early {}
query:1 %.*% | Exit data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% Redo data.test.p = _ {_: true, data.test.p: true}
query:1 %.*% | Redo data.test.p = _ {_: true, data.test.p: true}
%.*%/test.rego:4 | Redo data.test.p {}
%.*%/test.rego:6 | | Redo __local0__ = data.z {__local0__: 3}
%.*%/test.rego:6 | | Redo plus(__local1__, __local2__, __local0__) {__local0__: 3, __local1__: 1, __local2__: 2}
%.*%/test.rego:6 | | Redo __local2__ = data.y {__local2__: 2}
%.*%/test.rego:6 | | Redo __local1__ = data.x {__local1__: 1}
%.*%/test.rego:5 | | Redo data.x = 1 {}
true
`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
var buf bytes.Buffer
test.WithTempFS(tc.files, func(path string) {
params := newEvalCommandParams()
_ = params.bundlePaths.Set(path)
inputFile := filepath.Join(path, "input.json")
if _, err := os.Stat(inputFile); err == nil {
params.inputPath = inputFile
}
_ = params.outputFormat.Set(formats.Pretty)
_ = params.explain.Set(explainModeFull)
params.traceVarValues = tc.includeVars
params.disableIndexing = true
_ = params.bundlePaths.Set(path)
_, err := eval([]string{tc.query}, params, &buf, nil)
if err != nil {
t.Fatalf("Unexpected error: %s\n\n%s", err, buf.String())
}
})
actual := buf.String()
if !stringsMatch(t, tc.expected, actual) {
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", tc.expected, actual)
}
})
}
}
func stringsMatch(t *testing.T, expected, actual string) bool {
t.Helper()
var expectedLines []string
for l := range strings.SplitSeq(expected, "\n") {
if !strings.Contains(l, "%SKIP_LINE%") {
expectedLines = append(expectedLines, l)
}
}
actualLines := strings.Split(actual, "\n")
if len(expectedLines) != len(actualLines) {
t.Errorf("Expected %d lines but got %d", len(expectedLines), len(actualLines))
return false
}
for i, expectedLine := range expectedLines {
actualLine := actualLines[i]
expectedParts := strings.Split(expectedLine, "%.*%")
if len(expectedParts) == 1 {
if expectedLine != actualLine {
t.Errorf("Mismatch on line %d. Expected:\n\n%s\n\nGot:\n\n%s", i, expectedLine, actualLine)
return false
}
} else if len(expectedParts) == 2 {
if !strings.HasPrefix(actualLine, expectedParts[0]) {
t.Errorf("Expected line %d to start with:\n\n%s\n\nbut got:\n\n%s", i, expectedParts[0], actualLine)
return false
}
if !strings.HasSuffix(actualLine, expectedParts[1]) {
t.Errorf("Expected line %d to end with:\n\n%s\n\nbut got:\n\n%s", i, expectedParts[1], actualLine)
return false
}
} else {
t.Fatalf("At most one .* is allowed per line but found %d on line %d:\n\n%s", len(expectedParts)-1, i, expectedLine)
return false
}
}
return true
}
func TestResetExprLocations(t *testing.T) {
// Make sure no panic if passed nil.
resetExprLocations(nil)
// Run partial evaluation on this fake module and check results.
// The content of the module is not very important it just has to generate
// support and cases where the locaiton is unset. The default causes support
// and exprs with no location information.
pq, err := rego.New(rego.Query("data.test.p = x"), rego.Module("test.rego", `
package test
default p = false
p if {
input.x = q[_]
}
q contains 1
q contains 2
`)).Partial(t.Context())
if err != nil {
t.Fatal(err)
}
resetExprLocations(pq)
var exp int
vis := ast.NewGenericVisitor(func(x any) bool {
if expr, ok := x.(*ast.Expr); ok {
if expr.Location.Row != exp {
t.Fatalf("Expected %v to have row %v but got %v", expr, exp, expr.Location.Row)
}
exp++
}
return false
})
for i := range pq.Queries {
vis.Walk(pq.Queries[i])
}
for i := range pq.Support {
vis.Walk(pq.Support[i])
}
}
func kubeSchemaServer(t *testing.T) *httptest.Server {
t.Helper()
bs, err := os.ReadFile("../v1/ast/testdata/_definitions.json")
if err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write(bs)
if err != nil {
panic(err)
}
}))
return ts
}
func TestEvalPartialFormattedOutput(t *testing.T) {
query := `time.clock(input.x) == time.clock(input.y)`
tests := []struct {
format, expected string
}{
{
format: formats.Pretty,
expected: `┌─────────┬──────────────────────────────────────────┐
│ Query 1 │ time.clock(input.y, time.clock(input.x)) │
└─────────┴──────────────────────────────────────────┘
`},
{
format: formats.Source,
expected: `# Query 1
time.clock(input.y, time.clock(input.x))
`},
}
for _, tc := range tests {
t.Run(tc.format, func(t *testing.T) {
buf := new(bytes.Buffer)
params := newEvalCommandParams()
params.partial = true
_ = params.outputFormat.Set(tc.format)
_, err := eval([]string{query}, params, buf, nil)
if err != nil {
t.Fatal("unexpected error:", err)
}
if diff := cmp.Diff(buf.String(), tc.expected); diff != "" {
t.Error("output mismatch (-want +got):\n", diff)
}
})
}
}
func TestEvalPartialOutput_RegoVersion(t *testing.T) {
tests := []struct {
note string
regoV1ImportCapable bool
v0Compatible bool
query string
module string
expected map[string]string
}{
{
note: "v0, no future keywords",
v0Compatible: true,
regoV1ImportCapable: true,
query: "data.test.p",
module: `package test
p[v] {
v := input.v
}
`,
expected: map[string]string{
formats.Source: `# Query 1
data.partial.test.p
# Module 1
package partial.test
import rego.v1
p contains __local0__1 if __local0__1 = input.v
`,
formats.Pretty: `┌───────────┬─────────────────────────────────────────────────┐
│ Query 1 │ data.partial.test.p │
├───────────┼─────────────────────────────────────────────────┤
│ Support 1 │ package partial.test │
│ │ │
│ │ import rego.v1 │
│ │ │
│ │ p contains __local0__1 if __local0__1 = input.v │
└───────────┴─────────────────────────────────────────────────┘
`,
},
},
{
note: "v0, no future keywords, not rego.v1 import capable",
v0Compatible: true,
regoV1ImportCapable: false,
query: "data.test.p",
module: `package test
p[v] {
v := input.v
}
`,
expected: map[string]string{
formats.Source: `# Query 1
data.partial.test.p
# Module 1
package partial.test
p[__local0__1] {
__local0__1 = input.v
}
`,
formats.Pretty: `┌───────────┬─────────────────────────┐
│ Query 1 │ data.partial.test.p │
├───────────┼─────────────────────────┤
│ Support 1 │ package partial.test │
│ │ │
│ │ p[__local0__1] { │
│ │ __local0__1 = input.v │
│ │ } │
└───────────┴─────────────────────────┘
`,
},
},
{
note: "v0, future keywords",
v0Compatible: true,
regoV1ImportCapable: true,
query: "data.test.p",
module: `package test
import rego.v1
p contains v if {
v := input.v
}
`,
expected: map[string]string{
formats.Source: `# Query 1
data.partial.test.p
# Module 1
package partial.test
import rego.v1
p contains __local0__1 if __local0__1 = input.v
`,
formats.Pretty: `┌───────────┬─────────────────────────────────────────────────┐
│ Query 1 │ data.partial.test.p │
├───────────┼─────────────────────────────────────────────────┤
│ Support 1 │ package partial.test │
│ │ │
│ │ import rego.v1 │
│ │ │
│ │ p contains __local0__1 if __local0__1 = input.v │
└───────────┴─────────────────────────────────────────────────┘
`,
},
},
{
note: "v1",
regoV1ImportCapable: true,
v0Compatible: false,
query: "data.test.p",
module: `package test
p contains v if {
v := input.v
}
`,
expected: map[string]string{
formats.Source: `# Query 1
data.partial.test.p
# Module 1
package partial.test
p contains __local0__1 if __local0__1 = input.v
`,
formats.Pretty: `┌───────────┬─────────────────────────────────────────────────┐
│ Query 1 │ data.partial.test.p │
├───────────┼─────────────────────────────────────────────────┤
│ Support 1 │ package partial.test │
│ │ │
│ │ p contains __local0__1 if __local0__1 = input.v │
└───────────┴─────────────────────────────────────────────────┘
`,
},
},
{
note: "v1, rego.v1 import",
regoV1ImportCapable: true,
v0Compatible: false,
query: "data.test.p",
module: `package test
import rego.v1
p contains v if {
v := input.v
}
`,
expected: map[string]string{
formats.Source: `# Query 1
data.partial.test.p
# Module 1
package partial.test
p contains __local0__1 if __local0__1 = input.v
`,
formats.Pretty: `┌───────────┬─────────────────────────────────────────────────┐
│ Query 1 │ data.partial.test.p │
├───────────┼─────────────────────────────────────────────────┤
│ Support 1 │ package partial.test │
│ │ │
│ │ p contains __local0__1 if __local0__1 = input.v │
└───────────┴─────────────────────────────────────────────────┘
`,
},
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
for format, expected := range tc.expected {
t.Run(format, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.module,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
_ = params.dataPaths.Set(filepath.Join(path, "test.rego"))
params.partial = true
params.v0Compatible = tc.v0Compatible
_ = params.outputFormat.Set(format)
if !tc.regoV1ImportCapable {
caps := newCapabilitiesFlag()
caps.C = ast.CapabilitiesForThisVersion()
caps.C.Features = []string{
ast.FeatureRefHeadStringPrefixes,
ast.FeatureRefHeads,
}
params.capabilities = caps
}
buf := new(bytes.Buffer)
_, err := eval([]string{tc.query}, params, buf, nil)
if err != nil {
t.Fatal("unexpected error:", err)
}
if diff := cmp.Diff(buf.String(), expected); diff != "" {
t.Error("output mismatch (-want +got):\n", diff)
}
})
})
}
})
}
}
func TestEvalDiscardOutput(t *testing.T) {
tests := map[string]struct {
query, format, expected string
params evalCommandParams
}{
"success example": {
query: "1*2+3",
params: func() evalCommandParams {
params := newEvalCommandParams()
err := params.outputFormat.Set(formats.Discard)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
return params
}(),
expected: `{
"result": "discarded"
}
`},
"error example": {
query: "1/0",
params: func() evalCommandParams {
params := newEvalCommandParams()
err := params.outputFormat.Set(formats.Discard)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
return params
}(),
expected: `{}
`},
"error example show built-in-errors": {
query: "1/0",
params: func() evalCommandParams {
params := newEvalCommandParams()
err := params.outputFormat.Set(formats.Discard)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
params.showBuiltinErrors = true
return params
}(),
expected: `{
"errors": [
{
"code": "eval_builtin_error",
"location": {
"col": 1,
"file": "",
"row": 1
},
"message": "div: divide by zero"
}
]
}
`},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
var buf bytes.Buffer
_, err := eval([]string{tc.query}, tc.params, &buf, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(buf.String(), tc.expected); diff != "" {
t.Error("output mismatch (-want +got):\n", diff)
}
})
}
}
func TestEvalDiscardProfilerOutput(t *testing.T) {
params := newEvalCommandParams()
err := params.outputFormat.Set(formats.Discard)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
params.profile = true
query := "1*2+3"
var buf bytes.Buffer
_, err = eval([]string{query}, params, &buf, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
var output map[string]any
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
// assert that the result is set to discarded
result, ok := output["result"].(string)
if !ok {
t.Fatal("error extracting result as string from output")
}
if result != "discarded" {
t.Fatal("Expected result field to be set to 'discarded'")
}
// assert that profile is still set
_, ok = output["profile"]
if !ok {
t.Fatal("error in parsing profile output")
}
}
func TestPolicyWithStrictFlag(t *testing.T) {
testsShouldError := []struct {
note string
v0Compatible bool
policy string
query string
expectedCode string
expectedMessage string
}{
{
note: "strict mode should error on unused imports",
policy: `package x
import future.keywords.if
import data.foo
foo = 2`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "import data.foo unused",
},
{
note: "v0 compat, strict mode should error on duplicate imports",
v0Compatible: true,
policy: `package x
import data.bar
import data.bar
foo = bar`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "import must not shadow import data.bar",
},
{
note: "v0 compat, strict mode should error on unused imports",
v0Compatible: true,
policy: `package x
import future.keywords.if
import data.foo
foo = 2`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "import data.foo unused",
},
{
note: "v0 compat, strict mode should error when reserved vars data or input is used",
v0Compatible: true,
policy: `package x
data { x = 1}`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "rules must not shadow data (use a different rule name)",
},
}
for _, tc := range testsShouldError {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(path string) {
for _, strict := range []bool{true, false} {
params := newEvalCommandParams()
params.strict = strict
params.v0Compatible = tc.v0Compatible
_ = params.dataPaths.Set(filepath.Join(path, "test.rego"))
var buf bytes.Buffer
_, err := eval([]string{tc.query}, params, &buf, nil)
if strict {
if err == nil {
t.Fatal("expected error, got nil")
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if code := output.Errors[0].Code; code != tc.expectedCode {
t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code)
}
if msg := output.Errors[0].Message; msg != tc.expectedMessage {
t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg)
}
} else if err != nil {
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
t.Fatal("unexpected error when non-strict:", output)
}
}
})
})
}
testsShouldPass := []struct {
note string
policy string
query string
}{
{
note: "This should not error as it is valid",
policy: `package x
import future.keywords.if
foo = 2`,
query: "data.foo",
},
{
note: "Strict mode should not validate the query, only the policy, this should not error",
policy: `package x
import future.keywords.if
foo = 2`,
query: "x := data.x.foo",
},
}
for _, tc := range testsShouldPass {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(_ string) {
params := newEvalCommandParams()
params.strict = true
var buf bytes.Buffer
_, err := eval([]string{tc.query}, params, &buf, nil)
if err != nil {
t.Errorf("Should not error, got error: '%v'", err)
}
})
})
}
}
func TestBundleWithStrictFlag(t *testing.T) {
testsShouldError := []struct {
note string
v0Compatible bool
policy string
query string
expectedCode string
expectedMessage string
}{
{
note: "strict mode should error on unused imports in this bundle",
policy: `package x
import data.foo
foo = 2`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "import data.foo unused",
},
{
note: "v0 compat, strict mode should error on duplicate imports in this bundle",
v0Compatible: true,
policy: `package x
import data.bar
import data.bar
foo = bar`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "import must not shadow import data.bar",
},
{
note: "v0 compat, strict mode should error on unused imports in this bundle",
v0Compatible: true,
policy: `package x
import data.foo
foo = 2`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "import data.foo unused",
},
{
note: "v0 compat, strict mode should error when reserved vars data or input is used in this bundle",
v0Compatible: true,
policy: `package x
data { x = 1}`,
query: "data.foo",
expectedCode: "rego_compile_error",
expectedMessage: "rules must not shadow data (use a different rule name)",
},
}
for _, tc := range testsShouldError {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(path string) {
for _, strict := range []bool{true, false} {
params := newEvalCommandParams()
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
params.strict = strict
params.v0Compatible = tc.v0Compatible
var buf bytes.Buffer
_, err := eval([]string{tc.query}, params, &buf, nil)
if strict {
if err == nil {
t.Fatal("expected error, got nil")
}
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
if code := output.Errors[0].Code; code != tc.expectedCode {
t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code)
}
if msg := output.Errors[0].Message; msg != tc.expectedMessage {
t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg)
}
} else if err != nil {
var output presentation.Output
if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil {
t.Fatal(err)
}
t.Fatal("unexpected error when non-strict:", output)
}
}
})
})
}
testsShouldPass := []struct {
note string
policy string
query string
}{
{
note: "This bundle should not error as it is valid",
policy: `package x
import future.keywords.if
foo = 2`,
query: "data.foo",
},
{
note: "Strict mode should not validate the query, only the policy, this bundle should not error",
policy: `package x
import future.keywords.if
foo = 2`,
query: "x := data.x.foo",
},
}
for _, tc := range testsShouldPass {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.policy,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
params.strict = true
var buf bytes.Buffer
_, err := eval([]string{tc.query}, params, &buf, nil)
if err != nil {
t.Errorf("Should not error, got error: '%v'", err)
}
})
})
}
}
func TestIfElseIfElseNoBrace(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
p if false
else := 1 if false
else := 2`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
var buf bytes.Buffer
defined, err := eval([]string{"data.bug.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func TestIfElseIfElseBrace(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
p if false
else := 1 if { false }
else := 2`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
var buf bytes.Buffer
defined, err := eval([]string{"data.bug.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func TestIfElse(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
p if false
else := 1 `,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
var buf bytes.Buffer
defined, err := eval([]string{"data.bug.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
// TestElseNoIfV0 only applies to v0 Rego
func TestElseNoIfV0(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
import future.keywords.if
p if false
else = x {
x=2
} `,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
params.v0Compatible = true
var buf bytes.Buffer
defined, err := eval([]string{"data.bug.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func TestElseIf(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
p if false
else := x if {
x=2
} `,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
var buf bytes.Buffer
defined, err := eval([]string{"data.bug.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
// TestElseIfElseV0 only applies to v0 Rego
func TestElseIfElseV0(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
import future.keywords.if
p if false
else := x if {
x=2
1==2
} else =x {
x=3
}`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
params.v0Compatible = true
var buf bytes.Buffer
defined, err := eval([]string{"data.bug.p"}, params, &buf, nil)
if !defined || err != nil {
t.Fatalf("Unexpected undefined or error: %v", err)
}
})
}
func TestUnexpectedElseIfElseErr(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
p if false
else := x if {
x=2
1==2
} else
x=3
`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
var buf bytes.Buffer
_, err := eval([]string{"data.bug.p"}, params, &buf, nil)
// Check if there was an error
if err == nil {
t.Fatalf("expected an error, but got nil")
}
// Check the error message
errorMessage := err.Error()
expectedErrorMessage := "rego_parse_error: unexpected identifier token: expected else value term or rule body"
if !strings.Contains(errorMessage, expectedErrorMessage) {
t.Fatalf("expected error message to contain '%s', but got '%s'", expectedErrorMessage, errorMessage)
}
})
}
func TestUnexpectedElseIfErr(t *testing.T) {
files := map[string]string{
"bug.rego": `package bug
q := 1 if false
else := 2 if
`,
}
test.WithTempFS(files, func(path string) {
params := newEvalCommandParams()
params.optimizationLevel = 1
params.dataPaths = newrepeatedStringFlag([]string{path})
params.entrypoints = newrepeatedStringFlag([]string{"bug/p"})
var buf bytes.Buffer
_, err := eval([]string{"data.bug.p"}, params, &buf, nil)
// Check if there was an error
if err == nil {
t.Fatalf("expected an error, but got nil")
}
// Check the error message
errorMessage := err.Error()
expectedErrorMessage := "rego_parse_error: unexpected eof token: rule body expected"
if !strings.Contains(errorMessage, expectedErrorMessage) {
t.Fatalf("expected error message to contain '%s', but got '%s'", expectedErrorMessage, errorMessage)
}
})
}
func TestEval_DefaultRegoVersion(t *testing.T) {
tests := []struct {
note string
modules map[string]string
query string
expErrs []string
}{
{
note: "v0 module",
modules: map[string]string{
"test.rego": `package test
a[x] {
x := 42
}`,
},
query: `data.test.a`,
expErrs: []string{
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
},
},
{
note: "v1 module",
modules: map[string]string{
"test.rego": `package test
a contains x if {
x := 42
}`,
},
query: `data.test.a`,
},
}
setup := []struct {
name string
commandParams func(params *evalCommandParams, path string)
}{
{
name: "Files",
commandParams: func(params *evalCommandParams, path string) {
params.dataPaths = newrepeatedStringFlag([]string{path})
},
},
{
name: "Bundle",
commandParams: func(params *evalCommandParams, path string) {
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
},
},
}
for _, s := range setup {
for _, tc := range tests {
t.Run(fmt.Sprintf("%s: %s", s.name, tc.note), func(t *testing.T) {
test.WithTempFS(tc.modules, func(path string) {
params := newEvalCommandParams()
_ = params.outputFormat.Set(formats.Pretty)
s.commandParams(&params, path)
var buf bytes.Buffer
defined, err := eval([]string{tc.query}, params, &buf, &buf)
if len(tc.expErrs) > 0 {
if err == nil {
t.Fatal("expected error, got none")
}
actual := buf.String()
for _, expErr := range tc.expErrs {
if !strings.Contains(actual, expErr) {
t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", expErr, actual)
}
}
} else {
if err != nil {
t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String())
} else if !defined {
t.Fatal("expected result to be defined")
}
}
})
})
}
}
}
func TestEvalPolicyWithCompatibleFlags(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
v1Compatible bool
modules map[string]string
query string
expectedErr string
}{
{
note: "v0 compatibility: policy with no rego.v1 or future.keywords imports",
v0Compatible: true,
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
query: "data.test.allow",
expectedErr: "rego_parse_error",
},
{
note: "v0 compatibility: policy with rego.v1 import",
v0Compatible: true,
modules: map[string]string{
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v0 compatibility: policy with future.keywords import",
v0Compatible: true,
modules: map[string]string{
"test.rego": `package test
import future.keywords
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1 compatibility: policy with no rego.v1 or future.keywords imports",
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1 compatibility: policy with rego.v1 import",
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1 compatibility: policy with future.keywords import",
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
import future.keywords.if
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v0 + v1 compatibility: policy with no rego.v1 or future.keywords imports",
v0Compatible: true,
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
query: "data.test.allow",
expectedErr: "rego_parse_error",
},
{
note: "v0 + v1 compatibility: policy with rego.v1 import",
v0Compatible: true,
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v0 + v1 compatibility: policy with future.keywords import",
v0Compatible: true,
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
import future.keywords
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1 compatibility: policy with no rego.v1 or future.keywords imports",
v1Compatible: true,
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
}
setup := []struct {
name string
commandParams func(params *evalCommandParams, path string)
}{
{
name: "Files",
commandParams: func(params *evalCommandParams, path string) {
params.dataPaths = newrepeatedStringFlag([]string{path})
},
},
{
name: "Bundle",
commandParams: func(params *evalCommandParams, path string) {
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
},
},
}
for _, s := range setup {
for _, tc := range tests {
t.Run(fmt.Sprintf("%s: %s", s.name, tc.note), func(t *testing.T) {
test.WithTempFS(tc.modules, func(path string) {
params := newEvalCommandParams()
s.commandParams(&params, path)
params.v0Compatible = tc.v0Compatible
params.v1Compatible = tc.v1Compatible
var buf bytes.Buffer
defined, err := eval([]string{tc.query}, params, &buf, nil)
if tc.expectedErr == "" {
if err != nil {
t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String())
} else if !defined {
t.Fatal("expected result to be defined")
}
} else {
if err == nil {
t.Fatal("expected error, got none")
}
actual := buf.String()
if !strings.Contains(actual, tc.expectedErr) {
t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", tc.expectedErr, actual)
}
}
})
})
}
}
}
func TestEvalPolicyWithRegoV1Capability(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
capabilities *ast.Capabilities
modules map[string]string
expErrs []string
}{
{
note: "v0 module, v0-compatible, no capabilities",
v0Compatible: true,
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
},
{
note: "v0 module, v0-compatible, v0 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
},
{
note: "v0 module, v0-compatible, v1 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
},
{
note: "v0 module, not v0-compatible, no capabilities",
v0Compatible: false,
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
expErrs: []string{
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v0 module, not v0-compatible, v0 capabilities",
v0Compatible: false,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
expErrs: []string{
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v0 module, not v0-compatible, v0 capabilities without rego_v1 feature",
v0Compatible: false,
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
expErrs: []string{
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
},
},
{
note: "v0 module, not v0-compatible, v1 capabilities",
v0Compatible: false,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
modules: map[string]string{
"test.rego": `package test
allow {
1 < 2
}`,
},
expErrs: []string{
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
},
},
{
note: "v1 module, v0-compatible, no capabilities",
v0Compatible: true,
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, v0-compatible, v0 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, v0-compatible, v1 capabilities",
v0Compatible: true,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
expErrs: []string{
"test.rego:2: rego_parse_error: var cannot be used for rule name",
},
},
{
note: "v1 module, not v0-compatible, no capabilities",
v0Compatible: false,
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
},
{
note: "v1 module, not v0-compatible, v0 capabilities",
v0Compatible: false,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
},
{
note: "v1 module, not v0-compatible, v0 capabilities without rego_v1 feature",
v0Compatible: false,
capabilities: capsWithoutFeat(ast.RegoV0, ast.FeatureRegoV1),
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
expErrs: []string{
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
},
},
{
note: "v1 module, not v0-compatible, v1 capabilities",
v0Compatible: false,
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
modules: map[string]string{
"test.rego": `package test
allow if {
1 < 2
}`,
},
},
}
setup := []struct {
name string
commandParams func(params *evalCommandParams, path string)
}{
{
name: "Files",
commandParams: func(params *evalCommandParams, path string) {
params.dataPaths = newrepeatedStringFlag([]string{path})
},
},
{
name: "Bundle",
commandParams: func(params *evalCommandParams, path string) {
if err := params.bundlePaths.Set(path); err != nil {
t.Fatal(err)
}
},
},
}
for _, s := range setup {
for _, tc := range tests {
t.Run(fmt.Sprintf("%s: %s", s.name, tc.note), func(t *testing.T) {
test.WithTempFS(tc.modules, func(path string) {
params := newEvalCommandParams()
s.commandParams(&params, path)
_ = params.outputFormat.Set(formats.Pretty)
params.v0Compatible = tc.v0Compatible
params.capabilities.C = tc.capabilities
var buf bytes.Buffer
defined, err := eval([]string{"data.test.allow"}, params, &buf, &buf)
if len(tc.expErrs) > 0 {
if err == nil {
t.Fatal("expected error, got none")
}
actual := buf.String()
for _, expErr := range tc.expErrs {
if !strings.Contains(actual, expErr) {
t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", expErr, actual)
}
}
} else {
if err != nil {
t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String())
} else if !defined {
t.Fatal("expected result to be defined")
}
}
})
})
}
}
}
func TestEvalPolicyWithBundleRegoVersion(t *testing.T) {
tests := []struct {
note string
files map[string]string
query string
expectedErr string
}{
{
note: "v0.x bundle, no rego.v1 or future.keywords imports",
files: map[string]string{
".manifest": `{"rego_version": 0}`,
"policy.rego": `package test
allow if {
1 < 2
}`,
},
query: "data.test.allow",
expectedErr: "rego_parse_error",
},
{
note: "v0 bundle, v1 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[1] {
1 < 2
}
`,
"policy2.rego": `package test
p contains 2 if {
1 < 2
}
`,
},
query: "data.test.p",
},
{
note: "v0 bundle, v1 per-file override (glob)",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/bar/*.rego": 1
}
}`,
"foo/policy1.rego": `package test
p[1] {
1 < 2
}
`,
"bar/policy1.rego": `package test
p contains 2 if {
1 < 2
}
`,
"bar/policy2.rego": `package test
p contains 3 if {
1 < 2
}
`,
},
query: "data.test.p",
},
{
note: "v0 bundle, v1 per-file override, incompliant",
files: map[string]string{
".manifest": `{
"rego_version": 0,
"file_rego_versions": {
"/policy2.rego": 1
}
}`,
"policy1.rego": `package test
p[1] {
1 < 2
}
`,
"policy2.rego": `package test
p[2] {
1 < 2
}
`,
},
query: "data.test.p",
expectedErr: "rego_parse_error",
},
{
note: "v1.0 bundle, no rego.v1 or future.keywords imports",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1.0 bundle, policy with rego.v1 import",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import rego.v1
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1.0 bundle, future.keywords import",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
import future.keywords.if
allow if {
1 < 2
}`,
},
query: "data.test.allow",
},
{
note: "v1.0 bundle, keywords not used",
files: map[string]string{
".manifest": `{"rego_version": 1}`,
"policy.rego": `package test
allow {
1 < 2
}`,
},
query: "data.test.allow",
expectedErr: "rego_parse_error",
},
{
note: "v1 bundle, v0 per-file override",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/policy1.rego": 0
}
}`,
"policy1.rego": `package test
p[1] {
1 < 2
}
`,
"policy2.rego": `package test
p contains 2 if {
1 < 2
}
`,
},
query: "data.test.p",
},
{
note: "v1 bundle, v0 per-file override (glob)",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"/foo/*.rego": 0
}
}`,
"foo/policy1.rego": `package test
p[1] {
1 < 2
}
`,
"foo/policy2.rego": `package test
p[2] {
1 < 2
}
`,
"bar/policy1.rego": `package test
p contains 3 if {
1 < 2
}
`,
},
query: "data.test.p",
},
{
note: "v1 bundle, v0 per-file override, incompliant",
files: map[string]string{
".manifest": `{
"rego_version": 1,
"file_rego_versions": {
"*/policy2.rego": 0
}
}`,
"policy1.rego": `package test
p contains 1 if {
input.x == 1
}
`,
"policy2.rego": `package test
p contains 2 if {
input.x == 1
}
`,
},
query: "data.test.p",
expectedErr: "rego_parse_error",
},
}
bundleTypeCases := []struct {
note string
tar bool
}{
{
"bundle dir", false,
},
{
"bundle tar", true,
},
}
v0CompatibleFlagCases := []struct {
note string
used bool
}{
{
"no --v0-compatible", false,
},
{
"--v0-compatible", true,
},
}
for _, bundleType := range bundleTypeCases {
for _, v0CompatibleFlag := range v0CompatibleFlagCases {
for _, tc := range tests {
t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v0CompatibleFlag.note, tc.note), func(t *testing.T) {
files := map[string]string{}
if bundleType.tar {
files["bundle.tar.gz"] = ""
} else {
maps.Copy(files, tc.files)
}
test.WithTempFS(files, func(root string) {
p := root
if bundleType.tar {
p = filepath.Join(root, "bundle.tar.gz")
files := make([][2]string, 0, len(tc.files))
for k, v := range tc.files {
files = append(files, [2]string{k, v})
}
buf := archive.MustWriteTarGz(files)
bf, err := os.Create(p)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
_, err = bf.Write(buf.Bytes())
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
params := newEvalCommandParams()
params.v0Compatible = v0CompatibleFlag.used
if err := params.bundlePaths.Set(p); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
defined, err := eval([]string{tc.query}, params, &buf, nil)
if tc.expectedErr == "" {
if err != nil {
t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String())
} else if !defined {
t.Fatal("expected result to be defined")
}
} else {
if err == nil {
t.Fatal("expected error, got none")
}
actual := buf.String()
if !strings.Contains(actual, tc.expectedErr) {
t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", tc.expectedErr, actual)
}
}
})
})
}
}
}
}
func TestWithQueryImports(t *testing.T) {
tests := []struct {
note string
query string
imports []string
v0Compatible bool
v1Compatible bool
exp string
expErrs []string
}{
{
note: "no imports, none required",
query: "1 + 2",
exp: "3\n",
},
{
note: "future keyword used, future.keywords imported",
query: `"b" in ["a", "b", "c"]`,
imports: []string{"future.keywords.in"},
exp: "true\n",
},
{
note: "future keyword used, rego.v1 imported",
query: `"b" in ["a", "b", "c"]`,
imports: []string{"rego.v1"},
exp: "true\n",
},
{
note: "future keyword used, invalid rego.v2 imported",
v0Compatible: true,
query: `"b" in ["a", "b", "c"]`,
imports: []string{"rego.v2"},
expErrs: []string{
"1:8: rego_parse_error: invalid import `rego.v2`, must be `rego.v1`",
},
},
{
note: "future keyword used, no imports (v0)",
v0Compatible: true,
query: `"b" in ["a", "b", "c"]`,
expErrs: []string{
"1:5: rego_unsafe_var_error: var in is unsafe (hint: `import future.keywords.in` to import a future keyword)",
},
},
{
note: "future keyword used, no imports (v1)",
v1Compatible: true,
query: `"b" in ["a", "b", "c"]`,
exp: "true\n",
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
params := newEvalCommandParams()
_ = params.outputFormat.Set(formats.Pretty)
params.imports = newrepeatedStringFlag(tc.imports)
params.v0Compatible = tc.v0Compatible
params.v1Compatible = tc.v1Compatible
var buf bytes.Buffer
defined, err := eval([]string{tc.query}, params, &buf, &buf)
if len(tc.expErrs) == 0 {
if err != nil {
t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String())
}
if !defined {
t.Fatal("expected result to be defined")
}
if buf.String() != tc.exp {
t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", tc.exp, buf.String())
}
} else {
if err == nil {
t.Fatal("expected error, got none")
}
actual := buf.String()
for _, expErr := range tc.expErrs {
if !strings.Contains(actual, expErr) {
t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", expErr, actual)
}
}
}
})
}
}
func TestEvalJSONOutputBytes(t *testing.T) {
params := newEvalCommandParams()
var buf bytes.Buffer
defined, err := eval([]string{"1 == 1"}, params, &buf, nil)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !defined {
t.Fatal("expected result to be defined")
}
expected := `{
"result": [
{
"expressions": [
{
"value": true,
"text": "1 == 1",
"location": {
"row": 1,
"col": 1
}
}
]
}
]
}
`
if diff := cmp.Diff(expected, buf.String()); diff != "" {
t.Fatalf("unexpected JSON output (-want +got):\n%s", diff)
}
}