blob: 55b31410b6855153d43da292240b81eb280e32ee [file]
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package policy
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"google.golang.org/grpc"
gproto "google.golang.org/protobuf/proto"
"github.com/hashicorp/terraform/internal/policy/callback"
"github.com/hashicorp/terraform/internal/policy/proto"
)
type stubPolicyClient struct {
proto.PolicyClient
setupFn func(*proto.PolicySetupRequest) (*proto.PolicySetupResponse, error)
evaluateResourceFn func(*proto.PolicyEvaluateResourceRequest) (*proto.PolicyEvaluateResourceResponse, error)
evaluateProviderFn func(*proto.PolicyEvaluateProviderRequest) (*proto.PolicyEvaluateProviderResponse, error)
evaluateModuleFn func(*proto.PolicyEvaluateModuleRequest) (*proto.PolicyEvaluateModuleResponse, error)
}
func (s *stubPolicyClient) Setup(ctx context.Context, req *proto.PolicySetupRequest, _ ...grpc.CallOption) (*proto.PolicySetupResponse, error) {
return s.setupFn(req)
}
func (s *stubPolicyClient) EvaluateResource(ctx context.Context, req *proto.PolicyEvaluateResourceRequest, _ ...grpc.CallOption) (*proto.PolicyEvaluateResourceResponse, error) {
return s.evaluateResourceFn(req)
}
func (s *stubPolicyClient) EvaluateProvider(ctx context.Context, req *proto.PolicyEvaluateProviderRequest, _ ...grpc.CallOption) (*proto.PolicyEvaluateProviderResponse, error) {
return s.evaluateProviderFn(req)
}
func (s *stubPolicyClient) EvaluateModule(ctx context.Context, req *proto.PolicyEvaluateModuleRequest, _ ...grpc.CallOption) (*proto.PolicyEvaluateModuleResponse, error) {
return s.evaluateModuleFn(req)
}
func TestClientEvaluate(t *testing.T) {
ctx := t.Context()
tests := []struct {
name string
attrs PolicyValue
priorAttrs PolicyValue
// an optional function to override the default evaluateResourceFn
evaluateResourceFn func(*proto.PolicyEvaluateResourceRequest) (*proto.PolicyEvaluateResourceResponse, error)
// assertResponse is a helper function for each case to further assert the response of an evaluation
assertResponse func(*testing.T, *callback.MockRegistry, *proto.PolicyEvaluateResourceRequest, EvaluationResponse)
}{
{
name: "nil attrs and prior attrs",
attrs: PolicyValue{Raw: cty.NilVal},
priorAttrs: PolicyValue{Raw: cty.NilVal},
assertResponse: func(t *testing.T, registry *callback.MockRegistry, req *proto.PolicyEvaluateResourceRequest, resp EvaluationResponse) {
t.Helper()
if resp.Overall != AllowResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, AllowResult)
}
if len(resp.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
if req == nil {
t.Fatal("expected request, got nil")
}
},
},
{
name: "non-nil attrs and prior attrs",
attrs: PolicyValue{
Raw: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("test")}),
RedactedPaths: []cty.Path{cty.GetAttrPath("secret")},
},
priorAttrs: PolicyValue{
Raw: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("prior")}),
},
assertResponse: func(t *testing.T, registry *callback.MockRegistry, req *proto.PolicyEvaluateResourceRequest, resp EvaluationResponse) {
t.Helper()
if resp.Overall != AllowResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, AllowResult)
}
if len(resp.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
want := &proto.AttributePath{Steps: []*proto.AttributePath_Step{{
Selector: &proto.AttributePath_Step_AttributeName{AttributeName: "secret"},
}}}
if len(req.Attrs.RedactedPaths) != 1 || !gproto.Equal(req.Attrs.RedactedPaths[0], want) {
t.Fatalf("unexpected redacted paths: %#v", req.Attrs.RedactedPaths)
}
},
},
{
name: "transforms diagnostics from response",
attrs: PolicyValue{Raw: cty.NilVal},
priorAttrs: PolicyValue{Raw: cty.NilVal},
evaluateResourceFn: func(req *proto.PolicyEvaluateResourceRequest) (*proto.PolicyEvaluateResourceResponse, error) {
return &proto.PolicyEvaluateResourceResponse{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
PolicyDetails: []*proto.PolicyEvaluationDetail{{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
Diagnostics: []*proto.Diagnostic{{
Severity: proto.Severity_WARNING,
Summary: "policy warning",
Detail: "transformed warning detail",
Result: &proto.DiagnosticResult{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
},
}},
}},
}, nil
},
assertResponse: func(t *testing.T, registry *callback.MockRegistry, req *proto.PolicyEvaluateResourceRequest, resp EvaluationResponse) {
t.Helper()
if resp.Overall != DenyResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, DenyResult)
}
if len(resp.Diagnostics) != 1 {
t.Fatalf("unexpected diagnostics count: got %d, want 1", len(resp.Diagnostics))
}
diag := resp.Diagnostics[0]
if diag.Severity() != tfdiags.Warning {
t.Fatalf("unexpected diagnostic severity: got %s, want %s", diag.Severity(), tfdiags.Warning)
}
desc := diag.Description()
if desc.Summary != "policy warning" {
t.Fatalf("unexpected diagnostic summary: got %q, want %q", desc.Summary, "policy warning")
}
if desc.Detail != "transformed warning detail" {
t.Fatalf("unexpected diagnostic detail: got %q, want %q", desc.Detail, "transformed warning detail")
}
extra := tfdiags.ExtraInfo[*PolicyExtra](diag)
expectedExtra := &PolicyExtra{
Severity: hcl.DiagWarning,
Result: DenyResult,
Policy: Policy{
Result: DenyResult,
Range: &hcl.Range{},
},
}
if diff := cmp.Diff(extra, expectedExtra); diff != "" {
t.Fatalf("unexpected diagnostic extra: %s", diff)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotReq *proto.PolicyEvaluateResourceRequest
registry := &callback.MockRegistry{NextIDValue: 23}
c := &client{
client: &stubPolicyClient{
evaluateResourceFn: func(req *proto.PolicyEvaluateResourceRequest) (*proto.PolicyEvaluateResourceResponse, error) {
gotReq = req
// assert that the evaluation id is registered with the callback registry
_, ok := registry.FunctionsStore[req.EvaluationId]
if !ok {
t.Fatalf("expected evaluation id %d to be registered", req.EvaluationId)
}
if test.evaluateResourceFn != nil {
return test.evaluateResourceFn(req)
}
return &proto.PolicyEvaluateResourceResponse{
Result: proto.EvaluateResult_ALLOW_EVALUATE_RESULT,
}, nil
},
},
callbackRegistry: registry,
}
resp := c.EvaluateResource(ctx, EvaluationRequest[*proto.PolicyEvaluateResourceRequest_ResourceMetadata]{
Target: "test_resource",
Attrs: test.attrs,
PriorAttrs: test.priorAttrs,
})
test.assertResponse(t, registry, gotReq, resp)
if gotReq == nil {
t.Fatal("expected EvaluateResource RPC to be called")
}
if gotReq.EvaluationId == 0 {
t.Fatal("expected non-zero evaluation id")
}
// assert the registry functions that should have been called
if !registry.NextIDCalled {
t.Fatal("expected callback registry NextID to be called")
}
if !registry.RegisterCalled {
t.Fatal("expected callback registry Register to be called")
}
if !registry.UnregisterCalled {
t.Fatal("expected callback registry Unregister to be called")
}
// after the evaluation, the callback registry should have been cleaned up
_, ok := registry.FunctionsStore[gotReq.EvaluationId]
if ok {
t.Fatalf("expected evaluation id %d to be unregistered", gotReq.EvaluationId)
}
})
}
}
func TestClientEvaluateProvider(t *testing.T) {
ctx := t.Context()
tests := []struct {
name string
attrs PolicyValue
evaluateProviderFn func(*proto.PolicyEvaluateProviderRequest) (*proto.PolicyEvaluateProviderResponse, error)
assertResponse func(*testing.T, EvaluationResponse)
}{
{
name: "nil attrs",
attrs: PolicyValue{Raw: cty.NilVal},
assertResponse: func(t *testing.T, resp EvaluationResponse) {
t.Helper()
if resp.Overall != AllowResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, AllowResult)
}
if len(resp.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
},
},
{
name: "unknown attrs",
attrs: PolicyValue{Raw: cty.UnknownVal(cty.EmptyObject)},
assertResponse: func(t *testing.T, resp EvaluationResponse) {
t.Helper()
if resp.Overall != AllowResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, AllowResult)
}
if len(resp.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
},
},
{
name: "non-nil attrs",
attrs: PolicyValue{Raw: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("test")})},
assertResponse: func(t *testing.T, resp EvaluationResponse) {
t.Helper()
if resp.Overall != AllowResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, AllowResult)
}
if len(resp.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
},
},
{
name: "transforms diagnostics from response",
attrs: PolicyValue{Raw: cty.NilVal},
evaluateProviderFn: func(req *proto.PolicyEvaluateProviderRequest) (*proto.PolicyEvaluateProviderResponse, error) {
return &proto.PolicyEvaluateProviderResponse{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
PolicyDetails: []*proto.PolicyEvaluationDetail{{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
Diagnostics: []*proto.Diagnostic{{
Severity: proto.Severity_WARNING,
Summary: "policy warning",
Detail: "transformed warning detail",
Result: &proto.DiagnosticResult{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
},
}},
}},
}, nil
},
assertResponse: func(t *testing.T, resp EvaluationResponse) {
t.Helper()
if resp.Overall != DenyResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, DenyResult)
}
if len(resp.Diagnostics) != 1 {
t.Fatalf("unexpected diagnostics count: got %d, want 1", len(resp.Diagnostics))
}
diag := resp.Diagnostics[0]
if diag.Severity() != tfdiags.Warning {
t.Fatalf("unexpected diagnostic severity: got %s, want %s", diag.Severity(), tfdiags.Warning)
}
desc := diag.Description()
if desc.Summary != "policy warning" {
t.Fatalf("unexpected diagnostic summary: got %q, want %q", desc.Summary, "policy warning")
}
if desc.Detail != "transformed warning detail" {
t.Fatalf("unexpected diagnostic detail: got %q, want %q", desc.Detail, "transformed warning detail")
}
extra := tfdiags.ExtraInfo[*PolicyExtra](diag)
expectedExtra := &PolicyExtra{
Severity: hcl.DiagWarning,
Result: DenyResult,
Policy: Policy{
Result: DenyResult,
Range: &hcl.Range{},
},
}
if diff := cmp.Diff(extra, expectedExtra); diff != "" {
t.Fatalf("unexpected diagnostic extra: %s", diff)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotReq *proto.PolicyEvaluateProviderRequest
c := &client{
client: &stubPolicyClient{
evaluateProviderFn: func(req *proto.PolicyEvaluateProviderRequest) (*proto.PolicyEvaluateProviderResponse, error) {
gotReq = req
if test.evaluateProviderFn != nil {
return test.evaluateProviderFn(req)
}
return &proto.PolicyEvaluateProviderResponse{
Result: proto.EvaluateResult_ALLOW_EVALUATE_RESULT,
}, nil
},
},
callbackRegistry: callback.NewRegistry(),
}
resp := c.EvaluateProvider(ctx, EvaluationRequest[*proto.PolicyEvaluateProviderRequest_ProviderMetadata]{
Target: "test_provider",
Attrs: test.attrs,
})
test.assertResponse(t, resp)
if gotReq == nil {
t.Fatal("expected EvaluateProvider RPC to be called")
}
if gotReq.ProviderType != "test_provider" {
t.Fatalf("unexpected provider type: got %q, want %q", gotReq.ProviderType, "test_provider")
}
})
}
}
func TestClientEvaluateModule(t *testing.T) {
ctx := t.Context()
tests := []struct {
name string
evaluateModuleFn func(*proto.PolicyEvaluateModuleRequest) (*proto.PolicyEvaluateModuleResponse, error)
assertResponse func(*testing.T, EvaluationResponse)
}{
{
name: "allow response",
assertResponse: func(t *testing.T, resp EvaluationResponse) {
t.Helper()
if resp.Overall != AllowResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, AllowResult)
}
if len(resp.Diagnostics) != 0 {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
},
},
{
name: "transforms diagnostics from response",
evaluateModuleFn: func(req *proto.PolicyEvaluateModuleRequest) (*proto.PolicyEvaluateModuleResponse, error) {
return &proto.PolicyEvaluateModuleResponse{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
PolicyDetails: []*proto.PolicyEvaluationDetail{{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
Diagnostics: []*proto.Diagnostic{{
Severity: proto.Severity_WARNING,
Summary: "policy warning",
Detail: "transformed warning detail",
Result: &proto.DiagnosticResult{
Result: proto.EvaluateResult_DENY_EVALUATE_RESULT,
},
}},
}},
}, nil
},
assertResponse: func(t *testing.T, resp EvaluationResponse) {
t.Helper()
if resp.Overall != DenyResult {
t.Fatalf("unexpected result: got %s, want %s", resp.Overall, DenyResult)
}
if len(resp.Diagnostics) != 1 {
t.Fatalf("unexpected diagnostics count: got %d, want 1", len(resp.Diagnostics))
}
diag := resp.Diagnostics[0]
if diag.Severity() != tfdiags.Warning {
t.Fatalf("unexpected diagnostic severity: got %s, want %s", diag.Severity(), tfdiags.Warning)
}
desc := diag.Description()
if desc.Summary != "policy warning" {
t.Fatalf("unexpected diagnostic summary: got %q, want %q", desc.Summary, "policy warning")
}
if desc.Detail != "transformed warning detail" {
t.Fatalf("unexpected diagnostic detail: got %q, want %q", desc.Detail, "transformed warning detail")
}
extra := tfdiags.ExtraInfo[*PolicyExtra](diag)
expectedExtra := &PolicyExtra{
Severity: hcl.DiagWarning,
Result: DenyResult,
Policy: Policy{
Result: DenyResult,
Range: &hcl.Range{},
},
}
if diff := cmp.Diff(extra, expectedExtra); diff != "" {
t.Fatalf("unexpected diagnostic extra: %s", diff)
}
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var gotReq *proto.PolicyEvaluateModuleRequest
c := &client{
client: &stubPolicyClient{
evaluateModuleFn: func(req *proto.PolicyEvaluateModuleRequest) (*proto.PolicyEvaluateModuleResponse, error) {
gotReq = req
if test.evaluateModuleFn != nil {
return test.evaluateModuleFn(req)
}
return &proto.PolicyEvaluateModuleResponse{
Result: proto.EvaluateResult_ALLOW_EVALUATE_RESULT,
}, nil
},
},
callbackRegistry: callback.NewRegistry(),
}
resp := c.EvaluateModule(ctx, EvaluationRequest[*proto.PolicyEvaluateModuleRequest_ModuleMetadata]{
Target: "./child",
})
test.assertResponse(t, resp)
if gotReq == nil {
t.Fatal("expected EvaluateModule RPC to be called")
}
if gotReq.ModuleSource != "./child" {
t.Fatalf("unexpected module source: got %q, want %q", gotReq.ModuleSource, "./child")
}
})
}
}
func TestClientSetupEntitlement(t *testing.T) {
ctx := t.Context()
tests := []struct {
name string
entitlement *Entitlement
want *proto.PolicySetupRequest_Entitlement
}{
{
name: "nil entitlement is not serialized",
entitlement: nil,
want: nil,
},
{
name: "entitlement is mapped onto the proto request",
entitlement: &Entitlement{
Host: "app.terraform.io",
Token: "secret",
Org: "hashicorp",
},
want: &proto.PolicySetupRequest_Entitlement{
Host: "app.terraform.io",
Token: "secret",
Org: "hashicorp",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var gotReq *proto.PolicySetupRequest
c := &client{
client: &stubPolicyClient{
setupFn: func(req *proto.PolicySetupRequest) (*proto.PolicySetupResponse, error) {
gotReq = req
return &proto.PolicySetupResponse{}, nil
},
},
}
resp := c.Setup(ctx, SetupRequest{
SourceLocations: []string{"./policies"},
Entitlement: tt.entitlement,
})
if resp.Diagnostics.HasErrors() {
t.Fatalf("unexpected diagnostics: %#v", resp.Diagnostics)
}
if gotReq == nil {
t.Fatal("expected a setup request to be sent")
}
got := gotReq.Entitlement
if tt.want == nil {
if got != nil {
t.Fatalf("expected nil entitlement, got %+v", got)
}
return
}
if got == nil {
t.Fatal("expected entitlement, got nil")
}
if got.Host != tt.want.Host || got.Token != tt.want.Token || got.Org != tt.want.Org {
t.Fatalf("unexpected entitlement: got %+v, want %+v", got, tt.want)
}
})
}
}