terraform/internal/command/arguments/init_test.go
Sarah French c56ec2943d
Some checks are pending
build / Determine intended Terraform version (push) Waiting to run
build / Determine Go toolchain version (push) Waiting to run
build / Generate release metadata (push) Blocked by required conditions
build / Build for freebsd_386 (push) Blocked by required conditions
build / Build for linux_386 (push) Blocked by required conditions
build / Build for openbsd_386 (push) Blocked by required conditions
build / Build for windows_386 (push) Blocked by required conditions
build / Build for darwin_amd64 (push) Blocked by required conditions
build / Build for freebsd_amd64 (push) Blocked by required conditions
build / Build for linux_amd64 (push) Blocked by required conditions
build / Build for openbsd_amd64 (push) Blocked by required conditions
build / Build for solaris_amd64 (push) Blocked by required conditions
build / Build for windows_amd64 (push) Blocked by required conditions
build / Build for freebsd_arm (push) Blocked by required conditions
build / Build for linux_arm (push) Blocked by required conditions
build / Build for darwin_arm64 (push) Blocked by required conditions
build / Build for linux_arm64 (push) Blocked by required conditions
build / Build for windows_arm64 (push) Blocked by required conditions
build / Build for linux_s390x (push) Blocked by required conditions
build / Build Docker image for linux_386 (push) Blocked by required conditions
build / Build Docker image for linux_amd64 (push) Blocked by required conditions
build / Build Docker image for linux_arm (push) Blocked by required conditions
build / Build Docker image for linux_arm64 (push) Blocked by required conditions
build / Build Docker image for linux_s390x (push) Blocked by required conditions
build / Build e2etest for linux_386 (push) Blocked by required conditions
build / Build e2etest for windows_386 (push) Blocked by required conditions
build / Build e2etest for darwin_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_amd64 (push) Blocked by required conditions
build / Build e2etest for windows_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_arm (push) Blocked by required conditions
build / Build e2etest for darwin_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_arm64 (push) Blocked by required conditions
build / Run e2e test for linux_386 (push) Blocked by required conditions
build / Run e2e test for windows_386 (push) Blocked by required conditions
build / Run e2e test for darwin_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_amd64 (push) Blocked by required conditions
build / Run e2e test for windows_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_arm (push) Blocked by required conditions
build / Run e2e test for linux_arm64 (push) Blocked by required conditions
build / Run terraform-exec test for linux amd64 (push) Blocked by required conditions
Quick Checks / Unit Tests (push) Waiting to run
Quick Checks / Race Tests (push) Waiting to run
Quick Checks / End-to-end Tests (push) Waiting to run
Quick Checks / Code Consistency Checks (push) Waiting to run
refactor: Move validation of init's -upgrade and -lockfile=readonly flags to the arguments package. (#38561)
The `-upgrade` and `-lockfile=readonly` flags are mutually exclusive, and this is now enforced by the arguments package. This refactor has also fixed a historical bug where that validation error was output twice.

The getProvidersFromConfig code now depends on the flag validation happening in the calling code. However there is another check in downstream code, in `saveDependencyLockFile` when we overwrite the dependency lock file's contents. There, an error occurs if a diff in the locks is detected during `readonly` mode. This protects against incorrect use of `getProvidersFromConfig` with unvalidated inputs, if that happens in future.
2026-05-14 14:42:31 +01:00

270 lines
7.7 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package arguments
import (
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestParseInit_basicValid(t *testing.T) {
var flagNameValue []FlagNameValue
testCases := map[string]struct {
args []string
want *Init
}{
"with default options": {
nil,
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: true,
Cloud: true,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: false,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: true,
CompactWarnings: false,
TargetFlags: nil,
},
},
"setting multiple options": {
[]string{
"-backend=false", "-force-copy=true",
"-from-module=./main-dir", "-json", "-get=false",
"-lock=false", "-lock-timeout=10s", "-reconfigure=true",
"-upgrade=true", "-compact-warnings=true",
"-ignore-remote-version=true", "-test-directory=./test-dir",
},
&Init{
FromModule: "./main-dir",
Lockfile: "",
TestsDirectory: "./test-dir",
ViewType: ViewJSON,
Backend: false,
Cloud: false,
Get: false,
ForceInitCopy: true,
StateLock: false,
StateLockTimeout: time.Duration(10) * time.Second,
Reconfigure: true,
MigrateState: false,
Upgrade: true,
Json: true,
IgnoreRemoteVersion: true,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &flagNameValue,
},
Vars: &Vars{},
InputEnabled: true,
Args: []string{},
CompactWarnings: true,
TargetFlags: nil,
},
},
"with cloud option": {
[]string{"-cloud=false", "-input=false", "-target=foo_bar.baz", "-backend-config", "backend.config"},
&Init{
FromModule: "",
Lockfile: "",
TestsDirectory: "tests",
ViewType: ViewHuman,
Backend: false,
Cloud: false,
Get: true,
ForceInitCopy: false,
StateLock: true,
StateLockTimeout: 0,
Reconfigure: false,
MigrateState: false,
Upgrade: false,
Json: false,
IgnoreRemoteVersion: false,
BackendConfig: FlagNameValueSlice{
FlagName: "-backend-config",
Items: &[]FlagNameValue{{Name: "-backend-config", Value: "backend.config"}},
},
Vars: &Vars{},
InputEnabled: false,
Args: []string{},
CompactWarnings: false,
TargetFlags: []string{"foo_bar.baz"},
},
},
}
cmpOpts := cmpopts.IgnoreUnexported(Vars{})
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
experimentsEnabled := false
got, diags := ParseInit(tc.args, experimentsEnabled)
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" {
t.Errorf("unexpected result\n%s", diff)
}
})
}
}
func TestParseInit_invalid(t *testing.T) {
testCases := map[string]struct {
args []string
wantErr string
wantViewType ViewType
}{
"with unsupported options": {
args: []string{"-raw"},
wantErr: "flag provided but not defined",
wantViewType: ViewHuman,
},
"with both -backend and -cloud options set": {
args: []string{"-backend=false", "-cloud=false"},
wantErr: "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use",
wantViewType: ViewHuman,
},
"with both -migrate-state and -json options set": {
args: []string{"-migrate-state", "-json"},
wantErr: "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.",
wantViewType: ViewJSON,
},
"with both -migrate-state and -reconfigure options set": {
args: []string{"-migrate-state", "-reconfigure"},
wantErr: "The -migrate-state and -reconfigure options are mutually-exclusive.",
wantViewType: ViewHuman,
},
"with both -upgrade and -lockfile=readonly options set": {
args: []string{"-upgrade", "-lockfile=readonly"},
wantErr: "The -upgrade flag conflicts with -lockfile=readonly.",
wantViewType: ViewHuman,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
experimentsEnabled := false
got, diags := ParseInit(tc.args, experimentsEnabled)
if len(diags) == 0 {
t.Fatal("expected diags but got none")
}
if got, want := diags.Err().Error(), tc.wantErr; !strings.Contains(got, want) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
}
if got.ViewType != tc.wantViewType {
t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman)
}
})
}
}
func TestParseInit_experimentalFlags(t *testing.T) {
testCases := map[string]struct {
args []string
envs map[string]string
wantErr string
experimentsEnabled bool
}{
"error: -enable-pluggable-state-storage-experiment and experiments are disabled": {
args: []string{"-enable-pluggable-state-storage-experiment"},
experimentsEnabled: false,
wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
},
"error: TF_ENABLE_PLUGGABLE_STATE_STORAGE is set and experiments are disabled": {
envs: map[string]string{
"TF_ENABLE_PLUGGABLE_STATE_STORAGE": "1",
},
experimentsEnabled: false,
wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
for k, v := range tc.envs {
t.Setenv(k, v)
}
_, diags := ParseInit(tc.args, tc.experimentsEnabled)
if len(diags) == 0 {
t.Fatal("expected diags but got none")
}
if got, want := diags.Err().Error(), tc.wantErr; !strings.Contains(got, want) {
t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want)
}
})
}
}
func TestParseInit_vars(t *testing.T) {
testCases := map[string]struct {
args []string
want []FlagNameValue
}{
"no var flags by default": {
args: nil,
want: nil,
},
"one var": {
args: []string{"-var", "foo=bar"},
want: []FlagNameValue{
{Name: "-var", Value: "foo=bar"},
},
},
"one var-file": {
args: []string{"-var-file", "cool.tfvars"},
want: []FlagNameValue{
{Name: "-var-file", Value: "cool.tfvars"},
},
},
"ordering preserved": {
args: []string{
"-var", "foo=bar",
"-var-file", "cool.tfvars",
"-var", "boop=beep",
},
want: []FlagNameValue{
{Name: "-var", Value: "foo=bar"},
{Name: "-var-file", Value: "cool.tfvars"},
{Name: "-var", Value: "boop=beep"},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
experimentsEnabled := false
got, diags := ParseInit(tc.args, experimentsEnabled)
if len(diags) > 0 {
t.Fatalf("unexpected diags: %v", diags)
}
if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) {
t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want))
}
if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want {
t.Fatalf("expected Empty() to return %t, but was %t", want, got)
}
})
}
}