mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-21 10:47:34 -05:00
Other parts of the language allow deciding the sensitivity of a value based on results that won't be known until the apply phase, which means that in practice we cannot predict the final sensitivity of an unknown value. Previously this function assumed that an unknown value would always be a placeholder for a final value of the same sensitivity, which is not a valid assumption in practice and so using the results of this function could cause downstream value consistency checks to fail. This does unfortunately create a situation where a new version of OpenTofu will return an unknown value in a situation that was previously always known, which could therefore begin causing a plan-time error if the result is then used to populate something that is required to be known at plan time. However, the previous behavior caused OpenTofu to produce confusing errors (blaming a provider for OpenTofu's mistake) during the apply phase, and so the potential new plan-time errors are arguably better than the previous behavior. Any unknown result is refined as definitely not null to shrink the potential impact: other parts of the language will still be able to assume that the result of this function is not null even if it's not yet known. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
241 lines
5.0 KiB
Go
241 lines
5.0 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package funcs
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestSensitive(t *testing.T) {
|
|
tests := []struct {
|
|
Input cty.Value
|
|
WantErr string
|
|
}{
|
|
{
|
|
cty.NumberIntVal(1),
|
|
``,
|
|
},
|
|
{
|
|
// Unknown values stay unknown while becoming sensitive
|
|
cty.UnknownVal(cty.String),
|
|
``,
|
|
},
|
|
{
|
|
// Null values stay unknown while becoming sensitive
|
|
cty.NullVal(cty.String),
|
|
``,
|
|
},
|
|
{
|
|
// DynamicVal can be marked as sensitive
|
|
cty.DynamicVal,
|
|
``,
|
|
},
|
|
{
|
|
// The marking is shallow only
|
|
cty.ListVal([]cty.Value{cty.NumberIntVal(1)}),
|
|
``,
|
|
},
|
|
{
|
|
// A value already marked is allowed and stays marked
|
|
cty.NumberIntVal(1).Mark(marks.Sensitive),
|
|
``,
|
|
},
|
|
{
|
|
// Any non-sensitive marks must be propagated alongside
|
|
// with a sensitive one.
|
|
cty.NumberIntVal(1).Mark("bloop"),
|
|
``,
|
|
},
|
|
{
|
|
// A value deep already marked is allowed and stays marked,
|
|
// _and_ we'll also mark the outer collection as sensitive.
|
|
cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}),
|
|
``,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("sensitive(%#v)", test.Input), func(t *testing.T) {
|
|
got, err := Sensitive(test.Input)
|
|
|
|
if test.WantErr != "" {
|
|
if err == nil {
|
|
t.Fatal("succeeded; want error")
|
|
}
|
|
if got, want := err.Error(), test.WantErr; got != want {
|
|
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
if !got.HasMark(marks.Sensitive) {
|
|
t.Errorf("result is not marked sensitive")
|
|
}
|
|
|
|
inputMarks := test.Input.Marks()
|
|
delete(inputMarks, marks.Sensitive)
|
|
|
|
gotRaw, gotMarks := got.Unmark()
|
|
if len(gotMarks) != len(inputMarks)+1 {
|
|
t.Errorf("extraneous marks %#v", gotMarks)
|
|
}
|
|
|
|
// Disregarding shallow marks, the result should have the same
|
|
// effective value as the input.
|
|
wantRaw, _ := test.Input.Unmark()
|
|
if !gotRaw.RawEquals(wantRaw) {
|
|
t.Errorf("wrong unmarked result\ngot: %#v\nwant: %#v", got, wantRaw)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNonsensitive(t *testing.T) {
|
|
tests := []struct {
|
|
Input cty.Value
|
|
WantErr string
|
|
}{
|
|
{
|
|
cty.NumberIntVal(1).Mark(marks.Sensitive),
|
|
``,
|
|
},
|
|
{
|
|
cty.DynamicVal.Mark(marks.Sensitive),
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Mark(marks.Sensitive),
|
|
``,
|
|
},
|
|
{
|
|
cty.NullVal(cty.EmptyObject).Mark(marks.Sensitive),
|
|
``,
|
|
},
|
|
{
|
|
// The inner sensitive remains afterwards
|
|
cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}).Mark(marks.Sensitive),
|
|
``,
|
|
},
|
|
|
|
// Passing a value that is already non-sensitive is not an error,
|
|
// as this function may be used with specific to ensure that all
|
|
// values are indeed non-sensitive
|
|
{
|
|
cty.NumberIntVal(1),
|
|
``,
|
|
},
|
|
{
|
|
cty.NullVal(cty.String),
|
|
``,
|
|
},
|
|
|
|
// Unknown values may become sensitive once they are known, so we
|
|
// permit them to be marked nonsensitive.
|
|
{
|
|
cty.DynamicVal,
|
|
``,
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String),
|
|
``,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("nonsensitive(%#v)", test.Input), func(t *testing.T) {
|
|
got, err := Nonsensitive(test.Input)
|
|
|
|
if test.WantErr != "" {
|
|
if err == nil {
|
|
t.Fatal("succeeded; want error")
|
|
}
|
|
if got, want := err.Error(), test.WantErr; got != want {
|
|
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
return
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
if got.HasMark(marks.Sensitive) {
|
|
t.Errorf("result is still marked sensitive")
|
|
}
|
|
wantRaw, _ := test.Input.Unmark()
|
|
if !got.RawEquals(wantRaw) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Input)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSensitive(t *testing.T) {
|
|
tests := []struct {
|
|
Input cty.Value
|
|
IsSensitive cty.Value
|
|
}{
|
|
{
|
|
cty.NumberIntVal(1).Mark(marks.Sensitive),
|
|
cty.True,
|
|
},
|
|
{
|
|
cty.NumberIntVal(1),
|
|
cty.False,
|
|
},
|
|
{
|
|
cty.DynamicVal.Mark(marks.Sensitive),
|
|
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
|
},
|
|
{
|
|
cty.DynamicVal,
|
|
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String).Mark(marks.Sensitive),
|
|
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
|
},
|
|
{
|
|
cty.UnknownVal(cty.String),
|
|
cty.UnknownVal(cty.Bool).RefineNotNull(),
|
|
},
|
|
{
|
|
cty.NullVal(cty.EmptyObject).Mark(marks.Sensitive),
|
|
cty.True,
|
|
},
|
|
{
|
|
cty.NullVal(cty.EmptyObject),
|
|
cty.False,
|
|
},
|
|
{
|
|
cty.NullVal(cty.DynamicPseudoType).Mark(marks.Sensitive),
|
|
cty.True,
|
|
},
|
|
{
|
|
cty.NullVal(cty.DynamicPseudoType),
|
|
cty.False,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("issensitive(%#v)", test.Input), func(t *testing.T) {
|
|
got, err := IsSensitive(test.Input)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %s", err)
|
|
}
|
|
|
|
if !got.RawEquals(test.IsSensitive) {
|
|
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.IsSensitive)
|
|
}
|
|
})
|
|
}
|
|
}
|