//go:build !go1.27 package cmd import ( "bytes" "context" "encoding/json" "fmt" "maps" "net/http" "os" "path/filepath" "regexp" "slices" "strings" "sync" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/open-policy-agent/opa/cmd/internal/exec" "github.com/open-policy-agent/opa/internal/file/archive" loggingtest "github.com/open-policy-agent/opa/v1/logging/test" "github.com/open-policy-agent/opa/v1/plugins" "github.com/open-policy-agent/opa/v1/sdk" sdk_test "github.com/open-policy-agent/opa/v1/sdk/test" "github.com/open-policy-agent/opa/v1/util/test" ) type execOutput struct { Result []execResultItem `json:"result"` } type execResultItem struct { DecisionID string `json:"decision_id,omitempty"` Path string `json:"path"` Error execResultItemError `json:"error"` Result *any `json:"result,omitempty"` } type execResultItemError struct { Code string `json:"code"` Message string `json:"message"` } func (r execResultItemError) isEmpty() bool { return r.Code == "" && r.Message == "" } func toAnyPtr(a any) *any { return &a } func toStringSlice(a *any) []string { switch a := (*a).(type) { case []string: return a case []any: strSlice := make([]string, len(a)) for i := range a { strSlice[i] = a[i].(string) } return strSlice } return nil } func resultSliceEquals(t *testing.T, expected, output []execResultItem) { t.Helper() if len(expected) != len(output) { t.Fatalf("Expected %d results but got %d", len(expected), len(output)) } for i := range output { if expected[i].Path != output[i].Path { t.Fatalf("Expected path %v but got %v", expected[i].Path, output[i].Path) } if expected[i].Error.isEmpty() { if !output[i].Error.isEmpty() { t.Fatalf("Expected no error but got %v", output[i].Error) } if !slices.Equal(toStringSlice(expected[i].Result), toStringSlice(output[i].Result)) { t.Fatalf("Expected result %v but got %v", expected[i].Result, output[i].Result) } if !uuidPattern.MatchString(output[i].DecisionID) { t.Fatalf("Expected decision ID to be a UUID but got %v", output[i].DecisionID) } } else { if expected[i].Error.Code != output[i].Error.Code { t.Fatalf("Expected error code %v but got %v", expected[i].Error.Code, output[i].Error.Code) } if expected[i].Error.Message != output[i].Error.Message { t.Fatalf("Expected error message %v but got %v", expected[i].Error.Message, output[i].Error.Message) } if output[i].DecisionID != "" { t.Fatalf("Expected no decision ID but got %v", output[i].DecisionID) } } } } var uuidPattern = regexp.MustCompile(`^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$`) func TestExecBasic(t *testing.T) { files := map[string]string{ "test.json": `{"foo": 7}`, "test2.yaml": `bar: 8`, "test3.yml": `baz: 9`, "ignore": `garbage`, // do not recognize this filetype } test.WithTempFS(files, func(dir string) { s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "test.rego": ` package system main contains "hello" `, })) defer s.Stop() var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.ConfigOverrides = []string{ "services.test.url=" + s.URL(), "bundles.test.resource=/bundles/bundle.tar.gz", } params.Paths = append(params.Paths, dir) err := runExec(params) if err != nil { t.Fatal(err) } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil { t.Fatal(err) } resultSliceEquals(t, []execResultItem{ { Path: "/test.json", Result: toAnyPtr([]string{"hello"}), }, { Path: "/test2.yaml", Result: toAnyPtr([]string{"hello"}), }, { Path: "/test3.yml", Result: toAnyPtr([]string{"hello"}), }, }, output.Result) }) } func TestExecDecisionOption(t *testing.T) { files := map[string]string{ "test.json": `{"foo": 7}`, } test.WithTempFS(files, func(dir string) { s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "test.rego": ` package foo main contains "hello" `, })) defer s.Stop() var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.Decision = "foo/main" params.ConfigOverrides = []string{ "services.test.url=" + s.URL(), "bundles.test.resource=/bundles/bundle.tar.gz", } params.Paths = append(params.Paths, dir) err := runExec(params) if err != nil { t.Fatal(err) } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil { t.Fatal(err) } resultSliceEquals(t, []execResultItem{ { Path: "/test.json", Result: toAnyPtr([]string{"hello"}), }, }, output.Result) }) } func TestExecBundleFlag(t *testing.T) { files := map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system main contains "hello"`, } test.WithTempFS(files, func(dir string) { var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.BundlePaths = []string{dir + "/bundle/"} params.Paths = append(params.Paths, dir+"/files/") err := runExec(params) if err != nil { t.Fatal(err) } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil { t.Fatal(err) } resultSliceEquals(t, []execResultItem{ { Path: "/files/test.json", Result: toAnyPtr([]string{"hello"}), }, }, output.Result) }) } func TestExec_DefaultRegoVersion(t *testing.T) { tests := []struct { note string module string expErrs []string }{ { note: "v0, module", module: `package system main["hello"] { input.foo == "bar" }`, expErrs: []string{ "test.rego:2: rego_parse_error: `if` keyword is required before rule body", "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", }, }, { note: "v1 module", module: `package system main contains "hello" if { input.foo == "bar" }`, }, } for _, tc := range tests { t.Run(tc.note, func(t *testing.T) { files := map[string]string{ "test.json": `{"foo": "bar"}`, } test.WithTempFS(files, func(dir string) { s := sdk_test.MustNewServer( sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{"test.rego": tc.module}), sdk_test.RawBundles(true), ) defer s.Stop() var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.ConfigOverrides = []string{ "services.test.url=" + s.URL(), "bundles.test.resource=/bundles/bundle.tar.gz", } params.Paths = append(params.Paths, dir) if len(tc.expErrs) > 0 { testLogger := loggingtest.New() params.Logger = testLogger // Wait for bundle server to be ready bundleURL := s.URL() + "/bundles/bundle.tar.gz" test.EventuallyOrFatal(t, 1*time.Second, func() bool { resp, err := http.Get(bundleURL) if resp != nil { defer resp.Body.Close() } return err == nil && resp.StatusCode == 200 }) _ = runExec(params) // Check logged errors for _, expErr := range tc.expErrs { found := false for _, e := range testLogger.Entries() { if strings.Contains(e.Message, expErr) { found = true break } } if !found { t.Errorf("Could not find expected logged error: %s in %v", expErr, testLogger.Entries()) } } } else { err := runExec(params) if err != nil { t.Fatal(err) } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil { t.Fatal(err) } resultSliceEquals(t, []execResultItem{ { Path: "/test.json", Result: toAnyPtr([]string{"hello"}), }, }, output.Result) } }) }) } } func TestExecCompatibleFlags(t *testing.T) { tests := []struct { note string v0Compatible bool v1Compatible bool module string expErrs []string }{ { note: "v0, no keywords used", v0Compatible: true, module: `package system main["hello"] { input.foo == "bar" }`, }, { note: "v0, no keywords imported", v0Compatible: true, module: `package system main contains "hello" if { input.foo == "bar" }`, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: string cannot be used for rule name", }, }, { note: "v0, keywords imported", v0Compatible: true, module: `package system import future.keywords main contains "hello" if { input.foo == "bar" }`, }, { note: "v0, rego.v1 imported", v0Compatible: true, module: `package system import rego.v1 main contains "hello" if { input.foo == "bar" }`, }, { note: "v1, no keywords used", v1Compatible: true, module: `package system main["hello"] { input.foo == "bar" }`, expErrs: []string{ "rego_parse_error: `if` keyword is required before rule body", "rego_parse_error: `contains` keyword is required for partial set rules", }, }, { note: "v1, no keywords imported", v1Compatible: true, module: `package system main contains "hello" if { input.foo == "bar" }`, }, { note: "v1, keywords imported", v1Compatible: true, module: `package system import future.keywords main contains "hello" if { input.foo == "bar" }`, }, { note: "v1, rego.v1 imported", v1Compatible: true, module: `package system import rego.v1 main contains "hello" if { input.foo == "bar" }`, }, // v0 takes precedence over v1 { note: "v0+v1, no keywords used", v0Compatible: true, v1Compatible: true, module: `package system main["hello"] { input.foo == "bar" }`, }, { note: "v0+v1, no keywords imported", v0Compatible: true, v1Compatible: true, module: `package system main contains "hello" if { input.foo == "bar" }`, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: string cannot be used for rule name", }, }, { note: "v0+v1, keywords imported", v0Compatible: true, v1Compatible: true, module: `package system import future.keywords main contains "hello" if { input.foo == "bar" }`, }, { note: "v0+v1, rego.v1 imported", v0Compatible: true, v1Compatible: true, module: `package system import rego.v1 main contains "hello" if { input.foo == "bar" }`, }, } for _, tc := range tests { t.Run(tc.note, func(t *testing.T) { files := map[string]string{ "test.json": `{"foo": "bar"}`, } test.WithTempFS(files, func(dir string) { s := sdk_test.MustNewServer( sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{"test.rego": tc.module}), sdk_test.RawBundles(true), ) defer s.Stop() var buf bytes.Buffer params := exec.NewParams(&buf) params.V0Compatible = tc.v0Compatible params.V1Compatible = tc.v1Compatible _ = params.OutputFormat.Set("json") params.ConfigOverrides = []string{ "services.test.url=" + s.URL(), "bundles.test.resource=/bundles/bundle.tar.gz", } params.Paths = append(params.Paths, dir) if len(tc.expErrs) > 0 { testLogger := loggingtest.New() params.Logger = testLogger // Wait for bundle server to be ready bundleURL := s.URL() + "/bundles/bundle.tar.gz" test.EventuallyOrFatal(t, 1*time.Second, func() bool { resp, err := http.Get(bundleURL) if resp != nil { defer resp.Body.Close() } return err == nil && resp.StatusCode == 200 }) _ = runExec(params) // Check logged errors for _, expErr := range tc.expErrs { found := false for _, e := range testLogger.Entries() { if strings.Contains(e.Message, expErr) { found = true break } } if !found { t.Errorf("Could not find expected logged error: %s in %v", expErr, testLogger.Entries()) } } } else { err := runExec(params) if err != nil { t.Fatal(err) } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil { t.Fatal(err) } resultSliceEquals(t, []execResultItem{ { Path: "/test.json", Result: toAnyPtr([]string{"hello"}), }, }, output.Result) } }) }) } } func TestExecWithBundleRegoVersion(t *testing.T) { tests := []struct { note string files map[string]string expErrs []string }{ { note: "v0.x bundle, no keywords used", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package system main["hello"] { input.foo == "bar" }`, }, }, { note: "v0.x bundle, no keywords imported", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package system main contains "hello" if { input.foo == "bar" }`, }, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: string cannot be used for rule name", }, }, { note: "v0.x bundle, keywords imported", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package system import future.keywords main contains "hello" if { input.foo == "bar" }`, }, }, { note: "v0.x bundle, rego.v1 imported", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package system import rego.v1 main contains "hello" if { input.foo == "bar" }`, }, }, { note: "v0 bundle, v1 per-file override", files: map[string]string{ ".manifest": `{ "rego_version": 0, "file_rego_versions": { "/policy2.rego": 1 } }`, "policy1.rego": `package system p[42] { input.foo == "bar" }`, "policy2.rego": `package system main contains "hello" if { 42 in p }`, }, }, { note: "v0 bundle, v1 per-file override (glob)", files: map[string]string{ ".manifest": `{ "rego_version": 0, "file_rego_versions": { "*/policy2.rego": 1 } }`, "policy1.rego": `package system p[42] { input.foo == "bar" }`, "policy2.rego": `package system main contains "hello" if { 42 in p }`, }, }, { note: "v0 bundle, v1 per-file override, incompatible", files: map[string]string{ ".manifest": `{ "rego_version": 0, "file_rego_versions": { "/policy2.rego": 1 } }`, "policy1.rego": `package system p[42] { input.foo == "bar" }`, "policy2.rego": `package system main["hello"] { p[_] == 42 }`, }, expErrs: []string{ "rego_parse_error: `if` keyword is required before rule body", "rego_parse_error: `contains` keyword is required for partial set rules", }, }, { note: "v1.0 bundle, no keywords used", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package system main["hello"] { input.foo == "bar" }`, }, expErrs: []string{ "rego_parse_error: `if` keyword is required before rule body", "rego_parse_error: `contains` keyword is required for partial set rules", }, }, { note: "v1.0 bundle, no keywords imported", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package system main contains "hello" if { input.foo == "bar" }`, }, }, { note: "v1.0 bundle, keywords imported", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package system import future.keywords main contains "hello" if { input.foo == "bar" }`, }, }, { note: "v1.0 bundle, rego.v1 imported", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package system import rego.v1 main contains "hello" if { input.foo == "bar" }`, }, }, { note: "v1 bundle, v0 per-file override", files: map[string]string{ ".manifest": `{ "rego_version": 1, "file_rego_versions": { "/policy1.rego": 0 } }`, "policy1.rego": `package system p[42] { input.foo == "bar" }`, "policy2.rego": `package system main contains "hello" if { 42 in p }`, }, }, { note: "v1 bundle, v0 per-file override (glob)", files: map[string]string{ ".manifest": `{ "rego_version": 1, "file_rego_versions": { "*/policy1.rego": 0 } }`, "policy1.rego": `package system p[42] { input.foo == "bar" }`, "policy2.rego": `package system main contains "hello" if { 42 in p }`, }, }, { note: "v1 bundle, v0 per-file override, incompatible", files: map[string]string{ ".manifest": `{ "rego_version": 1, "file_rego_versions": { "/policy1.rego": 0 } }`, "policy1.rego": `package system p contains 42 { input.foo == "bar" }`, "policy2.rego": `package system main contains "hello" if { 42 in p }`, }, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: number cannot be used for rule name", "rego_parse_error: set cannot be used for rule name", }, }, } bundleTypeCases := []struct { note string tar bool }{ { "bundle dir", false, }, { "bundle tar", true, }, } v1CompatibleFlagCases := []struct { note string used bool }{ { "no --v1-compatible", false, }, { "--v1-compatible", true, }, } for _, bundleType := range bundleTypeCases { for _, v1CompatibleFlag := range v1CompatibleFlagCases { for _, tc := range tests { t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v1CompatibleFlag.note, tc.note), func(t *testing.T) { files := map[string]string{ "files/test.json": `{"foo": "bar"}`, } if bundleType.tar { files["bundle.tar.gz"] = "" } else { maps.Copy(files, tc.files) } test.WithTempFS(files, func(root string) { p := root if bundleType.tar { p = filepath.Join(root, "bundle.tar.gz") files := make([][2]string, 0, len(tc.files)) for k, v := range tc.files { files = append(files, [2]string{k, v}) } buf := archive.MustWriteTarGz(files) bf, err := os.Create(p) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = bf.Write(buf.Bytes()) if err != nil { t.Fatalf("Unexpected error: %v", err) } } var buf bytes.Buffer params := exec.NewParams(&buf) params.Paths = append(params.Paths, root+"/files/") params.BundlePaths = []string{p} params.V1Compatible = v1CompatibleFlag.used _ = params.OutputFormat.Set("json") if len(tc.expErrs) > 0 { testLogger := loggingtest.New() params.Logger = testLogger ctx, cancel := context.WithCancel(t.Context()) defer cancel() // when bundles fail to parse, OPA never signals the ready channel, causing // runExec to hang indefinitely. WithContext allows us to cancel the context // when we have the required errors logged. var wg sync.WaitGroup wg.Go(func() { err := runExecWithContext(ctx, params) // we cancelled the context, so we expect that error if err != nil && err.Error() != "context canceled" { // make sure it isn't an expected error for _, msg := range tc.expErrs { if strings.Contains(err.Error(), msg) { return } } t.Error(err) return } }) test.EventuallyOrFatal(t, 5*time.Second, func() bool { for _, expErr := range tc.expErrs { found := false for _, e := range testLogger.Entries() { if strings.Contains(e.Message, expErr) { found = true break } } if !found { return false } } return true }) cancel() wg.Wait() } else { err := runExec(params) if err != nil { t.Fatal(err) } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(root), nil), &output); err != nil { t.Fatal(err) } resultSliceEquals(t, []execResultItem{ { Path: "/files/test.json", Result: toAnyPtr([]string{"hello"}), }, }, output.Result) } }) }) } } } } func TestInvalidConfig(t *testing.T) { var buf bytes.Buffer params := exec.NewParams(&buf) params.Fail = true params.FailDefined = true err := exec.Exec(t.Context(), nil, params) if err == nil || err.Error() != "specify --fail or --fail-defined but not both" { t.Fatalf("Expected error '%s' but got '%s'", "specify --fail or --fail-defined but not both", err.Error()) } } func TestInvalidConfigAllThree(t *testing.T) { var buf bytes.Buffer params := exec.NewParams(&buf) params.Fail = true params.FailDefined = true params.FailNonEmpty = true err := exec.Exec(t.Context(), nil, params) if err == nil || err.Error() != "specify --fail or --fail-defined but not both" { t.Fatalf("Expected error '%s' but got '%s'", "specify --fail or --fail-defined but not both", err.Error()) } } func TestInvalidConfigNonEmptyAndFail(t *testing.T) { var buf bytes.Buffer params := exec.NewParams(&buf) params.FailNonEmpty = true params.Fail = true err := exec.Exec(t.Context(), nil, params) if err == nil || err.Error() != "specify --fail-non-empty or --fail but not both" { t.Fatalf("Expected error '%s' but got '%s'", "specify --fail-non-empty or --fail but not both", err.Error()) } } func TestInvalidConfigNonEmptyAndFailDefined(t *testing.T) { var buf bytes.Buffer params := exec.NewParams(&buf) params.FailNonEmpty = true params.FailDefined = true err := exec.Exec(t.Context(), nil, params) if err == nil || err.Error() != "specify --fail-non-empty or --fail-defined but not both" { t.Fatalf("Expected error '%s' but got '%s'", "specify --fail-non-empty or --fail-defined but not both", err.Error()) } } func TestFailFlagCases(t *testing.T) { tests := []struct { description string files map[string]string decision string expectError bool expected []byte fail bool failDefined bool failNonEmpty bool }{ { description: "--fail-defined with undefined result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, }, expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "error": { "code": "opa_undefined_error", "message": "/system/main decision was undefined" } }]}`), failDefined: true, }, { description: "--fail-defined with populated result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 main contains "hello"`, }, decision: "", expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": ["hello"] }]}`), failDefined: true, }, { description: "--fail-defined with true boolean result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.defined.flag import rego.v1 some_function if { input.foo == 7 } default fail_test := false fail_test if { some_function }`, }, decision: "fail/defined/flag/fail_test", expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": true }]}`), failDefined: true, }, { description: "--fail-defined with false boolean result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.defined.flag import rego.v1 default fail_test := false fail_test if { false }`, }, decision: "fail/defined/flag/fail_test", expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": false }]}`), failDefined: true, }, { description: "--fail with undefined result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, }, expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "error": { "code": "opa_undefined_error", "message": "/system/main decision was undefined" } }]}`), fail: true, }, { description: "--fail with populated result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 main contains "hello"`, }, expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": ["hello"] }]}`), fail: true, }, { description: "--fail with true boolean result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.defined.flag import rego.v1 some_function if { input.foo == 7 } default fail_test := false fail_test if { some_function }`, }, decision: "fail/defined/flag/fail_test", expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": true }]}`), fail: true, }, { description: "--fail with false boolean result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.defined.flag import rego.v1 default fail_test := false fail_test if { false }`, }, decision: "fail/defined/flag/fail_test", expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": false }]}`), fail: true, }, { description: "--fail-non-empty with undefined result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, }, expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "error": { "code": "opa_undefined_error", "message": "/system/main decision was undefined" } }]}`), failNonEmpty: true, }, { description: "--fail-non-empty with populated result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 main contains "hello"`, }, decision: "", expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": ["hello"] }]}`), failNonEmpty: true, }, { description: "--fail-non-empty with true boolean result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.non.empty.flag import rego.v1 some_function if { input.foo == 7 } default fail_test := false fail_test if { some_function }`, }, decision: "fail/non/empty/flag/fail_test", expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": true }]}`), failNonEmpty: true, }, { description: "--fail-non-empty with false boolean result", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.non.empty.flag import rego.v1 default fail_test := false fail_test if { false }`, }, decision: "fail/non/empty/flag/fail_test", expectError: true, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": false }]}`), failNonEmpty: true, }, { description: "--fail-non-empty with an empty array", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.non.empty.flag import rego.v1 default fail_test := ["something", "hello"] fail_test := [] if { input.foo == 7 }`, }, decision: "fail/non/empty/flag/fail_test", expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": [] }]}`), failNonEmpty: true, }, { description: "--fail-non-empty for an empty set coming from a partial rule", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package fail.non.empty.flag import rego.v1 fail_test contains message if { false message := "not gonna happen" }`, }, decision: "fail/non/empty/flag/fail_test", expectError: false, expected: []byte(`{"result": [{ "path": "/files/test.json", "result": [] }]}`), failNonEmpty: true, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { test.WithTempFS(tt.files, func(dir string) { var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.BundlePaths = []string{dir + "/bundle/"} params.Paths = append(params.Paths, dir+"/files/") if tt.decision != "" { params.Decision = tt.decision } params.FailDefined = tt.failDefined params.Fail = tt.fail params.FailNonEmpty = tt.failNonEmpty err := runExec(params) if err != nil && !tt.expectError { t.Fatal("unexpected error in test") } if err == nil && tt.expectError { t.Fatal("expected error, but none occurred in test") } var output execOutput if err := json.Unmarshal(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil), &output); err != nil { t.Fatal(err) } var expected execOutput if err := json.Unmarshal(tt.expected, &expected); err != nil { t.Fatal(err) } resultSliceEquals(t, expected.Result, output.Result) }) }) } } func TestExecJSONOutputBytes(t *testing.T) { files := map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, } test.WithTempFS(files, func(dir string) { var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.BundlePaths = []string{dir + "/bundle/"} params.Paths = append(params.Paths, dir+"/files/") params.FailDefined = true if err := runExec(params); err != nil { t.Fatal("unexpected error in test:", err) } actual := bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil) expected := `{ "result": [ { "path": "/files/test.json", "error": { "code": "opa_undefined_error", "message": "/system/main decision was undefined" } } ] } ` if diff := cmp.Diff(expected, string(actual)); diff != "" { t.Errorf("unexpected result (-want, +got):\n%s", diff) } }) } func TestExecWithInvalidInputOptions(t *testing.T) { tests := []struct { description string files map[string]string stdIn bool input string expectError bool expected string }{ { description: "path passed in as arg should not raise error", files: map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, }, expectError: false, expected: "", }, { description: "no paths passed in as args should raise error if --stdin-input flag not set", files: map[string]string{ "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, }, expectError: true, expected: "requires at least 1 path arg, or the --stdin-input flag", }, { description: "should not raise error if --stdin-input flag is set when no paths passed in as args", files: map[string]string{ "bundle/x.rego": `package system import rego.v1 test_fun := x if { x = false x } undefined_test if { test_fun }`, }, stdIn: true, input: `{"foo": 7}`, expectError: false, expected: "", }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { test.WithTempFS(tt.files, func(dir string) { var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.BundlePaths = []string{dir + "/bundle/"} if tt.stdIn { params.StdIn = true tempFile, err := os.CreateTemp(t.TempDir(), "test") if err != nil { t.Fatalf("unexpected error creating temp file: %q", err.Error()) } if _, err := tempFile.WriteString(tt.input); err != nil { t.Fatalf("unexpeced error when writing to temp file: %q", err.Error()) } if _, err := tempFile.Seek(0, 0); err != nil { t.Fatalf("unexpected error when rewinding temp file: %q", err.Error()) } oldStdin := os.Stdin defer func() { os.Stdin = oldStdin os.Remove(tempFile.Name()) }() os.Stdin = tempFile } else { if _, ok := tt.files["files/test.json"]; ok { params.Paths = append(params.Paths, dir+"/files/") } } err := runExec(params) if err != nil && !tt.expectError { t.Fatalf("unexpected error in test: %q", err.Error()) } if err == nil && tt.expectError { t.Fatalf("expected error %q, but none occurred in test", tt.expected) } if err != nil && err.Error() != tt.expected { t.Fatalf("expected error %q, but got %q", tt.expected, err.Error()) } }) }) } } func TestExecMalformedRemoteBundle(t *testing.T) { bundlePath := "/bundles/bundle.tar.gz" // Note(philipc): We add the "raw bundles" flag so that we can stuff a // malformed bundle into the mock bundle server. Otherwise, the server // will just return 503 errors forever, because it won't be able to // build the bundle on its end. s := sdk_test.MustNewServer( sdk_test.RawBundles(true), sdk_test.MockBundle(bundlePath, map[string]string{ "example.rego": ` package example p := bits.sand(42, 43) # typo of bits.and `, })) defer s.Stop() // Wait for the bundle server to be ready before running exec bundleURL := s.URL() + bundlePath test.EventuallyOrFatal(t, 1*time.Second, func() bool { resp, err := http.Get(bundleURL) if resp != nil { defer resp.Body.Close() } return err == nil && resp.StatusCode == 200 }) var buf bytes.Buffer params := exec.NewParams(&buf) _ = params.OutputFormat.Set("json") params.ConfigOverrides = []string{ "services.test.url=" + s.URL(), "bundles.test.resource=" + bundlePath, } // Note(philipc): We can set this timeout almost arbitrarily high or // low-- the test will time out before it ever succeeds, due to the // faulty bundle. params.Timeout = time.Millisecond * 50 params.Paths = append(params.Paths, t.TempDir()) err := runExec(params) if err == nil { t.Fatalf("Expected error, got nil instead.") } exp := "runtime error: Bundle name: test, Code: bundle_error, HTTPCode: -1, Message: 1 error occurred: /example.rego:4: rego_type_error: undefined function bits.sand" if !strings.HasPrefix(err.Error(), exp) { t.Fatalf("Expected error: %s, got %s", exp, err.Error()) } } func TestExecStopsPlugins(t *testing.T) { fact := &factory{} sdk.SetDefaultOptions(sdk.Options{ Plugins: map[string]plugins.Factory{ "test_plugin": fact, }, }) s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "test.rego": ` package system main contains "hello" `, })) defer s.Stop() cfg := filepath.Join(t.TempDir(), "opa.yaml") if err := os.WriteFile(cfg, []byte(` plugins: test_plugin: {} `), 0x777); err != nil { t.Fatal(err) } var buf bytes.Buffer params := exec.NewParams(&buf) params.ConfigFile = cfg _ = params.OutputFormat.Set("json") params.ConfigOverrides = []string{ "services.test.url=" + s.URL(), "bundles.test.resource=/bundles/bundle.tar.gz", } dir := t.TempDir() params.Paths = append(params.Paths, dir) err := runExec(params) if err != nil { t.Fatal(err) } if !fact.stopped { t.Errorf("expected plugin to be stopped") } } type factory struct { stopped bool m *plugins.Manager } func (f *factory) New(m *plugins.Manager, _ any) plugins.Plugin { f.m = m return f } func (*factory) Validate(*plugins.Manager, []byte) (any, error) { return nil, nil } func (f *factory) Start(context.Context) error { f.m.UpdatePluginStatus("test_plugin", &plugins.Status{State: plugins.StateOK}) return nil } func (f *factory) Stop(context.Context) { f.stopped = true } func (*factory) Reconfigure(context.Context, any) { }