mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
d271cdfff8
Add rego.Data function to allow setting data directly from a map[string]any, providing a simpler alternative to using Store(inmem.NewFromObject(data)). This improves the Go SDK API by reducing boilerplate for the common case of using an in-memory store with static data. Fixes: #5961 Signed-off-by: majiayu000 <1835304752@qq.com>
3520 lines
80 KiB
Go
3520 lines
80 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"
|
|
"maps"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"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 any
|
|
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 := t.Context()
|
|
|
|
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 any
|
|
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{
|
|
"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 without rego_v1 feature",
|
|
regoVersion: ast.RegoV1,
|
|
capabilities: func() *ast.Capabilities {
|
|
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0))
|
|
|
|
feats := make([]string, 0, len(caps.Features))
|
|
for _, feat := range caps.Features {
|
|
if feat != ast.FeatureRegoV1 {
|
|
feats = append(feats, feat)
|
|
}
|
|
}
|
|
caps.Features = feats
|
|
|
|
return caps
|
|
}(),
|
|
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"]
|
|
}`,
|
|
},
|
|
{
|
|
note: "v1 module, rego-v1, v0 capabilities without rego_v1 feature",
|
|
regoVersion: ast.RegoV1,
|
|
capabilities: func() *ast.Capabilities {
|
|
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(ast.RegoV0))
|
|
|
|
feats := make([]string, 0, len(caps.Features))
|
|
for _, feat := range caps.Features {
|
|
if feat != ast.FeatureRegoV1 {
|
|
feats = append(feats, feat)
|
|
}
|
|
}
|
|
caps.Features = feats
|
|
|
|
return caps
|
|
}(),
|
|
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 := t.Context()
|
|
|
|
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(t.Context())
|
|
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(t.Context(), 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 := []any{}
|
|
|
|
for i := range rs {
|
|
values := []any{}
|
|
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 any
|
|
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() any {
|
|
a := &struct {
|
|
Foo string `json:"baz"`
|
|
}{"bar"}
|
|
return &a
|
|
}(), `[[{"baz":"bar"}]]`},
|
|
"slice": {[]string{"a", "b"}, `[[["a", "b"]]]`},
|
|
"nil": {nil, `[[null]]`},
|
|
"slice of interface": {[]any{"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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
|
|
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.Nl,
|
|
),
|
|
})
|
|
|
|
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(t.Context(), 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.Nl,
|
|
),
|
|
},
|
|
func(BuiltinContext, *ast.Term) (*ast.Term, error) {
|
|
return nil, NewHaltError(errors.New("stop"))
|
|
},
|
|
)
|
|
r := New(Query(`halt_func("")`), funOpt)
|
|
rs, err := r.Eval(t.Context())
|
|
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 := t.Context()
|
|
_, 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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
_, 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 := t.Context()
|
|
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 := t.Context()
|
|
_, 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 := t.Context()
|
|
_, 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]any{"x": 10})).PrepareForEval(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
_, err = pq.Eval(t.Context()) // 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]any{"x": 10})).PrepareForEval(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
_, err = pq.Eval(t.Context()) // 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(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
_, err = pq.Eval(
|
|
t.Context(),
|
|
EvalQueryTracer(tracer),
|
|
EvalRuleIndexing(false),
|
|
EvalInput(map[string]any{"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 := slices.Contains(evalNodes, expected)
|
|
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(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error %s", err)
|
|
}
|
|
|
|
rs, err := pq.Eval(
|
|
t.Context(),
|
|
EvalQueryTracer(tracer),
|
|
EvalRuleIndexing(false),
|
|
EvalInput(map[string]any{"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 := slices.Contains(evalNodes, expected)
|
|
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]any{
|
|
"x": map[string]any{"p": 1},
|
|
})),
|
|
)
|
|
|
|
ctx := t.Context()
|
|
_, 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 := t.Context()
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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 := t.Context()
|
|
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]any{"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]any{"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]any{"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(t.Context())
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
|
|
// 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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
|
|
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(t.Context())
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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(t.Context())
|
|
|
|
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(t.Context())
|
|
|
|
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(t.Context())
|
|
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 := t.Context()
|
|
|
|
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(t.Context())
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
rs, err := pr.Rego(
|
|
Input(map[string]any{"foo": "/foo/bar/baz/"}),
|
|
).Eval(t.Context())
|
|
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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(t.Context())
|
|
|
|
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 := t.Context()
|
|
|
|
_, 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()
|
|
|
|
maps.Copy(headers, newHeaders)
|
|
|
|
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 := t.Context()
|
|
_, 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 := t.Context()
|
|
|
|
// 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 := t.Context()
|
|
_, 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 := t.Context()
|
|
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 := t.Context()
|
|
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 := t.Context()
|
|
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(t.Context())
|
|
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(t.Context())
|
|
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 := t.Context()
|
|
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 any
|
|
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 := t.Context()
|
|
|
|
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 := t.Context()
|
|
|
|
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) (any, error) {
|
|
return "converted-input", nil
|
|
}),
|
|
)
|
|
assertEval(t, r, `[["converted-input"]]`)
|
|
}
|
|
|
|
func TestRegoLazyObjDefault(t *testing.T) {
|
|
foo := map[string]any{"foo": "bar", "other": 1}
|
|
store := inmem.NewFromObjectWithOpts(map[string]any{
|
|
"stored": foo,
|
|
})
|
|
r := New(
|
|
Query("x = data.stored"),
|
|
Store(store),
|
|
)
|
|
|
|
ctx := t.Context()
|
|
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]any)
|
|
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]any{"foo": "bar", "other": 1}
|
|
store := inmem.NewFromObjectWithOpts(map[string]any{
|
|
"stored": foo,
|
|
}, inmem.OptRoundTripOnWrite(false))
|
|
r := New(
|
|
Query("x = data.stored"),
|
|
Store(store),
|
|
)
|
|
|
|
ctx := t.Context()
|
|
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]any)
|
|
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]any{"foo": "bar", "other": 1}
|
|
store := inmem.NewFromObjectWithOpts(map[string]any{
|
|
"stored": foo,
|
|
}, inmem.OptRoundTripOnWrite(false))
|
|
r := New(
|
|
Query("x = data.stored"),
|
|
Store(store),
|
|
)
|
|
|
|
ctx := t.Context()
|
|
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]any)
|
|
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) {
|
|
ast.Builtins = slices.DeleteFunc(ast.Builtins, func(b *ast.Builtin) bool { return b.Name == name })
|
|
}
|
|
|
|
func TestCompilerContextViaRegoModuleBuiltin(t *testing.T) {
|
|
moduleSource := `package test
|
|
|
|
result := test.module("policy.rego")
|
|
`
|
|
|
|
t.Run("compiler not passed", func(t *testing.T) {
|
|
ctx := t.Context()
|
|
r := New(
|
|
Query("data.test.result"),
|
|
CompilerHook(func(c *ast.Compiler) { ctx = ast.WithCompiler(ctx, c) }),
|
|
Module("policy.rego", moduleSource),
|
|
Function1(&Function{
|
|
Name: "test.module",
|
|
Decl: types.NewFunction(types.Args(types.S), types.S),
|
|
}, func(bctx BuiltinContext, a *ast.Term) (*ast.Term, error) {
|
|
moduleName, ok := a.Value.(ast.String)
|
|
if !ok {
|
|
return nil, fmt.Errorf("bad arg type: %T", a.Value)
|
|
}
|
|
|
|
comp, ok := ast.CompilerFromContext(bctx.Context)
|
|
if !ok {
|
|
return nil, errors.New("no compiler on context")
|
|
}
|
|
|
|
return ast.StringTerm(comp.Modules[string(moduleName)].String()), nil
|
|
}),
|
|
)
|
|
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("rego Eval error: %v", err)
|
|
}
|
|
if len(rs) == 0 || len(rs[0].Expressions) == 0 {
|
|
t.Fatalf("No results")
|
|
}
|
|
got := rs[0].Expressions[0].Value
|
|
want := "package test\n\nresult := __local0__ if { true; test.module(\"policy.rego\", __local0__) }"
|
|
if got != want {
|
|
t.Errorf("Expected %q, got %q", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("compiler passed in", func(t *testing.T) { // when the compiler is passed, no hook is run
|
|
ctx := t.Context()
|
|
r := New(
|
|
Compiler(ast.NewCompiler()),
|
|
Query("data.test.result"),
|
|
CompilerHook(func(*ast.Compiler) { t.Fatal("unexpected hook call") }),
|
|
Module("policy.rego", "package test\nresult:=true"),
|
|
)
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("rego Eval error: %v", err)
|
|
}
|
|
if act, exp := rs.Allowed(), true; exp != act {
|
|
t.Errorf("expected %v, got %v", exp, act)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRegoData(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
r := New(
|
|
Query("data.x.y"),
|
|
Data(map[string]any{
|
|
"x": map[string]any{
|
|
"y": "hello",
|
|
},
|
|
}),
|
|
)
|
|
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(rs) != 1 || len(rs[0].Expressions) != 1 {
|
|
t.Fatalf("Expected one result with one expression but got: %v", rs)
|
|
}
|
|
|
|
if rs[0].Expressions[0].Value != "hello" {
|
|
t.Fatalf("Expected 'hello' but got: %v", rs[0].Expressions[0].Value)
|
|
}
|
|
}
|
|
|
|
func TestRegoDataWithModule(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
result := data.users[input.user_id].role
|
|
`
|
|
|
|
r := New(
|
|
Query("data.test.result"),
|
|
Module("test.rego", mod),
|
|
Data(map[string]any{
|
|
"users": map[string]any{
|
|
"alice": map[string]any{
|
|
"role": "admin",
|
|
},
|
|
"bob": map[string]any{
|
|
"role": "viewer",
|
|
},
|
|
},
|
|
}),
|
|
Input(map[string]any{
|
|
"user_id": "alice",
|
|
}),
|
|
)
|
|
|
|
rs, err := r.Eval(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(rs) != 1 || len(rs[0].Expressions) != 1 {
|
|
t.Fatalf("Expected one result with one expression but got: %v", rs)
|
|
}
|
|
|
|
if rs[0].Expressions[0].Value != "admin" {
|
|
t.Fatalf("Expected 'admin' but got: %v", rs[0].Expressions[0].Value)
|
|
}
|
|
}
|