github.com/opentofu/opentofu@v1.7.1/internal/command/views/show_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package views
     7  
     8  import (
     9  	"encoding/json"
    10  	"os"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/cloud/cloudplan"
    16  	"github.com/opentofu/opentofu/internal/command/arguments"
    17  	"github.com/opentofu/opentofu/internal/configs/configschema"
    18  	"github.com/opentofu/opentofu/internal/initwd"
    19  	"github.com/opentofu/opentofu/internal/plans"
    20  	"github.com/opentofu/opentofu/internal/providers"
    21  	"github.com/opentofu/opentofu/internal/states"
    22  	"github.com/opentofu/opentofu/internal/states/statefile"
    23  	"github.com/opentofu/opentofu/internal/terminal"
    24  	"github.com/opentofu/opentofu/internal/tofu"
    25  
    26  	"github.com/zclconf/go-cty/cty"
    27  )
    28  
    29  func TestShowHuman(t *testing.T) {
    30  	redactedPath := "./testdata/plans/redacted-plan.json"
    31  	redactedPlanJson, err := os.ReadFile(redactedPath)
    32  	if err != nil {
    33  		t.Fatalf("couldn't read json plan test data at %s for showing a cloud plan. Did the file get moved?", redactedPath)
    34  	}
    35  	testCases := map[string]struct {
    36  		plan       *plans.Plan
    37  		jsonPlan   *cloudplan.RemotePlanJSON
    38  		stateFile  *statefile.File
    39  		schemas    *tofu.Schemas
    40  		wantExact  bool
    41  		wantString string
    42  	}{
    43  		"plan file": {
    44  			testPlan(t),
    45  			nil,
    46  			nil,
    47  			testSchemas(),
    48  			false,
    49  			"# test_resource.foo will be created",
    50  		},
    51  		"cloud plan file": {
    52  			nil,
    53  			&cloudplan.RemotePlanJSON{
    54  				JSONBytes: redactedPlanJson,
    55  				Redacted:  true,
    56  				Mode:      plans.NormalMode,
    57  				Qualities: []plans.Quality{},
    58  				RunHeader: "[reset][yellow]To view this run in a browser, visit:\nhttps://app.example.com/app/example_org/example_workspace/runs/run-run-bugsBUGSbugsBUGS[reset]",
    59  				RunFooter: "[reset][green]Run status: planned and saved (confirmable)[reset]\n[green]Workspace is unlocked[reset]",
    60  			},
    61  			nil,
    62  			nil,
    63  			false,
    64  			"# null_resource.foo will be created",
    65  		},
    66  		"statefile": {
    67  			nil,
    68  			nil,
    69  			&statefile.File{
    70  				Serial:  0,
    71  				Lineage: "fake-for-testing",
    72  				State:   testState(),
    73  			},
    74  			testSchemas(),
    75  			false,
    76  			"# test_resource.foo:",
    77  		},
    78  		"empty statefile": {
    79  			nil,
    80  			nil,
    81  			&statefile.File{
    82  				Serial:  0,
    83  				Lineage: "fake-for-testing",
    84  				State:   states.NewState(),
    85  			},
    86  			testSchemas(),
    87  			true,
    88  			"The state file is empty. No resources are represented.\n",
    89  		},
    90  		"nothing": {
    91  			nil,
    92  			nil,
    93  			nil,
    94  			nil,
    95  			true,
    96  			"No state.\n",
    97  		},
    98  	}
    99  	for name, testCase := range testCases {
   100  		t.Run(name, func(t *testing.T) {
   101  			streams, done := terminal.StreamsForTesting(t)
   102  			view := NewView(streams)
   103  			view.Configure(&arguments.View{NoColor: true})
   104  			v := NewShow(arguments.ViewHuman, view)
   105  
   106  			code := v.Display(nil, testCase.plan, testCase.jsonPlan, testCase.stateFile, testCase.schemas)
   107  			if code != 0 {
   108  				t.Errorf("expected 0 return code, got %d", code)
   109  			}
   110  
   111  			output := done(t)
   112  			got := output.Stdout()
   113  			want := testCase.wantString
   114  			if (testCase.wantExact && got != want) || (!testCase.wantExact && !strings.Contains(got, want)) {
   115  				t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want)
   116  			}
   117  		})
   118  	}
   119  }
   120  
   121  func TestShowJSON(t *testing.T) {
   122  	unredactedPath := "../testdata/show-json/basic-create/output.json"
   123  	unredactedPlanJson, err := os.ReadFile(unredactedPath)
   124  	if err != nil {
   125  		t.Fatalf("couldn't read json plan test data at %s for showing a cloud plan. Did the file get moved?", unredactedPath)
   126  	}
   127  	testCases := map[string]struct {
   128  		plan      *plans.Plan
   129  		jsonPlan  *cloudplan.RemotePlanJSON
   130  		stateFile *statefile.File
   131  	}{
   132  		"plan file": {
   133  			testPlan(t),
   134  			nil,
   135  			nil,
   136  		},
   137  		"cloud plan file": {
   138  			nil,
   139  			&cloudplan.RemotePlanJSON{
   140  				JSONBytes: unredactedPlanJson,
   141  				Redacted:  false,
   142  				Mode:      plans.NormalMode,
   143  				Qualities: []plans.Quality{},
   144  				RunHeader: "[reset][yellow]To view this run in a browser, visit:\nhttps://app.example.com/app/example_org/example_workspace/runs/run-run-bugsBUGSbugsBUGS[reset]",
   145  				RunFooter: "[reset][green]Run status: planned and saved (confirmable)[reset]\n[green]Workspace is unlocked[reset]",
   146  			},
   147  			nil,
   148  		},
   149  		"statefile": {
   150  			nil,
   151  			nil,
   152  			&statefile.File{
   153  				Serial:  0,
   154  				Lineage: "fake-for-testing",
   155  				State:   testState(),
   156  			},
   157  		},
   158  		"empty statefile": {
   159  			nil,
   160  			nil,
   161  			&statefile.File{
   162  				Serial:  0,
   163  				Lineage: "fake-for-testing",
   164  				State:   states.NewState(),
   165  			},
   166  		},
   167  		"nothing": {
   168  			nil,
   169  			nil,
   170  			nil,
   171  		},
   172  	}
   173  
   174  	config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show", "tests")
   175  	defer configCleanup()
   176  
   177  	for name, testCase := range testCases {
   178  		t.Run(name, func(t *testing.T) {
   179  			streams, done := terminal.StreamsForTesting(t)
   180  			view := NewView(streams)
   181  			view.Configure(&arguments.View{NoColor: true})
   182  			v := NewShow(arguments.ViewJSON, view)
   183  
   184  			schemas := &tofu.Schemas{
   185  				Providers: map[addrs.Provider]providers.ProviderSchema{
   186  					addrs.NewDefaultProvider("test"): {
   187  						ResourceTypes: map[string]providers.Schema{
   188  							"test_resource": {
   189  								Block: &configschema.Block{
   190  									Attributes: map[string]*configschema.Attribute{
   191  										"id":  {Type: cty.String, Optional: true, Computed: true},
   192  										"foo": {Type: cty.String, Optional: true},
   193  									},
   194  								},
   195  							},
   196  						},
   197  					},
   198  				},
   199  			}
   200  
   201  			code := v.Display(config, testCase.plan, testCase.jsonPlan, testCase.stateFile, schemas)
   202  
   203  			if code != 0 {
   204  				t.Errorf("expected 0 return code, got %d", code)
   205  			}
   206  
   207  			// Make sure the result looks like JSON; we comprehensively test
   208  			// the structure of this output in the command package tests.
   209  			var result map[string]interface{}
   210  			got := done(t).All()
   211  			t.Logf("output: %s", got)
   212  			if err := json.Unmarshal([]byte(got), &result); err != nil {
   213  				t.Fatal(err)
   214  			}
   215  		})
   216  	}
   217  }
   218  
   219  // testState returns a test State structure.
   220  func testState() *states.State {
   221  	return states.BuildState(func(s *states.SyncState) {
   222  		s.SetResourceInstanceCurrent(
   223  			addrs.Resource{
   224  				Mode: addrs.ManagedResourceMode,
   225  				Type: "test_resource",
   226  				Name: "foo",
   227  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   228  			&states.ResourceInstanceObjectSrc{
   229  				AttrsJSON: []byte(`{"id":"bar","foo":"value"}`),
   230  				Status:    states.ObjectReady,
   231  			},
   232  			addrs.AbsProviderConfig{
   233  				Provider: addrs.NewDefaultProvider("test"),
   234  				Module:   addrs.RootModule,
   235  			},
   236  		)
   237  		// DeepCopy is used here to ensure our synthetic state matches exactly
   238  		// with a state that will have been copied during the command
   239  		// operation, and all fields have been copied correctly.
   240  	}).DeepCopy()
   241  }