//go:build go1.27 // Copyright 2024 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 cmd import ( "bytes" "fmt" "io" "maps" "os" "path/filepath" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/open-policy-agent/opa/cmd/formats" "github.com/open-policy-agent/opa/internal/file/archive" "github.com/open-policy-agent/opa/v1/util/test" ) func TestDepsJSONOutputBytes(t *testing.T) { files := map[string]string{ "test.rego": `package test p if { input.x }`, } test.WithTempFS(files, func(rootPath string) { params := newDepsCommandParams() _ = params.outputFormat.Set(formats.JSON) for f := range files { _ = params.dataPaths.Set(filepath.Join(rootPath, f)) } var buf bytes.Buffer if err := deps([]string{"data.test.p"}, params, &buf); err != nil { t.Fatalf("Unexpected error: %v", err) } expectedOutput := `{ "base": [ [ { "type": "var", "value": "input" }, { "type": "string", "value": "x" } ] ], "virtual": [ [ { "type": "var", "value": "data" }, { "type": "string", "value": "test" }, { "type": "string", "value": "p" } ] ] } ` if diff := cmp.Diff(expectedOutput, buf.String()); diff != "" { t.Errorf("unexpected result (-want, +got):\n%s", diff) } }) } func TestDeps_DefaultRegoVersion(t *testing.T) { tests := []struct { note string module string query string expErrs []string }{ { note: "v0 module", module: `package test a[x] { x := 42 }`, query: `data.test.p`, 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 test a contains x if { x := 42 }`, query: `data.test.a`, }, } for _, tc := range tests { t.Run(tc.note, func(t *testing.T) { files := map[string]string{ "test.rego": tc.module, } test.WithTempFS(files, func(rootPath string) { params := newDepsCommandParams() _ = params.outputFormat.Set(formats.Pretty) for f := range files { _ = params.dataPaths.Set(filepath.Join(rootPath, f)) } err := deps([]string{tc.query}, params, io.Discard) if len(tc.expErrs) > 0 { 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:\n\n%s\n\ngot:\n\n%s", expErr, err.Error()) } } } else if err != nil { t.Fatalf("Unexpected error: %v", err) } }) }) } } func TestDepsCompatibleFlags(t *testing.T) { tests := []struct { note string v0Compatible bool v1Compatible bool module string query string expErrs []string }{ { note: "v0, no keywords", v0Compatible: true, module: `package test p[3] { input.x = 1 }`, query: `data.test.p`, }, { note: "v0, keywords not imported, but used", v0Compatible: true, module: `package test p contains 3 if { input.x = 1 }`, query: `data.test.p`, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: number cannot be used for rule name", }, }, { note: "v0, keywords imported", v0Compatible: true, module: `package test import future.keywords p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, { note: "v0, rego.v1 imported", v0Compatible: true, module: `package test import rego.v1 p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, { note: "v1, no keywords", v1Compatible: true, module: `package test p[3] { input.x = 1 }`, query: `data.test.p`, 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 keyword imports", v1Compatible: true, module: `package test p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, { note: "v1, keywords imported", v1Compatible: true, module: `package test import future.keywords p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, { note: "v1, rego.v1 imported", v1Compatible: true, module: `package test import rego.v1 p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, // v0 takes precedence over v1 { note: "v0+v1, no keywords", v0Compatible: true, v1Compatible: true, module: `package test p[3] { input.x = 1 }`, query: `data.test.p`, }, { note: "v0+v1, keywords not imported, but used", v0Compatible: true, v1Compatible: true, module: `package test p contains 3 if { input.x = 1 }`, query: `data.test.p`, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: number cannot be used for rule name", }, }, { note: "v0+v1, keywords imported", v0Compatible: true, v1Compatible: true, module: `package test import future.keywords p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, { note: "v0+v1, rego.v1 imported", v0Compatible: true, v1Compatible: true, module: `package test import rego.v1 p contains 3 if { input.x = 1 }`, query: `data.test.p`, }, } for _, tc := range tests { t.Run(tc.note, func(t *testing.T) { files := map[string]string{ "test.rego": tc.module, } test.WithTempFS(files, func(rootPath string) { params := newDepsCommandParams() params.v0Compatible = tc.v0Compatible params.v1Compatible = tc.v1Compatible _ = params.outputFormat.Set(formats.Pretty) for f := range files { _ = params.dataPaths.Set(filepath.Join(rootPath, f)) } err := deps([]string{tc.query}, params, io.Discard) if len(tc.expErrs) > 0 { 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:\n\n%s\n\ngot:\n\n%s", expErr, err.Error()) } } } else if err != nil { t.Fatalf("Unexpected error: %v", err) } }) }) } } func TestDepsV1WithBundleRegoVersion(t *testing.T) { tests := []struct { note string files map[string]string query string expErrs []string }{ { note: "v0.x bundle, no keywords", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package test p[3] { input.x = 1 }`, }, query: `data.test.p`, }, { note: "v0.x bundle, keywords not imported, but used", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package test p contains 3 if { input.x = 1 }`, }, query: `data.test.p`, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: number cannot be used for rule name", }, }, { note: "v0.x bundle, keywords imported", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package test import future.keywords p contains 3 if { input.x = 1 }`, }, query: `data.test.p`, }, { note: "v0.x bundle, rego.v1 imported", files: map[string]string{ ".manifest": `{"rego_version": 0}`, "policy.rego": `package test import rego.v1 p contains 3 if { input.x = 1 }`, }, query: `data.test.p`, }, { note: "v0 bundle, v1 per-file override", files: map[string]string{ ".manifest": `{ "rego_version": 0, "file_rego_versions": { "/policy2.rego": 1 } }`, "policy1.rego": `package test p[3] { input.x = 1 }`, "policy2.rego": `package test p contains 4 if { input.x = 1 }`, }, }, { note: "v0 bundle, v1 per-file override (glob)", files: map[string]string{ ".manifest": `{ "rego_version": 0, "file_rego_versions": { "/bar/*.rego": 1 } }`, "foo/policy1.rego": `package test p[3] { input.x = 1 }`, "bar/policy2.rego": `package test p contains 4 if { input.x = 1 }`, }, }, { note: "v0 bundle, v1 per-file override, incompliant", files: map[string]string{ ".manifest": `{ "rego_version": 0, "file_rego_versions": { "/policy2.rego": 1 } }`, "policy1.rego": `package test p[3] { input.x = 1 }`, "policy2.rego": `package test p[4] { input.x = 1 }`, }, 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", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package test p[3] { input.x = 1 }`, }, query: `data.test.p`, 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 keyword imports", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package test p contains 3 if { input.x = 1 }`, }, query: `data.test.p`, }, { note: "v1.0 bundle, keywords imported", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package test import future.keywords p contains 3 if { input.x = 1 }`, }, query: `data.test.p`, }, { note: "v1.0 bundle, rego.v1 imported", files: map[string]string{ ".manifest": `{"rego_version": 1}`, "policy.rego": `package test import rego.v1 p contains 3 if { input.x = 1 }`, }, query: `data.test.p`, }, { note: "v1 bundle, v0 per-file override", files: map[string]string{ ".manifest": `{ "rego_version": 1, "file_rego_versions": { "/policy1.rego": 0 } }`, "policy1.rego": `package test p[3] { input.x = 1 }`, "policy2.rego": `package test p contains 4 if { input.x = 1 }`, }, }, { note: "v1 bundle, v0 per-file override (glob)", files: map[string]string{ ".manifest": `{ "rego_version": 1, "file_rego_versions": { "/foo/*.rego": 0 } }`, "foo/policy1.rego": `package test p[3] { input.x = 1 }`, "bar/policy2.rego": `package test p contains 4 if { input.x = 1 }`, }, }, { note: "v1 bundle, v0 per-file override, incompliant", files: map[string]string{ ".manifest": `{ "rego_version": 1, "file_rego_versions": { "/policy1.rego": 0 } }`, "policy1.rego": `package test p contains 3 if { input.x = 1 }`, "policy2.rego": `package test p contains 4 if { input.x = 1 }`, }, expErrs: []string{ "rego_parse_error: var cannot be used for rule name", "rego_parse_error: number 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{} 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) } } params := newDepsCommandParams() if err := params.bundlePaths.Set(p); err != nil { t.Fatalf("Unexpected error: %s", err) } params.v1Compatible = v1CompatibleFlag.used _ = params.outputFormat.Set(formats.Pretty) err := deps([]string{tc.query}, params, io.Discard) if len(tc.expErrs) > 0 { 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:\n\n%s\n\ngot:\n\n%s", expErr, err.Error()) } } } else if err != nil { t.Fatalf("Unexpected error: %v", err) } }) }) } } } }