pulumi/pkg/codegen/schema/schema_test.go
Ian Wahbe 5e65204463
Fixup for 7874 (#7921)
* Fix some nits from 7874

This was a premature merge

* Fix #7940

We don't surface recursion warnings if there is no child where `replaceOnChanges` is set.
2021-09-10 14:56:56 -07:00

500 lines
14 KiB
Go

// Copyright 2016-2020, 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 (
"encoding/json"
"io/ioutil"
"net/url"
"path/filepath"
"reflect"
"sort"
"testing"
"github.com/blang/semver"
"github.com/stretchr/testify/assert"
)
func readSchemaFile(file string) (pkgSpec PackageSpec) {
// Read in, decode, and import the schema.
schemaBytes, err := ioutil.ReadFile(filepath.Join("..", "internal", "test", "testdata", file))
if err != nil {
panic(err)
}
if err = json.Unmarshal(schemaBytes, &pkgSpec); err != nil {
panic(err)
}
return pkgSpec
}
func TestImportSpec(t *testing.T) {
// Read in, decode, and import the schema.
pkgSpec := readSchemaFile("kubernetes.json")
pkg, err := ImportSpec(pkgSpec, nil)
if err != nil {
t.Errorf("ImportSpec() error = %v", err)
}
for _, r := range pkg.Resources {
assert.NotNil(t, r.Package, "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 TestEnums(t *testing.T) {
for _, tt := range enumTests {
t.Run(tt.filename, func(t *testing.T) {
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.Package = pkg
assert.Equal(t, tt.expected, result)
}
})
}
}
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)
}
}
}
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Read in, decode, and import the schema.
schemaBytes, err := ioutil.ReadFile(
filepath.Join("..", "internal", "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) {
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 {
t.Run(tt.name, func(t *testing.T) {
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 TestMethods(t *testing.T) {
var 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)
assert.NotNil(t, pkg.Resources[0].Methods[0].Function.Outputs)
assert.Len(t, pkg.Resources[0].Methods[0].Function.Outputs.Properties, 1)
outputs := pkg.Resources[0].Methods[0].Function.Outputs.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: "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 {
t.Run(tt.filename, func(t *testing.T) {
pkgSpec := readSchemaFile(filepath.Join("schema", tt.filename))
pkg, err := ImportSpec(pkgSpec, nil)
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
} else {
if err != nil {
t.Error(err)
}
tt.validator(pkg)
}
})
}
}
// Tests that the method ReplaceOnChanges works as expected. Does not test
// codegen.
func TestReplaceOnChanges(t *testing.T) {
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{},
},
} {
t.Run(tt.name, func(t *testing.T) {
// 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")
}
})
}
}