Files
opentf/internal/command/jsonconfig/config_test.go
Martin Atkins cbfeb0fdc8 jsonconfig: Additional details about input variables
The JSON object describing an input variable can now include two additional
properties:

- "type" provides a JSON representation of the variable's type constraint,
  if one is set. Omitted if either there is no constraint declared at all
  or if it's set to "any", which are equivalent and both mean that the
  type is completely unconstrained.

  This uses the standard cty representation of a type constraint, which
  matches how OpenTofu already describes types in the provider protocol,
  in state snapshots, and in saved plan files.
- "required" directly represents whether callers are required to provide
  a value for the variable. This is technically redundant since it is
  set to true unless "default" is also set, but this avoids the need for
  consuming software to reimplement this rule and potentially allows us to
  make this rule more complicated/subtle in future if needed.

For some reason the documentation about the JSON configuration
representation did not previously mention the "variables" property at all,
so this adds documentation for both the new properties and the pre-existing
properties.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-07-11 09:25:30 -07:00

278 lines
6.9 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package jsonconfig
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/tofu"
)
func TestFindSourceProviderConfig(t *testing.T) {
tests := []struct {
StartKey string
FullName string
ProviderMap map[string]providerConfig
Want string
}{
{
StartKey: "null",
FullName: "hashicorp/null",
ProviderMap: map[string]providerConfig{},
Want: "",
},
{
StartKey: "null",
FullName: "hashicorp/null",
ProviderMap: map[string]providerConfig{
"null": {
Name: "null",
FullName: "hashicorp/null",
ModuleAddress: "",
},
},
Want: "null",
},
{
StartKey: "null2",
FullName: "hashicorp/null",
ProviderMap: map[string]providerConfig{
"null": {
Name: "null",
FullName: "hashicorp/null",
ModuleAddress: "",
},
},
Want: "",
},
{
StartKey: "null",
FullName: "hashicorp2/null",
ProviderMap: map[string]providerConfig{
"null": {
Name: "null",
FullName: "hashicorp/null",
ModuleAddress: "",
},
},
Want: "",
},
{
StartKey: "module.a:null",
FullName: "hashicorp/null",
ProviderMap: map[string]providerConfig{
"null": {
Name: "null",
FullName: "hashicorp/null",
ModuleAddress: "",
},
"module.a:null": {
Name: "module.a:null",
FullName: "hashicorp/null",
ModuleAddress: "module.a",
parentKey: "null",
},
},
Want: "null",
},
{
StartKey: "module.a:null",
FullName: "hashicorp2/null",
ProviderMap: map[string]providerConfig{
"null": {
Name: "null",
FullName: "hashicorp/null",
ModuleAddress: "",
},
"module.a:null": {
Name: "module.a:null",
FullName: "hashicorp2/null",
ModuleAddress: "module.a",
parentKey: "null",
},
},
Want: "module.a:null",
},
}
for _, test := range tests {
got := findSourceProviderKey(test.StartKey, test.FullName, test.ProviderMap)
if got != test.Want {
t.Errorf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
}
}
}
func TestMarshalModule(t *testing.T) {
emptySchemas := &tofu.Schemas{}
tests := map[string]struct {
Input *configs.Config
Schemas *tofu.Schemas
Want module
}{
"empty": {
Input: &configs.Config{
Module: &configs.Module{},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{},
ModuleCalls: map[string]moduleCall{},
},
},
"variable, minimal": {
Input: &configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"example": &configs.Variable{
Name: "example",
},
},
},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{},
ModuleCalls: map[string]moduleCall{},
Variables: variables{
"example": {
Required: true,
},
},
},
},
"variable, elaborate": {
Input: &configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"example": {
Name: "example",
Description: "description",
Deprecated: "deprecation message",
Sensitive: true,
ConstraintType: cty.String,
Type: cty.String, // similar to ConstraintType; unfortunate historical quirk
Default: cty.StringVal("hello"),
},
},
},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{},
ModuleCalls: map[string]moduleCall{},
Variables: variables{
"example": {
Type: json.RawMessage(`"string"`),
Default: json.RawMessage(`"hello"`),
Required: false,
Description: "description",
Deprecated: "deprecation message",
Sensitive: true,
},
},
},
},
"variable, collection type": {
Input: &configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"example": {
Name: "example",
ConstraintType: cty.List(cty.String),
Type: cty.List(cty.String), // similar to ConstraintType; unfortunate historical quirk
},
},
},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{},
ModuleCalls: map[string]moduleCall{},
Variables: variables{
"example": {
// The following is how cty serializes collection types
// as JSON: a two-element array where the first is
// the kind of collection and the second is the
// element type.
Type: json.RawMessage(`["list","string"]`),
Required: true,
},
},
},
},
"variable, object type": {
Input: &configs.Config{
Module: &configs.Module{
Variables: map[string]*configs.Variable{
"example": {
Name: "example",
ConstraintType: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
"foo": cty.String,
"bar": cty.String,
}, []string{"bar"}),
Type: cty.Object(map[string]cty.Type{
"foo": cty.String,
}),
},
},
},
},
Schemas: emptySchemas,
Want: module{
Outputs: map[string]output{},
ModuleCalls: map[string]moduleCall{},
Variables: variables{
"example": {
// The following is how cty serializes structural types
// as JSON: a two- or three-element array where the
// first is the kind of structure and the second is the
// kind-specific structure description, which in
// this case is a JSON object mapping attribute names
// to their types. For object types in particular,
// when at least one optional attribute is included
// the array has a third element listing the names
// of the optional attributes.
Type: json.RawMessage(`["object",{"bar":"string","foo":"string"},["bar"]]`),
Required: true,
},
},
},
},
// TODO: More test cases covering things other than input variables.
// (For now the other details are mainly tested in package command,
// as part of the tests for "tofu show".)
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
schemas := test.Schemas
// We'll make the input a little more realistic by including some
// of the cyclic pointers that would normally be inserted by the
// config loader.
input := *test.Input
input.Root = &input
input.Parent = &input
got, err := marshalModule(&input, schemas, addrs.RootModule.String())
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if diff := cmp.Diff(test.Want, got); diff != "" {
t.Error("wrong result\n" + diff)
}
})
}
}