From 4e59263a9c517e097bca05ac14686028f231d24f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 19 Nov 2019 21:10:51 -0800 Subject: [PATCH] Add tests for serializing PropertyMaps (#3533) * WIP - Add tests for serializing PropertyMaps * Apply suggestions from code review Co-Authored-By: Pat Gavlin * Cleanup tests --- pkg/resource/config/crypt.go | 17 ++- pkg/resource/stack/deployment_test.go | 152 ++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 5 deletions(-) diff --git a/pkg/resource/config/crypt.go b/pkg/resource/config/crypt.go index cdbba4966..4bbc5f4a2 100644 --- a/pkg/resource/config/crypt.go +++ b/pkg/resource/config/crypt.go @@ -88,16 +88,23 @@ func (t *trackingDecrypter) SecureValues() []string { return t.secureValues } -// NewBlindingDecrypter returns a Decrypter that instead of decrypting data, just returns "[secret]", it can +// BlindingCrypter returns a Crypter that instead of decrypting or encrypting data, just returns "[secret]", it can // be used when you want to display configuration information to a user but don't want to prompt for a password -// so secrets will not be decrypted. +// so secrets will not be decrypted or encrypted. +var BlindingCrypter Crypter = blindingCrypter{} + +// NewBlindingDecrypter returns a blinding decrypter. func NewBlindingDecrypter() Decrypter { - return blindingDecrypter{} + return blindingCrypter{} } -type blindingDecrypter struct{} +type blindingCrypter struct{} -func (b blindingDecrypter) DecryptValue(ciphertext string) (string, error) { +func (b blindingCrypter) DecryptValue(ciphertext string) (string, error) { + return "[secret]", nil +} + +func (b blindingCrypter) EncryptValue(plaintext string) (string, error) { return "[secret]", nil } diff --git a/pkg/resource/stack/deployment_test.go b/pkg/resource/stack/deployment_test.go index e714a7806..767e7b754 100644 --- a/pkg/resource/stack/deployment_test.go +++ b/pkg/resource/stack/deployment_test.go @@ -15,6 +15,8 @@ package stack import ( + "encoding/json" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -191,3 +193,153 @@ func TestUnknownSig(t *testing.T) { _, err := DeserializePropertyValue(rawProp, config.NewPanicCrypter()) assert.Error(t, err) } + +func TestCustomSerialization(t *testing.T) { + textAsset, err := resource.NewTextAsset("alpha beta gamma") + assert.NoError(t, err) + + strProp := resource.NewStringProperty("strProp") + + computed := resource.Computed{Element: strProp} + output := resource.Output{Element: strProp} + secret := &resource.Secret{Element: strProp} + + propMap := resource.NewPropertyMapFromMap(map[string]interface{}{ + // Primitive types + "nil": nil, + "bool": true, + "int32": int64(41), + "int64": int64(42), + "float32": float32(2.5), + "float64": float64(1.5), + "string": "string literal", + + // Data structures + "array": []interface{}{"a", true, float64(32)}, + "array-empty": []interface{}{}, + + "map": map[string]interface{}{ + "a": true, + "b": float64(88), + "c": "c-see-saw", + "d": "d-dee-daw", + }, + "map-empty": map[string]interface{}{}, + + // Specialized resource types + "asset-text": textAsset, + + "computed": computed, + "output": output, + "secret": secret, + }) + + assert.True(t, propMap.ContainsSecrets()) + assert.True(t, propMap.ContainsUnknowns()) + + // Confirm the expected shape of serializing a ResourceProperty and PropertyMap using the + // reflection-based default JSON encoder. This should NOT be used when serializing resources, + // but we confirm the expected shape here while we migrate older code that relied on the + // specific format. + t.Run("SerializeToJSON", func(t *testing.T) { + b, err := json.Marshal(propMap) + if err != nil { + t.Fatalf("Marshalling PropertyMap: %v", err) + } + json := string(b) + + // Look for the specific JSON serialization of the properties. + tests := []string{ + // Primitives + `"nil":{"V":null}`, + `"bool":{"V":true}`, + `"string":{"V":"string literal"}}`, + `"float32":{"V":2.5}`, + `"float64":{"V":1.5}`, + `"int32":{"V":41}`, + `"int64":{"V":42}`, + + // Data structures + `array":{"V":[{"V":"a"},{"V":true},{"V":32}]}`, + `"array-empty":{"V":[]}`, + `"map":{"V":{"a":{"V":true},"b":{"V":88},"c":{"V":"c-see-saw"},"d":{"V":"d-dee-daw"}}}`, + `"map-empty":{"V":{}}`, + + // Specialized resource types + // nolint: lll + `"asset-text":{"V":{"4dabf18193072939515e22adb298388d":"c44067f5952c0a294b673a41bacd8c17","hash":"64989ccbf3efa9c84e2afe7cee9bc5828bf0fcb91e44f8c1e591638a2c2e90e3","text":"alpha beta gamma"}}`, + + `"computed":{"V":{"Element":{"V":"strProp"}}}`, + `"output":{"V":{"Element":{"V":"strProp"}}}`, + `"secret":{"V":{"Element":{"V":"strProp"}}}`, + } + + for _, want := range tests { + if !strings.Contains(json, want) { + t.Errorf("Did not find expected snippet: %v", want) + } + } + + if t.Failed() { + t.Logf("Full JSON encoding:\n%v", json) + } + }) + + // Using stack.SerializeProperties will get the correct behavior and should be used + // whenever persisting resources into some durable form. + t.Run("SerializeProperties", func(t *testing.T) { + serializedPropMap, err := SerializeProperties(propMap, config.BlindingCrypter) + assert.NoError(t, err) + + // Now JSON encode the results? + b, err := json.Marshal(serializedPropMap) + if err != nil { + t.Fatalf("Marshalling PropertyMap: %v", err) + } + json := string(b) + + // Look for the specific JSON serialization of the properties. + tests := []string{ + // Primitives + `"bool":true`, + `"string":"string literal"`, + `"float32":2.5`, + `"float64":1.5`, + `"int32":41`, + `"int64":42`, + + // Data structures + `"array":["a",true,32]`, + `"array-empty":[]`, + `"map":{"a":true,"b":88,"c":"c-see-saw","d":"d-dee-daw"}`, + `"map-empty":{}`, + + // Specialized resource types + // nolint: lll + `"asset-text":{"4dabf18193072939515e22adb298388d":"c44067f5952c0a294b673a41bacd8c17","hash":"64989ccbf3efa9c84e2afe7cee9bc5828bf0fcb91e44f8c1e591638a2c2e90e3","text":"alpha beta gamma"}`, + + `"secret":{"4dabf18193072939515e22adb298388d":"1b47061264138c4ac30d75fd1eb44270","ciphertext":"[secret]"}`, + } + for _, want := range tests { + if !strings.Contains(json, want) { + t.Errorf("Did not find expected snippet: %v", want) + } + } + + // Some properties are explicitly _not_ in serialized output. + negativeTests := []string{ + `"nil"`, + `"computed"`, + `"output"`, + } + for _, doNotWant := range negativeTests { + if strings.Contains(json, doNotWant) { + t.Errorf("Found unexpected snippet: %v", doNotWant) + } + } + + if t.Failed() { + t.Logf("Full JSON encoding:\n%v", json) + } + }) +}