Files
releases/cmd/parse_test.go
Anders Eknert afb30d3f9d Add gocritic linter, fix a bunch of stuff (#7377)
Brace yourselves! For there are many touched files here. No changes
in semantics however.

Spent a long time trying out the various optional rules gocritic
provides, and settled for a few of them. There are more I really
like, but that would take many hours to address across the codebase.

Perhaps others find gocritic too pedantic? If so, we can merge the
fixes without enabling the rule.

Signed-off-by: Anders Eknert <anders@styra.com>
2025-02-24 16:28:41 +01:00

1201 lines
26 KiB
Go

package cmd
import (
"bytes"
"path/filepath"
"strings"
"testing"
"github.com/open-policy-agent/opa/v1/util"
"github.com/open-policy-agent/opa/v1/util/test"
)
func TestParseExit0(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p = 1
`,
}
errc, stdout, stderr, _ := testParse(t, files, &configuredParseParams)
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedOutput := `module
package
ref
data
"x"
rule
head
ref
p
1
body
expr index=0
true
`
if got, want := string(stdout), expectedOutput; got != want {
t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
}
}
func TestParseExit1(t *testing.T) {
files := map[string]string{
"x.rego": `???`,
}
errc, _, stderr, _ := testParse(t, files, &configuredParseParams)
if errc != 1 {
t.Fatalf("Expected exit code 1, got %v", errc)
}
if len(stderr) == 0 {
t.Fatalf("Expected output in stderr")
}
}
func TestParseJSONOutput(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p = 1
`,
}
errc, stdout, stderr, _ := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}),
})
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedOutput := `{
"package": {
"path": [
{
"type": "var",
"value": "data"
},
{
"type": "string",
"value": "x"
}
]
},
"rules": [
{
"body": [
{
"index": 0,
"terms": {
"type": "boolean",
"value": true
}
}
],
"head": {
"name": "p",
"value": {
"type": "number",
"value": 1
},
"ref": [
{
"type": "var",
"value": "p"
}
]
}
}
]
}
`
if got, want := string(stdout), expectedOutput; got != want {
t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
}
}
func TestParseJSONOutputWithLocations(t *testing.T) {
files := map[string]string{
"x.rego": `package x
p = 1
`,
}
errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}),
jsonInclude: "locations",
})
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedOutput := strings.ReplaceAll(`{
"package": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 1,
"text": "cGFja2FnZQ=="
},
"path": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 9,
"text": "eA=="
},
"type": "var",
"value": "data"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 9,
"text": "eA=="
},
"type": "string",
"value": "x"
}
]
},
"rules": [
{
"body": [
{
"index": 0,
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 5,
"text": "MQ=="
},
"terms": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 5,
"text": "MQ=="
},
"type": "boolean",
"value": true
}
}
],
"head": {
"name": "p",
"value": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 5,
"text": "MQ=="
},
"type": "number",
"value": 1
},
"ref": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 1,
"text": "cA=="
},
"type": "var",
"value": "p"
}
],
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 1,
"text": "cCA9IDE="
}
},
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 1,
"text": "cCA9IDE="
}
}
]
}
`, "TEMPDIR", tempDirPath)
gotLines := strings.Split(string(stdout), "\n")
wantLines := strings.Split(expectedOutput, "\n")
min := len(gotLines)
if len(wantLines) < min {
min = len(wantLines)
}
for i := range min {
if gotLines[i] != wantLines[i] {
t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
}
}
if len(gotLines) != len(wantLines) {
t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
}
}
func TestParseRefsJSONOutput(t *testing.T) {
files := map[string]string{
"x.rego": `package x
a.b.c := true
`,
}
errc, stdout, stderr, _ := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}),
})
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedOutput := `{
"package": {
"path": [
{
"type": "var",
"value": "data"
},
{
"type": "string",
"value": "x"
}
]
},
"rules": [
{
"body": [
{
"index": 0,
"terms": {
"type": "boolean",
"value": true
}
}
],
"head": {
"value": {
"type": "boolean",
"value": true
},
"assign": true,
"ref": [
{
"type": "var",
"value": "a"
},
{
"type": "string",
"value": "b"
},
{
"type": "string",
"value": "c"
}
]
}
}
]
}
`
if got, want := string(stdout), expectedOutput; got != want {
t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
}
}
func TestParseRefsJSONOutputWithLocations(t *testing.T) {
files := map[string]string{
"x.rego": `package x
a.b.c := true
`,
}
errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}),
jsonInclude: "locations",
})
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedOutput := strings.ReplaceAll(`{
"package": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 1,
"text": "cGFja2FnZQ=="
},
"path": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 9,
"text": "eA=="
},
"type": "var",
"value": "data"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 9,
"text": "eA=="
},
"type": "string",
"value": "x"
}
]
},
"rules": [
{
"body": [
{
"index": 0,
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 10,
"text": "dHJ1ZQ=="
},
"terms": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 10,
"text": "dHJ1ZQ=="
},
"type": "boolean",
"value": true
}
}
],
"head": {
"value": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 10,
"text": "dHJ1ZQ=="
},
"type": "boolean",
"value": true
},
"assign": true,
"ref": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 1,
"text": "YQ=="
},
"type": "var",
"value": "a"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 3,
"text": "Yg=="
},
"type": "string",
"value": "b"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 5,
"text": "Yw=="
},
"type": "string",
"value": "c"
}
],
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 1,
"text": "YS5iLmMgOj0gdHJ1ZQ=="
}
},
"location": {
"file": "TEMPDIR/x.rego",
"row": 3,
"col": 1,
"text": "YS5iLmMgOj0gdHJ1ZQ=="
}
}
]
}
`, "TEMPDIR", tempDirPath)
gotLines := strings.Split(string(stdout), "\n")
wantLines := strings.Split(expectedOutput, "\n")
min := len(gotLines)
if len(wantLines) < min {
min = len(wantLines)
}
for i := range min {
if gotLines[i] != wantLines[i] {
t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
}
}
if len(gotLines) != len(wantLines) {
t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
}
}
func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) {
files := map[string]string{
"x.rego": `package x
import rego.v1
default allow = false
allow = true if {
input.method == "GET"
input.path = ["getUser", user]
input.user == user
}
`,
}
errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}),
jsonInclude: "locations",
})
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedOutput := strings.ReplaceAll(`{
"package": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 1,
"text": "cGFja2FnZQ=="
},
"path": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 9,
"text": "eA=="
},
"type": "var",
"value": "data"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 1,
"col": 9,
"text": "eA=="
},
"type": "string",
"value": "x"
}
]
},
"imports": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 2,
"col": 1,
"text": "aW1wb3J0"
},
"path": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 2,
"col": 8,
"text": "cmVnby52MQ=="
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 2,
"col": 8,
"text": "cmVnbw=="
},
"type": "var",
"value": "rego"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 2,
"col": 13,
"text": "djE="
},
"type": "string",
"value": "v1"
}
]
}
}
],
"rules": [
{
"body": [
{
"index": 0,
"location": {
"file": "TEMPDIR/x.rego",
"row": 4,
"col": 1,
"text": "ZGVmYXVsdA=="
},
"terms": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 4,
"col": 1,
"text": "ZGVmYXVsdA=="
},
"type": "boolean",
"value": true
}
}
],
"default": true,
"head": {
"name": "allow",
"value": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 4,
"col": 17,
"text": "ZmFsc2U="
},
"type": "boolean",
"value": false
},
"ref": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 4,
"col": 9,
"text": "YWxsb3c="
},
"type": "var",
"value": "allow"
}
],
"location": {
"file": "TEMPDIR/x.rego",
"row": 4,
"col": 9,
"text": "YWxsb3cgPSBmYWxzZQ=="
}
},
"location": {
"file": "TEMPDIR/x.rego",
"row": 4,
"col": 1,
"text": "ZGVmYXVsdA=="
}
},
{
"body": [
{
"index": 0,
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 3,
"text": "aW5wdXQubWV0aG9kID09ICJHRVQi"
},
"terms": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 16,
"text": "PT0="
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 16,
"text": "PT0="
},
"type": "var",
"value": "equal"
}
]
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 3,
"text": "aW5wdXQubWV0aG9k"
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 3,
"text": "aW5wdXQ="
},
"type": "var",
"value": "input"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 9,
"text": "bWV0aG9k"
},
"type": "string",
"value": "method"
}
]
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 6,
"col": 19,
"text": "IkdFVCI="
},
"type": "string",
"value": "GET"
}
]
},
{
"index": 1,
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 3,
"text": "aW5wdXQucGF0aCA9IFsiZ2V0VXNlciIsIHVzZXJd"
},
"terms": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 14,
"text": "PQ=="
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 14,
"text": "PQ=="
},
"type": "var",
"value": "eq"
}
]
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 3,
"text": "aW5wdXQucGF0aA=="
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 3,
"text": "aW5wdXQ="
},
"type": "var",
"value": "input"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 9,
"text": "cGF0aA=="
},
"type": "string",
"value": "path"
}
]
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 16,
"text": "WyJnZXRVc2VyIiwgdXNlcl0="
},
"type": "array",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 17,
"text": "ImdldFVzZXIi"
},
"type": "string",
"value": "getUser"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 7,
"col": 28,
"text": "dXNlcg=="
},
"type": "var",
"value": "user"
}
]
}
]
},
{
"index": 2,
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 3,
"text": "aW5wdXQudXNlciA9PSB1c2Vy"
},
"terms": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 14,
"text": "PT0="
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 14,
"text": "PT0="
},
"type": "var",
"value": "equal"
}
]
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 3,
"text": "aW5wdXQudXNlcg=="
},
"type": "ref",
"value": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 3,
"text": "aW5wdXQ="
},
"type": "var",
"value": "input"
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 9,
"text": "dXNlcg=="
},
"type": "string",
"value": "user"
}
]
},
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 8,
"col": 17,
"text": "dXNlcg=="
},
"type": "var",
"value": "user"
}
]
}
],
"head": {
"name": "allow",
"value": {
"location": {
"file": "TEMPDIR/x.rego",
"row": 5,
"col": 9,
"text": "dHJ1ZQ=="
},
"type": "boolean",
"value": true
},
"ref": [
{
"location": {
"file": "TEMPDIR/x.rego",
"row": 5,
"col": 1,
"text": "YWxsb3c="
},
"type": "var",
"value": "allow"
}
],
"location": {
"file": "TEMPDIR/x.rego",
"row": 5,
"col": 1,
"text": "YWxsb3cgPSB0cnVl"
}
},
"location": {
"file": "TEMPDIR/x.rego",
"row": 5,
"col": 1,
"text": "YWxsb3cgPSB0cnVlIGlmIHsKICBpbnB1dC5tZXRob2QgPT0gIkdFVCIKICBpbnB1dC5wYXRoID0gWyJnZXRVc2VyIiwgdXNlcl0KICBpbnB1dC51c2VyID09IHVzZXIKfQ=="
}
}
]
}
`, "TEMPDIR", tempDirPath)
gotLines := strings.Split(string(stdout), "\n")
wantLines := strings.Split(expectedOutput, "\n")
min := len(gotLines)
if len(wantLines) < min {
min = len(wantLines)
}
for i := range min {
if gotLines[i] != wantLines[i] {
t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
}
}
if len(gotLines) != len(wantLines) {
t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
}
}
func TestParseJSONOutputComments(t *testing.T) {
files := map[string]string{
"x.rego": `package x
# comment
p = 1
`,
}
errc, stdout, stderr, _ := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}),
jsonInclude: "comments",
})
if errc != 0 {
t.Fatalf("Expected exit code 0, got %v", errc)
}
if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
expectedCommentTextValue := "IGNvbW1lbnQ="
if !strings.Contains(string(stdout), expectedCommentTextValue) {
t.Fatalf("Comment text value %q missing in output: %s", expectedCommentTextValue, string(stdout))
}
}
func TestParse_DefaultRegoVersion(t *testing.T) {
tests := []struct {
note string
module string
expErrs []string
}{
{
note: "v0 module",
module: `package test
a[x] {
x := 42
}`,
expErrs: []string{
"`if` keyword is required before rule body",
"`contains` keyword is required for partial set rules",
},
},
{
note: "v1 module",
module: `package test
a contains x if {
x := 42
}`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"test.rego": tc.module,
}
_, _, stderr, _ := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatPretty, []string{parseFormatPretty, parseFormatJSON}),
})
if len(tc.expErrs) > 0 {
errs := string(stderr)
for _, expErr := range tc.expErrs {
if !strings.Contains(errs, expErr) {
t.Fatalf("Expected error:\n\n%q\n\ngot:\n\n%s", expErr, errs)
}
}
} else if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
})
}
}
func TestParseCompatibleFlags(t *testing.T) {
tests := []struct {
note string
v0Compatible bool
v1Compatible bool
policy string
expErrs []string
}{
{
note: "v0, keywords not used",
v0Compatible: true,
policy: `package test
p[v] {
v := input.x
}`,
},
{
note: "v0, keywords not imported",
v0Compatible: true,
policy: `package test
p contains v if {
v := input.x
}`,
expErrs: []string{
"var cannot be used for rule name",
},
},
{
note: "v0, keywords imported",
v0Compatible: true,
policy: `package test
import future.keywords
p contains v if {
v := input.x
}`,
},
{
note: "v0, rego.v1 imported",
v0Compatible: true,
policy: `package test
import rego.v1
p contains v if {
v := input.x
}`,
},
{
note: "v1, keywords not used",
v1Compatible: true,
policy: `package test
p[v] {
v := input.x
}`,
expErrs: []string{
"`if` keyword is required before rule body",
"`contains` keyword is required for partial set rules",
},
},
{
note: "v1, keywords not imported",
v1Compatible: true,
policy: `package test
p contains v if {
v := input.x
}`,
},
{
note: "v1, keywords imported",
v1Compatible: true,
policy: `package test
import future.keywords
p contains v if {
v := input.x
}`,
},
{
note: "v1, rego.v1 imported",
v1Compatible: true,
policy: `package test
import rego.v1
p contains v if {
v := input.x
}`,
},
// v0 takes precedence over v1
{
note: "v0+v1, keywords not used",
v0Compatible: true,
v1Compatible: true,
policy: `package test
p[v] {
v := input.x
}`,
},
{
note: "v0+v1, keywords not imported",
v0Compatible: true,
v1Compatible: true,
policy: `package test
p contains v if {
v := input.x
}`,
expErrs: []string{
"var cannot be used for rule name",
},
},
{
note: "v0+1, keywords imported",
v0Compatible: true,
v1Compatible: true,
policy: `package test
import future.keywords
p contains v if {
v := input.x
}`,
},
{
note: "v0+v1, rego.v1 imported",
v0Compatible: true,
v1Compatible: true,
policy: `package test
import rego.v1
p contains v if {
v := input.x
}`,
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
files := map[string]string{
"policy.rego": tc.policy,
}
_, _, stderr, _ := testParse(t, files, &parseParams{
format: util.NewEnumFlag(parseFormatPretty, []string{parseFormatPretty, parseFormatJSON}),
v0Compatible: tc.v0Compatible,
v1Compatible: tc.v1Compatible,
})
if len(tc.expErrs) > 0 {
errs := string(stderr)
for _, expErr := range tc.expErrs {
if !strings.Contains(errs, expErr) {
t.Fatalf("Expected error:\n\n%q\n\ngot:\n\n%s", expErr, errs)
}
}
} else if len(stderr) > 0 {
t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr))
}
})
}
}
// Runs parse and returns the exit code, stdout, and stderr contents
func testParse(t *testing.T, files map[string]string, params *parseParams) (int, []byte, []byte, string) {
t.Helper()
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
var errc int
var tempDirUsed string
test.WithTempFS(files, func(path string) {
var args []string
for file := range files {
args = append(args, filepath.Join(path, file))
}
errc = parse(args, params, stdout, stderr)
tempDirUsed = path
})
return errc, stdout.Bytes(), stderr.Bytes(), tempDirUsed
}