mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
e43ef0a979
Earlier this evening I tried to run the Go [modernize](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize) analyzer on OPA. That didn't go as planned: - https://github.com/golang/go/issues/73661 - https://github.com/golang/go/issues/73663 While we wait for that to be fixed, I figured an old-fashioned search-and-replace across the repo may work for at least the `interface{}` to `any` conversion. That should help make it easier to see the other fixes as applied by the modernize tool once it has had those issues resolved. Signed-off-by: Anders Eknert <anders@styra.com>
580 lines
11 KiB
Go
580 lines
11 KiB
Go
// Copyright 2019 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.
|
|
|
|
package presentation
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/v1/ast"
|
|
"github.com/open-policy-agent/opa/v1/loader"
|
|
"github.com/open-policy-agent/opa/v1/rego"
|
|
"github.com/open-policy-agent/opa/v1/storage"
|
|
inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
)
|
|
|
|
type testErrorWithMarshaller struct {
|
|
msg string
|
|
}
|
|
|
|
func (t *testErrorWithMarshaller) Error() string {
|
|
return t.msg
|
|
}
|
|
|
|
func (t *testErrorWithMarshaller) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
Text string `json:"text"`
|
|
}{
|
|
Text: t.msg,
|
|
})
|
|
}
|
|
|
|
type testErrorWithDetails struct{}
|
|
|
|
func (*testErrorWithDetails) Error() string { return "something went wrong" }
|
|
func (*testErrorWithDetails) Lines() []string { return []string{"oh", "so", "wrong"} }
|
|
|
|
func validateJSONOutput(t *testing.T, testErr error, expected string) {
|
|
t.Helper()
|
|
output := Output{Errors: NewOutputErrors(testErr)}
|
|
var buf bytes.Buffer
|
|
err := JSON(&buf, output)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
result := util.MustUnmarshalJSON(buf.Bytes())
|
|
exp := util.MustUnmarshalJSON([]byte(expected))
|
|
|
|
if !reflect.DeepEqual(result, exp) {
|
|
t.Fatal("expected:", exp, "got:", result)
|
|
}
|
|
}
|
|
|
|
func TestOutputJSONErrorUnstructured(t *testing.T) {
|
|
err := errors.New("some text")
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "some text"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorCustomMarshaller(t *testing.T) {
|
|
err := &testErrorWithMarshaller{
|
|
msg: "custom message",
|
|
}
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "custom message"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorWithDetails(t *testing.T) {
|
|
err := &testErrorWithDetails{}
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "something went wrong",
|
|
"details": "oh\nso\nwrong"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredASTErr(t *testing.T) {
|
|
err := &ast.Error{
|
|
Code: "1",
|
|
Message: "error message",
|
|
}
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "error message",
|
|
"code": "1"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredStorageErr(t *testing.T) {
|
|
store := inmem.New()
|
|
txn := storage.NewTransactionOrDie(context.Background(), store)
|
|
err := store.Write(context.Background(), txn, storage.AddOp, storage.Path{}, map[string]any{"foo": 1})
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "data write during read transaction",
|
|
"code": "storage_invalid_txn_error"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredTopdownErr(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
p(x) = y if {
|
|
y = x[_]
|
|
}
|
|
|
|
z := p([1, 2, 3])
|
|
`
|
|
|
|
_, err := rego.New(
|
|
rego.Module("test.rego", mod),
|
|
rego.Query("data.test.z"),
|
|
).Eval(context.Background())
|
|
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "functions must not produce multiple outputs for same inputs",
|
|
"code": "eval_conflict_error",
|
|
"location": {
|
|
"file": "test.rego",
|
|
"row": 5,
|
|
"col": 3
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredAstErr(t *testing.T) {
|
|
_, err := rego.New(rego.Query("count(0)")).Eval(context.Background())
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "count: invalid argument(s)",
|
|
"code": "rego_type_error",
|
|
"location": {
|
|
"file": "",
|
|
"row": 1,
|
|
"col": 1
|
|
},
|
|
"details": {
|
|
"have": [
|
|
{
|
|
"type": "number"
|
|
},
|
|
null
|
|
],
|
|
"want": {
|
|
"args": [
|
|
{
|
|
"description": "the set/array/object/string to be counted",
|
|
"name": "collection",
|
|
"of": [
|
|
{
|
|
"type": "string"
|
|
},
|
|
{
|
|
"dynamic": {
|
|
"type": "any"
|
|
},
|
|
"type": "array"
|
|
},
|
|
{
|
|
"dynamic": {
|
|
"key": {
|
|
"type": "any"
|
|
},
|
|
"value": {
|
|
"type": "any"
|
|
}
|
|
},
|
|
"type": "object"
|
|
},
|
|
{
|
|
"of": {
|
|
"type": "any"
|
|
},
|
|
"type": "set"
|
|
}
|
|
],
|
|
"type": "any"
|
|
},
|
|
{
|
|
"description": "the count of elements, key/val pairs, or characters, respectively.",
|
|
"name": "n",
|
|
"type": "number"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredAstParseErr(t *testing.T) {
|
|
_, err := rego.New(
|
|
rego.Module("parse-err.rego", "!!!"),
|
|
rego.Query("!!!"),
|
|
).Eval(context.Background())
|
|
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "illegal ! character",
|
|
"code": "rego_parse_error",
|
|
"location": {
|
|
"file": "parse-err.rego",
|
|
"row": 1,
|
|
"col": 1
|
|
},
|
|
"details": {
|
|
"line": "!!!",
|
|
"idx": 0
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredASTErrList(t *testing.T) {
|
|
c := ast.NewCompiler()
|
|
c.Compile(map[string]*ast.Module{
|
|
"error.rego": ast.MustParseModule(`
|
|
package test
|
|
import rego.v1
|
|
|
|
q if {
|
|
bad[reference]
|
|
}
|
|
`)})
|
|
c.Errors.Sort()
|
|
err := c.Errors
|
|
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "var bad is unsafe",
|
|
"code": "rego_unsafe_var_error",
|
|
"location": {
|
|
"file": "",
|
|
"row": 6,
|
|
"col": 2
|
|
}
|
|
},
|
|
{
|
|
"message": "var reference is unsafe",
|
|
"code": "rego_unsafe_var_error",
|
|
"location": {
|
|
"file": "",
|
|
"row": 6,
|
|
"col": 2
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredLoaderErrList(t *testing.T) {
|
|
files := map[string]string{
|
|
// bundle a
|
|
"a/data.json": "{{{",
|
|
"b/data.json": "...",
|
|
}
|
|
|
|
var err error
|
|
var tmpPath string
|
|
test.WithTempFS(files, func(path string) {
|
|
tmpPath = path
|
|
_, err = loader.NewFileLoader().All([]string{path})
|
|
})
|
|
|
|
expected := fmt.Sprintf(`{
|
|
"errors": [
|
|
{
|
|
"message": "%s/a/data.json: invalid character '{' looking for beginning of object key string"
|
|
},
|
|
{
|
|
"message": "%s/b/data.json: invalid character '.' looking for beginning of value"
|
|
}
|
|
]
|
|
}
|
|
`, tmpPath, tmpPath)
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestOutputJSONErrorStructuredRegoErrList(t *testing.T) {
|
|
mod := `
|
|
package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
bad_func1()
|
|
}
|
|
|
|
q if {
|
|
bad_func2()
|
|
}
|
|
`
|
|
_, err := rego.New(
|
|
rego.Module("error.rego", mod),
|
|
rego.Query("data"),
|
|
).PrepareForEval(context.Background())
|
|
|
|
expected := `{
|
|
"errors": [
|
|
{
|
|
"message": "undefined function bad_func1",
|
|
"code": "rego_type_error",
|
|
"location": {
|
|
"file": "error.rego",
|
|
"row": 6,
|
|
"col": 2
|
|
}
|
|
},
|
|
{
|
|
"message": "undefined function bad_func2",
|
|
"code": "rego_type_error",
|
|
"location": {
|
|
"file": "error.rego",
|
|
"row": 10,
|
|
"col": 2
|
|
}
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
validateJSONOutput(t, err, expected)
|
|
}
|
|
|
|
func TestSource(t *testing.T) {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
err := Source(buf, Output{
|
|
Partial: ®o.PartialQueries{
|
|
Queries: []ast.Body{
|
|
ast.MustParseBody("a = 1; b = 2"),
|
|
},
|
|
Support: []*ast.Module{
|
|
ast.MustParseModule(`
|
|
package test
|
|
p := 1
|
|
`),
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
exp := `# Query 1
|
|
a = 1
|
|
b = 2
|
|
|
|
# Module 1
|
|
package test
|
|
|
|
p := 1
|
|
`
|
|
|
|
if buf.String() != exp {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, buf.String())
|
|
}
|
|
|
|
}
|
|
|
|
func TestRaw(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
output Output
|
|
want string
|
|
}{
|
|
{
|
|
note: "simple single string",
|
|
output: Output{
|
|
Result: []rego.Result{
|
|
{
|
|
Expressions: []*rego.ExpressionValue{
|
|
{Value: "Hello world"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "Hello world\n",
|
|
},
|
|
{
|
|
note: "table format",
|
|
output: Output{
|
|
Result: []rego.Result{
|
|
{
|
|
Expressions: []*rego.ExpressionValue{
|
|
{Value: "one"},
|
|
{Value: 1},
|
|
},
|
|
},
|
|
{
|
|
Expressions: []*rego.ExpressionValue{
|
|
{Value: "two"},
|
|
{Value: 2},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "one 1\ntwo 2\n",
|
|
},
|
|
{
|
|
note: "compound values",
|
|
output: Output{
|
|
Result: []rego.Result{
|
|
{
|
|
Expressions: []*rego.ExpressionValue{
|
|
{Value: []any{"one"}},
|
|
{Value: map[string]any{
|
|
"key": []any{},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: "[\"one\"] {\"key\":[]}\n",
|
|
},
|
|
{
|
|
note: "error",
|
|
output: Output{
|
|
Errors: NewOutputErrors(errors.New("boom")),
|
|
},
|
|
want: "1 error occurred: boom\n",
|
|
},
|
|
{
|
|
// NOTE(sr): The presentation package outputs whatever Error() on
|
|
// the errors it is given yields. So even though NewOutputErrors
|
|
// will pick up the error details, they won't be output, as they
|
|
// are not included in the error's Error() string.
|
|
note: "error with details",
|
|
output: Output{
|
|
Errors: NewOutputErrors(&testErrorWithDetails{}),
|
|
},
|
|
want: "1 error occurred: something went wrong\noh\nso\nwrong\n",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
buf := new(bytes.Buffer)
|
|
err := Raw(buf, tc.output)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
if buf.String() != tc.want {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", tc.want, buf.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDepsAnalysisPrettyOutput(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
output DepAnalysisOutput
|
|
want []string
|
|
}{
|
|
{
|
|
note: "base document",
|
|
output: DepAnalysisOutput{Base: []ast.Ref{ast.InputRootRef}},
|
|
want: []string{
|
|
"+----------------+",
|
|
"| BASE DOCUMENTS |",
|
|
"+----------------+",
|
|
"| input |",
|
|
"+----------------+",
|
|
},
|
|
},
|
|
{
|
|
note: "virtual document",
|
|
output: DepAnalysisOutput{
|
|
Virtual: []ast.Ref{[]*ast.Term{
|
|
ast.VarTerm("data"), ast.StringTerm("policy"), ast.StringTerm("allow")},
|
|
},
|
|
},
|
|
want: []string{
|
|
"+-------------------+",
|
|
"| VIRTUAL DOCUMENTS |",
|
|
"+-------------------+",
|
|
"| data.policy.allow |",
|
|
"+-------------------+",
|
|
},
|
|
},
|
|
{
|
|
note: "base document and virtual document",
|
|
output: DepAnalysisOutput{
|
|
Base: []ast.Ref{ast.InputRootRef},
|
|
Virtual: []ast.Ref{[]*ast.Term{
|
|
ast.VarTerm("data"), ast.StringTerm("policy"), ast.StringTerm("allow")},
|
|
},
|
|
},
|
|
want: []string{
|
|
"+----------------+-------------------+",
|
|
"| BASE DOCUMENTS | VIRTUAL DOCUMENTS |",
|
|
"+----------------+-------------------+",
|
|
"| input | data.policy.allow |",
|
|
"+----------------+-------------------+",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
buf := new(bytes.Buffer)
|
|
if err := tc.output.Pretty(buf); err != nil {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
expected := strings.Join(tc.want, "\n") + "\n"
|
|
if buf.String() != expected {
|
|
t.Errorf("expected %v, got %v", expected, buf.String())
|
|
}
|
|
})
|
|
}
|
|
}
|