mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
afb30d3f9d
Brace yourselves! For there are many touched files here. No changes in semantics however. Spent a long time trying out the various optional rules gocritic provides, and settled for a few of them. There are more I really like, but that would take many hours to address across the codebase. Perhaps others find gocritic too pedantic? If so, we can merge the fixes without enabling the rule. Signed-off-by: Anders Eknert <anders@styra.com>
3361 lines
77 KiB
Go
3361 lines
77 KiB
Go
// Copyright 2017 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 rego
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/open-policy-agent/opa/internal/storage/mock"
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/ast/location"
|
|
"github.com/open-policy-agent/opa/v1/bundle"
|
|
"github.com/open-policy-agent/opa/v1/metrics"
|
|
"github.com/open-policy-agent/opa/v1/storage"
|
|
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
|
"github.com/open-policy-agent/opa/v1/topdown"
|
|
"github.com/open-policy-agent/opa/v1/topdown/builtins"
|
|
"github.com/open-policy-agent/opa/v1/topdown/cache"
|
|
"github.com/open-policy-agent/opa/v1/types"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
)
|
|
|
|
func TestRegoEval_DefaultRegoVersion(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
expResult interface{}
|
|
expErrs []string
|
|
}{
|
|
{
|
|
note: "v0 module", // v0 in NOT the default version
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
|
|
"test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
{
|
|
note: "import rego.v1",
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
note: "v1 module ", // v1 is the default version
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
note: "v1 module, v1 compile-time violations", // v1 is the default version
|
|
module: `package test
|
|
import data.foo
|
|
import data.bar as foo
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:3: rego_compile_error: import must not shadow import data.foo",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
files := map[string]string{
|
|
"test.rego": tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(root string) {
|
|
ctx := context.Background()
|
|
|
|
pq, err := New(
|
|
Load([]string{root}, nil),
|
|
Query("data.test.p"),
|
|
).PrepareForEval(ctx)
|
|
|
|
if tc.expErrs != nil {
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got nil")
|
|
}
|
|
|
|
for _, expErr := range tc.expErrs {
|
|
if !strings.Contains(err.Error(), expErr) {
|
|
t.Fatalf("Expected error to contain %q but got: %v", expErr, err)
|
|
}
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(rs) != 1 {
|
|
t.Fatalf("Expected exactly one result but got: %v", rs)
|
|
}
|
|
|
|
if reflect.DeepEqual(rs[0].Expressions[0].Value, tc.expResult) {
|
|
t.Fatalf("Expected %v but got: %v", tc.expResult, rs[0].Expressions[0].Value)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegoEval_Capabilities(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
regoVersion ast.RegoVersion
|
|
capabilities *ast.Capabilities
|
|
module string
|
|
expResult interface{}
|
|
expErrs []string
|
|
}{
|
|
{
|
|
note: "v0 module, rego-v0, no capabilities",
|
|
regoVersion: ast.RegoV0,
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
note: "v0 module, rego-v0, v0 capabilities",
|
|
regoVersion: ast.RegoV0,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
note: "v0 module, rego-v0, v1 capabilities",
|
|
regoVersion: ast.RegoV0,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
|
|
{
|
|
note: "v0 module, rego-v1, no capabilities",
|
|
regoVersion: ast.RegoV1,
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
|
|
"test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
{
|
|
note: "v0 module, rego-v1, v0 capabilities",
|
|
regoVersion: ast.RegoV1,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expErrs: []string{
|
|
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
|
},
|
|
},
|
|
{
|
|
note: "v0 module, rego-v1, v1 capabilities",
|
|
regoVersion: ast.RegoV1,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
|
module: `package test
|
|
|
|
p[x] {
|
|
x = ["a", "b", "c"][_]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:3: rego_parse_error: `if` keyword is required before rule body",
|
|
"test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules",
|
|
},
|
|
},
|
|
|
|
{
|
|
note: "v1 module, rego-v0, no capabilities",
|
|
regoVersion: ast.RegoV0,
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:4: rego_parse_error: unexpected identifier token",
|
|
},
|
|
},
|
|
{
|
|
note: "v1 module, rego-v0, v0 capabilities",
|
|
regoVersion: ast.RegoV0,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:4: rego_parse_error: unexpected identifier token",
|
|
},
|
|
},
|
|
{
|
|
note: "v1 module, rego-v0, v1 capabilities",
|
|
regoVersion: ast.RegoV0,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expErrs: []string{
|
|
"test.rego:4: rego_parse_error: unexpected identifier token",
|
|
},
|
|
},
|
|
|
|
{
|
|
note: "v1 module, rego-v1, no capabilities",
|
|
regoVersion: ast.RegoV1,
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
{
|
|
note: "v1 module, rego-v1, v0 capabilities",
|
|
regoVersion: ast.RegoV1,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0)),
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expErrs: []string{
|
|
"rego_parse_error: illegal capabilities: rego_v1 feature required for parsing v1 Rego",
|
|
},
|
|
},
|
|
{
|
|
note: "v1 module, rego-v1, v1 capabilities",
|
|
regoVersion: ast.RegoV1,
|
|
capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV1)),
|
|
module: `package test
|
|
|
|
p contains x if {
|
|
some x in ["a", "b", "c"]
|
|
}`,
|
|
expResult: []string{"a", "b", "c"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
files := map[string]string{
|
|
"test.rego": tc.module,
|
|
}
|
|
|
|
test.WithTempFS(files, func(root string) {
|
|
ctx := context.Background()
|
|
|
|
pq, err := New(
|
|
SetRegoVersion(tc.regoVersion),
|
|
Capabilities(tc.capabilities),
|
|
Load([]string{root}, nil),
|
|
Query("data.test.p"),
|
|
).PrepareForEval(ctx)
|
|
|
|
if tc.expErrs != nil {
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got nil")
|
|
}
|
|
|
|
for _, expErr := range tc.expErrs {
|
|
if !strings.Contains(err.Error(), expErr) {
|
|
t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err)
|
|
}
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(rs) != 1 {
|
|
t.Fatalf("Expected exactly one result but got:\n\n%v", rs)
|
|
}
|
|
|
|
if reflect.DeepEqual(rs[0].Expressions[0].Value, tc.expResult) {
|
|
t.Fatalf("Expected %v but got: %v", tc.expResult, rs[0].Expressions[0].Value)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertEval(t *testing.T, r *Rego, expected string) {
|
|
t.Helper()
|
|
rs, err := r.Eval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
assertResultSet(t, rs, expected)
|
|
}
|
|
|
|
func assertPreparedEvalQueryEval(t *testing.T, pq PreparedEvalQuery, options []EvalOption, expected string) {
|
|
t.Helper()
|
|
rs, err := pq.Eval(context.Background(), options...)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
assertResultSet(t, rs, expected)
|
|
}
|
|
|
|
func assertResultSet(t *testing.T, rs ResultSet, expected string) {
|
|
t.Helper()
|
|
result := []interface{}{}
|
|
|
|
for i := range rs {
|
|
values := []interface{}{}
|
|
for j := range rs[i].Expressions {
|
|
values = append(values, rs[i].Expressions[j].Value)
|
|
}
|
|
result = append(result, values)
|
|
}
|
|
|
|
if !reflect.DeepEqual(result, util.MustUnmarshalJSON([]byte(expected))) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", expected, result)
|
|
}
|
|
}
|
|
|
|
func TestRegoEvalExpressionValue(t *testing.T) {
|
|
|
|
module := `package test
|
|
|
|
arr = [1,false,true]
|
|
f(x) = x
|
|
g(x, y) = x + y
|
|
h(x) = false`
|
|
|
|
tests := []struct {
|
|
query string
|
|
expected string
|
|
}{
|
|
{
|
|
query: "1",
|
|
expected: "[[1]]",
|
|
},
|
|
{
|
|
query: "1+2",
|
|
expected: "[[3]]",
|
|
},
|
|
{
|
|
query: "1+(2*3)",
|
|
expected: "[[7]]",
|
|
},
|
|
{
|
|
query: "data.test.arr[0]",
|
|
expected: "[[1]]",
|
|
},
|
|
{
|
|
query: "data.test.arr[1]",
|
|
expected: "[[false]]",
|
|
},
|
|
{
|
|
query: "data.test.f(1)",
|
|
expected: "[[1]]",
|
|
},
|
|
{
|
|
query: "data.test.f(1,x)",
|
|
expected: "[[true]]",
|
|
},
|
|
{
|
|
query: "data.test.g(1,2)",
|
|
expected: "[[3]]",
|
|
},
|
|
{
|
|
query: "data.test.g(1,2,x)",
|
|
expected: "[[true]]",
|
|
},
|
|
{
|
|
query: "false",
|
|
expected: "[[false]]",
|
|
},
|
|
{
|
|
query: "1 == 2",
|
|
expected: "[[false]]",
|
|
},
|
|
{
|
|
query: "data.test.h(1)",
|
|
expected: "[[false]]",
|
|
},
|
|
{
|
|
query: "data.test.g(1,2) != 3",
|
|
expected: "[[false]]",
|
|
},
|
|
{
|
|
query: "data.test.arr[i]",
|
|
expected: "[[1], [true]]",
|
|
},
|
|
{
|
|
query: "[x | data.test.arr[_] = x]",
|
|
expected: "[[[1, false, true]]]",
|
|
},
|
|
{
|
|
query: "a = 1; b = 2; a > b",
|
|
expected: `[]`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.query, func(t *testing.T) {
|
|
r := New(
|
|
Query(tc.query),
|
|
Module("", module),
|
|
)
|
|
assertEval(t, r, tc.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegoInputs(t *testing.T) {
|
|
tests := map[string]struct {
|
|
input interface{}
|
|
expected string
|
|
}{
|
|
"map": {map[string]bool{"foo": true}, `[[{"foo": true}]]`},
|
|
"int": {1, `[[1]]`},
|
|
"bool": {false, `[[false]]`},
|
|
"struct": {struct {
|
|
Foo string `json:"baz"`
|
|
}{"bar"}, `[[{"baz":"bar"}]]`},
|
|
"pointer to struct": {&struct {
|
|
Foo string `json:"baz"`
|
|
}{"bar"}, `[[{"baz":"bar"}]]`},
|
|
"pointer to pointer to struct": {
|
|
func() interface{} {
|
|
a := &struct {
|
|
Foo string `json:"baz"`
|
|
}{"bar"}
|
|
return &a
|
|
}(), `[[{"baz":"bar"}]]`},
|
|
"slice": {[]string{"a", "b"}, `[[["a", "b"]]]`},
|
|
"nil": {nil, `[[null]]`},
|
|
"slice of interface": {[]interface{}{"a", 2, true}, `[[["a", 2, true]]]`},
|
|
}
|
|
|
|
for desc, tc := range tests {
|
|
t.Run(desc, func(t *testing.T) {
|
|
r := New(
|
|
Query("input"),
|
|
Input(tc.input),
|
|
Schemas(nil),
|
|
)
|
|
assertEval(t, r, tc.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegoRewrittenVarsCapture(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
r := New(
|
|
Query("a := 1; a != 0; a"),
|
|
)
|
|
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil || len(rs) != 1 {
|
|
t.Fatalf("Unexpected result: %v (err: %v)", rs, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(rs[0].Bindings["a"], json.Number("1")) {
|
|
t.Fatal("Expected a to be 1 but got:", rs[0].Bindings["a"])
|
|
}
|
|
|
|
}
|
|
|
|
func TestRegoDoNotCaptureVoidCalls(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
r := New(Query("print(1)"))
|
|
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil || len(rs) != 1 {
|
|
t.Fatal(err, "rs:", rs)
|
|
}
|
|
|
|
if !rs[0].Expressions[0].Value.(bool) {
|
|
t.Fatal("expected expression value to be true")
|
|
}
|
|
}
|
|
|
|
func TestRegoCancellation(t *testing.T) {
|
|
|
|
ast.RegisterBuiltin(&ast.Builtin{
|
|
Name: "test.sleep",
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S),
|
|
types.NewNull(),
|
|
),
|
|
})
|
|
|
|
topdown.RegisterBuiltinFunc("test.sleep", func(_ topdown.BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
|
d, _ := time.ParseDuration(string(operands[0].Value.(ast.String)))
|
|
time.Sleep(d)
|
|
return iter(ast.NullTerm())
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10)
|
|
r := New(Query(`test.sleep("1s")`))
|
|
rs, err := r.Eval(ctx)
|
|
cancel()
|
|
|
|
if err == nil {
|
|
t.Fatalf("Expected cancellation error but got: %v", rs)
|
|
}
|
|
exp := topdown.Error{Code: topdown.CancelErr, Message: context.DeadlineExceeded.Error()}
|
|
if !errors.Is(err, &exp) {
|
|
t.Errorf("error: expected %v, got: %v", exp, err)
|
|
}
|
|
}
|
|
|
|
func TestRegoCustomBuiltinHalt(t *testing.T) {
|
|
|
|
funOpt := Function1(
|
|
&Function{
|
|
Name: "halt_func",
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S),
|
|
types.NewNull(),
|
|
),
|
|
},
|
|
func(BuiltinContext, *ast.Term) (*ast.Term, error) {
|
|
return nil, NewHaltError(errors.New("stop"))
|
|
},
|
|
)
|
|
r := New(Query(`halt_func("")`), funOpt)
|
|
rs, err := r.Eval(context.Background())
|
|
if err == nil {
|
|
t.Fatalf("Expected halt error but got: %v", rs)
|
|
}
|
|
// exp is the error topdown returns after unwrapping the Halt
|
|
exp := topdown.Error{Code: topdown.BuiltinErr, Message: "halt_func: stop",
|
|
Location: location.NewLocation([]byte(`halt_func("")`), "", 1, 1)}
|
|
if !errors.Is(err, &exp) {
|
|
t.Fatalf("error: expected %v, got: %v", exp, err)
|
|
}
|
|
}
|
|
|
|
func TestRegoMetrics(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x"), Metrics(m))
|
|
ctx := context.Background()
|
|
_, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
validateRegoMetrics(t, m, []string{
|
|
"timer_rego_query_parse_ns",
|
|
"timer_rego_query_eval_ns",
|
|
"timer_rego_query_compile_ns",
|
|
"timer_rego_module_parse_ns",
|
|
"timer_rego_module_compile_ns",
|
|
})
|
|
}
|
|
|
|
func TestPreparedRegoMetrics(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x"), Metrics(m))
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = pq.Eval(ctx, EvalMetrics(m))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
validateRegoMetrics(t, m, []string{
|
|
"timer_rego_query_parse_ns",
|
|
"timer_rego_query_eval_ns",
|
|
"timer_rego_query_compile_ns",
|
|
"timer_rego_module_parse_ns",
|
|
"timer_rego_module_compile_ns",
|
|
})
|
|
}
|
|
|
|
func TestPreparedRegoMetricsPrepareOnly(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x"), Metrics(m))
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = pq.Eval(ctx) // No EvalMetrics() passed in
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
validateRegoMetrics(t, m, []string{
|
|
"timer_rego_query_parse_ns",
|
|
"timer_rego_query_compile_ns",
|
|
"timer_rego_module_parse_ns",
|
|
"timer_rego_module_compile_ns",
|
|
})
|
|
}
|
|
|
|
func TestPreparedRegoMetricsEvalOnly(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x")) // No Metrics() passed in
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = pq.Eval(ctx, EvalMetrics(m))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
validateRegoMetrics(t, m, []string{
|
|
"timer_rego_query_eval_ns",
|
|
})
|
|
}
|
|
|
|
func validateRegoMetrics(t *testing.T, m metrics.Metrics, expectedFields []string) {
|
|
t.Helper()
|
|
|
|
all := m.All()
|
|
|
|
for _, name := range expectedFields {
|
|
value, ok := all[name]
|
|
if !ok {
|
|
t.Errorf("expected to find %v but did not", name)
|
|
}
|
|
if value.(int64) == 0 {
|
|
t.Errorf("expected metric %v to have some non-zero value, but found 0", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoInstrumentExtraEvalCompilerStage(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x"), Metrics(m), Instrument(true))
|
|
ctx := context.Background()
|
|
_, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := []string{
|
|
"timer_query_compile_stage_rewrite_to_capture_value_ns",
|
|
}
|
|
|
|
all := m.All()
|
|
|
|
for _, name := range exp {
|
|
if _, ok := all[name]; !ok {
|
|
t.Errorf("expected to find %v but did not", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPreparedRegoInstrumentExtraEvalCompilerStage(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x"), Metrics(m), Instrument(true))
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// No metrics flag is passed in, should not affect results for compiler stage
|
|
// but expect to turn off instrumentation for evaluation.
|
|
_, err = pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := []string{
|
|
"timer_query_compile_stage_rewrite_to_capture_value_ns",
|
|
}
|
|
|
|
nExp := []string{
|
|
"timer_eval_op_plug_ns", // We should *not* see the eval timers
|
|
}
|
|
|
|
all := m.All()
|
|
|
|
for _, name := range exp {
|
|
if _, ok := all[name]; !ok {
|
|
t.Errorf("expected to find %v but did not", name)
|
|
}
|
|
}
|
|
|
|
for _, name := range nExp {
|
|
if _, ok := all[name]; ok {
|
|
t.Errorf("did not expect to find %v", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoInstrumentExtraPartialCompilerStage(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("foo = 1"), Module("foo.rego", "package x"), Metrics(m), Instrument(true))
|
|
ctx := context.Background()
|
|
_, err := r.Partial(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := []string{
|
|
"timer_query_compile_stage_rewrite_equals_ns",
|
|
}
|
|
|
|
all := m.All()
|
|
|
|
for _, name := range exp {
|
|
if _, ok := all[name]; !ok {
|
|
t.Errorf("Expected to find %v but did not", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoInstrumentExtraPartialResultCompilerStage(t *testing.T) {
|
|
m := metrics.New()
|
|
r := New(Query("input.x"), Module("foo.rego", "package x"), Metrics(m), Instrument(true))
|
|
ctx := context.Background()
|
|
_, err := r.PartialResult(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := []string{
|
|
"timer_query_compile_stage_rewrite_for_partial_eval_ns",
|
|
}
|
|
|
|
all := m.All()
|
|
|
|
for _, name := range exp {
|
|
if _, ok := all[name]; !ok {
|
|
t.Errorf("Expected to find '%v' in metrics\n\nActual:\n %+v", name, all)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPreparedRegoTracerNoPropagate(t *testing.T) {
|
|
tracer := topdown.NewBufferTracer()
|
|
mod := `
|
|
package test
|
|
|
|
p = {
|
|
input.x == 10
|
|
}
|
|
`
|
|
pq, err := New(
|
|
Query("data"),
|
|
Module("foo.rego", mod),
|
|
Tracer(tracer),
|
|
Input(map[string]interface{}{"x": 10})).PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
_, err = pq.Eval(context.Background()) // no EvalTracer option
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
if len(*tracer) > 0 {
|
|
t.Fatal("expected 0 traces to be collected")
|
|
}
|
|
}
|
|
|
|
func TestPreparedRegoQueryTracerNoPropagate(t *testing.T) {
|
|
tracer := topdown.NewBufferTracer()
|
|
mod := `
|
|
package test
|
|
|
|
p = {
|
|
input.x == 10
|
|
}
|
|
`
|
|
pq, err := New(
|
|
Query("data"),
|
|
Module("foo.rego", mod),
|
|
QueryTracer(tracer),
|
|
Input(map[string]interface{}{"x": 10})).PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
_, err = pq.Eval(context.Background()) // no EvalQueryTracer option
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
if len(*tracer) > 0 {
|
|
t.Fatal("expected 0 traces to be collected")
|
|
}
|
|
}
|
|
|
|
func TestRegoDisableIndexing(t *testing.T) {
|
|
tracer := topdown.NewBufferTracer()
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
input.x = 1
|
|
}
|
|
|
|
p if {
|
|
input.y = 1
|
|
}
|
|
`
|
|
pq, err := New(
|
|
Query("data"),
|
|
Module("foo.rego", mod),
|
|
).PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
_, err = pq.Eval(
|
|
context.Background(),
|
|
EvalQueryTracer(tracer),
|
|
EvalRuleIndexing(false),
|
|
EvalInput(map[string]interface{}{"x": 10}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
var evalNodes []string
|
|
for _, e := range *tracer {
|
|
if e.Op == topdown.EvalOp {
|
|
evalNodes = append(evalNodes, string(e.Node.Loc().Text))
|
|
}
|
|
}
|
|
|
|
expectedEvalNodes := []string{
|
|
"input.x = 1",
|
|
"input.y = 1",
|
|
}
|
|
|
|
for _, expected := range expectedEvalNodes {
|
|
found := false
|
|
for _, actual := range evalNodes {
|
|
if actual == expected {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("Missing expected eval node in trace: %q\nGot: %q\n", expected, evalNodes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoDisableIndexingWithMatch(t *testing.T) {
|
|
tracer := topdown.NewBufferTracer()
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
input.x = 1
|
|
}
|
|
|
|
p if {
|
|
input.y = 1
|
|
}
|
|
`
|
|
pq, err := New(
|
|
Query("data"),
|
|
Module("foo.rego", mod),
|
|
).PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(
|
|
context.Background(),
|
|
EvalQueryTracer(tracer),
|
|
EvalRuleIndexing(false),
|
|
EvalInput(map[string]interface{}{"x": 1}),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
assertResultSet(t, rs, `[[{"test": {"p": true}}]]`)
|
|
|
|
var evalNodes []string
|
|
for _, e := range *tracer {
|
|
if e.Op == topdown.EvalOp {
|
|
evalNodes = append(evalNodes, string(e.Node.Loc().Text))
|
|
}
|
|
}
|
|
|
|
expectedEvalNodes := []string{
|
|
"input.x = 1",
|
|
"input.y = 1",
|
|
}
|
|
|
|
for _, expected := range expectedEvalNodes {
|
|
found := false
|
|
for _, actual := range evalNodes {
|
|
if actual == expected {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("Missing expected eval node in trace: %q\nGot: %q\n", expected, evalNodes)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoCatchPathConflicts(t *testing.T) {
|
|
r := New(
|
|
Query("data"),
|
|
Module("test.rego", "package x\np=1"),
|
|
Store(inmem.NewFromObject(map[string]interface{}{
|
|
"x": map[string]interface{}{"p": 1},
|
|
})),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
_, err := r.Eval(ctx)
|
|
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|
|
|
|
func TestPartialRewriteEquals(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
default p = false
|
|
p if {
|
|
input.x = 1
|
|
}
|
|
`
|
|
r := New(
|
|
Query("data.test.p == true"),
|
|
Module("test.rego", mod),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
pq, err := r.Partial(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from Rego.Partial(): %s", err.Error())
|
|
}
|
|
|
|
// Expect to not have any "support" in the resulting queries
|
|
if len(pq.Support) > 0 {
|
|
t.Errorf("expected to not have any Support in PartialQueries: %+v", pq)
|
|
}
|
|
|
|
expectedQuery := "input.x = 1"
|
|
if len(pq.Queries) != 1 {
|
|
t.Errorf("expected 1 query but found %d: %+v", len(pq.Queries), pq)
|
|
}
|
|
if pq.Queries[0].String() != expectedQuery {
|
|
t.Errorf("unexpected query in result, expected='%s' found='%s'",
|
|
expectedQuery, pq.Queries[0].String())
|
|
}
|
|
}
|
|
|
|
// NOTE(sr): https://github.com/open-policy-agent/opa/issues/4345
|
|
func TestPrepareAndEvalRaceConditions(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "object",
|
|
module: `package test
|
|
import rego.v1
|
|
p contains {"x":"y"}`,
|
|
exp: `[[[{"x":"y"}]]]`,
|
|
},
|
|
{
|
|
note: "set",
|
|
module: `package test
|
|
import rego.v1
|
|
p contains {"x"}`,
|
|
exp: `[[[["x"]]]]`,
|
|
},
|
|
{
|
|
note: "array",
|
|
module: `package test
|
|
import rego.v1
|
|
p contains ["x"]`,
|
|
exp: `[[[["x"]]]]`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
r := New(
|
|
Query("data.test.p"),
|
|
Module("", tc.module),
|
|
Package("foo"),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
// run this 1000 times concurrently
|
|
var wg sync.WaitGroup
|
|
wg.Add(1000)
|
|
for range 1000 {
|
|
go func(t *testing.T) {
|
|
t.Helper()
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{}, tc.exp)
|
|
wg.Done()
|
|
}(t)
|
|
}
|
|
wg.Wait()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndEvalNewInput(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
}
|
|
|
|
func TestPrepareAndEvalNewMetrics(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
originalMetrics := metrics.New()
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
Metrics(originalMetrics),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
if len(originalMetrics.All()) == 0 {
|
|
t.Errorf("Expected metrics stored on 'originalMetrics' after Prepare()")
|
|
}
|
|
|
|
// Reset the original ones (for testing)
|
|
// and make a new one for the Eval
|
|
originalMetrics.Clear()
|
|
newMetrics := metrics.New()
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
EvalMetrics(newMetrics),
|
|
}, "[[1]]")
|
|
|
|
if len(originalMetrics.All()) > 0 {
|
|
t.Errorf("Expected no metrics stored on original Rego object metrics but found: %s",
|
|
originalMetrics.All())
|
|
}
|
|
|
|
if len(newMetrics.All()) == 0 {
|
|
t.Errorf("Expected metrics stored on 'newMetrics' after Prepare()")
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndEvalTransaction(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = data.foo.y
|
|
`
|
|
ctx := context.Background()
|
|
store := mock.New()
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
path, ok := storage.ParsePath("/foo")
|
|
if !ok {
|
|
t.Fatalf("Unexpected error parsing path")
|
|
}
|
|
|
|
err := storage.MakeDir(ctx, store, txn, path)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error writing to store: %s", err.Error())
|
|
}
|
|
|
|
err = store.Write(ctx, txn, storage.AddOp, path, map[string]interface{}{"y": 1})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error writing to store: %s", err.Error())
|
|
}
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Store(store),
|
|
Transaction(txn),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
// Base case, expect it to use the transaction provided
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{EvalTransaction(txn)}, "[[1]]")
|
|
|
|
mockTxn := store.GetTransaction(txn.ID())
|
|
for _, read := range store.Reads {
|
|
if read.Transaction != mockTxn {
|
|
t.Errorf("Found read operation with an invalid transaction, expected: %d, found: %d", mockTxn.ID(), read.Transaction.ID())
|
|
}
|
|
}
|
|
|
|
store.AssertValid(t)
|
|
store.Reset()
|
|
|
|
// Case with an update to the store and a new transaction
|
|
txn = storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
err = store.Write(ctx, txn, storage.AddOp, path, map[string]interface{}{"y": 2})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error writing to store: %s", err.Error())
|
|
}
|
|
|
|
// Expect the new result from the updated value on this transaction
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{EvalTransaction(txn)}, "[[2]]")
|
|
|
|
err = store.Commit(ctx, txn)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error committing to store: %s", err)
|
|
}
|
|
|
|
newMockTxn := store.GetTransaction(txn.ID())
|
|
for _, read := range store.Reads {
|
|
if read.Transaction != newMockTxn {
|
|
t.Errorf("Found read operation with an invalid transaction, expected: %d, found: %d", mockTxn.ID(), read.Transaction.ID())
|
|
}
|
|
}
|
|
|
|
store.AssertValid(t)
|
|
store.Reset()
|
|
|
|
// Case with no transaction provided, should create a new one and see the latest value
|
|
txn = storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
err = store.Write(ctx, txn, storage.AddOp, path, map[string]interface{}{"y": 3})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error writing to store: %s", err.Error())
|
|
}
|
|
err = store.Commit(ctx, txn)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error committing to store: %s", err)
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, nil, "[[3]]")
|
|
|
|
if len(store.Transactions) != 2 {
|
|
t.Fatalf("Expected only two transactions on store, found %d", len(store.Transactions))
|
|
}
|
|
|
|
autoTxn := store.Transactions[1]
|
|
for _, read := range store.Reads {
|
|
if read.Transaction != autoTxn {
|
|
t.Errorf("Found read operation with an invalid transaction, expected: %d, found: %d", autoTxn, read.Transaction.ID())
|
|
}
|
|
}
|
|
store.AssertValid(t)
|
|
|
|
}
|
|
|
|
func TestPrepareAndEvalIdempotent(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
// Expect evaluating the same thing >1 time gives the same
|
|
// results each time.
|
|
for range 5 {
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndEvalOriginal(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
Input(map[string]int{"y": 2}),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
|
|
// Even after prepare and eval with different input
|
|
// expect that the original Rego object behaves
|
|
// as expected for Eval.
|
|
|
|
assertEval(t, r, "[[2]]")
|
|
}
|
|
|
|
func TestPrepareAndEvalOnlyOneErrorOccurredPrintOnce(t *testing.T) {
|
|
module := `
|
|
package test
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
Input(map[string]int{"y": 2}),
|
|
)
|
|
|
|
_, err := r.PrepareForEval(context.Background())
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
if strings.Count(err.Error(), "1 error occurred") > 1 {
|
|
t.Fatalf("Expected to print '1 error occurred' only once")
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndEvalNewPrintHook(t *testing.T) {
|
|
module := `
|
|
package test
|
|
import rego.v1
|
|
x if { print(input) }
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
EnablePrintStatements(true),
|
|
)
|
|
|
|
pq, err := r.PrepareForEval(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
var buf0 bytes.Buffer
|
|
ph0 := topdown.NewPrintHook(&buf0)
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput("hello"),
|
|
EvalPrintHook(ph0),
|
|
}, "[[true]]")
|
|
|
|
if exp, act := "hello\n", buf0.String(); exp != act {
|
|
t.Fatalf("print hook, expected %q, got %q", exp, act)
|
|
}
|
|
|
|
// repeat
|
|
var buf1 bytes.Buffer
|
|
ph1 := topdown.NewPrintHook(&buf1)
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput("world"),
|
|
EvalPrintHook(ph1),
|
|
}, "[[true]]")
|
|
|
|
if exp, act := "world\n", buf1.String(); exp != act {
|
|
t.Fatalf("print hook, expected %q, got %q", exp, act)
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndPartialResult(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
Input(map[string]int{"y": 2}),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
|
|
// Even after prepare and eval with different input
|
|
// expect that the original Rego object behaves
|
|
// as expected for PartialResult.
|
|
|
|
partial, err := r.PartialResult(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r2 := partial.Rego(
|
|
Input(map[string]int{"y": 7}),
|
|
)
|
|
assertEval(t, r2, "[[7]]")
|
|
}
|
|
|
|
func TestPrepareWithPartialEval(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Prepare the query and partially evaluate it
|
|
pq, err := r.PrepareForEval(ctx, WithPartialEval())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
}
|
|
|
|
func TestPrepareAndPartial(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
default p = false
|
|
p if {
|
|
input.x = 1
|
|
}
|
|
`
|
|
r := New(
|
|
Query("data.test.p == true"),
|
|
Module("test.rego", mod),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"x": 1}),
|
|
}, "[[true]]")
|
|
|
|
// Even after prepare and eval with different input
|
|
// expect that the original Rego object behaves
|
|
// as expected for Partial.
|
|
|
|
partialQuery, err := r.Partial(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expectedQuery := "input.x = 1"
|
|
if len(partialQuery.Queries) != 1 {
|
|
t.Errorf("expected 1 query but found %d: %+v", len(partialQuery.Queries), pq)
|
|
}
|
|
if partialQuery.Queries[0].String() != expectedQuery {
|
|
t.Errorf("unexpected query in result, expected='%s' found='%s'",
|
|
expectedQuery, partialQuery.Queries[0].String())
|
|
}
|
|
}
|
|
|
|
func TestPartialWithRegoV1(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
expQuery string
|
|
expSupport string
|
|
}{
|
|
{
|
|
note: "No imports",
|
|
module: `package test
|
|
p[k] contains v if {
|
|
k := "foo"
|
|
v := input.v
|
|
}`,
|
|
expQuery: `data.partial.test.p = x`,
|
|
expSupport: `package partial.test.p
|
|
|
|
foo contains __local1__1 if { __local1__1 = input.v }`,
|
|
},
|
|
{
|
|
note: "rego.v1 imported",
|
|
module: `package test
|
|
import rego.v1
|
|
p[k] contains v if {
|
|
k := "foo"
|
|
v := input.v
|
|
}`,
|
|
expQuery: `data.partial.test.p = x`,
|
|
expSupport: `package partial.test.p
|
|
|
|
foo contains __local1__1 if { __local1__1 = input.v }`,
|
|
},
|
|
{
|
|
note: "future.keywords imported",
|
|
module: `package test
|
|
import future.keywords
|
|
p[k] contains v if {
|
|
k := "foo"
|
|
v := input.v
|
|
}`,
|
|
expQuery: `data.partial.test.p = x`,
|
|
expSupport: `package partial.test.p
|
|
|
|
foo contains __local1__1 if { __local1__1 = input.v }`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
r := New(
|
|
Query("data.test.p = x"),
|
|
Module("test.rego", tc.module),
|
|
SetRegoVersion(ast.RegoV1),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
partialQuery, err := r.Partial(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
actualQuery := partialQuery.Queries[0].String()
|
|
if tc.expQuery != actualQuery {
|
|
t.Fatalf("Expected partial query to be:\n\n%s\n\nbut got:\n\n%s", tc.expQuery, actualQuery)
|
|
}
|
|
|
|
actualSupport := partialQuery.Support[0].String()
|
|
if tc.expSupport != actualSupport {
|
|
t.Fatalf("Expected support module to be:\n\n%s\n\nbut got:\n\n%s", tc.expSupport, actualSupport)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPartialNamespace(t *testing.T) {
|
|
|
|
r := New(
|
|
PartialNamespace("foo"),
|
|
Query("data.test.p = x"),
|
|
SetRegoVersion(ast.RegoV1),
|
|
Module("test.rego", `
|
|
package test
|
|
|
|
default p = false
|
|
|
|
p if { input.x = 1 }
|
|
`),
|
|
)
|
|
|
|
pq, err := r.Partial(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expQuery := ast.MustParseBody(`data.foo.test.p = x`)
|
|
|
|
if len(pq.Queries) != 1 || !pq.Queries[0].Equal(expQuery) {
|
|
t.Fatalf("Expected exactly one query %v but got: %v", expQuery, pq.Queries)
|
|
}
|
|
|
|
expSupport := ast.MustParseModuleWithOpts(`
|
|
package foo.test
|
|
|
|
default p = false
|
|
|
|
p if { input.x = 1 }
|
|
`, ast.ParserOptions{RegoVersion: ast.RegoV1})
|
|
|
|
if len(pq.Support) != 1 || !pq.Support[0].Equal(expSupport) {
|
|
t.Fatalf("Expected exactly one support:\n\n%v\n\nGot:\n\n%v", expSupport, pq.Support[0])
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndCompile(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
|
|
// Ensure that Compile still works after Prepare
|
|
// and its Eval has been called.
|
|
_, err = r.Compile(ctx)
|
|
if err != nil {
|
|
t.Errorf("Unexpected error when compiling: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestPartialResultWithInput(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
default p = false
|
|
p if {
|
|
input.x == 1
|
|
}
|
|
`
|
|
r := New(
|
|
Query("data.test.p"),
|
|
Module("test.rego", mod),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
pr, err := r.PartialResult(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from Rego.PartialResult(): %s", err.Error())
|
|
}
|
|
|
|
r2 := pr.Rego(
|
|
Input(map[string]int{"x": 1}),
|
|
)
|
|
|
|
assertEval(t, r2, "[[true]]")
|
|
}
|
|
|
|
func TestPartialResultWithNamespace(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
true
|
|
}
|
|
`
|
|
c := ast.NewCompiler()
|
|
r := New(
|
|
Query("data.test.p"),
|
|
Module("test.rego", mod),
|
|
PartialNamespace("test_ns1"),
|
|
Compiler(c),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
pr, err := r.PartialResult(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from Rego.PartialResult(): %s", err.Error())
|
|
}
|
|
|
|
expectedQuery := "data.test_ns1.__result__"
|
|
if pr.body.String() != expectedQuery {
|
|
t.Fatalf("Expected partial result query %s got %s", expectedQuery, pr.body)
|
|
}
|
|
|
|
r2 := pr.Rego()
|
|
|
|
assertEval(t, r2, "[[true]]")
|
|
|
|
if len(c.Modules) != 2 {
|
|
t.Fatalf("Expected two modules on the compiler, got: %v", c.Modules)
|
|
}
|
|
|
|
expectedModuleID := "__partialresult__test_ns1__"
|
|
if _, ok := c.Modules[expectedModuleID]; !ok {
|
|
t.Fatalf("Expected to find module %s in compiler Modules, got: %v", expectedModuleID, c.Modules)
|
|
}
|
|
}
|
|
|
|
func TestPreparedPartialResultWithTracer(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
default p = false
|
|
p if {
|
|
input.x = 1
|
|
}
|
|
`
|
|
r := New(
|
|
Query("data.test.p == true"),
|
|
Module("test.rego", mod),
|
|
)
|
|
|
|
tracer := topdown.NewBufferTracer()
|
|
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForPartial(ctx)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from Rego.PrepareForPartial(): %s", err.Error())
|
|
}
|
|
|
|
pqs, err := pq.Partial(ctx, EvalTracer(tracer))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from PreparedEvalQuery.Partial(): %s", err.Error())
|
|
}
|
|
|
|
expectedQuery := "input.x = 1"
|
|
if len(pqs.Queries) != 1 {
|
|
t.Errorf("expected 1 query but found %d: %+v", len(pqs.Queries), pqs)
|
|
}
|
|
if pqs.Queries[0].String() != expectedQuery {
|
|
t.Errorf("unexpected query in result, expected='%s' found='%s'",
|
|
expectedQuery, pqs.Queries[0].String())
|
|
}
|
|
|
|
if len(*tracer) == 0 {
|
|
t.Errorf("Expected buffer tracer to contain > 0 traces")
|
|
}
|
|
}
|
|
|
|
func TestPreparedPartialResultWithQueryTracer(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
default p = false
|
|
p if {
|
|
input.x = 1
|
|
}
|
|
`
|
|
r := New(
|
|
Query("data.test.p == true"),
|
|
Module("test.rego", mod),
|
|
)
|
|
|
|
tracer := topdown.NewBufferTracer()
|
|
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForPartial(ctx)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from Rego.PrepareForPartial(): %s", err.Error())
|
|
}
|
|
|
|
pqs, err := pq.Partial(ctx, EvalQueryTracer(tracer))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from PreparedEvalQuery.Partial(): %s", err.Error())
|
|
}
|
|
|
|
expectedQuery := "input.x = 1"
|
|
if len(pqs.Queries) != 1 {
|
|
t.Errorf("expected 1 query but found %d: %+v", len(pqs.Queries), pqs)
|
|
}
|
|
if pqs.Queries[0].String() != expectedQuery {
|
|
t.Errorf("unexpected query in result, expected='%s' found='%s'",
|
|
expectedQuery, pqs.Queries[0].String())
|
|
}
|
|
|
|
if len(*tracer) == 0 {
|
|
t.Errorf("Expected buffer tracer to contain > 0 traces")
|
|
}
|
|
}
|
|
|
|
func TestPartialResultSetsValidConflictChecker(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
true
|
|
}
|
|
`
|
|
|
|
c := ast.NewCompiler().WithPathConflictsCheck(func(_ []string) (bool, error) {
|
|
t.Fatal("Conflict check should not have been called")
|
|
return false, nil
|
|
})
|
|
|
|
r := New(
|
|
Query("data.test.p"),
|
|
Module("test.rego", mod),
|
|
PartialNamespace("test_ns1"),
|
|
Compiler(c),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
pr, err := r.PartialResult(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error from Rego.PartialResult(): %s", err.Error())
|
|
}
|
|
|
|
r2 := pr.Rego()
|
|
|
|
assertEval(t, r2, "[[true]]")
|
|
}
|
|
|
|
func TestMissingLocation(t *testing.T) {
|
|
|
|
// Create a query programmatically and evaluate it. The Location information
|
|
// is not set so the resulting expression value will not have it.
|
|
r := New(ParsedQuery(ast.NewBody(ast.NewExpr(ast.BooleanTerm(true)))))
|
|
rs, err := r.Eval(context.Background())
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if len(rs) == 0 || !rs[0].Expressions[0].Value.(bool) {
|
|
t.Fatal("Unexpected result set:", rs)
|
|
}
|
|
|
|
if rs[0].Expressions[0].Location != nil {
|
|
t.Fatal("Expected location data to be unset.")
|
|
}
|
|
}
|
|
|
|
func TestBundlePassing(t *testing.T) {
|
|
|
|
opaBundle := bundle.Bundle{
|
|
Modules: []bundle.ModuleFile{
|
|
{
|
|
Path: "policy.rego",
|
|
Parsed: ast.MustParseModule(`package foo
|
|
allow = true`),
|
|
Raw: []byte(`package foo
|
|
allow = true`),
|
|
},
|
|
},
|
|
Manifest: bundle.Manifest{Revision: "test", Roots: &[]string{"/"}},
|
|
}
|
|
|
|
// Pass a bundle
|
|
r := New(
|
|
ParsedBundle("123", &opaBundle),
|
|
Query("x = data.foo.allow"),
|
|
)
|
|
|
|
res, err := r.Eval(context.Background())
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertResultSet(t, res, `[[true]]`)
|
|
}
|
|
|
|
func TestModulePassing(t *testing.T) {
|
|
|
|
// This module will not be loaded since it has the same filename as the
|
|
// file2.rego below and the raw modules override parsed modules.
|
|
module1, err := ast.ParseModule("file2.rego", `package file2
|
|
|
|
p = "deadbeef"
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r := New(
|
|
Query("data"),
|
|
Module("file1.rego", `package file1
|
|
|
|
p = 1
|
|
`),
|
|
Module("file2.rego", `package file2
|
|
|
|
p = 2`),
|
|
ParsedModule(module1),
|
|
ParsedModule(ast.MustParseModule(`package file4
|
|
|
|
p = 4`)),
|
|
)
|
|
|
|
rs, err := r.Eval(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := util.MustUnmarshalJSON([]byte(`
|
|
{
|
|
"file1": {
|
|
"p": 1
|
|
},
|
|
"file2": {
|
|
"p": 2
|
|
},
|
|
"file4": {
|
|
"p": 4
|
|
}
|
|
}
|
|
`))
|
|
|
|
if !reflect.DeepEqual(rs[0].Expressions[0].Value, exp) {
|
|
t.Fatalf("Expected %v but got %v", exp, rs[0].Expressions[0].Value)
|
|
}
|
|
}
|
|
|
|
func TestUnsafeBuiltins(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
unsafeCountExpr := "unsafe built-in function calls in expression: count"
|
|
unsafeCountExprWith := `with keyword replacing built-in function: target must not be unsafe: "count"`
|
|
|
|
t.Run("unsafe query", func(t *testing.T) {
|
|
r := New(
|
|
Query(`count([1, 2, 3])`),
|
|
UnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExpr) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unsafe query, 'with' replacement", func(t *testing.T) {
|
|
r := New(
|
|
Query(`is_array([1, 2, 3]) with is_array as count`),
|
|
UnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unsafe module", func(t *testing.T) {
|
|
r := New(
|
|
Query(`data.pkg.deny`),
|
|
Module("pkg.rego", `package pkg
|
|
import rego.v1
|
|
deny if{
|
|
count(input.requests) > 10
|
|
}
|
|
`),
|
|
UnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExpr) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unsafe module, 'with' replacement in query", func(t *testing.T) {
|
|
r := New(
|
|
Query(`data.pkg.deny with is_array as count`),
|
|
Module("pkg.rego", `package pkg
|
|
import rego.v1
|
|
deny if {
|
|
is_array(input.requests) > 10
|
|
}
|
|
`),
|
|
UnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unsafe module, 'with' replacement in module", func(t *testing.T) {
|
|
r := New(
|
|
Query(`data.pkg.deny`),
|
|
Module("pkg.rego", `package pkg
|
|
import rego.v1
|
|
deny if {
|
|
is_array(input.requests) > 10 with is_array as count
|
|
}
|
|
`),
|
|
UnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("inherit in query", func(t *testing.T) {
|
|
r := New(
|
|
Compiler(ast.NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}})),
|
|
Query("count([])"),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExpr) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("inherit in query, 'with' replacement", func(t *testing.T) {
|
|
r := New(
|
|
Compiler(ast.NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}})),
|
|
Query("is_array([]) with is_array as count"),
|
|
)
|
|
if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
|
|
t.Fatalf("Expected unsafe built-in error but got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("override/disable in query", func(t *testing.T) {
|
|
r := New(
|
|
Compiler(ast.NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}})),
|
|
UnsafeBuiltins(map[string]struct{}{}),
|
|
Query("count([])"),
|
|
)
|
|
if _, err := r.Eval(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("override/change in query", func(t *testing.T) {
|
|
r := New(
|
|
Compiler(ast.NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}})),
|
|
UnsafeBuiltins(map[string]struct{}{"max": {}}),
|
|
Query("count([]); max([1,2])"),
|
|
)
|
|
|
|
_, err := r.Eval(ctx)
|
|
if err == nil || err.Error() != "1 error occurred: 1:12: rego_type_error: unsafe built-in function calls in expression: max" {
|
|
t.Fatalf("expected error for max but got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ignore if given compiler", func(_ *testing.T) {
|
|
r := New(
|
|
Compiler(ast.NewCompiler()),
|
|
UnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
Query("data.test.p = 0"),
|
|
Module("test.rego", `package test
|
|
|
|
p = count([])`),
|
|
)
|
|
rs, err := r.Eval(context.Background())
|
|
if err != nil || len(rs) != 1 {
|
|
log.Fatalf("Unexpected error or result. Result: %v. Error: %v", rs, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPreparedQueryGetModules(t *testing.T) {
|
|
mods := map[string]string{
|
|
"a.rego": "package a\np = 1",
|
|
"b.rego": "package b\nq = 1",
|
|
"c.rego": "package c\nr = 1",
|
|
}
|
|
|
|
regoArgs := make([]func(r *Rego), 0, len(mods)+1)
|
|
|
|
for name, mod := range mods {
|
|
regoArgs = append(regoArgs, Module(name, mod))
|
|
}
|
|
|
|
regoArgs = append(regoArgs, Query("data"))
|
|
|
|
ctx := context.Background()
|
|
pq, err := New(regoArgs...).PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
actualMods := pq.Modules()
|
|
|
|
if len(actualMods) != len(mods) {
|
|
t.Fatalf("Expected %d modules, got %d", len(mods), len(actualMods))
|
|
}
|
|
|
|
for name, actualMod := range actualMods {
|
|
expectedMod, found := mods[name]
|
|
if !found {
|
|
t.Fatalf("Unexpected module %s", name)
|
|
}
|
|
if actualMod.String() != ast.MustParseModule(expectedMod).String() {
|
|
t.Fatalf("Modules for %s do not match.\n\nExpected:\n%s\n\nActual:\n%s\n\n",
|
|
name, actualMod.String(), expectedMod)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoEvalWithFile(t *testing.T) {
|
|
files := map[string]string{
|
|
"x/x.rego": "package x\np = 1",
|
|
"x/x.json": `{"y": "foo"}`,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
ctx := context.Background()
|
|
|
|
pq, err := New(
|
|
Load([]string{path}, nil),
|
|
Query("data"),
|
|
).PrepareForEval(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
assertResultSet(t, rs, `[[{"x":{"p":1,"y":"foo"}}]]`)
|
|
})
|
|
}
|
|
|
|
func TestRegoEvalWithBundle(t *testing.T) {
|
|
files := map[string]string{
|
|
"x/x.rego": "package x\np = data.x.b",
|
|
"x/data.json": `{"b": "bar"}`,
|
|
"other/not-data.json": `{"ignored": "data"}`,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
ctx := context.Background()
|
|
|
|
pq, err := New(
|
|
LoadBundle(path),
|
|
Query("data.x.p"),
|
|
).PrepareForEval(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
assertResultSet(t, rs, `[["bar"]]`)
|
|
|
|
mods := pq.Modules()
|
|
if exp, act := 1, len(mods); exp != act {
|
|
t.Fatalf("expected %d modules, found %d", exp, act)
|
|
}
|
|
for act := range mods {
|
|
if exp := filepath.Join(path, "x", "x.rego"); exp != act {
|
|
t.Errorf("expected module name %q, got %q", exp, act)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegoEvalWithBundleURL(t *testing.T) {
|
|
files := map[string]string{
|
|
"x/x.rego": "package x\np = data.x.b",
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
ctx := context.Background()
|
|
pq, err := New(
|
|
LoadBundle("file://"+path),
|
|
Query("data.x.p"),
|
|
).PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
mods := pq.Modules()
|
|
if exp, act := 1, len(mods); exp != act {
|
|
t.Fatalf("expected %d modules, found %d", exp, act)
|
|
}
|
|
for act := range mods {
|
|
if exp := filepath.Join(path, "x", "x.rego"); exp != act {
|
|
t.Errorf("expected module name %q, got %q", exp, act)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegoEvalPoliciesInStore(t *testing.T) {
|
|
store := mock.New()
|
|
ctx := context.Background()
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
err := store.UpsertPolicy(ctx, txn, "a.rego", []byte("package a\np=1"))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
err = store.Commit(ctx, txn)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
pq, err := New(
|
|
Store(store),
|
|
Module("b.rego", "package b\np = data.a.p"),
|
|
Query("data.b.p"),
|
|
).PrepareForEval(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
assertResultSet(t, rs, `[[1]]`)
|
|
}
|
|
|
|
func TestRegoEvalModulesOnCompiler(t *testing.T) {
|
|
compiler := ast.NewCompiler()
|
|
|
|
compiler.Compile(map[string]*ast.Module{
|
|
"a.rego": ast.MustParseModule("package a\np = 1"),
|
|
})
|
|
|
|
if len(compiler.Errors) > 0 {
|
|
t.Fatalf("Unexpected compile errors: %s", compiler.Errors)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
pq, err := New(
|
|
Compiler(compiler),
|
|
Query("data.a.p"),
|
|
Schemas(nil),
|
|
).PrepareForEval(ctx)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
assertResultSet(t, rs, `[[1]]`)
|
|
}
|
|
|
|
func TestRegoEvalWithRegoV1(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
regoVersion ast.RegoVersion
|
|
policies map[string]string
|
|
query string
|
|
expectedResult string
|
|
expectedErr string
|
|
}{
|
|
{
|
|
note: "Rego v0",
|
|
regoVersion: ast.RegoV0,
|
|
policies: map[string]string{
|
|
"policy.rego": `package test
|
|
x[y] { y := 1 }`,
|
|
},
|
|
expectedResult: `[[{"x": [1]}]]`,
|
|
},
|
|
{
|
|
note: "Rego v0, forced v1 compatibility",
|
|
regoVersion: ast.RegoV0CompatV1,
|
|
policies: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
x contains y if { y := 1 }`,
|
|
},
|
|
expectedResult: `[[{"x": [1]}]]`,
|
|
},
|
|
{
|
|
note: "Rego v0, forced v1 compatibility, invalid rule head",
|
|
regoVersion: ast.RegoV0CompatV1,
|
|
policies: map[string]string{
|
|
"policy.rego": `package test
|
|
import future.keywords.contains
|
|
x contains y { y := 1 }`,
|
|
},
|
|
expectedErr: "rego_parse_error: `if` keyword is required before rule body",
|
|
},
|
|
{
|
|
note: "Rego v0, forced v1 compatibility, missing required imports",
|
|
regoVersion: ast.RegoV0CompatV1,
|
|
policies: map[string]string{
|
|
"policy.rego": `package test
|
|
x contains y if { y := 1 }`,
|
|
},
|
|
expectedErr: "rego_parse_error: var cannot be used for rule name", // FIXME: Improve error message
|
|
},
|
|
{
|
|
note: "Rego v1",
|
|
regoVersion: ast.RegoV1,
|
|
policies: map[string]string{
|
|
"policy.rego": `package test
|
|
x contains y if { y := 1 }`,
|
|
},
|
|
expectedResult: `[[{"x": [1]}]]`,
|
|
},
|
|
{
|
|
note: "Rego v1, invalid rule head",
|
|
regoVersion: ast.RegoV1,
|
|
policies: map[string]string{
|
|
"policy.rego": `package test
|
|
x contains y { y := 1 }`,
|
|
},
|
|
expectedErr: "rego_parse_error: `if` keyword is required before rule body",
|
|
},
|
|
{
|
|
note: "Rego v1, multiple files",
|
|
regoVersion: ast.RegoV1,
|
|
policies: map[string]string{
|
|
"one.rego": `package test
|
|
x contains v if { v := 1 }`,
|
|
"two.rego": `package test
|
|
import rego.v1
|
|
y contains v if { v := 1 }`,
|
|
"three.rego": `package test
|
|
import future.keywords
|
|
z contains v if { v := 1 }`,
|
|
},
|
|
expectedResult: `[[{"x": [1], "y": [1], "z": [1]}]]`,
|
|
},
|
|
}
|
|
|
|
setup := []struct {
|
|
name string
|
|
options func(path string, policies map[string]string, t *testing.T, ctx context.Context) []func(*Rego)
|
|
}{
|
|
{
|
|
name: "File",
|
|
options: func(path string, _ map[string]string, _ *testing.T, _ context.Context) []func(*Rego) {
|
|
return []func(*Rego){
|
|
Load([]string{path}, nil),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Bundle",
|
|
options: func(path string, _ map[string]string, _ *testing.T, _ context.Context) []func(*Rego) {
|
|
return []func(*Rego){
|
|
LoadBundle(path),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Bundle URL",
|
|
options: func(path string, _ map[string]string, _ *testing.T, _ context.Context) []func(*Rego) {
|
|
return []func(*Rego){
|
|
LoadBundle("file://" + path),
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Store",
|
|
options: func(_ string, policies map[string]string, t *testing.T, ctx context.Context) []func(*Rego) {
|
|
t.Helper()
|
|
store := mock.New()
|
|
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
|
|
|
|
for name, policy := range policies {
|
|
err := store.UpsertPolicy(ctx, txn, name, []byte(policy))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
}
|
|
|
|
err := store.Commit(ctx, txn)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
return []func(*Rego){
|
|
// This extra module is required for modules in the store to be parsed
|
|
Module("extra.rego", "package extra\np = 1"),
|
|
Store(store),
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
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.policies, func(path string) {
|
|
ctx := context.Background()
|
|
|
|
options := append(s.options(path, tc.policies, t, ctx),
|
|
Query("data.test"),
|
|
func(r *Rego) {
|
|
SetRegoVersion(tc.regoVersion)(r)
|
|
},
|
|
)
|
|
|
|
pq, err := New(
|
|
options...,
|
|
).PrepareForEval(ctx)
|
|
|
|
if tc.expectedErr != "" {
|
|
if err == nil {
|
|
t.Fatal("Expected error, got none")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.expectedErr) {
|
|
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", err, tc.expectedErr)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if tc.expectedResult != "" {
|
|
assertResultSet(t, rs, tc.expectedResult)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegoLoadFilesWithProvidedStore(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := mock.New()
|
|
|
|
files := map[string]string{
|
|
"x.rego": "package x\np = data.x.b",
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
pq, err := New(
|
|
Store(store),
|
|
Query("data"),
|
|
Load([]string{path}, nil),
|
|
).PrepareForEval(ctx)
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected an error but err == nil")
|
|
}
|
|
|
|
if pq.r != nil {
|
|
t.Fatalf("Expected pq.r == nil, got: %+v", pq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegoLoadBundleWithProvidedStore(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := mock.New()
|
|
|
|
files := map[string]string{
|
|
"x/x.rego": "package x\np = data.x.b",
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
pq, err := New(
|
|
Store(store),
|
|
Query("data"),
|
|
LoadBundle(path),
|
|
).PrepareForEval(ctx)
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected an error but err == nil")
|
|
}
|
|
|
|
if pq.r != nil {
|
|
t.Fatalf("Expected pq.r == nil, got: %+v", pq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegoCustomBuiltinPartialPropagate(t *testing.T) {
|
|
mod := `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
x = trim_and_split(input.foo, "/")
|
|
x == ["foo", "bar", "baz"]
|
|
}
|
|
`
|
|
|
|
originalRego := New(
|
|
Module("test.rego", mod),
|
|
Query(`data.test.p`),
|
|
Function2(
|
|
&Function{
|
|
Name: "trim_and_split",
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S, types.S), // two string inputs
|
|
types.NewArray(nil, types.S), // variable-length string array output
|
|
),
|
|
},
|
|
func(_ BuiltinContext, a, b *ast.Term) (*ast.Term, error) {
|
|
|
|
str, ok1 := a.Value.(ast.String)
|
|
delim, ok2 := b.Value.(ast.String)
|
|
|
|
// The function is undefined for non-string inputs. Built-in
|
|
// functions should only return errors in unrecoverable cases.
|
|
if !ok1 || !ok2 {
|
|
return nil, nil
|
|
}
|
|
|
|
result := strings.Split(strings.Trim(string(str), string(delim)), string(delim))
|
|
|
|
arr := make([]*ast.Term, len(result))
|
|
for i := range result {
|
|
arr[i] = ast.StringTerm(result[i])
|
|
}
|
|
|
|
return ast.ArrayTerm(arr...), nil
|
|
},
|
|
),
|
|
)
|
|
|
|
pr, err := originalRego.PartialResult(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pr.Rego(
|
|
Input(map[string]interface{}{"foo": "/foo/bar/baz/"}),
|
|
).Eval(context.Background())
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
assertResultSet(t, rs, `[[true]]`)
|
|
|
|
}
|
|
|
|
func TestRegoPartialResultRecursiveRefs(t *testing.T) {
|
|
r := New(Query("data"), Module("test.rego", `package foo.bar
|
|
import rego.v1
|
|
|
|
default p = false
|
|
|
|
p if { input.x = 1 }`))
|
|
|
|
_, err := r.PartialResult(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
if !IsPartialEvaluationNotEffectiveErr(err) {
|
|
t.Fatal("expected ineffective partial eval error")
|
|
}
|
|
|
|
}
|
|
|
|
func TestSkipPartialNamespaceOption(t *testing.T) {
|
|
r := New(Query("data.test.p"), Module("example.rego", `
|
|
package test
|
|
import rego.v1
|
|
|
|
default p = false
|
|
|
|
p = true if { input }
|
|
`), SkipPartialNamespace(true))
|
|
|
|
pq, err := r.Partial(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(pq.Queries) != 1 || !pq.Queries[0].Equal(ast.MustParseBody("data.test.p")) {
|
|
t.Fatal("expected exactly one query and for reference to not have been rewritten but got:", pq.Queries)
|
|
}
|
|
|
|
if len(pq.Support) != 1 || !pq.Support[0].Package.Equal(ast.MustParsePackage("package test")) {
|
|
t.Fatal("expected exactly one support and for package to be same as input but got:", pq.Support)
|
|
}
|
|
}
|
|
|
|
func TestShallowInliningOption(t *testing.T) {
|
|
r := New(Query("data.test.p = true"),
|
|
SetRegoVersion(ast.RegoV1),
|
|
Module("example.rego", `
|
|
package test
|
|
|
|
p if {
|
|
q = true
|
|
}
|
|
|
|
q if {
|
|
input.x = r
|
|
}
|
|
|
|
r = 7
|
|
`),
|
|
ShallowInlining(true))
|
|
|
|
pq, err := r.Partial(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(pq.Queries) != 1 || !pq.Queries[0].Equal(ast.MustParseBody("data.partial.test.p = true")) {
|
|
t.Fatal("expected exactly one query and ref to be rewritten but got:", pq.Queries)
|
|
}
|
|
|
|
exp := ast.MustParseModuleWithOpts(`
|
|
package partial.test
|
|
|
|
p if { data.partial.test.q = true }
|
|
q if { 7 = input.x }
|
|
`, ast.ParserOptions{RegoVersion: ast.RegoV1})
|
|
|
|
if len(pq.Support) != 1 || !pq.Support[0].Equal(exp) {
|
|
t.Fatal("expected module:", exp, "\n\ngot module:", pq.Support[0])
|
|
}
|
|
}
|
|
|
|
func TestRegoPartialResultSortedRules(t *testing.T) {
|
|
r := New(Query("data.test.p"),
|
|
SetRegoVersion(ast.RegoV1),
|
|
Module("example.rego", `
|
|
package test
|
|
|
|
default p = false
|
|
|
|
p if {
|
|
r = (input.d * input.a) + input.c
|
|
r < s
|
|
}
|
|
|
|
p if {
|
|
r = (input.d * input.b) + input.c
|
|
r < s
|
|
}
|
|
|
|
s = 100
|
|
`))
|
|
|
|
pq, err := r.Partial(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Without sorting of support rules, the output of the above partial evaluation
|
|
// resulted in a random order of the support rules (in this case two different possible outputs)
|
|
exp := ast.MustParseModuleWithOpts(
|
|
`package partial.test
|
|
|
|
default p = false
|
|
|
|
p = true if { lt(plus(mul(input.d, input.a), input.c), 100) }
|
|
p = true if { lt(plus(mul(input.d, input.b), input.c), 100) }
|
|
`,
|
|
ast.ParserOptions{RegoVersion: ast.RegoV1})
|
|
|
|
if len(pq.Support) != 1 || !pq.Support[0].Equal(exp) {
|
|
t.Fatal("expected module:", exp, "\n\ngot module:", pq.Support[0])
|
|
}
|
|
|
|
}
|
|
|
|
func TestPrepareWithEmptyModule(t *testing.T) {
|
|
_, err := New(
|
|
Query("d"),
|
|
Module("example.rego", ""),
|
|
).PrepareForEval(context.Background())
|
|
|
|
expected := "1 error occurred: example.rego:0: rego_parse_error: empty module"
|
|
if err == nil || err.Error() != expected {
|
|
t.Fatalf("Expected error %s, got %s", expected, err)
|
|
}
|
|
}
|
|
|
|
func TestPrepareWithWasmTargetNotSupported(t *testing.T) {
|
|
files := map[string]string{
|
|
"x/x.rego": "package x\np = data.x.b",
|
|
"x/data.json": `{"b": "bar"}`,
|
|
"/policy.wasm": `modules-compiled-as-wasm-binary`,
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
ctx := context.Background()
|
|
|
|
_, err := New(
|
|
LoadBundle(path),
|
|
Query("data.x.p"),
|
|
Target("wasm"),
|
|
).PrepareForEval(ctx)
|
|
|
|
expected := "wasm target not supported"
|
|
if err == nil || err.Error() != expected {
|
|
t.Fatalf("Expected error %s, got %s", expected, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEvalWithInterQueryCache(t *testing.T) {
|
|
newHeaders := map[string][]string{"Cache-Control": {"max-age=290304000, public"}}
|
|
|
|
var requests []*http.Request
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests = append(requests, r)
|
|
headers := w.Header()
|
|
|
|
for k, v := range newHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"x": 1}`))
|
|
}))
|
|
defer ts.Close()
|
|
query := fmt.Sprintf(`http.send({"method": "get", "url": "%s", "force_json_decode": true, "cache": true})`, ts.URL)
|
|
|
|
// add an inter-query cache
|
|
config, _ := cache.ParseCachingConfig(nil)
|
|
interQueryCache := cache.NewInterQueryCache(config)
|
|
|
|
ctx := context.Background()
|
|
_, err := New(Query(query), InterQueryBuiltinCache(interQueryCache)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// eval again with same query
|
|
// this request should be served by the cache
|
|
_, err = New(Query(query), InterQueryBuiltinCache(interQueryCache)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if len(requests) != 1 {
|
|
t.Fatal("Expected server to be called only once")
|
|
}
|
|
}
|
|
|
|
func TestEvalWithInterQueryValueCache(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// add an inter-query value cache
|
|
config, _ := cache.ParseCachingConfig(nil)
|
|
interQueryValueCache := cache.NewInterQueryValueCache(ctx, config)
|
|
|
|
m := metrics.New()
|
|
|
|
query := `regex.match("foo.*", "foobar")`
|
|
_, err := New(Query(query), InterQueryBuiltinValueCache(interQueryValueCache), Metrics(m)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// eval again with same query
|
|
// this request should be served by the cache
|
|
_, err = New(Query(query), InterQueryBuiltinValueCache(interQueryValueCache), Metrics(m)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if exp, act := uint64(1), m.Counter("rego_builtin_regex_interquery_value_cache_hits").Value(); exp != act {
|
|
t.Fatalf("expected %d cache hits, got %d", exp, act)
|
|
}
|
|
|
|
query = `glob.match("*.example.com", ["."], "api.example.com")`
|
|
_, err = New(Query(query), InterQueryBuiltinValueCache(interQueryValueCache), Metrics(m)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// eval again with same query
|
|
// this request should be served by the cache
|
|
_, err = New(Query(query), InterQueryBuiltinValueCache(interQueryValueCache), Metrics(m)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = New(Query(query), InterQueryBuiltinValueCache(interQueryValueCache), Metrics(m)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if exp, act := uint64(2), m.Counter("rego_builtin_glob_interquery_value_cache_hits").Value(); exp != act {
|
|
t.Fatalf("expected %d cache hits, got %d", exp, act)
|
|
}
|
|
}
|
|
|
|
// We use http.send to ensure the NDBuiltinCache is involved.
|
|
func TestEvalWithNDCache(t *testing.T) {
|
|
var requests []*http.Request
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests = append(requests, r)
|
|
_, _ = w.Write([]byte(`{"x": 1}`))
|
|
}))
|
|
defer ts.Close()
|
|
query := fmt.Sprintf(`http.send({"method": "get", "url": "%s", "force_json_decode": true})`, ts.URL)
|
|
|
|
// Set up the ND cache, and put in some arbitrary constants for the first K/V pair.
|
|
arbitraryKey := ast.Number(strconv.Itoa(2015))
|
|
arbitraryValue := ast.String("First commit year")
|
|
ndBC := builtins.NDBCache{}
|
|
ndBC.Put("arbitrary_experiment", arbitraryKey, arbitraryValue)
|
|
|
|
// Query execution of http.send should add an entry to the NDBuiltinCache.
|
|
ctx := context.Background()
|
|
_, err := New(Query(query), NDBuiltinCache(ndBC)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check and make sure we got exactly 2x items back in the ND builtin cache.
|
|
// NDBuiltinCache always has the structure: map[ast.String]map[ast.Array]ast.Value
|
|
if len(ndBC) != 2 {
|
|
t.Fatalf("Expected exactly 2 items in non-deterministic builtin cache. Found %d items.\n", len(ndBC))
|
|
}
|
|
// Check the cached k/v types for the HTTP section of the cache.
|
|
if cachedResults, ok := ndBC["http.send"]; ok {
|
|
err := cachedResults.Iter(func(k, v *ast.Term) error {
|
|
if _, ok := k.Value.(*ast.Array); !ok {
|
|
t.Fatalf("http.send failed to store Object key in the ND builtins cache")
|
|
}
|
|
if _, ok := v.Value.(ast.Object); !ok {
|
|
t.Fatalf("http.send failed to store Object value in the ND builtins cache")
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Ensure our original arbitrary data in the cache was preserved.
|
|
if v, ok := ndBC.Get("arbitrary_experiment", arbitraryKey); ok {
|
|
if v != arbitraryValue {
|
|
t.Fatalf("Non-deterministic builtins cache value was mangled. Expected: %v, got: %v\n", arbitraryValue, v)
|
|
}
|
|
} else {
|
|
t.Fatal("Non-deterministic builtins cache lookup failed.")
|
|
}
|
|
}
|
|
|
|
func TestEvalWithPrebuiltNDCache(t *testing.T) {
|
|
query := "time.now_ns()"
|
|
ndBC := builtins.NDBCache{}
|
|
|
|
// Populate the cache for time.now_ns with an arbitrary timestamp.
|
|
timeValue, err := time.Parse("2006-01-02T15:04:05Z", "2015-12-28T14:08:25Z")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Timestamp ns value will be: 1451311705000000000
|
|
ndBC.Put("time.now_ns", ast.NewArray(), ast.Number(json.Number(strconv.FormatInt(timeValue.UnixNano(), 10))))
|
|
// time.now_ns should use the cached entry instead of the current time.
|
|
ctx := context.Background()
|
|
rs, err := New(Query(query), NDBuiltinCache(ndBC)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check that we got the correct time value in the result set.
|
|
assertResultSet(t, rs, "[[1451311705000000000]]")
|
|
}
|
|
|
|
func TestNDBCacheWithRuleBody(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
|
defer ts.Close()
|
|
|
|
ndBC := builtins.NDBCache{}
|
|
query := "data.foo.p = x"
|
|
_, err := New(
|
|
Query(query),
|
|
NDBuiltinCache(ndBC),
|
|
Module("test.rego", fmt.Sprintf(`package foo
|
|
import rego.v1
|
|
p if {
|
|
http.send({"url": "%s", "method":"get"})
|
|
}`, ts.URL)),
|
|
).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, ok := ndBC["http.send"]
|
|
if !ok {
|
|
t.Fatalf("expected http.send cache entry")
|
|
}
|
|
}
|
|
|
|
// Catches issues around iteration with ND builtins.
|
|
func TestNDBCacheWithRuleBodyAndIteration(t *testing.T) {
|
|
ctx := context.Background()
|
|
ts := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
|
}))
|
|
defer ts.Close()
|
|
|
|
ndBC := builtins.NDBCache{}
|
|
query := "data.foo.results = x"
|
|
_, err := New(
|
|
Query(query),
|
|
NDBuiltinCache(ndBC),
|
|
Module("test.rego", fmt.Sprintf(`package foo
|
|
|
|
import rego.v1
|
|
|
|
urls := [
|
|
"%[1]s/headers",
|
|
"%[1]s/ip",
|
|
"%[1]s/user-agent"
|
|
]
|
|
|
|
results contains response if {
|
|
some url in urls
|
|
response := http.send({
|
|
"method": "GET",
|
|
"url": url
|
|
})
|
|
}`, ts.URL)),
|
|
).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Ensure that the cache exists, and has exactly 3 entries.
|
|
entries, ok := ndBC["http.send"]
|
|
if !ok {
|
|
t.Fatalf("expected http.send cache entry")
|
|
}
|
|
if entries.Len() != 3 {
|
|
t.Fatalf("expected 3 http.send cache entries, received:\n%v", ndBC)
|
|
}
|
|
}
|
|
|
|
// This test ensures that the NDBCache correctly serializes/deserializes.
|
|
func TestNDBCacheMarshalUnmarshalJSON(t *testing.T) {
|
|
original := builtins.NDBCache{}
|
|
|
|
// Populate the cache for time.now_ns with an arbitrary timestamp.
|
|
original.Put("time.now_ns", ast.NewArray(), ast.Number(json.Number(strconv.FormatInt(1451311705000000000, 10))))
|
|
jOriginal, err := json.Marshal(original)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var other builtins.NDBCache
|
|
err = json.Unmarshal(jOriginal, &other)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
jOther, err := json.Marshal(other)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Check that the two NDBCache value's JSONified forms match exactly.
|
|
if !bytes.Equal(jOriginal, jOther) {
|
|
t.Fatalf("JSONified values of NDBCaches do not match; expected %s, got %s", string(jOriginal), string(jOther))
|
|
}
|
|
}
|
|
|
|
func TestStrictBuiltinErrors(t *testing.T) {
|
|
_, err := New(Query("1/0"), StrictBuiltinErrors(true)).Eval(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
topdownErr, ok := err.(*topdown.Error)
|
|
if !ok {
|
|
t.Fatal("expected topdown error but got:", err)
|
|
}
|
|
|
|
if topdownErr.Code != topdown.BuiltinErr {
|
|
t.Fatal("expected builtin error code but got:", topdownErr.Code)
|
|
}
|
|
|
|
if topdownErr.Message != "div: divide by zero" {
|
|
t.Fatal("expected divide by zero error but got:", topdownErr.Message)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinErrorList(t *testing.T) {
|
|
var buf []topdown.Error
|
|
|
|
_, err := New(Query("1/0"), BuiltinErrorList(&buf)).Eval(context.Background())
|
|
if err != nil {
|
|
t.Fatal("unexpected error")
|
|
}
|
|
|
|
if len(buf) != 1 {
|
|
t.Fatal("expected 1 error in buffer")
|
|
}
|
|
|
|
if buf[0].Error() != "1/0: eval_builtin_error: div: divide by zero" {
|
|
t.Fatal("expected divide by zero error but got:", buf[0].Error())
|
|
}
|
|
}
|
|
|
|
func TestTimeSeedingOptions(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
clock := time.Now()
|
|
|
|
// Check expected time is returned.
|
|
rs, err := New(Query("time.now_ns(x)"), Time(clock)).Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if len(rs) != 1 || !reflect.DeepEqual(rs[0].Bindings["x"], int64ToJSONNumber(clock.UnixNano())) {
|
|
t.Fatal("unexpected wall clock value")
|
|
}
|
|
|
|
// Check that time is not propagated to prepared query.
|
|
eval, err := New(Query("time.now_ns(x)"), Time(clock)).PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rs2, err := eval.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if len(rs2) != 1 || reflect.DeepEqual(rs[0].Bindings["x"], rs2[0].Bindings["x"]) {
|
|
t.Fatal("expected new wall clock value")
|
|
}
|
|
|
|
// Check that prepared query returns provided time.
|
|
rs3, err := eval.Eval(ctx, EvalTime(clock))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if len(rs2) != 1 || !reflect.DeepEqual(rs[0].Bindings["x"], rs3[0].Bindings["x"]) {
|
|
t.Fatal("expected old wall clock value")
|
|
}
|
|
|
|
}
|
|
|
|
func int64ToJSONNumber(i int64) json.Number {
|
|
return json.Number(strconv.FormatInt(i, 10))
|
|
}
|
|
|
|
func TestPrepareAndCompileWithSchema(t *testing.T) {
|
|
module := `
|
|
package test
|
|
x = input.y
|
|
`
|
|
|
|
schemaBytes := `{
|
|
"$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": [],
|
|
"properties": {
|
|
"y": {
|
|
"$id": "#/properties/y",
|
|
"type": "integer",
|
|
"title": "The y schema",
|
|
"description": "An explanation about the purpose of this instance."
|
|
}
|
|
},
|
|
"additionalProperties": false
|
|
}`
|
|
|
|
var schema interface{}
|
|
err := util.Unmarshal([]byte(schemaBytes), &schema)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
schemaSet := ast.NewSchemaSet()
|
|
schemaSet.Put(ast.InputRootRef, schema)
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
Package("foo"),
|
|
Schemas(schemaSet),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[1]]")
|
|
|
|
// Ensure that Compile still works after Prepare
|
|
// and its Eval has been called.
|
|
_, err = r.Compile(ctx)
|
|
if err != nil {
|
|
t.Errorf("Unexpected error when compiling: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestPrepareAndCompileWithRegoV1(t *testing.T) {
|
|
module := `package test
|
|
x contains v if {
|
|
v := input.y
|
|
}`
|
|
|
|
r := New(
|
|
Query("data.test.x"),
|
|
Module("", module),
|
|
SetRegoVersion(ast.RegoV1),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err.Error())
|
|
}
|
|
|
|
assertPreparedEvalQueryEval(t, pq, []EvalOption{
|
|
EvalInput(map[string]int{"y": 1}),
|
|
}, "[[[1]]]")
|
|
|
|
_, err = r.Compile(ctx)
|
|
if err != nil {
|
|
t.Errorf("Unexpected error when compiling: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestGenerateJSON(t *testing.T) {
|
|
r := New(
|
|
Query("input"),
|
|
Input("original-input"),
|
|
GenerateJSON(func(*ast.Term, *EvalContext) (interface{}, error) {
|
|
return "converted-input", nil
|
|
}),
|
|
)
|
|
assertEval(t, r, `[["converted-input"]]`)
|
|
}
|
|
|
|
func TestRegoLazyObjDefault(t *testing.T) {
|
|
foo := map[string]interface{}{"foo": "bar", "other": 1}
|
|
store := inmem.NewFromObjectWithOpts(map[string]interface{}{
|
|
"stored": foo,
|
|
})
|
|
r := New(
|
|
Query("x = data.stored"),
|
|
Store(store),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
act, ok := rs[0].Bindings["x"]
|
|
if !ok {
|
|
t.Fatalf("expected binding for \"x\", got %v", rs[0].Bindings)
|
|
}
|
|
m, ok := act.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected %T, got %T: %[2]v", m, act)
|
|
}
|
|
m["fox"] = true
|
|
|
|
if _, ok := foo["fox"]; ok {
|
|
t.Errorf("expected no change in foo, found one: %v", foo)
|
|
}
|
|
}
|
|
|
|
func TestRegoLazyObjNoRoundTripOnWrite(t *testing.T) {
|
|
foo := map[string]interface{}{"foo": "bar", "other": 1}
|
|
store := inmem.NewFromObjectWithOpts(map[string]interface{}{
|
|
"stored": foo,
|
|
}, inmem.OptRoundTripOnWrite(false))
|
|
r := New(
|
|
Query("x = data.stored"),
|
|
Store(store),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
act, ok := rs[0].Bindings["x"]
|
|
if !ok {
|
|
t.Fatalf("expected binding for \"x\", got %v", rs[0].Bindings)
|
|
}
|
|
m, ok := act.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected %T, got %T: %[2]v", m, act)
|
|
}
|
|
m["fox"] = true
|
|
|
|
if v, ok := foo["fox"]; !ok || !v.(bool) {
|
|
t.Errorf("expected change in foo, found none: %v", foo)
|
|
}
|
|
}
|
|
|
|
func TestRegoLazyObjCopyMaps(t *testing.T) {
|
|
foo := map[string]interface{}{"foo": "bar", "other": 1}
|
|
store := inmem.NewFromObjectWithOpts(map[string]interface{}{
|
|
"stored": foo,
|
|
}, inmem.OptRoundTripOnWrite(false))
|
|
r := New(
|
|
Query("x = data.stored"),
|
|
Store(store),
|
|
)
|
|
|
|
ctx := context.Background()
|
|
pq, err := r.PrepareForEval(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rs, err := pq.Eval(ctx, EvalCopyMaps(true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
act, ok := rs[0].Bindings["x"]
|
|
if !ok {
|
|
t.Fatalf("expected binding for \"x\", got %v", rs[0].Bindings)
|
|
}
|
|
m, ok := act.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected %T, got %T: %[2]v", m, act)
|
|
}
|
|
m["fox"] = true
|
|
|
|
if _, ok := foo["fox"]; ok {
|
|
t.Errorf("expected no change in foo, found one: %v", foo)
|
|
}
|
|
}
|
|
|
|
func TestDescriptionRegisterBuiltin1(t *testing.T) {
|
|
description := "custom-arity-1"
|
|
|
|
decl := &Function{
|
|
Name: "foo",
|
|
Description: description,
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S),
|
|
types.S,
|
|
),
|
|
}
|
|
|
|
RegisterBuiltin1(decl, func(_ BuiltinContext, _ *ast.Term) (*ast.Term, error) {
|
|
return ast.StringTerm("bar"), nil
|
|
})
|
|
defer unregisterBuiltin("foo")
|
|
|
|
got := ast.Builtins[len(ast.Builtins)-1].Description
|
|
if got != description {
|
|
t.Fatalf("expected %q, got %q", description, got)
|
|
}
|
|
}
|
|
|
|
func TestDescriptionRegisterBuiltin2(t *testing.T) {
|
|
description := "custom-arity-2"
|
|
|
|
decl := &Function{
|
|
Name: "foo",
|
|
Description: description,
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S, types.S),
|
|
types.S,
|
|
),
|
|
}
|
|
|
|
RegisterBuiltin2(decl, func(_ BuiltinContext, _, _ *ast.Term) (*ast.Term, error) {
|
|
return ast.StringTerm("bar"), nil
|
|
})
|
|
defer unregisterBuiltin("foo")
|
|
|
|
got := ast.Builtins[len(ast.Builtins)-1].Description
|
|
if got != description {
|
|
t.Fatalf("expected %q, got %q", description, got)
|
|
}
|
|
}
|
|
|
|
func TestDescriptionRegisterBuiltin3(t *testing.T) {
|
|
description := "custom-arity-3"
|
|
|
|
decl := &Function{
|
|
Name: "foo",
|
|
Description: description,
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S, types.S, types.S),
|
|
types.S,
|
|
),
|
|
}
|
|
|
|
RegisterBuiltin3(decl, func(_ BuiltinContext, _, _, _ *ast.Term) (*ast.Term, error) {
|
|
return ast.StringTerm("bar"), nil
|
|
})
|
|
defer unregisterBuiltin("foo")
|
|
|
|
got := ast.Builtins[len(ast.Builtins)-1].Description
|
|
if got != description {
|
|
t.Fatalf("expected %q, got %q", description, got)
|
|
}
|
|
}
|
|
|
|
func TestDescriptionRegisterBuiltin4(t *testing.T) {
|
|
description := "custom-arity-4"
|
|
|
|
decl := &Function{
|
|
Name: "foo",
|
|
Description: description,
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S, types.S, types.S, types.S),
|
|
types.S,
|
|
),
|
|
}
|
|
|
|
RegisterBuiltin4(decl, func(_ BuiltinContext, _, _, _, _ *ast.Term) (*ast.Term, error) {
|
|
return ast.StringTerm("bar"), nil
|
|
})
|
|
defer unregisterBuiltin("foo")
|
|
|
|
got := ast.Builtins[len(ast.Builtins)-1].Description
|
|
if got != description {
|
|
t.Fatalf("expected %q, got %q", description, got)
|
|
}
|
|
}
|
|
|
|
func TestDescriptionRegisterBuiltinDyn(t *testing.T) {
|
|
description := "custom-arity-dyn"
|
|
|
|
decl := &Function{
|
|
Name: "foo",
|
|
Description: description,
|
|
Decl: types.NewFunction(
|
|
types.Args(types.S),
|
|
types.S,
|
|
),
|
|
}
|
|
|
|
RegisterBuiltinDyn(decl, func(_ BuiltinContext, _ []*ast.Term) (*ast.Term, error) {
|
|
return ast.StringTerm("bar"), nil
|
|
})
|
|
defer unregisterBuiltin("foo")
|
|
|
|
got := ast.Builtins[len(ast.Builtins)-1].Description
|
|
if got != description {
|
|
t.Fatalf("expected %q, got %q", description, got)
|
|
}
|
|
}
|
|
|
|
// unregisterBuiltin removes the builtin of the given name from ast.Builtins. This assists in
|
|
// cleaning up custom functions added as part of certain test cases.
|
|
func unregisterBuiltin(name string) {
|
|
builtins := make([]*ast.Builtin, 0, len(ast.Builtins))
|
|
for _, builtin := range ast.Builtins {
|
|
if builtin.Name == name {
|
|
continue
|
|
}
|
|
builtins = append(builtins, builtin)
|
|
}
|
|
ast.Builtins = builtins
|
|
}
|