pulumi/pkg/codegen/schema/schema_test.go

1934 lines
49 KiB
Go

// Copyright 2016-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//nolint:lll
package schema
import (
"context"
"encoding/json"
"fmt"
"math"
"net/url"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"github.com/blang/semver"
"github.com/hashicorp/hcl/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
emptypb "google.golang.org/protobuf/types/known/emptypb"
"gopkg.in/yaml.v3"
"github.com/pulumi/pulumi/pkg/v3/codegen/testing/utils"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
)
func readSchemaFile(file string) (pkgSpec PackageSpec) {
// Read in, decode, and import the schema.
schemaBytes, err := os.ReadFile(filepath.Join("..", "testing", "test", "testdata", file))
if err != nil {
panic(err)
}
if strings.HasSuffix(file, ".json") {
if err = json.Unmarshal(schemaBytes, &pkgSpec); err != nil {
panic(err)
}
} else if strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml") {
if err = yaml.Unmarshal(schemaBytes, &pkgSpec); err != nil {
panic(err)
}
} else {
panic("unknown schema file extension while parsing " + file)
}
return pkgSpec
}
func TestRoundtripRemoteTypeRef(t *testing.T) {
// Regression test for https://github.com/pulumi/pulumi/issues/13000
t.Parallel()
testdataPath := filepath.Join("..", "testing", "test", "testdata")
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := readSchemaFile("remoteref-1.0.0.json")
pkg, diags, err := BindSpec(pkgSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
newSpec, err := pkg.MarshalSpec()
require.NoError(t, err)
require.NotNil(t, newSpec)
// Try and bind again
_, diags, err = BindSpec(*newSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
}
func TestRoundtripLocalTypeRef(t *testing.T) {
// Regression test for https://github.com/pulumi/pulumi/issues/13671
t.Parallel()
testdataPath := filepath.Join("..", "testing", "test", "testdata")
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := readSchemaFile("localref-1.0.0.json")
pkg, diags, err := BindSpec(pkgSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
newSpec, err := pkg.MarshalSpec()
require.NoError(t, err)
require.NotNil(t, newSpec)
// Try and bind again
_, diags, err = BindSpec(*newSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
}
func TestRoundtripEnum(t *testing.T) {
// Regression test for https://github.com/pulumi/pulumi/issues/13921
t.Parallel()
assertEnum := func(t *testing.T, pkg *Package) {
typ, ok := pkg.GetType("enum:index:Color")
assert.True(t, ok)
enum, ok := typ.(*EnumType)
assert.True(t, ok)
assert.Equal(t, "An enum representing a color", enum.Comment)
assert.ElementsMatch(t, []*Enum{
{Value: "red"},
{Value: "green"},
{Value: "blue"},
}, enum.Elements)
}
testdataPath := filepath.Join("..", "testing", "test", "testdata")
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := readSchemaFile("enum-1.0.0.json")
pkg, diags, err := BindSpec(pkgSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
assertEnum(t, pkg)
newSpec, err := pkg.MarshalSpec()
require.NoError(t, err)
require.NotNil(t, newSpec)
// Try and bind again
pkg, diags, err = BindSpec(*newSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
assertEnum(t, pkg)
}
func TestRoundtripPlainProperties(t *testing.T) {
t.Parallel()
assertPlainnessFromType := func(t *testing.T, pkg *Package) {
exampleType, ok := pkg.GetType("plain-properties:index:ExampleType")
assert.True(t, ok)
exampleObjectType, ok := exampleType.(*ObjectType)
assert.True(t, ok)
assert.Equal(t, 3, len(exampleObjectType.Properties))
var exampleProperty *Property
var nonPlainProperty *Property
var nestedProperty *Property
for _, p := range exampleObjectType.Properties {
if p.Name == "exampleProperty" {
exampleProperty = p
}
if p.Name == "nonPlainProperty" {
nonPlainProperty = p
}
if p.Name == "nestedProperty" {
nestedProperty = p
}
}
assert.NotNil(t, exampleProperty)
assert.NotNil(t, nonPlainProperty)
assert.NotNil(t, nestedProperty)
assert.True(t, exampleProperty.Plain)
assert.False(t, nonPlainProperty.Plain)
assert.True(t, nestedProperty.Plain)
opt, ok := nestedProperty.Type.(*OptionalType)
assert.True(t, ok)
arr, ok := opt.ElementType.(*ArrayType)
assert.True(t, ok)
str, ok := arr.ElementType.(primitiveType)
assert.True(t, ok)
assert.Equal(t, stringType, str)
}
assertPlainnessFromResource := func(t *testing.T, pkg *Package) {
exampleResource, ok := pkg.GetResource("plain-properties:index:ExampleResource")
assert.True(t, ok)
check := func(properties []*Property) {
assert.Equal(t, 3, len(properties))
var exampleProperty *Property
var nonPlainProperty *Property
var nestedProperty *Property
for _, p := range properties {
if p.Name == "exampleProperty" {
exampleProperty = p
}
if p.Name == "nonPlainProperty" {
nonPlainProperty = p
}
if p.Name == "nestedProperty" {
nestedProperty = p
}
}
assert.NotNil(t, exampleProperty)
assert.NotNil(t, nonPlainProperty)
assert.NotNil(t, nestedProperty)
assert.True(t, exampleProperty.Plain)
assert.False(t, nonPlainProperty.Plain)
assert.True(t, nestedProperty.Plain)
opt, ok := nestedProperty.Type.(*OptionalType)
assert.True(t, ok)
arr, ok := opt.ElementType.(*ArrayType)
assert.True(t, ok)
str, ok := arr.ElementType.(primitiveType)
assert.True(t, ok)
assert.Equal(t, stringType, str)
}
check(exampleResource.InputProperties)
check(exampleResource.Properties)
}
testdataPath := filepath.Join("..", "testing", "test", "testdata")
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := readSchemaFile("plain-properties-1.0.0.json")
pkg, diags, err := BindSpec(pkgSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
assertPlainnessFromType(t, pkg)
assertPlainnessFromResource(t, pkg)
newSpec, err := pkg.MarshalSpec()
require.NoError(t, err)
require.NotNil(t, newSpec)
// Try and bind again
pkg, diags, err = BindSpec(*newSpec, loader)
require.NoError(t, err)
assert.Empty(t, diags)
assertPlainnessFromType(t, pkg)
assertPlainnessFromResource(t, pkg)
}
func TestImportSpec(t *testing.T) {
t.Parallel()
// Read in, decode, and import the schema.
pkgSpec := readSchemaFile("kubernetes-3.7.2.json")
pkg, err := ImportSpec(pkgSpec, nil)
if err != nil {
t.Errorf("ImportSpec() error = %v", err)
}
for _, r := range pkg.Resources {
assert.NotNil(t, r.PackageReference, "expected resource %s to have an associated Package", r.Token)
}
}
var enumTests = []struct {
filename string
shouldError bool
expected *EnumType
}{
{"bad-enum-1.json", true, nil},
{"bad-enum-2.json", true, nil},
{"bad-enum-3.json", true, nil},
{"bad-enum-4.json", true, nil},
{"good-enum-1.json", false, &EnumType{
Token: "fake-provider:module1:Color",
ElementType: stringType,
Elements: []*Enum{
{Value: "Red"},
{Value: "Orange"},
{Value: "Yellow"},
{Value: "Green"},
},
}},
{"good-enum-2.json", false, &EnumType{
Token: "fake-provider:module1:Number",
ElementType: intType,
Elements: []*Enum{
{Value: int32(1), Name: "One"},
{Value: int32(2), Name: "Two"},
{Value: int32(3), Name: "Three"},
{Value: int32(6), Name: "Six"},
},
}},
{"good-enum-3.json", false, &EnumType{
Token: "fake-provider:module1:Boolean",
ElementType: boolType,
Elements: []*Enum{
{Value: true, Name: "One"},
{Value: false, Name: "Zero"},
},
}},
{"good-enum-4.json", false, &EnumType{
Token: "fake-provider:module1:Number2",
ElementType: numberType,
Comment: "what a great description",
Elements: []*Enum{
{Value: float64(1), Comment: "one", Name: "One"},
{Value: float64(2), Comment: "two", Name: "Two"},
{Value: 3.4, Comment: "3.4", Name: "ThreePointFour"},
{Value: float64(6), Comment: "six", Name: "Six"},
},
}},
}
func TestUnmarshalYAMLFunctionSpec(t *testing.T) {
t.Parallel()
var functionSpec *FunctionSpec
fnYaml := `
description: Test function
outputs:
type: number`
err := yaml.Unmarshal([]byte(fnYaml), &functionSpec)
assert.Nil(t, err, "Unmarshalling should work")
assert.Equal(t, "Test function", functionSpec.Description)
assert.NotNil(t, functionSpec.ReturnType, "Return type is not nil")
assert.NotNil(t, functionSpec.ReturnType.TypeSpec, "Return type is a type spec")
assert.Equal(t, "number", functionSpec.ReturnType.TypeSpec.Type, "Return type is a number")
}
func TestUnmarshalJSONFunctionSpec(t *testing.T) {
t.Parallel()
var functionSpec *FunctionSpec
fnJSON := `{"description":"Test function", "outputs": { "type": "number" } }`
err := json.Unmarshal([]byte(fnJSON), &functionSpec)
assert.Nil(t, err, "Unmarshalling should work")
assert.Equal(t, "Test function", functionSpec.Description)
assert.NotNil(t, functionSpec.ReturnType, "Return type is not nil")
assert.NotNil(t, functionSpec.ReturnType.TypeSpec, "Return type is a type spec")
assert.Equal(t, "number", functionSpec.ReturnType.TypeSpec.Type, "Return type is a number")
}
func TestMarshalJSONFunctionSpec(t *testing.T) {
t.Parallel()
functionSpec := &FunctionSpec{
Description: "Test function",
ReturnType: &ReturnTypeSpec{
TypeSpec: &TypeSpec{Type: "number"},
},
}
dataJSON, err := json.Marshal(functionSpec)
data := string(dataJSON)
expectedJSON := `{"description":"Test function","outputs":{"type":"number"}}`
assert.Nil(t, err, "Unmarshalling should work")
assert.Equal(t, expectedJSON, data)
}
func TestMarshalJSONFunctionSpecWithOutputs(t *testing.T) {
t.Parallel()
functionSpec := &FunctionSpec{
Description: "Test function",
Outputs: &ObjectTypeSpec{
Type: "object",
Properties: map[string]PropertySpec{
"foo": {
TypeSpec: TypeSpec{Type: "string"},
},
},
},
}
dataJSON, err := json.Marshal(functionSpec)
data := string(dataJSON)
expectedJSON := `{"description":"Test function","outputs":{"properties":{"foo":{"type":"string"}},"type":"object"}}`
assert.Nil(t, err, "Unmarshalling should work")
assert.Equal(t, expectedJSON, data)
}
func TestMarshalYAMLFunctionSpec(t *testing.T) {
t.Parallel()
functionSpec := &FunctionSpec{
Description: "Test function",
ReturnType: &ReturnTypeSpec{
TypeSpec: &TypeSpec{Type: "number"},
},
}
dataYAML, err := yaml.Marshal(functionSpec)
data := string(dataYAML)
expectedYAML := `description: Test function
outputs:
type: number
`
assert.Nil(t, err, "Unmarshalling should work")
assert.Equal(t, expectedYAML, data)
}
func TestInvalidTypes(t *testing.T) {
t.Parallel()
tests := []struct {
filename string
expected string
}{
{"bad-type-1.json", "invalid token 'fake-provider:index:provider' (provider is a reserved word for the root module)"},
{"bad-type-2.json", "invalid token 'fake-provider::provider' (provider is a reserved word for the root module)"},
{"bad-type-3.json", "invalid token 'fake-provider:noModulePart' (should have three parts)"},
{"bad-type-4.json", "invalid token 'noParts' (should have three parts); "},
}
for _, tt := range tests {
tt := tt
t.Run(tt.filename, func(t *testing.T) {
t.Parallel()
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
_, err := ImportSpec(pkgSpec, nil)
assert.ErrorContains(t, err, tt.expected)
})
}
}
func TestEnums(t *testing.T) {
t.Parallel()
for _, tt := range enumTests {
tt := tt
t.Run(tt.filename, func(t *testing.T) {
t.Parallel()
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
pkg, err := ImportSpec(pkgSpec, nil)
if tt.shouldError {
assert.Error(t, err)
} else {
if err != nil {
t.Error(err)
}
result := pkg.Types[0]
tt.expected.PackageReference = pkg.Reference()
assert.Equal(t, tt.expected, result)
}
})
}
}
func TestRejectDuplicateNames(t *testing.T) {
t.Parallel()
tests := []struct {
name string
spec PackageSpec
expected hcl.Diagnostics
}{
{
"resource function no duplicate",
PackageSpec{
Name: "test",
Resources: map[string]ResourceSpec{
"test:index:Res": {},
},
Functions: map[string]FunctionSpec{
"test:index:Fun": {},
},
},
hcl.Diagnostics(nil),
},
{
"resource function duplicate",
PackageSpec{
Name: "test",
Resources: map[string]ResourceSpec{
"test:index:Duplicate": {},
"test:index:Res2": {},
},
Functions: map[string]FunctionSpec{
"test:index:Duplicate": {},
},
},
hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "#/functions/test:index:Duplicate: multiple tokens map to test:index:duplicate",
Detail: "other paths(s) are #/resources/test:index:Duplicate",
},
{
Severity: hcl.DiagError,
Summary: "#/resources/test:index:Duplicate: multiple tokens map to test:index:duplicate",
Detail: "other paths(s) are #/functions/test:index:Duplicate",
},
},
},
{
"resource function type duplicate",
PackageSpec{
Name: "test",
Resources: map[string]ResourceSpec{
"test:index:Duplicate": {},
},
Functions: map[string]FunctionSpec{
"test:index:Duplicate": {},
},
Types: map[string]ComplexTypeSpec{
"test:index:Duplicate": {ObjectTypeSpec: ObjectTypeSpec{Type: "object"}},
},
},
hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "#/functions/test:index:Duplicate: multiple tokens map to test:index:duplicate",
Detail: "other paths(s) are #/resources/test:index:Duplicate",
},
{
Severity: hcl.DiagError,
Summary: "#/resources/test:index:Duplicate: multiple tokens map to test:index:duplicate",
Detail: "other paths(s) are #/functions/test:index:Duplicate",
},
},
},
{
"difference by case",
PackageSpec{
Name: "test",
Resources: map[string]ResourceSpec{
"test:index:Duplicate": {},
"test:index:duplicatE": {},
},
},
hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "#/resources/test:index:Duplicate: multiple tokens map to test:index:duplicate",
Detail: "other paths(s) are #/resources/test:index:duplicatE",
},
{
Severity: hcl.DiagError,
Summary: "#/resources/test:index:duplicatE: multiple tokens map to test:index:duplicate",
Detail: "other paths(s) are #/resources/test:index:Duplicate",
},
},
},
{
"difference by case across kinds",
PackageSpec{
Name: "test",
Resources: map[string]ResourceSpec{
"test:index:DupeName": {},
},
Functions: map[string]FunctionSpec{
"test:index:dupeName": {},
},
Types: map[string]ComplexTypeSpec{
"test:index:Dupename": {ObjectTypeSpec: ObjectTypeSpec{Type: "object"}},
},
},
hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "#/functions/test:index:dupeName: multiple tokens map to test:index:dupename",
Detail: "other paths(s) are #/resources/test:index:DupeName",
},
{
Severity: hcl.DiagError,
Summary: "#/resources/test:index:DupeName: multiple tokens map to test:index:dupename",
Detail: "other paths(s) are #/functions/test:index:dupeName",
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, diags, err := BindSpec(tt.spec, NewPluginLoader(utils.NewHost(testdataPath)))
require.NoError(t, err)
assert.Equal(t, tt.expected, diags)
})
}
}
//nolint:paralleltest // needs to set plugin acquisition env var
func TestImportResourceRef(t *testing.T) {
tests := []struct {
name string
schemaFile string
wantErr bool
validator func(pkg *Package)
}{
{
"simple",
"simple-resource-schema/schema.json",
false,
func(pkg *Package) {
for _, r := range pkg.Resources {
if r.Token == "example::OtherResource" {
for _, p := range r.Properties {
if p.Name == "foo" {
assert.IsType(t, &ResourceType{}, plainType(p.Type))
}
}
}
}
},
},
{
"external-ref",
"external-resource-schema/schema.json",
false,
func(pkg *Package) {
typ, ok := pkg.GetType("example::Pet")
assert.True(t, ok)
pet, ok := typ.(*ObjectType)
assert.True(t, ok)
name, ok := pet.Property("name")
assert.True(t, ok)
assert.IsType(t, &ResourceType{}, plainType(name.Type))
resource := plainType(name.Type).(*ResourceType)
assert.NotNil(t, resource.Resource)
for _, r := range pkg.Resources {
switch r.Token {
case "example::Cat":
for _, p := range r.Properties {
if p.Name == "name" {
assert.IsType(t, stringType, plainType(p.Type))
}
}
case "example::Workload":
for _, p := range r.Properties {
if p.Name == "pod" {
assert.IsType(t, &ObjectType{}, plainType(p.Type))
obj := plainType(p.Type).(*ObjectType)
assert.NotNil(t, obj.Properties)
}
}
}
}
},
},
}
//nolint:paralleltest // needs to set plugin acquisition env var
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Setenv("PULUMI_DISABLE_AUTOMATIC_PLUGIN_ACQUISITION", "false")
// Read in, decode, and import the schema.
schemaBytes, err := os.ReadFile(
filepath.Join("..", "testing", "test", "testdata", tt.schemaFile))
assert.NoError(t, err)
var pkgSpec PackageSpec
err = json.Unmarshal(schemaBytes, &pkgSpec)
assert.NoError(t, err)
pkg, err := ImportSpec(pkgSpec, nil)
if (err != nil) != tt.wantErr {
t.Errorf("ImportSpec() error = %v, wantErr %v", err, tt.wantErr)
return
}
tt.validator(pkg)
})
}
}
func Test_parseTypeSpecRef(t *testing.T) {
t.Parallel()
toVersionPtr := func(version string) *semver.Version { v := semver.MustParse(version); return &v }
toURL := func(rawurl string) *url.URL {
parsed, err := url.Parse(rawurl)
assert.NoError(t, err, "failed to parse ref")
return parsed
}
typs := &types{
pkg: &Package{
Name: "test",
Version: toVersionPtr("1.2.3"),
},
}
tests := []struct {
name string
ref string
want typeSpecRef
wantErr bool
}{
{
name: "resourceRef",
ref: "#/resources/example::Resource",
want: typeSpecRef{
URL: toURL("#/resources/example::Resource"),
Package: "test",
Version: toVersionPtr("1.2.3"),
Kind: "resources",
Token: "example::Resource",
},
},
{
name: "typeRef",
ref: "#/types/kubernetes:admissionregistration.k8s.io%2fv1:WebhookClientConfig",
want: typeSpecRef{
URL: toURL("#/types/kubernetes:admissionregistration.k8s.io%2fv1:WebhookClientConfig"),
Package: "test",
Version: toVersionPtr("1.2.3"),
Kind: "types",
Token: "kubernetes:admissionregistration.k8s.io/v1:WebhookClientConfig",
},
},
{
name: "providerRef",
ref: "#/provider",
want: typeSpecRef{
URL: toURL("#/provider"),
Package: "test",
Version: toVersionPtr("1.2.3"),
Kind: "provider",
Token: "pulumi:providers:test",
},
},
{
name: "externalResourceRef",
ref: "/random/v2.3.1/schema.json#/resources/random:index%2frandomPet:RandomPet",
want: typeSpecRef{
URL: toURL("/random/v2.3.1/schema.json#/resources/random:index%2frandomPet:RandomPet"),
Package: "random",
Version: toVersionPtr("2.3.1"),
Kind: "resources",
Token: "random:index/randomPet:RandomPet",
},
},
{
name: "invalid externalResourceRef",
ref: "/random/schema.json#/resources/random:index%2frandomPet:RandomPet",
wantErr: true,
},
{
name: "externalTypeRef",
ref: "/kubernetes/v2.6.3/schema.json#/types/kubernetes:admissionregistration.k8s.io%2Fv1:WebhookClientConfig",
want: typeSpecRef{
URL: toURL("/kubernetes/v2.6.3/schema.json#/types/kubernetes:admissionregistration.k8s.io%2Fv1:WebhookClientConfig"),
Package: "kubernetes",
Version: toVersionPtr("2.6.3"),
Kind: "types",
Token: "kubernetes:admissionregistration.k8s.io/v1:WebhookClientConfig",
},
},
{
name: "externalHostResourceRef",
ref: "https://example.com/random/v2.3.1/schema.json#/resources/random:index%2FrandomPet:RandomPet",
want: typeSpecRef{
URL: toURL("https://example.com/random/v2.3.1/schema.json#/resources/random:index%2FrandomPet:RandomPet"),
Package: "random",
Version: toVersionPtr("2.3.1"),
Kind: "resources",
Token: "random:index/randomPet:RandomPet",
},
},
{
name: "externalProviderRef",
ref: "/kubernetes/v2.6.3/schema.json#/provider",
want: typeSpecRef{
URL: toURL("/kubernetes/v2.6.3/schema.json#/provider"),
Package: "kubernetes",
Version: toVersionPtr("2.6.3"),
Kind: "provider",
Token: "pulumi:providers:kubernetes",
},
},
{
name: "hyphenatedUrlPath",
ref: "/azure-native/v1.22.0/schema.json#/resources/azure-native:web:WebApp",
want: typeSpecRef{
URL: toURL("/azure-native/v1.22.0/schema.json#/resources/azure-native:web:WebApp"),
Package: "azure-native",
Version: toVersionPtr("1.22.0"),
Kind: "resources",
Token: "azure-native:web:WebApp",
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, diags := typs.parseTypeSpecRef("ref", tt.ref)
if diags.HasErrors() != tt.wantErr {
t.Errorf("parseTypeSpecRef() diags = %v, wantErr %v", diags, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseTypeSpecRef() got = %v, want %v", got, tt.want)
}
})
}
}
func TestUsingUrnInResourcePropertiesEmitsWarning(t *testing.T) {
t.Parallel()
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := PackageSpec{
Name: "test",
Version: "1.0.0",
Resources: map[string]ResourceSpec{
"test:index:TestResource": {
ObjectTypeSpec: ObjectTypeSpec{
Properties: map[string]PropertySpec{
"urn": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
},
"test:index:TestComponent": {
IsComponent: true,
ObjectTypeSpec: ObjectTypeSpec{
Properties: map[string]PropertySpec{
"urn": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
},
},
}
pkg, diags, err := BindSpec(pkgSpec, loader)
// No error as binding should work fine even with warnings
assert.NoError(t, err)
// assert that there are 2 warnings in the diagnostics because of using URN as a property
assert.Len(t, diags, 2)
for _, diag := range diags {
assert.Equal(t, diag.Severity, hcl.DiagWarning)
assert.Contains(t, diag.Summary, "urn is a reserved property name")
}
assert.NotNil(t, pkg)
}
func TestUsingIdInResourcePropertiesEmitsWarning(t *testing.T) {
t.Parallel()
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := PackageSpec{
Name: "test",
Version: "1.0.0",
Resources: map[string]ResourceSpec{
"test:index:TestResource": {
ObjectTypeSpec: ObjectTypeSpec{
Properties: map[string]PropertySpec{
"id": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
},
},
}
pkg, diags, err := BindSpec(pkgSpec, loader)
// No error as binding should work fine even with warnings
assert.NoError(t, err)
assert.NotNil(t, pkg)
// assert that there is 1 warning in the diagnostics because of using ID as a property
assert.Len(t, diags, 1)
assert.Equal(t, diags[0].Severity, hcl.DiagWarning)
assert.Contains(t, diags[0].Summary, "id is a reserved property name")
}
func TestOmittingVersionWhenSupportsPackEnabledGivesError(t *testing.T) {
t.Parallel()
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := PackageSpec{
Name: "test",
Meta: &MetadataSpec{
SupportPack: true,
},
Resources: map[string]ResourceSpec{},
}
_, diags, _ := BindSpec(pkgSpec, loader)
assert.Len(t, diags, 1)
assert.Equal(t, diags[0].Severity, hcl.DiagError)
assert.Contains(t, diags[0].Summary, "version must be provided when package supports packing")
}
func TestUsingIdInComponentResourcePropertiesEmitsNoWarning(t *testing.T) {
t.Parallel()
loader := NewPluginLoader(utils.NewHost(testdataPath))
pkgSpec := PackageSpec{
Name: "test",
Version: "1.0.0",
Resources: map[string]ResourceSpec{
"test:index:TestComponent": {
IsComponent: true,
ObjectTypeSpec: ObjectTypeSpec{
Properties: map[string]PropertySpec{
"id": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
},
},
}
pkg, diags, err := BindSpec(pkgSpec, loader)
assert.NoError(t, err)
assert.Empty(t, diags)
assert.NotNil(t, pkg)
}
func TestMethods(t *testing.T) {
t.Parallel()
tests := []struct {
filename string
validator func(pkg *Package)
expectedError string
}{
{
filename: "good-methods-1.json",
validator: func(pkg *Package) {
assert.Len(t, pkg.Resources, 1)
assert.Len(t, pkg.Resources[0].Methods, 1)
assert.NotNil(t, pkg.Resources[0].Methods[0].Function.Inputs)
assert.Len(t, pkg.Resources[0].Methods[0].Function.Inputs.Properties, 1)
inputs := pkg.Resources[0].Methods[0].Function.Inputs.Properties
assert.Equal(t, "__self__", inputs[0].Name)
assert.Equal(t, &ResourceType{
Token: pkg.Resources[0].Token,
Resource: pkg.Resources[0],
}, inputs[0].Type)
var objectReturnType *ObjectType
if objectType, ok := pkg.Resources[0].Methods[0].Function.ReturnType.(*ObjectType); ok && objectType != nil {
objectReturnType = objectType
}
assert.NotNil(t, objectReturnType)
assert.Len(t, objectReturnType.Properties, 1)
outputs := objectReturnType.Properties
assert.Equal(t, "someValue", outputs[0].Name)
assert.Equal(t, StringType, outputs[0].Type)
assert.Len(t, pkg.Functions, 1)
assert.True(t, pkg.Functions[0].IsMethod)
assert.Same(t, pkg.Resources[0].Methods[0].Function, pkg.Functions[0])
},
},
{
filename: "good-simplified-methods.json",
validator: func(pkg *Package) {
assert.Len(t, pkg.Functions, 1)
assert.NotNil(t, pkg.Functions[0].ReturnType, "There should be a return type")
assert.Equal(t, pkg.Functions[0].ReturnType, NumberType)
},
},
{
filename: "good-simplified-methods.yml",
validator: func(pkg *Package) {
assert.Len(t, pkg.Functions, 1)
assert.NotNil(t, pkg.Functions[0].ReturnType, "There should be a return type")
assert.Equal(t, pkg.Functions[0].ReturnType, NumberType)
},
},
{
filename: "bad-methods-1.json",
expectedError: "unknown function xyz:index:Foo/bar",
},
{
filename: "bad-methods-2.json",
expectedError: "function xyz:index:Foo/bar is already a method",
},
{
filename: "bad-methods-3.json",
expectedError: "invalid function token format xyz:index:Foo",
},
{
filename: "bad-methods-4.json",
expectedError: "invalid function token format xyz:index:Baz/bar",
},
{
filename: "bad-methods-5.json",
expectedError: "function xyz:index:Foo/bar has no __self__ parameter",
},
{
filename: "bad-methods-6.json",
expectedError: "xyz:index:Foo already has a property named bar",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.filename, func(t *testing.T) {
t.Parallel()
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
pkg, err := ImportSpec(pkgSpec, nil)
if tt.expectedError != "" {
assert.ErrorContains(t, err, tt.expectedError)
} else {
if err != nil {
t.Error(err)
}
tt.validator(pkg)
}
})
}
}
// TestIsOverlay tests that the IsOverlay field is set correctly for resources, types, and functions. Does not test
// codegen.
func TestIsOverlay(t *testing.T) {
t.Parallel()
t.Run("overlay", func(t *testing.T) {
t.Parallel()
pkgSpec := readSchemaFile(filepath.Join("schema", "overlay.json"))
pkg, err := ImportSpec(pkgSpec, nil)
if err != nil {
t.Error(err)
}
for _, v := range pkg.Resources {
if strings.Contains(v.Token, "Overlay") {
assert.Truef(t, v.IsOverlay, "resource %q", v.Token)
} else {
assert.Falsef(t, v.IsOverlay, "resource %q", v.Token)
}
}
for _, v := range pkg.Types {
switch v := v.(type) {
case *ObjectType:
if strings.Contains(v.Token, "Overlay") {
assert.Truef(t, v.IsOverlay, "object type %q", v.Token)
} else {
assert.Falsef(t, v.IsOverlay, "object type %q", v.Token)
}
}
}
for _, v := range pkg.Functions {
if strings.Contains(v.Token, "Overlay") {
assert.Truef(t, v.IsOverlay, "function %q", v.Token)
} else {
assert.Falsef(t, v.IsOverlay, "function %q", v.Token)
}
}
})
}
// TestOverlaySupportedLanguages tests that the OverlaySupportedLanguages field is set correctly for resources, types, and functions.
// Does not test codegen.
func TestOverlaySupportedLanguages(t *testing.T) {
t.Parallel()
t.Run("overlay", func(t *testing.T) {
t.Parallel()
pkgSpec := readSchemaFile(filepath.Join("schema", "overlay-supported-languages.json"))
pkg, err := ImportSpec(pkgSpec, nil)
if err != nil {
t.Error(err)
}
for _, v := range pkg.Resources {
if strings.Contains(v.Token, "Overlay") {
assert.Truef(t, v.IsOverlay, "resource %q", v.Token)
} else {
assert.Falsef(t, v.IsOverlay, "resource %q", v.Token)
}
if strings.Contains(v.Token, "ConstrainedLanguages") {
assert.Equalf(t, []string{"go", "nodejs", "python"}, v.OverlaySupportedLanguages, "resource %q", v.Token)
} else {
assert.Nilf(t, v.OverlaySupportedLanguages, "resource %q", v.Token)
}
}
for _, v := range pkg.Types {
switch v := v.(type) {
case *ObjectType:
if strings.Contains(v.Token, "Overlay") {
assert.Truef(t, v.IsOverlay, "object type %q", v.Token)
} else {
assert.Falsef(t, v.IsOverlay, "object type %q", v.Token)
}
assert.Nilf(t, v.OverlaySupportedLanguages, "resource %q", v.Token)
}
}
for _, v := range pkg.Functions {
if strings.Contains(v.Token, "Overlay") {
assert.Truef(t, v.IsOverlay, "function %q", v.Token)
} else {
assert.Falsef(t, v.IsOverlay, "function %q", v.Token)
}
if strings.Contains(v.Token, "ConstrainedLanguages") {
assert.Equalf(t, []string{"go", "nodejs", "python"}, v.OverlaySupportedLanguages, "resource %q", v.Token)
} else {
assert.Nilf(t, v.OverlaySupportedLanguages, "resource %q", v.Token)
}
}
})
}
func TestBindingOutputsPopulatesReturnType(t *testing.T) {
t.Parallel()
// Test that using Outputs in PackageSpec correctly populates the return type of the function.
pkgSpec := PackageSpec{
Name: "xyz",
Version: "0.0.1",
Functions: map[string]FunctionSpec{
"xyz:index:abs": {
MultiArgumentInputs: []string{"value"},
Inputs: &ObjectTypeSpec{
Properties: map[string]PropertySpec{
"value": {
TypeSpec: TypeSpec{
Type: "number",
},
},
},
},
Outputs: &ObjectTypeSpec{
Required: []string{"result"},
Properties: map[string]PropertySpec{
"result": {
TypeSpec: TypeSpec{
Type: "number",
},
},
},
},
},
},
}
pkg, err := ImportSpec(pkgSpec, nil)
if err != nil {
t.Error(err)
}
assert.NotNil(t, pkg.Functions[0].ReturnType)
objectType, ok := pkg.Functions[0].ReturnType.(*ObjectType)
assert.True(t, ok)
assert.Equal(t, NumberType, objectType.Properties[0].Type)
}
// Tests that the method ReplaceOnChanges works as expected. Does not test
// codegen.
func TestReplaceOnChanges(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
name string
filePath string
resource string
result []string
errors []string
}{
{
name: "Simple case",
filePath: "replace-on-changes-1.json",
resource: "example::Dog",
result: []string{"bone"},
},
{
name: "No replaceOnChanges",
filePath: "replace-on-changes-2.json",
resource: "example::Dog",
},
{
name: "Mutually Recursive",
filePath: "replace-on-changes-3.json",
resource: "example::Pets",
result: []string{
"cat.fish",
"dog.bone",
"dog.cat.fish",
"cat.dog.bone",
},
errors: []string{
"Failed to genereate full `ReplaceOnChanges`: Found recursive object \"cat\"",
"Failed to genereate full `ReplaceOnChanges`: Found recursive object \"dog\"",
},
},
{
name: "Singularly Recursive",
filePath: "replace-on-changes-4.json",
resource: "example::Pets",
result: []string{"dog.bone"},
errors: []string{"Failed to genereate full `ReplaceOnChanges`: Found recursive object \"dog\""},
},
{
name: "Drill Correctly",
filePath: "replace-on-changes-5.json",
resource: "example::Pets",
result: []string{"foes.*.color", "friends[*].color", "name", "toy.color"},
},
{
name: "No replace on changes and recursive",
filePath: "replace-on-changes-6.json",
resource: "example::Child",
result: []string{},
errors: []string{},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// We sort each result before comparison. We don't enforce that the
// results have the same order, just the same content.
sort.Strings(tt.result)
sort.Strings(tt.errors)
pkgSpec := readSchemaFile(
filepath.Join("schema", tt.filePath))
pkg, err := ImportSpec(pkgSpec, nil)
assert.NoError(t, err, "Import should be successful")
resource, found := pkg.GetResource(tt.resource)
assert.True(t, found, "The resource should exist")
replaceOnChanges, errListErrors := resource.ReplaceOnChanges()
errList := make([]string, len(errListErrors))
for i, e := range errListErrors {
errList[i] = e.Error()
}
actualResult := PropertyListJoinToString(replaceOnChanges,
func(x string) string { return x })
sort.Strings(actualResult)
if tt.result != nil || len(actualResult) > 0 {
assert.Equal(t, tt.result, actualResult,
"Get the correct result")
}
if tt.errors != nil || len(errList) > 0 {
assert.Equal(t, tt.errors, errList,
"Get correct error messages")
}
})
}
}
func TestValidateTypeToken(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
expectError bool
allowedExtras []string
}{
{
name: "valid",
input: "example::typename",
},
{
name: "invalid",
input: "xyz::typename",
expectError: true,
},
{
name: "valid-has-subsection",
input: "example:index:typename",
},
{
name: "invalid-has-subsection",
input: "not:index:typename",
expectError: true,
},
{
name: "allowed-extras-valid",
input: "other:index:typename",
allowedExtras: []string{"other"},
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
spec := &PackageSpec{Name: "example"}
allowed := map[string]bool{"example": true}
for _, e := range c.allowedExtras {
allowed[e] = true
}
errors := spec.validateTypeToken(allowed, "type", c.input)
if c.expectError {
assert.True(t, errors.HasErrors())
} else {
assert.False(t, errors.HasErrors())
}
})
}
}
func TestTypeString(t *testing.T) {
t.Parallel()
cases := []struct {
input Type
output string
}{
{
input: &UnionType{
ElementTypes: []Type{
StringType,
NumberType,
},
},
output: "Union<string, number>",
},
{
input: &UnionType{
ElementTypes: []Type{
StringType,
},
DefaultType: NumberType,
},
output: "Union<string, default=number>",
},
}
for _, c := range cases {
c := c
t.Run(c.output, func(t *testing.T) {
t.Parallel()
assert.Equal(t, c.output, c.input.String())
})
}
}
func TestPackageIdentity(t *testing.T) {
t.Parallel()
cases := []struct {
nameA string
versionA string
nameB string
versionB string
equal bool
}{
{
nameA: "example",
nameB: "example",
equal: true,
},
{
nameA: "example",
versionA: "1.0.0",
nameB: "example",
versionB: "1.0.0",
equal: true,
},
{
nameA: "example",
versionA: "1.2.3-beta",
nameB: "example",
versionB: "1.2.3-beta",
equal: true,
},
{
nameA: "example",
versionA: "1.2.3-beta+1234",
nameB: "example",
versionB: "1.2.3-beta+1234",
equal: true,
},
{
nameA: "example",
versionA: "1.0.0",
nameB: "example",
},
{
nameA: "example",
nameB: "example",
versionB: "1.0.0",
},
{
nameA: "example",
versionA: "1.0.0",
nameB: "example",
versionB: "1.2.3-beta",
},
{
nameA: "example",
versionA: "1.2.3-beta+1234",
nameB: "example",
versionB: "1.2.3-beta+5678",
},
}
for _, c := range cases {
c := c
t.Run(c.nameA, func(t *testing.T) {
t.Parallel()
var verA *semver.Version
if c.versionA != "" {
v := semver.MustParse(c.versionA)
verA = &v
}
var verB *semver.Version
if c.versionB != "" {
v := semver.MustParse(c.versionB)
verB = &v
}
pkgA := &Package{Name: c.nameA, Version: verA}
pkgB := &Package{Name: c.nameB, Version: verB}
if c.equal {
assert.Equal(t, packageIdentity(c.nameA, verA), packageIdentity(c.nameB, verB))
assert.Equal(t, pkgA.Identity(), pkgB.Identity())
assert.True(t, pkgA.Equals(pkgB))
} else {
assert.NotEqual(t, packageIdentity(c.nameA, verA), packageIdentity(c.nameB, verB))
assert.NotEqual(t, pkgA.Identity(), pkgB.Identity())
assert.False(t, pkgA.Equals(pkgB))
}
})
}
}
func TestBindDefaultInt(t *testing.T) {
t.Parallel()
dv, diag := bindDefaultValue("fake-path", int(32), nil, IntType)
if diag.HasErrors() {
t.Fail()
}
assert.Equal(t, int32(32), dv.Value)
// Check that we error on overflow/underflow when casting int to int32.
if _, diag := bindDefaultValue("fake-path", int(math.MaxInt64), nil, IntType); !diag.HasErrors() {
assert.Fail(t, "did not catch oveflow")
t.Fail()
}
if _, diag := bindDefaultValue("fake-path", int(math.MinInt64), nil, IntType); !diag.HasErrors() {
assert.Fail(t, "did not catch underflow")
}
}
func TestMarshalResourceWithLanguageSettings(t *testing.T) {
t.Parallel()
prop := &Property{
Name: "prop1",
Language: map[string]interface{}{
"csharp": map[string]string{
"name": "CSharpProp1",
},
},
Type: stringType,
}
r := Resource{
Token: "xyz:index:resource",
Properties: []*Property{
prop,
},
Language: map[string]interface{}{
"csharp": map[string]string{
"name": "CSharpResource",
},
},
}
p := Package{
Name: "xyz",
DisplayName: "xyz package",
Version: &semver.Version{
Major: 0,
Minor: 0,
Patch: 0,
},
Provider: &Resource{
IsProvider: true,
Token: "provider",
},
Resources: []*Resource{
&r,
},
}
pspec, err := p.MarshalSpec()
assert.NoError(t, err)
res, ok := pspec.Resources[r.Token]
assert.True(t, ok)
assert.Contains(t, res.Language, "csharp")
assert.IsType(t, RawMessage{}, res.Language["csharp"])
prspec, ok := res.Properties[prop.Name]
assert.True(t, ok)
assert.Contains(t, prspec.Language, "csharp")
assert.IsType(t, RawMessage{}, prspec.Language["csharp"])
}
func TestFunctionSpecToJSONAndYAMLTurnaround(t *testing.T) {
t.Parallel()
type testCase struct {
name string
fspec FunctionSpec
serial any
// For legacy forms, after turning around through serde FunctionSpec will be
// normalized and not exactly equal to the original; tests will check against the
// normalized form if provided.
normalized *FunctionSpec
}
ots := &ObjectTypeSpec{
Type: "object",
Properties: map[string]PropertySpec{
"x": {
TypeSpec: TypeSpec{
Type: "integer",
},
},
},
}
otsPlain := &ObjectTypeSpec{
Type: "object",
Properties: map[string]PropertySpec{
"x": {
TypeSpec: TypeSpec{
Type: "integer",
},
},
},
Plain: []string{"x"},
}
testCases := []testCase{
{
name: "legacy-outputs-form",
fspec: FunctionSpec{
Outputs: ots,
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"properties": map[string]interface{}{
"x": map[string]interface{}{
"type": "integer",
},
},
"type": "object",
},
},
normalized: &FunctionSpec{
ReturnType: &ReturnTypeSpec{
ObjectTypeSpec: ots,
},
},
},
{
name: "legacy-outputs-form-plain-array",
fspec: FunctionSpec{
Outputs: otsPlain,
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"properties": map[string]interface{}{
"x": map[string]interface{}{
"type": "integer",
},
},
"plain": []interface{}{"x"},
"type": "object",
},
},
normalized: &FunctionSpec{
ReturnType: &ReturnTypeSpec{
ObjectTypeSpec: otsPlain,
},
},
},
{
name: "return-plain-integer",
fspec: FunctionSpec{
ReturnType: &ReturnTypeSpec{
TypeSpec: &TypeSpec{
Type: "integer",
Plain: true,
},
},
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"plain": true,
"type": "integer",
},
},
},
{
name: "return-integer",
fspec: FunctionSpec{
ReturnType: &ReturnTypeSpec{
TypeSpec: &TypeSpec{
Type: "integer",
},
},
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"type": "integer",
},
},
},
{
name: "return-plain-object",
fspec: FunctionSpec{
ReturnType: &ReturnTypeSpec{
ObjectTypeSpec: ots,
ObjectTypeSpecIsPlain: true,
},
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"plain": true,
"properties": map[string]interface{}{
"x": map[string]interface{}{
"type": "integer",
},
},
"type": "object",
},
},
},
{
name: "return-object",
fspec: FunctionSpec{
ReturnType: &ReturnTypeSpec{
ObjectTypeSpec: ots,
},
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"properties": map[string]interface{}{
"x": map[string]interface{}{
"type": "integer",
},
},
"type": "object",
},
},
},
{
name: "return-object-plain-array",
fspec: FunctionSpec{
ReturnType: &ReturnTypeSpec{
ObjectTypeSpec: otsPlain,
},
},
serial: map[string]interface{}{
"outputs": map[string]interface{}{
"plain": []interface{}{"x"},
"properties": map[string]interface{}{
"x": map[string]interface{}{
"type": "integer",
},
},
"type": "object",
},
},
},
}
for _, tc := range testCases {
tc := tc
fspec := tc.fspec
expectSerial := tc.serial
expectFSpec := fspec
if tc.normalized != nil {
expectFSpec = *tc.normalized
}
// Test JSON serialization and turnaround.
t.Run(tc.name+"/json", func(t *testing.T) {
t.Parallel()
var serial any
bytes, err := json.MarshalIndent(fspec, "", " ")
require.NoError(t, err)
err = json.Unmarshal(bytes, &serial)
require.NoError(t, err)
require.Equalf(t, expectSerial, serial, "Unexpected JSON serial form")
var actual FunctionSpec
err = json.Unmarshal(bytes, &actual)
require.NoError(t, err)
require.Equal(t, expectFSpec, actual)
})
// Test YAML serialization and turnaround.
t.Run(tc.name+"/yaml", func(t *testing.T) {
t.Parallel()
var serial any
bytes, err := yaml.Marshal(fspec)
require.NoError(t, err)
err = yaml.Unmarshal(bytes, &serial)
require.NoError(t, err)
require.Equalf(t, expectSerial, serial, "Unexpected YAML serial form")
var actual FunctionSpec
err = yaml.Unmarshal(bytes, &actual)
require.NoError(t, err)
require.Equal(t, expectFSpec, actual)
})
}
}
func TestFunctionToFunctionSpecTurnaround(t *testing.T) {
t.Parallel()
type testCase struct {
name string
fn *Function
fspec FunctionSpec
}
testCases := []testCase{
{
name: "return-type-plain",
fn: &Function{
PackageReference: packageDefRef{},
Token: "token",
ReturnType: IntType,
ReturnTypePlain: true,
Language: map[string]interface{}{},
},
fspec: FunctionSpec{
ReturnType: &ReturnTypeSpec{
TypeSpec: &TypeSpec{
Type: "integer",
Plain: true,
},
},
},
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name+"/marshalFunction", func(t *testing.T) {
t.Parallel()
pkg := Package{}
fspec, err := pkg.marshalFunction(tc.fn)
require.NoError(t, err)
require.Equal(t, tc.fspec, fspec)
})
t.Run(tc.name+"/bindFunctionDef", func(t *testing.T) {
t.Parallel()
ts := types{
spec: packageSpecSource{
&PackageSpec{
Functions: map[string]FunctionSpec{
"token": tc.fspec,
},
},
},
functionDefs: map[string]*Function{},
}
fn, diags, err := ts.bindFunctionDef("token")
require.NoError(t, err)
require.False(t, diags.HasErrors())
require.Equal(t, tc.fn, fn)
})
}
}
//nolint:paralleltest // using t.Setenv which is incompatible with t.Parallel
func TestLoaderRespectsDebugProviders(t *testing.T) {
host := debugProvidersHelperHost(t)
loader := NewPluginLoader(host)
cancel := make(chan bool)
handle, err := rpcutil.ServeWithOptions(rpcutil.ServeOptions{
Cancel: cancel,
Init: func(srv *grpc.Server) error {
pulumirpc.RegisterResourceProviderServer(srv, &debugProvidersHelperServer{})
return nil
},
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, host.SignalCancellation())
cancel <- true
require.NoError(t, <-handle.Done)
require.NoError(t, host.Close())
})
// Instruct to attach to the imaginary provider.
t.Setenv("PULUMI_DEBUG_PROVIDERS", fmt.Sprintf("imaginary:%d", handle.Port))
// Load from the in-process provider.
pref, err := loader.LoadPackageReference("imaginary", nil)
require.NoError(t, err)
require.Equal(t, "imaginary", pref.Name())
}
type debugProvidersHelperServer struct {
pulumirpc.UnimplementedResourceProviderServer
}
func (*debugProvidersHelperServer) GetSchema(
ctx context.Context, req *pulumirpc.GetSchemaRequest,
) (*pulumirpc.GetSchemaResponse, error) {
schema := PackageSpec{
Name: "imaginary",
Version: "0.0.1",
}
bytes, err := json.Marshal(schema)
if err != nil {
return nil, err
}
return &pulumirpc.GetSchemaResponse{Schema: string(bytes)}, nil
}
func (*debugProvidersHelperServer) GetPluginInfo(
context.Context, *emptypb.Empty,
) (*pulumirpc.PluginInfo, error) {
return &pulumirpc.PluginInfo{Version: "0.0.1"}, nil
}
func (*debugProvidersHelperServer) Attach(
context.Context, *pulumirpc.PluginAttach,
) (*emptypb.Empty, error) {
return &emptypb.Empty{}, nil
}
// This is the host that pulumi-yaml is using. Somehow the test does not work with utils.NewHost,
// perhaps that does not support PULUMI_DEBUG_PROVIDERS yet.
func debugProvidersHelperHost(t *testing.T) plugin.Host {
cwd := t.TempDir()
sink := diag.DefaultSink(os.Stderr, os.Stderr, diag.FormatOptions{
Color: cmdutil.GetGlobalColorization(),
})
pluginCtx, err := plugin.NewContext(sink, sink, nil, nil, cwd, nil, true, nil)
require.NoError(t, err)
return pluginCtx.Host
}
func TestProviderVersionIsAnError(t *testing.T) {
// c.f. https://github.com/pulumi/pulumi/issues/16757
t.Parallel()
loader := NewPluginLoader(utils.NewHost(testdataPath))
// Test that "version" isn't allowed as a property in the package config.
pkgSpec := PackageSpec{
Name: "xyz",
Version: "0.0.1",
Config: ConfigSpec{
Variables: map[string]PropertySpec{
"version": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
}
_, diags, err := BindSpec(pkgSpec, loader)
require.NoError(t, err)
require.True(t, diags.HasErrors())
assert.Equal(t, diags[0].Summary, "#/config/variables/version: version is a reserved configuration key")
// Test that "version" isn't allowed as an input property on the provider object.
pkgSpec = PackageSpec{
Name: "xyz",
Version: "0.0.1",
Provider: ResourceSpec{
InputProperties: map[string]PropertySpec{
"version": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
}
_, diags, err = BindSpec(pkgSpec, loader)
require.NoError(t, err)
require.True(t, diags.HasErrors())
assert.Equal(t, diags[0].Summary, "#/provider/properties/version: version is a reserved property name")
// Test that "version" is allowed as an output property on the provider object. Most providers probably won't add
// this, but it's there if they want to expose it.
pkgSpec = PackageSpec{
Name: "xyz",
Version: "0.0.1",
Provider: ResourceSpec{
ObjectTypeSpec: ObjectTypeSpec{
Properties: map[string]PropertySpec{
"version": {
TypeSpec: TypeSpec{
Type: "string",
},
},
},
},
},
}
pkg, diags, err := BindSpec(pkgSpec, loader)
require.NoError(t, err)
assert.False(t, diags.HasErrors())
assert.NotNil(t, pkg)
}