// 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) } }) } }