resource/preparer_test.go
// Copyright © 2016 Asteris, LLC
//
// 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.
package resource_test
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/asteris-llc/converge/helpers/fakerenderer"
"github.com/asteris-llc/converge/helpers/logging"
"github.com/asteris-llc/converge/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
// TestPreparerPrepare tests the Unmarshalling of Preparer into Resource
// structs.
func TestPreparerPrepare(t *testing.T) {
defer logging.HideLogs(t)()
// newWithField is a little utility. We have this pattern over and over
// where we need to set a target field and then check that we render without
// errors. This just encapsulates that logic (and error handling) so we
// don't have to repeat it so much.
newWithField := func(t *testing.T, key string, value interface{}) *testPreparerTarget {
target := new(testPreparerTarget)
prep := &resource.Preparer{
Source: map[string]interface{}{
key: value,
},
Destination: target,
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
require.NoError(t, err, "%v", err)
return target
}
// strings are table stakes, so let's start with those...
t.Run("string", func(t *testing.T) {
target := newWithField(t, "string", "a")
assert.Equal(t, "a", target.String)
})
// lists of strings are also important (argument lists, etc)
t.Run("strings", func(t *testing.T) {
target := newWithField(t, "strings", []string{"a"})
assert.Equal(t, []string{"a"}, target.Strings)
})
// We're only testing maps with strings and bools in the tested slot, but
// this should work with any value.
t.Run("maps", func(t *testing.T) {
t.Run("string-key", func(t *testing.T) {
value := map[string]interface{}{"x": 1}
target := newWithField(t, "stringmapkey", value)
assert.Equal(t, value, target.StringMapKey)
})
t.Run("bool-key", func(t *testing.T) {
value := map[bool]interface{}{true: 1}
target := newWithField(t, "boolmapkey", value)
assert.Equal(t, value, target.BoolMapKey)
})
t.Run("string-value", func(t *testing.T) {
value := map[interface{}]string{1: "x"}
target := newWithField(t, "stringmapvalue", value)
assert.Equal(t, value, target.StringMapValue)
})
t.Run("bool-value", func(t *testing.T) {
value := map[interface{}]bool{1: true}
target := newWithField(t, "boolmapvalue", value)
assert.Equal(t, value, target.BoolMapValue)
})
})
// test time.Duration with both int64 and string
t.Run("duration", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
target := newWithField(t, "duration", nil)
assert.Equal(t, time.Duration(0), target.Duration)
})
t.Run("int64", func(t *testing.T) {
target := newWithField(t, "duration", 1)
assert.Equal(t, 1*time.Second, target.Duration)
})
t.Run("string", func(t *testing.T) {
duration, err := time.ParseDuration("1h")
require.NoError(t, err)
target := newWithField(t, "duration", "1h")
assert.Equal(t, duration, target.Duration)
})
t.Run("invalid", func(t *testing.T) {
t.Run("string", func(t *testing.T) {
val := "1"
prep := &resource.Preparer{
Source: map[string]interface{}{"duration": val},
Destination: new(testPreparerTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.EqualError(t, err, fmt.Sprintf("could not convert %s to duration: time: missing unit in duration %s", val, val))
})
t.Run("unknown", func(t *testing.T) {
val := 3.2
prep := &resource.Preparer{
Source: map[string]interface{}{"duration": val},
Destination: new(testPreparerTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.EqualError(t, err, fmt.Sprintf("cannot handle duration conversion of %v", reflect.ValueOf(val).Kind()))
})
})
})
// test time.Time
t.Run("time", func(t *testing.T) {
zone := time.FixedZone(time.Now().In(time.Local).Zone())
longForm := "2006-01-02T15:04:05"
shortForm := "2006-01-02"
t.Run("nil", func(t *testing.T) {
target := newWithField(t, "time", nil)
assert.Equal(t, time.Time{}, target.Time)
})
t.Run("short form", func(t *testing.T) {
val := "2003-01-02"
expected, err := time.ParseInLocation(shortForm, val, zone)
require.NoError(t, err)
target := newWithField(t, "time", val)
assert.Equal(t, expected.String(), target.Time.String())
})
t.Run("long form", func(t *testing.T) {
val := "2003-01-02T01:02:03"
expected, err := time.ParseInLocation(longForm, val, zone)
require.NoError(t, err)
target := newWithField(t, "time", val)
assert.Equal(t, expected.String(), target.Time.String())
})
t.Run("zone provided", func(t *testing.T) {
val := "2003-01-02T01:02:03-07:00"
expected, err := time.Parse(time.RFC3339, val)
require.NoError(t, err)
target := newWithField(t, "time", val)
assert.Equal(t, expected.String(), target.Time.String())
})
t.Run("invalid", func(t *testing.T) {
t.Run("unknown", func(t *testing.T) {
val := "3.2"
prep := &resource.Preparer{
Source: map[string]interface{}{"time": val},
Destination: new(testPreparerTarget),
}
_, ztErr := time.Parse(time.RFC3339, val)
_, ltErr := time.ParseInLocation(longForm, val, zone)
_, stErr := time.ParseInLocation(shortForm, val, zone)
expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.Equal(t, expectedErr, err)
})
t.Run("short form", func(t *testing.T) {
val := "2003-01-"
prep := &resource.Preparer{
Source: map[string]interface{}{"time": val},
Destination: new(testPreparerTarget),
}
_, ztErr := time.Parse(time.RFC3339, val)
_, ltErr := time.ParseInLocation(longForm, val, zone)
_, stErr := time.ParseInLocation(shortForm, val, zone)
expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.Equal(t, expectedErr, err)
})
t.Run("long form-second out of range", func(t *testing.T) {
val := "2003-01-02T01:02:61"
prep := &resource.Preparer{
Source: map[string]interface{}{"time": val},
Destination: new(testPreparerTarget),
}
_, ztErr := time.Parse(time.RFC3339, val)
_, ltErr := time.ParseInLocation(longForm, val, zone)
_, stErr := time.ParseInLocation(shortForm, val, zone)
expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.Equal(t, expectedErr, err)
})
t.Run("zone provided-extra text", func(t *testing.T) {
val := "2003-01-02T01:02:03-25:00:00"
prep := &resource.Preparer{
Source: map[string]interface{}{"time": val},
Destination: new(testPreparerTarget),
}
_, ztErr := time.Parse(time.RFC3339, val)
_, ltErr := time.ParseInLocation(longForm, val, zone)
_, stErr := time.ParseInLocation(shortForm, val, zone)
expectedErr := fmt.Errorf("could not convert time to time.Time any of:\n1. %v\n2. %v\n3. %v\n", ztErr, ltErr, stErr)
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.Equal(t, expectedErr, err)
})
})
})
// boolean values are special. We want to support a bunch of different cases
// and forms of truth values, so we're going to test them all in a table
// here.
t.Run("bool", func(t *testing.T) {
truthTable := []struct {
val interface{}
truth bool
}{
// true - any casing of "true" or "t", or the boolean value
{true, true},
{"true", true},
{"TRUE", true},
{"t", true},
{"T", true},
// false
{false, false},
{"false", false},
{"FALSE", false},
{"f", false},
{"F", false},
{"bananas", false}, // or anything other string except as defined above
}
for _, pair := range truthTable {
t.Run(fmt.Sprintf("%T-%v", pair.val, pair.val), func(t *testing.T) {
target := newWithField(t, "bool", pair.val)
assert.Equal(t, pair.truth, target.Bool)
})
t.Run(fmt.Sprintf("slice-of-%T-%v", pair.val, pair.val), func(t *testing.T) {
target := newWithField(t, "bools", []interface{}{pair.val})
assert.Equal(t, []bool{pair.truth}, target.Bools)
})
}
})
// next is our "anything" escape hatch of interface{}.
t.Run("interface", func(t *testing.T) {
target := newWithField(t, "anything", 1)
assert.Equal(t, 1, target.Anything)
})
// numbers! We want to be able to parse either a numeric value or a string.
numberTests := []struct {
key string
value interface{}
test func(*testing.T, *testPreparerTarget)
}{
// ints
{"int", 1, func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, 1, tpt.Int) }},
{"int", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, 1, tpt.Int) }},
{"int8", int8(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int8(1), tpt.Int8) }},
{"int8", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int8(1), tpt.Int8) }},
{"int16", int16(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int16(1), tpt.Int16) }},
{"int16", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int16(1), tpt.Int16) }},
{"int32", int32(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int32(1), tpt.Int32) }},
{"int32", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int32(1), tpt.Int32) }},
{"int64", int64(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int64(1), tpt.Int64) }},
{"int64", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, int64(1), tpt.Int64) }},
// uints
{"uint", 1, func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint(1), tpt.Uint) }},
{"uint", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint(1), tpt.Uint) }},
{"uint8", uint8(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint8(1), tpt.Uint8) }},
{"uint8", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint8(1), tpt.Uint8) }},
{"uint16", uint16(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint16(1), tpt.Uint16) }},
{"uint16", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint16(1), tpt.Uint16) }},
{"uint32", uint32(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(1), tpt.Uint32) }},
{"uint32", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(1), tpt.Uint32) }},
{"uint64", uint64(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint64(1), tpt.Uint64) }},
{"uint64", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint64(1), tpt.Uint64) }},
// floats
{"float32", float32(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float32(1), tpt.Float32) }},
{"float32", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float32(1), tpt.Float32) }},
{"float64", float64(1), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float64(1), tpt.Float64) }},
{"float64", "1", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, float64(1), tpt.Float64) }},
// octal conversion
{"octal", uint32(0777), func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(0777), tpt.Octal) }},
{"octal", "0777", func(t *testing.T, tpt *testPreparerTarget) { assert.Equal(t, uint32(0777), tpt.Octal) }},
}
for _, test := range numberTests {
t.Run(fmt.Sprintf("%s-%T", test.key, test.value), func(t *testing.T) {
test.test(t, newWithField(t, test.key, test.value))
})
}
// pointers
t.Run("pointers", func(t *testing.T) {
val := "test"
target := newWithField(t, "pointer", val)
if assert.NotNil(t, target.Pointer) {
assert.Equal(t, *target.Pointer, val)
}
})
// we do some very basic validations, let's test those too
t.Run("valid_values", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
target := newWithField(t, "valid_values", "a")
assert.Equal(t, "a", target.ValidValues)
})
t.Run("invalid", func(t *testing.T) {
prep := &resource.Preparer{
Source: map[string]interface{}{"valid_values": "invalid"},
Destination: new(testPreparerTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.EqualError(t, err, "value did not pass validation. Must be one of \"a\", was \"invalid\"")
})
})
// type aliases are important for enum-like behavior
t.Run("alias", func(t *testing.T) {
target := newWithField(t, "alias", "a")
assert.Equal(t, testAlias("a"), target.Alias)
})
// parameters can be required
t.Run("required", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
prep := &resource.Preparer{
Source: map[string]interface{}{"required": "a"},
Destination: new(testRequiredTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.NoError(t, err)
})
t.Run("invalid", func(t *testing.T) {
prep := &resource.Preparer{
Source: map[string]interface{}{},
Destination: new(testRequiredTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.EqualError(t, err, `"required" is required`)
})
})
// parameters can be required to be nonempty
t.Run("nonempty", func(t *testing.T) {
t.Run("valid", func(t *testing.T) {
prep := &resource.Preparer{
Source: map[string]interface{}{"nonempty": "a"},
Destination: new(testNonemptyTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.NoError(t, err)
})
t.Run("invalid", func(t *testing.T) {
prep := &resource.Preparer{
Source: map[string]interface{}{"nonempty": ""},
Destination: new(testNonemptyTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.EqualError(t, err, `"nonempty" must be nonempty`)
})
})
// two parameters can also be mutually exclusive
t.Run("mutually_exclusive", func(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
prep := &resource.Preparer{
Source: map[string]interface{}{
"a": 1,
"b": 2,
},
Destination: new(testMutuallyExclusiveTarget),
}
_, err := prep.Prepare(context.Background(), fakerenderer.New())
assert.EqualError(t, err, `only one of "a" or "b" can be set`)
})
})
}
// testAlias is a type alias... can we deserialize those?
type testAlias string
// testPreparerTarget is a big 'ol bucket for tested values. See comments in
// TestPreparerPrepare for how these are being used.
type testPreparerTarget struct {
String string `hcl:"string"`
Strings []string `hcl:"strings"`
StringMapKey map[string]interface{} `hcl:"stringmapkey"`
StringMapValue map[interface{}]string `hcl:"stringmapvalue"`
Duration time.Duration `hcl:"duration"`
Time time.Time `hcl:"time"`
Bool bool `hcl:"bool"`
Bools []bool `hcl:"bools"`
BoolMapKey map[bool]interface{} `hcl:"boolmapkey"`
BoolMapValue map[interface{}]bool `hcl:"boolmapvalue"`
Anything interface{} `hcl:"anything"`
// one of each numeric type
Int int `hcl:"int"`
Int8 int8 `hcl:"int8"`
Int16 int16 `hcl:"int16"`
Int32 int32 `hcl:"int32"`
Int64 int64 `hcl:"int64"`
Uint uint `hcl:"uint"`
Uint8 uint8 `hcl:"uint8"`
Uint16 uint16 `hcl:"uint16"`
Uint32 uint32 `hcl:"uint32"`
Uint64 uint64 `hcl:"uint64"`
Float32 float32 `hcl:"float32"`
Float64 float64 `hcl:"float64"`
Octal uint32 `hcl:"octal" base:"8"`
// simple validation
ValidValues string `hcl:"valid_values" valid_values:"a"`
// aliasing
Alias testAlias `hcl:"alias"`
// pointers
Pointer *string `hcl:"pointer"`
}
func (tpt *testPreparerTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
return tpt, nil
}
func (tpt *testPreparerTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
return nil, nil
}
func (tpt *testPreparerTarget) Apply(context.Context) (resource.TaskStatus, error) { return nil, nil }
// testRequiredTarget tests required fields. Those are invalid when empty, so
// we've got to include it separately
type testRequiredTarget struct {
Required string `hcl:"required" required:"true"`
}
func (tpt *testRequiredTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
return tpt, nil
}
func (tpt *testRequiredTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
return nil, nil
}
func (tpt *testRequiredTarget) Apply(context.Context) (resource.TaskStatus, error) { return nil, nil }
// testNonemptyTarget tests nonempty fields. Those are invalid when empty, so
// we've got to include it separately
type testNonemptyTarget struct {
Nonempty string `hcl:"nonempty" nonempty:"true"`
}
func (tpt *testNonemptyTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
return tpt, nil
}
func (tpt *testNonemptyTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
return nil, nil
}
func (tpt *testNonemptyTarget) Apply(context.Context) (resource.TaskStatus, error) { return nil, nil }
// testMutuallyExclusiveTarget tests mutually_exclusive fields. Those are
// invalid when empty, so we've got to include it separately
type testMutuallyExclusiveTarget struct {
A string `hcl:"a" mutually_exclusive:"a,b"`
B string `hcl:"b" mutually_exclusive:"a,b"`
}
func (tpt *testMutuallyExclusiveTarget) Prepare(context.Context, resource.Renderer) (resource.Task, error) {
return tpt, nil
}
func (tpt *testMutuallyExclusiveTarget) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
return nil, nil
}
func (tpt *testMutuallyExclusiveTarget) Apply(context.Context) (resource.TaskStatus, error) {
return nil, nil
}