Files
opentf/internal/lang/marks/marks_test.go
Andrei Ciobanu 013097b631 Ephemeral variables (#3108)
Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2025-09-10 07:45:23 -04:00

300 lines
8.8 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 marks
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// Integration test for the marks to see if consolidate warnings is
// checking by the key of the mark together with the address
func TestMarkConsolidateWarnings(t *testing.T) {
var diags tfdiags.Diagnostics
for i := 0; i < 2; i++ {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
// Summary is the same for both diagnostics, but key is different
Summary: "Output deprecated",
Detail: fmt.Sprintf("This one has an output %d", i),
Subject: &hcl.Range{
Filename: "foo.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 1, Byte: 0},
},
Extra: DeprecationCause{
By: addrs.OutputValue{
Name: "output",
},
Key: fmt.Sprintf("output%d", i),
Message: "output deprecate",
},
})
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Variable deprecated",
Detail: fmt.Sprintf("This one has a var %d", i),
Subject: &hcl.Range{
Filename: "foo.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 1, Byte: 0},
},
Extra: DeprecationCause{
By: addrs.InputVariable{
Name: "variable",
},
Key: fmt.Sprintf("variable%d", i),
Message: "variable deprecate",
},
})
}
// Adding an extra diagnostic with the same key to be consolidated
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Variable deprecated",
Detail: "This one has a var 1",
Subject: &hcl.Range{
Filename: "foo.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 1, Byte: 0},
},
Extra: DeprecationCause{
By: addrs.InputVariable{
Name: "variable",
},
Key: "variable1",
Message: "variable deprecate",
},
})
// Adding an extra diagnostic with a variable in a module
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Variable deprecated",
Detail: "This one has a var 1 in a module",
Subject: &hcl.Range{
Filename: "foo.tf",
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 1, Byte: 0},
},
Extra: DeprecationCause{
By: addrs.InputVariable{
Name: "mod1.variable",
},
Key: "mod1.variable1",
Message: "variable deprecate",
},
})
consolidatedDiags := diags.Consolidate(1, tfdiags.Warning)
expectedDescriptions := [][2]string{
{"Output deprecated", "This one has an output 0"},
{"Variable deprecated", "This one has a var 0"},
{"Output deprecated", "This one has an output 1"},
{"Variable deprecated", "This one has a var 1\n\n(and one more similar warning elsewhere)"},
{"Variable deprecated", "This one has a var 1 in a module"},
}
// We created 5 diagnostics, but the last one is consolidated
expectedLen := len(expectedDescriptions)
if len(consolidatedDiags) != expectedLen {
t.Errorf("len %d is expected, got %d", expectedLen, len(consolidatedDiags))
}
for i, vals := range expectedDescriptions {
if diff := cmp.Diff(vals[0], consolidatedDiags[i].Description().Summary); diff != "" {
t.Errorf("%d: wrong summary: %s", i, diff)
}
if diff := cmp.Diff(vals[1], consolidatedDiags[i].Description().Detail); diff != "" {
t.Errorf("%d: wrong detail msg: %s", i, diff)
}
}
}
func TestHasDeprecated(t *testing.T) {
tests := []struct {
name string
input cty.Value
want bool
}{
{
name: "no marks",
input: cty.StringVal("test"),
want: false,
},
{
name: "only sensitive mark",
input: cty.StringVal("test").Mark(Sensitive),
want: false,
},
{
name: "has deprecation mark",
input: Deprecated(cty.StringVal("test"), DeprecationCause{
By: addrs.InputVariable{Name: "var1"},
Key: "var1",
Message: "deprecated",
}),
want: true,
},
{
name: "mixed marks with deprecation",
input: Deprecated(cty.StringVal("test").Mark(Sensitive), DeprecationCause{
By: addrs.InputVariable{Name: "var1"},
Key: "var1",
Message: "deprecated",
}),
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := HasDeprecated(tt.input)
if got != tt.want {
t.Errorf("HasDeprecated() = %v, want %v", got, tt.want)
}
})
}
}
func TestExtractDeprecatedDiagnosticsWithExpr(t *testing.T) {
// This is a small unit test focused just on how we detect a deprecation
// mark and translate it into a diagnostic message. The main tests for
// the overall dynamic deprecation handling are in
// [tofu.TestContext2Apply_deprecationWarnings].
input := cty.ObjectVal(map[string]cty.Value{
"okay": cty.StringVal("not deprecated").Mark(Sensitive),
"warn": DeprecatedOutput(
cty.StringVal("deprecated"),
addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.StringKey("beep"))),
"Blah blah blah don't use this!",
false,
),
})
got, gotDiags := ExtractDeprecatedDiagnosticsWithExpr(
input,
// This expression is used just for its source location information.
hcl.StaticExpr(cty.DynamicVal, hcl.Range{Filename: "test.tofu"}),
)
want := cty.ObjectVal(map[string]cty.Value{
"okay": cty.StringVal("not deprecated").Mark(Sensitive), // non-deprecation marks should be preserved
"warn": cty.StringVal("deprecated"), // deprecation marks are removed
})
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Error("wrong result value\n" + diff)
}
// We'll use the "ForRPC" representation of diagnostics just because it
// compares well with cmp. We don't actually care what type of diagnostic
// is returned here, only that it has the expected content.
gotDiags = gotDiags.ForRPC()
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Value derived from a deprecated source",
Detail: "This value's attribute warn is derived from module.child[\"beep\"].foo, which is deprecated with the following message:\n\nBlah blah blah don't use this!",
Subject: &hcl.Range{Filename: "test.tofu"}, // source location should come from the given expression
})
wantDiags = wantDiags.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Error("wrong diagnostics\n" + diff)
}
}
func TestContainsMarks(t *testing.T) {
cases := []struct {
v cty.Value
check map[valueMark]bool
wantOnContainsMarks bool
}{
{
cty.StringVal("test").Mark(Ephemeral).Mark(Sensitive),
map[valueMark]bool{Ephemeral: true, Sensitive: true},
true,
},
{
cty.StringVal("test").Mark(Ephemeral),
map[valueMark]bool{Ephemeral: true, Sensitive: false},
true,
},
{
cty.StringVal("test").Mark(Sensitive),
map[valueMark]bool{Ephemeral: false, Sensitive: true},
true,
},
{
cty.StringVal("test").Mark(valueMark("non-existing-mark")),
map[valueMark]bool{Ephemeral: false, Sensitive: false},
false,
},
{
cty.ObjectVal(map[string]cty.Value{
"nested": cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{cty.NumberIntVal(42).Mark(Ephemeral).Mark(Sensitive)}),
}),
}),
map[valueMark]bool{Ephemeral: true, Sensitive: true},
true,
},
{
cty.ObjectVal(map[string]cty.Value{
"nested": cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{cty.NumberIntVal(42).Mark(Ephemeral)}),
}),
}),
map[valueMark]bool{Ephemeral: true, Sensitive: false},
true,
},
{
cty.ObjectVal(map[string]cty.Value{
"nested": cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{cty.NumberIntVal(42).Mark(Sensitive)}),
}),
}),
map[valueMark]bool{Ephemeral: false, Sensitive: true},
true,
},
{
cty.ObjectVal(map[string]cty.Value{
"nested": cty.ObjectVal(map[string]cty.Value{
"set": cty.SetVal([]cty.Value{cty.NumberIntVal(42).Mark(valueMark("non-existing-mark"))}),
}),
}),
map[valueMark]bool{Ephemeral: false, Sensitive: false},
false,
},
}
for _, tt := range cases {
t.Run(tt.v.GoString(), func(t *testing.T) {
var allMarks []valueMark
for mark, want := range tt.check {
allMarks = append(allMarks, mark)
if got := Contains(tt.v, mark); want != got {
t.Errorf("Contains - expected mark %s to return %t but got %t", mark, want, got)
}
}
if got := ContainsAnyMark(tt.v, allMarks...); tt.wantOnContainsMarks != got {
t.Errorf("ContainsAnyMark - expected checking marks %#v to return %t but got %t", allMarks, tt.wantOnContainsMarks, got)
}
})
}
}