github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_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 cloud 7 8 import ( 9 "context" 10 "path/filepath" 11 "strings" 12 "testing" 13 14 tfe "github.com/hashicorp/go-tfe" 15 "github.com/opentofu/opentofu/internal/plans" 16 ) 17 18 // A brief discourse on the theory of testing for this feature. Doing 19 // `tofu show cloudplan.tfplan` relies on the correctness of the following 20 // behaviors: 21 // 22 // 1. TFC API returns redacted or unredacted plan JSON on request, if permission 23 // requirements are met and the run is in a condition where that JSON exists. 24 // 2. Cloud.ShowPlanForRun() makes correct API calls, calculates metadata 25 // properly given a tfe.Run, and returns either a cloudplan.RemotePlanJSON or an err. 26 // 3. The Show command instantiates Cloud backend when given a cloud planfile, 27 // calls .ShowPlanForRun() on it, and passes result to Display() impls. 28 // 4. Display() impls yield the correct output when given a cloud plan json biscuit. 29 // 30 // 1 is axiomatic and outside our domain. 3 is regrettably totally untestable 31 // unless we refactor the Meta command to enable stubbing out a backend factory 32 // or something, which seems inadvisable at this juncture. 4 is exercised over 33 // in internal/command/views/show_test.go. And thus, this file only cares about 34 // item 2. 35 36 // 404 on run: special error message 37 func TestCloud_showMissingRun(t *testing.T) { 38 b, bCleanup := testBackendWithName(t) 39 defer bCleanup() 40 mockSROWorkspace(t, b, testBackendSingleWorkspaceName) 41 42 absentRunID := "run-WwwwXxxxYyyyZzzz" 43 _, err := b.ShowPlanForRun(context.Background(), absentRunID, tfeHost, true) 44 if !strings.Contains(err.Error(), "tofu login") { 45 t.Fatalf("expected error message to suggest checking your login status, instead got: %s", err) 46 } 47 } 48 49 // If redacted json is available but unredacted is not 50 func TestCloud_showMissingUnredactedJson(t *testing.T) { 51 b, mc, bCleanup := testBackendAndMocksWithName(t) 52 defer bCleanup() 53 mockSROWorkspace(t, b, testBackendSingleWorkspaceName) 54 55 ctx := context.Background() 56 57 runID, err := testCloudRunForShow(mc, "./testdata/plan-json-basic-no-unredacted", tfe.RunPlannedAndSaved, tfe.PlanFinished) 58 if err != nil { 59 t.Fatalf("failed to init test data: %s", err) 60 } 61 // Showing the human-formatted plan should still work as expected! 62 redacted, err := b.ShowPlanForRun(ctx, runID, tfeHost, true) 63 if err != nil { 64 t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) 65 } 66 if !strings.Contains(string(redacted.JSONBytes), `"plan_format_version":`) { 67 t.Fatalf("show for human doesn't include expected redacted json content") 68 } 69 // Should be marked as containing changes and non-errored 70 canNotApply := false 71 errored := false 72 for _, opt := range redacted.Qualities { 73 if opt == plans.NoChanges { 74 canNotApply = true 75 } 76 if opt == plans.Errored { 77 errored = true 78 } 79 } 80 if canNotApply || errored { 81 t.Fatalf("expected neither errored nor can't-apply in opts, instead got: %#v", redacted.Qualities) 82 } 83 84 // But show -json should result in a special error. 85 _, err = b.ShowPlanForRun(ctx, runID, tfeHost, false) 86 if err == nil { 87 t.Fatalf("unexpected success: reading unredacted json without admin permissions should have errored") 88 } 89 if !strings.Contains(err.Error(), "admin") { 90 t.Fatalf("expected error message to suggest your permissions are wrong, instead got: %s", err) 91 } 92 } 93 94 // If both kinds of json are available, both kinds of show should work 95 func TestCloud_showIncludesUnredactedJson(t *testing.T) { 96 b, mc, bCleanup := testBackendAndMocksWithName(t) 97 defer bCleanup() 98 mockSROWorkspace(t, b, testBackendSingleWorkspaceName) 99 100 ctx := context.Background() 101 102 runID, err := testCloudRunForShow(mc, "./testdata/plan-json-basic", tfe.RunPlannedAndSaved, tfe.PlanFinished) 103 if err != nil { 104 t.Fatalf("failed to init test data: %s", err) 105 } 106 // Showing the human-formatted plan should work as expected: 107 redacted, err := b.ShowPlanForRun(ctx, runID, tfeHost, true) 108 if err != nil { 109 t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) 110 } 111 if !strings.Contains(string(redacted.JSONBytes), `"plan_format_version":`) { 112 t.Fatalf("show for human doesn't include expected redacted json content") 113 } 114 // Showing the external json plan format should work as expected: 115 unredacted, err := b.ShowPlanForRun(ctx, runID, tfeHost, false) 116 if err != nil { 117 t.Fatalf("failed to show plan for robot, even though unredacted json should be present: %s", err) 118 } 119 if !strings.Contains(string(unredacted.JSONBytes), `"format_version":`) { 120 t.Fatalf("show for robot doesn't include expected unredacted json content") 121 } 122 } 123 124 func TestCloud_showNoChanges(t *testing.T) { 125 b, mc, bCleanup := testBackendAndMocksWithName(t) 126 defer bCleanup() 127 mockSROWorkspace(t, b, testBackendSingleWorkspaceName) 128 129 ctx := context.Background() 130 131 runID, err := testCloudRunForShow(mc, "./testdata/plan-json-no-changes", tfe.RunPlannedAndSaved, tfe.PlanFinished) 132 if err != nil { 133 t.Fatalf("failed to init test data: %s", err) 134 } 135 // Showing the human-formatted plan should work as expected: 136 redacted, err := b.ShowPlanForRun(ctx, runID, tfeHost, true) 137 if err != nil { 138 t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) 139 } 140 // Should be marked as no changes 141 canNotApply := false 142 for _, opt := range redacted.Qualities { 143 if opt == plans.NoChanges { 144 canNotApply = true 145 } 146 } 147 if !canNotApply { 148 t.Fatalf("expected opts to include CanNotApply, instead got: %#v", redacted.Qualities) 149 } 150 } 151 152 func TestCloud_showFooterNotConfirmable(t *testing.T) { 153 b, mc, bCleanup := testBackendAndMocksWithName(t) 154 defer bCleanup() 155 mockSROWorkspace(t, b, testBackendSingleWorkspaceName) 156 157 ctx := context.Background() 158 159 runID, err := testCloudRunForShow(mc, "./testdata/plan-json-full", tfe.RunDiscarded, tfe.PlanFinished) 160 if err != nil { 161 t.Fatalf("failed to init test data: %s", err) 162 } 163 164 // A little more custom run tweaking: 165 mc.Runs.Runs[runID].Actions.IsConfirmable = false 166 167 // Showing the human-formatted plan should work as expected: 168 redacted, err := b.ShowPlanForRun(ctx, runID, tfeHost, true) 169 if err != nil { 170 t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) 171 } 172 173 // Footer should mention that you can't apply it: 174 if !strings.Contains(redacted.RunFooter, "not confirmable") { 175 t.Fatalf("footer should call out that run isn't confirmable, instead got: %s", redacted.RunFooter) 176 } 177 } 178 179 func testCloudRunForShow(mc *MockClient, configDir string, runStatus tfe.RunStatus, planStatus tfe.PlanStatus) (string, error) { 180 ctx := context.Background() 181 182 // get workspace ID 183 wsID := mc.Workspaces.workspaceNames[testBackendSingleWorkspaceName].ID 184 // create and upload config version 185 cvOpts := tfe.ConfigurationVersionCreateOptions{ 186 AutoQueueRuns: tfe.Bool(false), 187 Speculative: tfe.Bool(false), 188 } 189 cv, err := mc.ConfigurationVersions.Create(ctx, wsID, cvOpts) 190 if err != nil { 191 return "", err 192 } 193 absDir, err := filepath.Abs(configDir) 194 if err != nil { 195 return "", err 196 } 197 err = mc.ConfigurationVersions.Upload(ctx, cv.UploadURL, absDir) 198 if err != nil { 199 return "", err 200 } 201 // create run 202 rOpts := tfe.RunCreateOptions{ 203 PlanOnly: tfe.Bool(false), 204 IsDestroy: tfe.Bool(false), 205 RefreshOnly: tfe.Bool(false), 206 ConfigurationVersion: cv, 207 Workspace: &tfe.Workspace{ID: wsID}, 208 } 209 r, err := mc.Runs.Create(ctx, rOpts) 210 if err != nil { 211 return "", err 212 } 213 // mess with statuses (this is what requires full access to mock client) 214 mc.Runs.Runs[r.ID].Status = runStatus 215 mc.Plans.plans[r.Plan.ID].Status = planStatus 216 217 // return the ID 218 return r.ID, nil 219 }