2023-09-27 20:00:36 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-07-12 20:16:16 -04:00
package stackruntime
import (
"context"
2024-02-21 05:07:51 -05:00
"path/filepath"
2023-07-12 20:16:16 -04:00
"testing"
2023-07-14 14:45:04 -04:00
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
2024-02-26 05:36:19 -05:00
"github.com/zclconf/go-cty/cty"
2024-02-21 04:58:44 -05:00
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing"
2023-07-14 14:45:04 -04:00
"github.com/hashicorp/terraform/internal/tfdiags"
2023-07-12 20:16:16 -04:00
)
2024-02-26 04:42:14 -05:00
type validateTestInput struct {
2024-02-28 02:24:53 -05:00
// skip lets us write tests for behaviour we want to add in the future. Set
// this to true for any tests that are not yet implemented.
skip bool
// diags is a function that returns the expected diagnostics for the
// test.
2024-02-26 04:42:14 -05:00
diags func ( ) tfdiags . Diagnostics
2024-02-26 05:36:19 -05:00
// planInputVars is used only in the plan tests to provide a set of input
// variables to use for the plan request. Validate operates statically so
// does not need any input variables.
planInputVars map [ string ] cty . Value
2024-02-26 04:42:14 -05:00
}
var (
// validConfigurations are shared between the validate and plan tests.
validConfigurations = map [ string ] validateTestInput {
2024-02-21 05:07:51 -05:00
"empty" : { } ,
2024-03-07 15:49:39 -05:00
"plan-variable-defaults" : { } ,
2024-02-21 05:07:51 -05:00
"variable-output-roundtrip" : { } ,
"variable-output-roundtrip-nested" : { } ,
2024-02-26 05:36:19 -05:00
filepath . Join ( "with-single-input" , "input-from-component" ) : { } ,
filepath . Join ( "with-single-input" , "input-from-component-list" ) : {
planInputVars : map [ string ] cty . Value {
"components" : cty . SetVal ( [ ] cty . Value {
cty . StringVal ( "one" ) ,
cty . StringVal ( "two" ) ,
cty . StringVal ( "three" ) ,
} ) ,
} ,
} ,
2024-03-04 16:20:17 -05:00
filepath . Join ( "with-single-input" , "provider-name-clash" ) : {
planInputVars : map [ string ] cty . Value {
"input" : cty . StringVal ( "input" ) ,
} ,
} ,
filepath . Join ( "with-single-input" , "valid" ) : {
planInputVars : map [ string ] cty . Value {
"input" : cty . StringVal ( "input" ) ,
} ,
} ,
2023-07-12 20:16:16 -04:00
}
2024-02-26 04:42:14 -05:00
// invalidConfigurations are shared between the validate and plan tests.
invalidConfigurations = map [ string ] validateTestInput {
2024-02-21 04:58:44 -05:00
"validate-undeclared-variable" : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reference to undeclared input variable" ,
Detail : ` There is no variable "a" block declared in this stack. ` ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "validate-undeclared-variable/validate-undeclared-variable.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 3 , Column : 11 , Byte : 40 } ,
End : hcl . Pos { Line : 3 , Column : 16 , Byte : 45 } ,
} ,
} )
return diags
} ,
} ,
"invalid-configuration" : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unsupported argument" ,
Detail : "An argument named \"invalid\" is not expected here." ,
Subject : & hcl . Range {
2024-02-23 13:18:17 -05:00
Filename : mainBundleSourceAddrStr ( "invalid-configuration/invalid-configuration.tf" ) ,
2024-02-21 04:58:44 -05:00
Start : hcl . Pos { Line : 11 , Column : 3 , Byte : 163 } ,
End : hcl . Pos { Line : 11 , Column : 10 , Byte : 170 } ,
} ,
} )
return diags
} ,
2023-07-14 14:45:04 -04:00
} ,
2024-02-21 05:07:51 -05:00
filepath . Join ( "with-single-input" , "undeclared-provider" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Component requires undeclared provider" ,
Detail : "The root module for component.self requires a configuration for provider \"hashicorp/testing\", which isn't declared as a dependency of this stack configuration.\n\nDeclare this provider in the stack's required_providers block, and then assign a configuration for that provider in this component's \"providers\" argument." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 5 , Column : 1 , Byte : 38 } ,
End : hcl . Pos { Line : 5 , Column : 17 , Byte : 54 } ,
} ,
} )
return diags
} ,
} ,
filepath . Join ( "with-single-input" , "missing-provider" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Missing required provider configuration" ,
Detail : "The root module for component.self requires a provider configuration named \"testing\" for provider \"hashicorp/testing\", which is not assigned in the component's \"providers\" argument." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/missing-provider/missing-provider.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 14 , Column : 1 , Byte : 169 } ,
End : hcl . Pos { Line : 14 , Column : 17 , Byte : 185 } ,
} ,
} )
return diags
} ,
} ,
2024-02-27 02:43:58 -05:00
filepath . Join ( "with-single-input" , "invalid-provider-type" ) : {
2024-02-28 02:24:53 -05:00
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider configuration" ,
Detail : "The provider configuration slot testing requires a configuration for provider \"registry.terraform.io/hashicorp/testing\", not for provider \"terraform.io/builtin/testing\"." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/invalid-provider-type/invalid-provider-type.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 22 , Column : 15 , Byte : 378 } ,
End : hcl . Pos { Line : 22 , Column : 39 , Byte : 402 } ,
} ,
} )
return diags
} ,
2024-02-21 05:07:51 -05:00
} ,
2024-02-27 02:43:58 -05:00
filepath . Join ( "with-single-input" , "invalid-provider-config" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Unsupported argument" ,
Detail : "An argument named \"imaginary\" is not expected here." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/invalid-provider-config/invalid-provider-config.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 11 , Column : 5 , Byte : 218 } ,
End : hcl . Pos { Line : 11 , Column : 14 , Byte : 227 } ,
} ,
} )
return diags
} ,
} ,
2024-02-26 05:36:19 -05:00
filepath . Join ( "with-single-input" , "undeclared-variable" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reference to undeclared input variable" ,
Detail : ` There is no variable "input" block declared in this stack. ` ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 19 , Column : 13 , Byte : 284 } ,
End : hcl . Pos { Line : 19 , Column : 22 , Byte : 293 } ,
} ,
} )
return diags
} ,
} ,
filepath . Join ( "with-single-input" , "missing-variable" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid inputs for component" ,
Detail : "Invalid input variable definition object: attribute \"input\" is required." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/missing-variable/missing-variable.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 22 , Column : 12 , Byte : 338 } ,
End : hcl . Pos { Line : 22 , Column : 14 , Byte : 340 } ,
} ,
} )
return diags
} ,
} ,
filepath . Join ( "with-single-input" , "input-from-missing-component" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Reference to undeclared component" ,
Detail : "There is no component \"output\" block declared in this stack." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 19 , Column : 13 , Byte : 314 } ,
End : hcl . Pos { Line : 19 , Column : 29 , Byte : 330 } ,
} ,
} )
return diags
} ,
} ,
filepath . Join ( "with-single-input" , "input-from-provider" ) : {
diags : func ( ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid inputs for component" ,
Detail : "Invalid input variable definition object: attribute \"input\": string required." ,
Subject : & hcl . Range {
Filename : mainBundleSourceAddrStr ( "with-single-input/input-from-provider/input-from-provider.tfstack.hcl" ) ,
Start : hcl . Pos { Line : 17 , Column : 12 , Byte : 239 } ,
End : hcl . Pos { Line : 20 , Column : 4 , Byte : 339 } ,
} ,
} )
return diags
} ,
} ,
2024-02-21 04:58:44 -05:00
}
2024-02-26 04:42:14 -05:00
)
2024-02-21 04:58:44 -05:00
2024-02-26 04:42:14 -05:00
// TestValidate_valid tests that a variety of configurations under the main
// test source bundle each generate no diagnostics at all, as a
// relatively-simple way to detect accidental regressions.
//
// Any stack configuration directory that we expect should be valid can
// potentially be included in here unless it depends on provider plugins
// to complete validation, since this test cannot supply provider plugins.
func TestValidate_valid ( t * testing . T ) {
for name , tc := range validConfigurations {
t . Run ( name , func ( t * testing . T ) {
if tc . skip {
// We've added this test before the implementation was ready.
t . SkipNow ( )
}
ctx := context . Background ( )
cfg := loadMainBundleConfigForTest ( t , name )
diags := Validate ( ctx , & ValidateRequest {
Config : cfg ,
2024-02-26 05:36:19 -05:00
ProviderFactories : map [ addrs . Provider ] providers . Factory {
2024-02-28 02:24:53 -05:00
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
2024-02-26 05:36:19 -05:00
addrs . NewDefaultProvider ( "testing" ) : func ( ) ( providers . Interface , error ) {
return stacks_testing_provider . NewProvider ( ) , nil
} ,
2024-02-28 02:24:53 -05:00
addrs . NewBuiltInProvider ( "testing" ) : func ( ) ( providers . Interface , error ) {
return stacks_testing_provider . NewProvider ( ) , nil
} ,
2024-02-26 05:36:19 -05:00
} ,
2024-02-26 04:42:14 -05:00
} )
// The following will fail the test if there are any error
// diagnostics.
reportDiagnosticsForTest ( t , diags )
// We also want to fail if there are just warnings, since the
// configurations here are supposed to be totally problem-free.
if len ( diags ) != 0 {
// reportDiagnosticsForTest already showed the diagnostics in
// the log
t . FailNow ( )
}
} )
}
}
func TestValidate_invalid ( t * testing . T ) {
for name , tc := range invalidConfigurations {
2024-02-21 04:58:44 -05:00
t . Run ( name , func ( t * testing . T ) {
2024-02-21 05:07:51 -05:00
if tc . skip {
// We've added this test before the implementation was ready.
t . SkipNow ( )
}
2024-02-21 04:58:44 -05:00
ctx := context . Background ( )
cfg := loadMainBundleConfigForTest ( t , name )
gotDiags := Validate ( ctx , & ValidateRequest {
Config : cfg ,
ProviderFactories : map [ addrs . Provider ] providers . Factory {
2024-02-28 02:24:53 -05:00
// We support both hashicorp/testing and
// terraform.io/builtin/testing as providers. This lets us
// test the provider aliasing feature. Both providers
// support the same set of resources and data sources.
2024-02-21 04:58:44 -05:00
addrs . NewDefaultProvider ( "testing" ) : func ( ) ( providers . Interface , error ) {
return stacks_testing_provider . NewProvider ( ) , nil
} ,
2024-02-28 02:24:53 -05:00
addrs . NewBuiltInProvider ( "testing" ) : func ( ) ( providers . Interface , error ) {
return stacks_testing_provider . NewProvider ( ) , nil
} ,
2024-02-21 04:58:44 -05:00
} ,
} ) . ForRPC ( )
wantDiags := tc . diags ( ) . ForRPC ( )
2023-07-14 14:45:04 -04:00
2024-02-21 04:58:44 -05:00
if diff := cmp . Diff ( wantDiags , gotDiags ) ; diff != "" {
t . Errorf ( "wrong diagnostics\n%s" , diff )
}
} )
2023-07-14 14:45:04 -04:00
}
}
func TestValidate_embeddedStackSelfRef ( t * testing . T ) {
ctx := context . Background ( )
// One possible failure mode for this test is to deadlock itself if
// our deadlock detection is incorrect, so we'll try to make it bail
// if it runs too long.
ctx , cancel := context . WithTimeout ( ctx , time . Second * 5 )
defer cancel ( )
ctx , span := tracer . Start ( ctx , "TestValidate_embeddedStackSelfRef" )
defer span . End ( )
cfg := loadMainBundleConfigForTest ( t , "validate-embedded-stack-selfref" )
gotDiags := Validate ( ctx , & ValidateRequest {
Config : cfg ,
} )
// We'll normalize the diagnostics to be of consistent underlying type
// using ForRPC, so that we can easily diff them; we don't actually care
// about which underlying implementation is in use.
gotDiags = gotDiags . ForRPC ( )
var wantDiags tfdiags . Diagnostics
wantDiags = wantDiags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Self-dependent items in configuration" ,
` The following items in your configuration form a circular dependency chain through their references :
- stack . a collected outputs
- stack . a . output . a value
- stack . a inputs
Terraform uses references to decide a suitable order for performing operations , so configuration items may not refer to their own results either directly or indirectly . ` ,
) )
wantDiags = wantDiags . ForRPC ( )
if diff := cmp . Diff ( wantDiags , gotDiags ) ; diff != "" {
t . Errorf ( "wrong diagnostics\n%s" , diff )
}
}