// 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/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/tofu" "github.com/zclconf/go-cty/cty" ) 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{} providerAddr := addrs.NewProvider("host", "namespace", "type") resSchema := map[string]providers.Schema{ "test_type": { Version: 0, Block: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.String, Optional: true, }, }, }, }, } 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, Ephemeral: 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, Ephemeral: 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, }, }, }, }, "resources": { Input: &configs.Config{ Module: &configs.Module{ ManagedResources: map[string]*configs.Resource{ "test_res": { Mode: addrs.ManagedResourceMode, Name: "test_res", Type: "test_type", Config: &hclsyntax.Body{ Attributes: map[string]*hclsyntax.Attribute{}, }, Provider: providerAddr, }, }, DataResources: map[string]*configs.Resource{ "test_data": { Mode: addrs.DataResourceMode, Name: "test_data", Type: "test_type", Config: &hclsyntax.Body{ Attributes: map[string]*hclsyntax.Attribute{}, }, Provider: providerAddr, }, }, EphemeralResources: map[string]*configs.Resource{ "test_ephemeral": { Mode: addrs.EphemeralResourceMode, Name: "test_ephemeral", Type: "test_type", Config: &hclsyntax.Body{ Attributes: map[string]*hclsyntax.Attribute{}, }, Provider: providerAddr, }, }, }, }, Schemas: &tofu.Schemas{ Providers: map[addrs.Provider]providers.ProviderSchema{ providerAddr: { ResourceTypes: resSchema, EphemeralResources: resSchema, DataSources: resSchema, }, }, }, Want: module{ Outputs: map[string]output{}, ModuleCalls: map[string]moduleCall{}, Resources: []resource{ { Address: "test_type.test_res", Mode: "managed", Type: "test_type", Name: "test_res", ProviderConfigKey: "test", SchemaVersion: ptrTo[uint64](0), Provisioners: nil, Expressions: make(map[string]any), }, { Address: "data.test_type.test_data", Mode: "data", Type: "test_type", Name: "test_data", ProviderConfigKey: "test", SchemaVersion: ptrTo[uint64](0), Provisioners: nil, Expressions: make(map[string]any), }, { Address: "ephemeral.test_type.test_ephemeral", Mode: "ephemeral", Type: "test_type", Name: "test_ephemeral", ProviderConfigKey: "test", SchemaVersion: ptrTo[uint64](0), Provisioners: nil, Expressions: make(map[string]any), }, }, }, }, "output, minimal": { Input: &configs.Config{ Module: &configs.Module{ Outputs: map[string]*configs.Output{ "example": { Name: "example", }, }, }, }, Schemas: emptySchemas, Want: module{ Outputs: map[string]output{ "example": { Expression: ptrTo(marshalExpression(nil)), }, }, ModuleCalls: map[string]moduleCall{}, }, }, "output, elaborate": { Input: &configs.Config{ Module: &configs.Module{ Outputs: map[string]*configs.Output{ "example": { Name: "example", Description: "description", Expr: &hclsyntax.LiteralValueExpr{Val: cty.StringVal("test")}, DependsOn: []hcl.Traversal{}, Sensitive: true, Deprecated: "deprecation message", Ephemeral: true, Preconditions: []*configs.CheckRule{ { Condition: &hclsyntax.ConditionalExpr{}, }, }, IsOverridden: false, }, }, }, }, Schemas: emptySchemas, Want: module{ Outputs: map[string]output{ "example": { Sensitive: true, Ephemeral: true, Deprecated: "deprecation message", Expression: ptrTo(marshalExpression(&hclsyntax.LiteralValueExpr{Val: cty.StringVal("test")})), Description: "description", }, }, ModuleCalls: map[string]moduleCall{}, }, }, // 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) } }) } } // ptrTo is a helper to compensate for the fact that Go doesn't allow // using the '&' operator unless the operand is directly addressable. // // Instead then, this function returns a pointer to a copy of the given // value. func ptrTo[T any](v T) *T { return &v }