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  }