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 }