Files
opentf/internal/command/views/hook_ui_test.go
Diógenes Fernandes 2c7cd8546c fix: showSensitive working for provisioners output (#3931)
Signed-off-by: Diogenes Fernandes <diofeher@gmail.com>
2026-04-15 09:39:53 -03:00

985 lines
26 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 views
import (
"fmt"
"regexp"
"strings"
"testing"
"time"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/command/arguments"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/terminal"
"github.com/opentofu/opentofu/internal/tofu"
)
// Test the PreApply hook for creating a new resource
func TestUiHookPreApply_create(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.resources = map[string]uiResourceState{
"test_instance.foo": {
Op: uiResourceCreate,
Start: time.Now(),
},
}
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
priorState := cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"bar": cty.List(cty.String),
}))
plannedNewState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"bar": cty.ListVal([]cty.Value{
cty.StringVal("baz"),
}),
})
action, err := h.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
// stop the background writer
uiState := h.resources[addr.String()]
close(uiState.DoneCh)
<-uiState.done
expectedOutput := "test_instance.foo: Creating...\n"
result := done(t)
output := result.Stdout()
if output != expectedOutput {
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
}
expectedErrOutput := ""
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
// Test the PreApply hook's use of a periodic timer to display "still working"
// log lines
func TestUiHookPreApply_periodicTimer(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.periodicUiTimer = 1 * time.Second
h.resources = map[string]uiResourceState{
"test_instance.foo": {
Op: uiResourceModify,
Start: time.Now(),
},
}
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
priorState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"bar": cty.ListValEmpty(cty.String),
})
plannedNewState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"bar": cty.ListVal([]cty.Value{
cty.StringVal("baz"),
}),
})
action, err := h.PreApply(addr, states.CurrentGen, plans.Update, priorState, plannedNewState)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
time.Sleep(3100 * time.Millisecond)
// stop the background writer
uiState := h.resources[addr.String()]
close(uiState.DoneCh)
<-uiState.done
expectedRegexp := `test_instance\.foo: Modifying... \[id=test\]
test_instance\.foo: Still modifying... \[id=test, \ds elapsed\]
test_instance\.foo: Still modifying... \[id=test, \ds elapsed\]
test_instance\.foo: Still modifying... \[id=test, \ds elapsed\]
`
result := done(t)
output := result.Stdout()
if matched, _ := regexp.MatchString(expectedRegexp, output); !matched {
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedRegexp, output)
}
expectedErrOutput := ""
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
// Test the ephemeral specific hooks
func TestUiHook_ephemeral(t *testing.T) {
addr := addrs.Resource{
Mode: addrs.EphemeralResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
cases := []struct {
name string
preF func(hook tofu.Hook) (tofu.HookAction, error)
postF func(hook tofu.Hook) (tofu.HookAction, error)
wantOutput string
}{
{
name: "opening",
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
return hook.PreOpen(addr)
},
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
return hook.PostOpen(addr, nil)
},
wantOutput: `ephemeral\.test_instance\.foo: Opening\.\.\.
ephemeral\.test_instance\.foo: Still opening\.\.\. \[\ds elapsed\]
`,
},
{
name: "renewing",
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
return hook.PreRenew(addr)
},
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
return hook.PostRenew(addr, nil)
},
wantOutput: `ephemeral\.test_instance\.foo: Renewing\.\.\.
ephemeral\.test_instance\.foo: Still renewing\.\.\. \[\ds elapsed\]
ephemeral\.test_instance\.foo: Renew complete after \ds
`,
},
{
name: "closing",
preF: func(hook tofu.Hook) (tofu.HookAction, error) {
return hook.PreClose(addr)
},
postF: func(hook tofu.Hook) (tofu.HookAction, error) {
return hook.PostClose(addr, nil)
},
wantOutput: `ephemeral\.test_instance\.foo: Closing\.\.\.
ephemeral\.test_instance\.foo: Still closing\.\.\. \[\ds elapsed\]
ephemeral\.test_instance\.foo: Close complete after \ds
`,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.periodicUiTimer = 1 * time.Second
action, err := tt.preF(h)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
<-time.After(1100 * time.Millisecond)
// stop the background writer
uiState := h.resources[addr.String()]
// call postF that will stop the waiting for the action
action, err = tt.postF(h)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Errorf("Expected hook to continue, given: %#v", action)
}
// wait for the waiting to stop completely
<-uiState.done
result := done(t)
output := result.Stdout()
if matched, _ := regexp.MatchString(tt.wantOutput, output); !matched {
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", tt.wantOutput, output)
}
expectedErrOutput := ""
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
})
}
}
// Test the PreApply hook's destroy path, including passing a deposed key as
// the gen argument.
func TestUiHookPreApply_destroy(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.resources = map[string]uiResourceState{
"test_instance.foo": {
Op: uiResourceDestroy,
Start: time.Now(),
},
}
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
priorState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("abc123"),
"verbs": cty.ListVal([]cty.Value{
cty.StringVal("boop"),
}),
})
plannedNewState := cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"verbs": cty.List(cty.String),
}))
key := states.NewDeposedKey()
action, err := h.PreApply(addr, key, plans.Delete, priorState, plannedNewState)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
// stop the background writer
uiState := h.resources[addr.String()]
close(uiState.DoneCh)
<-uiState.done
result := done(t)
expectedOutput := fmt.Sprintf("test_instance.foo (deposed object %s): Destroying... [id=abc123]\n", key)
output := result.Stdout()
if output != expectedOutput {
t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output)
}
expectedErrOutput := ""
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
// Verify that colorize is called on format strings, not user input, by adding
// valid color codes as resource names and IDs.
func TestUiHookPostApply_colorInterpolation(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
view.Configure(&arguments.View{NoColor: false})
h := NewUiHook(view)
h.resources = map[string]uiResourceState{
"test_instance.foo[\"[red]\"]": {
Op: uiResourceCreate,
Start: time.Now(),
},
}
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.StringKey("[red]")).Absolute(addrs.RootModuleInstance)
newState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("[blue]"),
})
action, err := h.PostApply(addr, states.CurrentGen, newState, nil)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
reset := "\x1b[0m"
bold := "\x1b[1m"
wantPrefix := reset + bold + `test_instance.foo["[red]"]: Creation complete after`
wantSuffix := "[id=[blue]]" + reset + "\n"
output := result.Stdout()
if !strings.HasPrefix(output, wantPrefix) {
t.Fatalf("wrong output prefix\n got: %#v\nwant: %#v", output, wantPrefix)
}
if !strings.HasSuffix(output, wantSuffix) {
t.Fatalf("wrong output suffix\n got: %#v\nwant: %#v", output, wantSuffix)
}
expectedErrOutput := ""
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
// Test that the PostApply hook renders a total time.
func TestUiHookPostApply_emptyState(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
h.resources = map[string]uiResourceState{
"data.google_compute_zones.available": {
Op: uiResourceDestroy,
Start: time.Now(),
},
}
addr := addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "google_compute_zones",
Name: "available",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
newState := cty.NullVal(cty.Object(map[string]cty.Type{
"id": cty.String,
"names": cty.List(cty.String),
}))
action, err := h.PostApply(addr, states.CurrentGen, newState, nil)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
expectedRegexp := "^data.google_compute_zones.available: Destruction complete after -?[a-z0-9µ.]+\n$"
output := result.Stdout()
if matched, _ := regexp.MatchString(expectedRegexp, output); !matched {
t.Fatalf("Output didn't match regexp.\nExpected: %q\nGiven: %q", expectedRegexp, output)
}
expectedErrOutput := ""
errOutput := result.Stderr()
if errOutput != expectedErrOutput {
t.Fatalf("Error output didn't match.\nExpected: %q\nGiven: %q", expectedErrOutput, errOutput)
}
}
func TestPreProvisionInstanceStep(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
action, err := h.PreProvisionInstanceStep(addr, "local-exec")
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
if got, want := result.Stdout(), "test_instance.foo: Provisioning with 'local-exec'...\n"; got != want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
}
}
// Test ProvisionOutput, including lots of edge cases for the output
// whitespace/line ending logic.
func TestProvisionOutput(t *testing.T) {
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
testCases := map[string]struct {
provisioner string
input string
configMarks cty.ValueMarks
showSensitive bool
wantOutput string
}{
"single line": {
provisioner: "local-exec",
input: "foo\n",
wantOutput: "test_instance.foo (local-exec): foo\n",
},
"multiple lines": {
provisioner: "x",
input: `foo
bar
baz
`,
wantOutput: `test_instance.foo (x): foo
test_instance.foo (x): bar
test_instance.foo (x): baz
`,
},
"trailing whitespace": {
provisioner: "x",
input: "foo \nbar\n",
wantOutput: "test_instance.foo (x): foo\ntest_instance.foo (x): bar\n",
},
"blank lines": {
provisioner: "x",
input: "foo\n\nbar\n\n\nbaz\n",
wantOutput: `test_instance.foo (x): foo
test_instance.foo (x): bar
test_instance.foo (x): baz
`,
},
"no final newline": {
provisioner: "x",
input: `foo
bar`,
wantOutput: `test_instance.foo (x): foo
test_instance.foo (x): bar
`,
},
"CR, no LF": {
provisioner: "MacOS 9?",
input: "foo\rbar\r",
wantOutput: `test_instance.foo (MacOS 9?): foo
test_instance.foo (MacOS 9?): bar
`,
},
"CRLF": {
provisioner: "winrm",
input: "foo\r\nbar\r\n",
wantOutput: `test_instance.foo (winrm): foo
test_instance.foo (winrm): bar
`,
},
"sensitive suppressed by default": {
provisioner: "local-exec",
input: "secret-value\n",
configMarks: cty.NewValueMarks(marks.Sensitive),
wantOutput: "test_instance.foo (local-exec): (output suppressed due to sensitive value in config)\n",
},
"sensitive shown with show-sensitive": {
provisioner: "local-exec",
input: "secret-value\n",
configMarks: cty.NewValueMarks(marks.Sensitive),
showSensitive: true,
wantOutput: "test_instance.foo (local-exec): secret-value\n",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
view.SetShowSensitive(tc.showSensitive)
h := NewUiHook(view)
h.ProvisionOutput(addr, tc.provisioner, tc.input, tc.configMarks)
result := done(t)
if got := result.Stdout(); got != tc.wantOutput {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.wantOutput)
}
})
}
}
// Test the PreRefresh hook in the normal path where the resource exists with
// an ID key and value in the state.
func TestPreRefresh(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
priorState := cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
"bar": cty.ListValEmpty(cty.String),
})
action, err := h.PreRefresh(addr, states.CurrentGen, priorState)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
if got, want := result.Stdout(), "test_instance.foo: Refreshing state... [id=test]\n"; got != want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
}
}
// Test that PreRefresh still works if no ID key and value can be determined
// from state.
func TestPreRefresh_noID(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
priorState := cty.ObjectVal(map[string]cty.Value{
"bar": cty.ListValEmpty(cty.String),
})
action, err := h.PreRefresh(addr, states.CurrentGen, priorState)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
if got, want := result.Stdout(), "test_instance.foo: Refreshing state...\n"; got != want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
}
}
// Test the very simple PreImportState hook.
func TestPreImportState(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
action, err := h.PreImportState(addr, "test")
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
if got, want := result.Stdout(), "test_instance.foo: Importing from ID \"test\"...\n"; got != want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
}
}
// Test the PostImportState UI hook. Again, this hook behaviour seems odd to
// me (see below), so please don't consider these tests as justification for
// keeping this behaviour.
func TestPostImportState(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
// The "Prepared [...] for import" lines display the type name of each of
// the imported resources passed to the hook. I'm not sure how it's
// possible for an import to result in a different resource type name than
// the target address, but the hook works like this so we're covering it.
imported := []providers.ImportedResource{
{
TypeName: "test_some_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
}),
},
{
TypeName: "test_other_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
}),
},
}
action, err := h.PostImportState(addr, imported)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
want := `test_instance.foo: Import prepared!
Prepared test_some_instance for import
Prepared test_other_instance for import
`
if got := result.Stdout(); got != want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want)
}
}
func mustNewDynamicValue(t *testing.T, val cty.Value, ty cty.Type) plans.DynamicValue {
t.Helper()
dv, err := plans.NewDynamicValue(val, ty)
if err != nil {
t.Fatal(err)
}
return dv
}
func TestPreApplyImport(t *testing.T) {
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
testCases := []struct {
name string
importing plans.ImportingSrc
want string
}{
{
name: "importing by id",
importing: plans.ImportingSrc{
ID: "test",
},
want: "test_instance.foo: Importing... [id=test]\n",
},
{
name: "importing by identity with id field",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
}),
cty.Object(map[string]cty.Type{
"id": cty.String,
}),
),
},
want: "test_instance.foo: Importing... [id=test]\n",
},
{
name: "importing by identity no string fields",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"id": cty.NumberIntVal(123),
}),
cty.Object(map[string]cty.Type{
"id": cty.Number,
}),
),
},
want: "test_instance.foo: Importing...\n",
},
{
name: "importing by identity with one string field",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"bucket": cty.StringVal("my-bucket"),
}),
cty.Object(map[string]cty.Type{
"bucket": cty.String,
}),
),
},
want: "test_instance.foo: Importing... [bucket=my-bucket]\n",
},
{
name: "importing by identity with multiple string fields (not id or name or tags) should select first field",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"bucket": cty.StringVal("my-bucket"),
"region": cty.StringVal("us-west-1"),
}),
cty.Object(map[string]cty.Type{
"bucket": cty.String,
"region": cty.String,
}),
),
},
want: "test_instance.foo: Importing... [bucket=my-bucket]\n",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
action, err := h.PreApplyImport(addr, tc.importing)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
if got := result.Stdout(); got != tc.want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.want)
}
})
}
}
func TestPostApplyImport(t *testing.T) {
addr := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
testCases := []struct {
name string
importing plans.ImportingSrc
want string
}{
{
name: "importing by id",
importing: plans.ImportingSrc{
ID: "test",
},
want: "test_instance.foo: Import complete [id=test]\n",
},
{
name: "importing by identity with id field",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("test"),
}),
cty.Object(map[string]cty.Type{
"id": cty.String,
}),
),
},
want: "test_instance.foo: Import complete [id=test]\n",
},
{
name: "importing by identity no string fields",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"id": cty.NumberIntVal(123),
}),
cty.Object(map[string]cty.Type{
"id": cty.Number,
}),
),
},
want: "test_instance.foo: Import complete\n",
},
{
name: "importing by identity with one string field",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"bucket": cty.StringVal("my-bucket"),
}),
cty.Object(map[string]cty.Type{
"bucket": cty.String,
}),
),
},
want: "test_instance.foo: Import complete [bucket=my-bucket]\n",
},
{
name: "importing by identity with multiple string fields (not id or name or tags) should select first field",
importing: plans.ImportingSrc{
Identity: mustNewDynamicValue(t,
cty.ObjectVal(map[string]cty.Value{
"bucket": cty.StringVal("my-bucket"),
"region": cty.StringVal("us-west-1"),
}),
cty.Object(map[string]cty.Type{
"bucket": cty.String,
"region": cty.String,
}),
),
},
want: "test_instance.foo: Import complete [bucket=my-bucket]\n",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
view := NewView(streams)
h := NewUiHook(view)
action, err := h.PostApplyImport(addr, tc.importing)
if err != nil {
t.Fatal(err)
}
if action != tofu.HookActionContinue {
t.Fatalf("Expected hook to continue, given: %#v", action)
}
result := done(t)
if got := result.Stdout(); got != tc.want {
t.Fatalf("unexpected output\n got: %q\nwant: %q", got, tc.want)
}
})
}
}
func TestTruncateId(t *testing.T) {
testCases := []struct {
Input string
Expected string
MaxLen int
}{
{
Input: "Hello world",
Expected: "H...d",
MaxLen: 3,
},
{
Input: "Hello world",
Expected: "H...d",
MaxLen: 5,
},
{
Input: "Hello world",
Expected: "He...d",
MaxLen: 6,
},
{
Input: "Hello world",
Expected: "He...ld",
MaxLen: 7,
},
{
Input: "Hello world",
Expected: "Hel...ld",
MaxLen: 8,
},
{
Input: "Hello world",
Expected: "Hel...rld",
MaxLen: 9,
},
{
Input: "Hello world",
Expected: "Hell...rld",
MaxLen: 10,
},
{
Input: "Hello world",
Expected: "Hello world",
MaxLen: 11,
},
{
Input: "Hello world",
Expected: "Hello world",
MaxLen: 12,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あ...さ",
MaxLen: 3,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あ...さ",
MaxLen: 5,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あい...さ",
MaxLen: 6,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あい...こさ",
MaxLen: 7,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あいう...こさ",
MaxLen: 8,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あいう...けこさ",
MaxLen: 9,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あいうえ...けこさ",
MaxLen: 10,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あいうえおかきくけこさ",
MaxLen: 11,
},
{
Input: "あいうえおかきくけこさ",
Expected: "あいうえおかきくけこさ",
MaxLen: 12,
},
}
for i, tc := range testCases {
testName := fmt.Sprintf("%d", i)
t.Run(testName, func(t *testing.T) {
out := truncateId(tc.Input, tc.MaxLen)
if out != tc.Expected {
t.Fatalf("Expected %q to be shortened to %d as %q (given: %q)",
tc.Input, tc.MaxLen, tc.Expected, out)
}
})
}
}