mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -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>
3986 lines
107 KiB
Go
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(¶ms, []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(¶ms, []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(¶ms, 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(¶ms, 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(¶ms, 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)
|
|
}
|
|
}
|