| // 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.showSensitive = 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) | |
| } | |
| }) | |
| } | |
| } |