opentofu/helper/schema/field_writer_map_test.go
Brian Flad a8e3787afc
helper/schema: Prevent setSet() panic with typed nil
References:

* https://github.com/hashicorp/terraform/issues/14418
* v0.9.5 (original bug report): a59ee0b30e/helper/schema/field_writer_map.go (L311)
* v0.11.12 (Terraform AWS Provider discovery): 057286e522/helper/schema/field_writer_map.go (L343)

When creating flatten functions in Terraform Providers that return *schema.Set, its possible to return a typed `nil`, e.g.

```go
func flattenHeaders(h *cloudfront.Headers) *schema.Set {
	if h.Items != nil {
		return schema.NewSet(schema.HashString, flattenStringList(h.Items))
	}
	return nil
}
```

This previously could cause a panic, e.g.

```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x1881911]

goroutine 1325 [running]:
github.com/hashicorp/terraform/helper/schema.(*MapFieldWriter).setSet(0xc00054bf00, 0xc00073efa0, 0x5, 0x5, 0x5828140, 0x0, 0xc0002cea50, 0xc000e996a8, 0xc001026e40)
	/Users/bflad/go/pkg/mod/github.com/hashicorp/terraform@v0.11.12/helper/schema/field_writer_map.go:343 +0x211
```

Here we catch the typed `nil` and return an empty list flatmap result instead. Unit testing result prior to code update:

```
--- FAIL: TestMapFieldWriter (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
  panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x1777cdc]

goroutine 913 [running]:
testing.tRunner.func1(0xc00045b800)
  /usr/local/Cellar/go/1.12.1/libexec/src/testing/testing.go:830 +0x392
panic(0x192cf20, 0x2267ca0)
  /usr/local/Cellar/go/1.12.1/libexec/src/runtime/panic.go:522 +0x1b5
github.com/hashicorp/terraform/helper/schema.(*MapFieldWriter).setSet(0xc0004648a0, 0xc0004408d0, 0x1, 0x1, 0x19e3de0, 0x0, 0xc00045c600, 0x30, 0x19e0080)
  /Users/bflad/src/github.com/hashicorp/terraform/helper/schema/field_writer_map.go:344 +0x68c
github.com/hashicorp/terraform/helper/schema.(*MapFieldWriter).set(0xc0004648a0, 0xc0004408d0, 0x1, 0x1, 0x19e3de0, 0x0, 0x1, 0x18)
  /Users/bflad/src/github.com/hashicorp/terraform/helper/schema/field_writer_map.go:107 +0x28b
github.com/hashicorp/terraform/helper/schema.(*MapFieldWriter).WriteField(0xc0004648a0, 0xc0004408d0, 0x1, 0x1, 0x19e3de0, 0x0, 0x0, 0x0)
  /Users/bflad/src/github.com/hashicorp/terraform/helper/schema/field_writer_map.go:89 +0x504
github.com/hashicorp/terraform/helper/schema.TestMapFieldWriter(0xc00045b800)
  /Users/bflad/src/github.com/hashicorp/terraform/helper/schema/field_writer_map_test.go:337 +0x2ddd
testing.tRunner(0xc00045b800, 0x1a44f90)
  /usr/local/Cellar/go/1.12.1/libexec/src/testing/testing.go:865 +0xc0
created by testing.(*T).Run
  /usr/local/Cellar/go/1.12.1/libexec/src/testing/testing.go:916 +0x35a
```
2019-04-01 20:10:32 -04:00

548 lines
9.6 KiB
Go

package schema
import (
"reflect"
"testing"
)
func TestMapFieldWriter_impl(t *testing.T) {
var _ FieldWriter = new(MapFieldWriter)
}
func TestMapFieldWriter(t *testing.T) {
schema := map[string]*Schema{
"bool": &Schema{Type: TypeBool},
"int": &Schema{Type: TypeInt},
"string": &Schema{Type: TypeString},
"list": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeString},
},
"listInt": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
},
"listResource": &Schema{
Type: TypeList,
Optional: true,
Computed: true,
Elem: &Resource{
Schema: map[string]*Schema{
"value": &Schema{
Type: TypeInt,
Optional: true,
},
},
},
},
"map": &Schema{Type: TypeMap},
"set": &Schema{
Type: TypeSet,
Elem: &Schema{Type: TypeInt},
Set: func(a interface{}) int {
return a.(int)
},
},
"setDeep": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{Type: TypeInt},
"value": &Schema{Type: TypeString},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["index"].(int)
},
},
}
cases := map[string]struct {
Addr []string
Value interface{}
Err bool
Out map[string]string
}{
"noexist": {
[]string{"noexist"},
42,
true,
map[string]string{},
},
"bool": {
[]string{"bool"},
false,
false,
map[string]string{
"bool": "false",
},
},
"int": {
[]string{"int"},
42,
false,
map[string]string{
"int": "42",
},
},
"string": {
[]string{"string"},
"42",
false,
map[string]string{
"string": "42",
},
},
"string nil": {
[]string{"string"},
nil,
false,
map[string]string{
"string": "",
},
},
"list of resources": {
[]string{"listResource"},
[]interface{}{
map[string]interface{}{
"value": 80,
},
},
false,
map[string]string{
"listResource.#": "1",
"listResource.0.value": "80",
},
},
"list of resources empty": {
[]string{"listResource"},
[]interface{}{},
false,
map[string]string{
"listResource.#": "0",
},
},
"list of resources nil": {
[]string{"listResource"},
nil,
false,
map[string]string{
"listResource.#": "0",
},
},
"list of strings": {
[]string{"list"},
[]interface{}{"foo", "bar"},
false,
map[string]string{
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
},
},
"list element": {
[]string{"list", "0"},
"string",
true,
map[string]string{},
},
"map": {
[]string{"map"},
map[string]interface{}{"foo": "bar"},
false,
map[string]string{
"map.%": "1",
"map.foo": "bar",
},
},
"map delete": {
[]string{"map"},
nil,
false,
map[string]string{
"map": "",
},
},
"map element": {
[]string{"map", "foo"},
"bar",
true,
map[string]string{},
},
"set": {
[]string{"set"},
[]interface{}{1, 2, 5},
false,
map[string]string{
"set.#": "3",
"set.1": "1",
"set.2": "2",
"set.5": "5",
},
},
"set nil": {
[]string{"set"},
nil,
false,
map[string]string{
"set.#": "0",
},
},
"set typed nil": {
[]string{"set"},
func() *Set { return nil }(),
false,
map[string]string{
"set.#": "0",
},
},
"set resource": {
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
false,
map[string]string{
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
},
},
"set element": {
[]string{"set", "5"},
5,
true,
map[string]string{},
},
"full object": {
nil,
map[string]interface{}{
"string": "foo",
"list": []interface{}{"foo", "bar"},
},
false,
map[string]string{
"string": "foo",
"list.#": "2",
"list.0": "foo",
"list.1": "bar",
},
},
}
for name, tc := range cases {
w := &MapFieldWriter{Schema: schema}
err := w.WriteField(tc.Addr, tc.Value)
if err != nil != tc.Err {
t.Fatalf("%s: err: %s", name, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%s: bad: %#v", name, actual)
}
}
}
func TestMapFieldWriterCleanSet(t *testing.T) {
schema := map[string]*Schema{
"setDeep": &Schema{
Type: TypeSet,
Elem: &Resource{
Schema: map[string]*Schema{
"index": &Schema{Type: TypeInt},
"value": &Schema{Type: TypeString},
},
},
Set: func(a interface{}) int {
return a.(map[string]interface{})["index"].(int)
},
},
}
values := []struct {
Addr []string
Value interface{}
Out map[string]string
}{
{
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 10,
"value": "foo",
},
map[string]interface{}{
"index": 50,
"value": "bar",
},
},
map[string]string{
"setDeep.#": "2",
"setDeep.10.index": "10",
"setDeep.10.value": "foo",
"setDeep.50.index": "50",
"setDeep.50.value": "bar",
},
},
{
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 20,
"value": "baz",
},
map[string]interface{}{
"index": 60,
"value": "qux",
},
},
map[string]string{
"setDeep.#": "2",
"setDeep.20.index": "20",
"setDeep.20.value": "baz",
"setDeep.60.index": "60",
"setDeep.60.value": "qux",
},
},
{
[]string{"setDeep"},
[]interface{}{
map[string]interface{}{
"index": 30,
"value": "one",
},
map[string]interface{}{
"index": 70,
"value": "two",
},
},
map[string]string{
"setDeep.#": "2",
"setDeep.30.index": "30",
"setDeep.30.value": "one",
"setDeep.70.index": "70",
"setDeep.70.value": "two",
},
},
}
w := &MapFieldWriter{Schema: schema}
for n, tc := range values {
err := w.WriteField(tc.Addr, tc.Value)
if err != nil {
t.Fatalf("%d: err: %s", n, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%d: bad: %#v", n, actual)
}
}
}
func TestMapFieldWriterCleanList(t *testing.T) {
schema := map[string]*Schema{
"listDeep": &Schema{
Type: TypeList,
Elem: &Resource{
Schema: map[string]*Schema{
"thing1": &Schema{Type: TypeString},
"thing2": &Schema{Type: TypeString},
},
},
},
}
values := []struct {
Addr []string
Value interface{}
Out map[string]string
}{
{
// Base list
[]string{"listDeep"},
[]interface{}{
map[string]interface{}{
"thing1": "a",
"thing2": "b",
},
map[string]interface{}{
"thing1": "c",
"thing2": "d",
},
map[string]interface{}{
"thing1": "e",
"thing2": "f",
},
map[string]interface{}{
"thing1": "g",
"thing2": "h",
},
},
map[string]string{
"listDeep.#": "4",
"listDeep.0.thing1": "a",
"listDeep.0.thing2": "b",
"listDeep.1.thing1": "c",
"listDeep.1.thing2": "d",
"listDeep.2.thing1": "e",
"listDeep.2.thing2": "f",
"listDeep.3.thing1": "g",
"listDeep.3.thing2": "h",
},
},
{
// Remove an element
[]string{"listDeep"},
[]interface{}{
map[string]interface{}{
"thing1": "a",
"thing2": "b",
},
map[string]interface{}{
"thing1": "c",
"thing2": "d",
},
map[string]interface{}{
"thing1": "e",
"thing2": "f",
},
},
map[string]string{
"listDeep.#": "3",
"listDeep.0.thing1": "a",
"listDeep.0.thing2": "b",
"listDeep.1.thing1": "c",
"listDeep.1.thing2": "d",
"listDeep.2.thing1": "e",
"listDeep.2.thing2": "f",
},
},
{
// Rewrite with missing keys. This should normally not be necessary, as
// hopefully the writers are writing zero values as necessary, but for
// brevity we want to make sure that what exists in the writer is exactly
// what the last write looked like coming from the provider.
[]string{"listDeep"},
[]interface{}{
map[string]interface{}{
"thing1": "a",
},
map[string]interface{}{
"thing1": "c",
},
map[string]interface{}{
"thing1": "e",
},
},
map[string]string{
"listDeep.#": "3",
"listDeep.0.thing1": "a",
"listDeep.1.thing1": "c",
"listDeep.2.thing1": "e",
},
},
}
w := &MapFieldWriter{Schema: schema}
for n, tc := range values {
err := w.WriteField(tc.Addr, tc.Value)
if err != nil {
t.Fatalf("%d: err: %s", n, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%d: bad: %#v", n, actual)
}
}
}
func TestMapFieldWriterCleanMap(t *testing.T) {
schema := map[string]*Schema{
"map": &Schema{
Type: TypeMap,
},
}
values := []struct {
Value interface{}
Out map[string]string
}{
{
// Base map
map[string]interface{}{
"thing1": "a",
"thing2": "b",
"thing3": "c",
"thing4": "d",
},
map[string]string{
"map.%": "4",
"map.thing1": "a",
"map.thing2": "b",
"map.thing3": "c",
"map.thing4": "d",
},
},
{
// Base map
map[string]interface{}{
"thing1": "a",
"thing2": "b",
"thing4": "d",
},
map[string]string{
"map.%": "3",
"map.thing1": "a",
"map.thing2": "b",
"map.thing4": "d",
},
},
}
w := &MapFieldWriter{Schema: schema}
for n, tc := range values {
err := w.WriteField([]string{"map"}, tc.Value)
if err != nil {
t.Fatalf("%d: err: %s", n, err)
}
actual := w.Map()
if !reflect.DeepEqual(actual, tc.Out) {
t.Fatalf("%d: bad: %#v", n, actual)
}
}
}