github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/resource_timeout_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package schema
     7  
     8  import (
     9  	"fmt"
    10  	"reflect"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/opentofu/opentofu/internal/legacy/tofu"
    15  )
    16  
    17  func TestResourceTimeout_ConfigDecode_badkey(t *testing.T) {
    18  	cases := []struct {
    19  		Name string
    20  		// what the resource has defined in source
    21  		ResourceDefaultTimeout *ResourceTimeout
    22  		// configuration provider by user in tf file
    23  		Config map[string]interface{}
    24  		// what we expect the parsed ResourceTimeout to be
    25  		Expected *ResourceTimeout
    26  		// Should we have an error (key not defined in source)
    27  		ShouldErr bool
    28  	}{
    29  		{
    30  			Name:                   "Source does not define 'delete' key",
    31  			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
    32  			Config:                 expectedConfigForValues(2, 0, 0, 1, 0),
    33  			Expected:               timeoutForValues(10, 0, 5, 0, 0),
    34  			ShouldErr:              true,
    35  		},
    36  		{
    37  			Name:                   "Config overrides create",
    38  			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 0),
    39  			Config:                 expectedConfigForValues(2, 0, 7, 0, 0),
    40  			Expected:               timeoutForValues(2, 0, 7, 0, 0),
    41  			ShouldErr:              false,
    42  		},
    43  		{
    44  			Name:                   "Config overrides create, default provided. Should still have zero values",
    45  			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
    46  			Config:                 expectedConfigForValues(2, 0, 7, 0, 0),
    47  			Expected:               timeoutForValues(2, 0, 7, 0, 3),
    48  			ShouldErr:              false,
    49  		},
    50  		{
    51  			Name:                   "Use something besides 'minutes'",
    52  			ResourceDefaultTimeout: timeoutForValues(10, 0, 5, 0, 3),
    53  			Config: map[string]interface{}{
    54  				"create": "2h",
    55  			},
    56  			Expected:  timeoutForValues(120, 0, 5, 0, 3),
    57  			ShouldErr: false,
    58  		},
    59  	}
    60  
    61  	for i, c := range cases {
    62  		t.Run(fmt.Sprintf("%d-%s", i, c.Name), func(t *testing.T) {
    63  			r := &Resource{
    64  				Timeouts: c.ResourceDefaultTimeout,
    65  			}
    66  
    67  			conf := tofu.NewResourceConfigRaw(
    68  				map[string]interface{}{
    69  					"foo":             "bar",
    70  					TimeoutsConfigKey: c.Config,
    71  				},
    72  			)
    73  
    74  			timeout := &ResourceTimeout{}
    75  			decodeErr := timeout.ConfigDecode(r, conf)
    76  			if c.ShouldErr {
    77  				if decodeErr == nil {
    78  					t.Fatalf("ConfigDecode case (%d): Expected bad timeout key: %s", i, decodeErr)
    79  				}
    80  				// should error, err was not nil, continue
    81  				return
    82  			} else {
    83  				if decodeErr != nil {
    84  					// should not error, error was not nil, fatal
    85  					t.Fatalf("decodeError was not nil: %s", decodeErr)
    86  				}
    87  			}
    88  
    89  			if !reflect.DeepEqual(c.Expected, timeout) {
    90  				t.Fatalf("ConfigDecode match error case (%d).\nExpected:\n%#v\nGot:\n%#v", i, c.Expected, timeout)
    91  			}
    92  		})
    93  	}
    94  }
    95  
    96  func TestResourceTimeout_ConfigDecode(t *testing.T) {
    97  	r := &Resource{
    98  		Timeouts: &ResourceTimeout{
    99  			Create: DefaultTimeout(10 * time.Minute),
   100  			Update: DefaultTimeout(5 * time.Minute),
   101  		},
   102  	}
   103  
   104  	c := tofu.NewResourceConfigRaw(
   105  		map[string]interface{}{
   106  			"foo": "bar",
   107  			TimeoutsConfigKey: map[string]interface{}{
   108  				"create": "2m",
   109  				"update": "1m",
   110  			},
   111  		},
   112  	)
   113  
   114  	timeout := &ResourceTimeout{}
   115  	err := timeout.ConfigDecode(r, c)
   116  	if err != nil {
   117  		t.Fatalf("Expected good timeout returned:, %s", err)
   118  	}
   119  
   120  	expected := &ResourceTimeout{
   121  		Create: DefaultTimeout(2 * time.Minute),
   122  		Update: DefaultTimeout(1 * time.Minute),
   123  	}
   124  
   125  	if !reflect.DeepEqual(timeout, expected) {
   126  		t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout)
   127  	}
   128  }
   129  
   130  func TestResourceTimeout_legacyConfigDecode(t *testing.T) {
   131  	r := &Resource{
   132  		Timeouts: &ResourceTimeout{
   133  			Create: DefaultTimeout(10 * time.Minute),
   134  			Update: DefaultTimeout(5 * time.Minute),
   135  		},
   136  	}
   137  
   138  	c := tofu.NewResourceConfigRaw(
   139  		map[string]interface{}{
   140  			"foo": "bar",
   141  			TimeoutsConfigKey: []interface{}{
   142  				map[string]interface{}{
   143  					"create": "2m",
   144  					"update": "1m",
   145  				},
   146  			},
   147  		},
   148  	)
   149  
   150  	timeout := &ResourceTimeout{}
   151  	err := timeout.ConfigDecode(r, c)
   152  	if err != nil {
   153  		t.Fatalf("Expected good timeout returned:, %s", err)
   154  	}
   155  
   156  	expected := &ResourceTimeout{
   157  		Create: DefaultTimeout(2 * time.Minute),
   158  		Update: DefaultTimeout(1 * time.Minute),
   159  	}
   160  
   161  	if !reflect.DeepEqual(timeout, expected) {
   162  		t.Fatalf("bad timeout decode.\nExpected:\n%#v\nGot:\n%#v\n", expected, timeout)
   163  	}
   164  }
   165  
   166  func TestResourceTimeout_DiffEncode_basic(t *testing.T) {
   167  	cases := []struct {
   168  		Timeout  *ResourceTimeout
   169  		Expected map[string]interface{}
   170  		// Not immediately clear when an error would hit
   171  		ShouldErr bool
   172  	}{
   173  		// Two fields
   174  		{
   175  			Timeout:   timeoutForValues(10, 0, 5, 0, 0),
   176  			Expected:  map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)},
   177  			ShouldErr: false,
   178  		},
   179  		// Two fields, one is Default
   180  		{
   181  			Timeout:   timeoutForValues(10, 0, 0, 0, 7),
   182  			Expected:  map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)},
   183  			ShouldErr: false,
   184  		},
   185  		// All fields
   186  		{
   187  			Timeout:   timeoutForValues(10, 3, 4, 1, 7),
   188  			Expected:  map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)},
   189  			ShouldErr: false,
   190  		},
   191  		// No fields
   192  		{
   193  			Timeout:   &ResourceTimeout{},
   194  			Expected:  nil,
   195  			ShouldErr: false,
   196  		},
   197  	}
   198  
   199  	for _, c := range cases {
   200  		state := &tofu.InstanceDiff{}
   201  		err := c.Timeout.DiffEncode(state)
   202  		if err != nil && !c.ShouldErr {
   203  			t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
   204  		}
   205  
   206  		// should maybe just compare [TimeoutKey] but for now we're assuming only
   207  		// that in Meta
   208  		if !reflect.DeepEqual(state.Meta, c.Expected) {
   209  			t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
   210  		}
   211  	}
   212  	// same test cases but for InstanceState
   213  	for _, c := range cases {
   214  		state := &tofu.InstanceState{}
   215  		err := c.Timeout.StateEncode(state)
   216  		if err != nil && !c.ShouldErr {
   217  			t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, state.Meta)
   218  		}
   219  
   220  		// should maybe just compare [TimeoutKey] but for now we're assuming only
   221  		// that in Meta
   222  		if !reflect.DeepEqual(state.Meta, c.Expected) {
   223  			t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, state.Meta)
   224  		}
   225  	}
   226  }
   227  
   228  func TestResourceTimeout_MetaDecode_basic(t *testing.T) {
   229  	cases := []struct {
   230  		State    *tofu.InstanceDiff
   231  		Expected *ResourceTimeout
   232  		// Not immediately clear when an error would hit
   233  		ShouldErr bool
   234  	}{
   235  		// Two fields
   236  		{
   237  			State:     &tofu.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 5, 0, 0)}},
   238  			Expected:  timeoutForValues(10, 0, 5, 0, 0),
   239  			ShouldErr: false,
   240  		},
   241  		// Two fields, one is Default
   242  		{
   243  			State:     &tofu.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 0, 0, 0, 7)}},
   244  			Expected:  timeoutForValues(10, 7, 7, 7, 7),
   245  			ShouldErr: false,
   246  		},
   247  		// All fields
   248  		{
   249  			State:     &tofu.InstanceDiff{Meta: map[string]interface{}{TimeoutKey: expectedForValues(10, 3, 4, 1, 7)}},
   250  			Expected:  timeoutForValues(10, 3, 4, 1, 7),
   251  			ShouldErr: false,
   252  		},
   253  		// No fields
   254  		{
   255  			State:     &tofu.InstanceDiff{},
   256  			Expected:  &ResourceTimeout{},
   257  			ShouldErr: false,
   258  		},
   259  	}
   260  
   261  	for _, c := range cases {
   262  		rt := &ResourceTimeout{}
   263  		err := rt.DiffDecode(c.State)
   264  		if err != nil && !c.ShouldErr {
   265  			t.Fatalf("Error, expected:\n%#v\n got:\n%#v\n", c.Expected, rt)
   266  		}
   267  
   268  		// should maybe just compare [TimeoutKey] but for now we're assuming only
   269  		// that in Meta
   270  		if !reflect.DeepEqual(rt, c.Expected) {
   271  			t.Fatalf("Encode not equal, expected:\n%#v\n\ngot:\n%#v\n", c.Expected, rt)
   272  		}
   273  	}
   274  }
   275  
   276  func timeoutForValues(create, read, update, del, def int) *ResourceTimeout {
   277  	rt := ResourceTimeout{}
   278  
   279  	if create != 0 {
   280  		rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
   281  	}
   282  	if read != 0 {
   283  		rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
   284  	}
   285  	if update != 0 {
   286  		rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
   287  	}
   288  	if del != 0 {
   289  		rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
   290  	}
   291  
   292  	if def != 0 {
   293  		rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
   294  	}
   295  
   296  	return &rt
   297  }
   298  
   299  // Generates a ResourceTimeout struct that should reflect the
   300  // d.Timeout("key") results
   301  func expectedTimeoutForValues(create, read, update, del, def int) *ResourceTimeout {
   302  	rt := ResourceTimeout{}
   303  
   304  	defaultValues := []*int{&create, &read, &update, &del, &def}
   305  	for _, v := range defaultValues {
   306  		if *v == 0 {
   307  			*v = 20
   308  		}
   309  	}
   310  
   311  	if create != 0 {
   312  		rt.Create = DefaultTimeout(time.Duration(create) * time.Minute)
   313  	}
   314  	if read != 0 {
   315  		rt.Read = DefaultTimeout(time.Duration(read) * time.Minute)
   316  	}
   317  	if update != 0 {
   318  		rt.Update = DefaultTimeout(time.Duration(update) * time.Minute)
   319  	}
   320  	if del != 0 {
   321  		rt.Delete = DefaultTimeout(time.Duration(del) * time.Minute)
   322  	}
   323  
   324  	if def != 0 {
   325  		rt.Default = DefaultTimeout(time.Duration(def) * time.Minute)
   326  	}
   327  
   328  	return &rt
   329  }
   330  
   331  func expectedForValues(create, read, update, del, def int) map[string]interface{} {
   332  	ex := make(map[string]interface{})
   333  
   334  	if create != 0 {
   335  		ex["create"] = DefaultTimeout(time.Duration(create) * time.Minute).Nanoseconds()
   336  	}
   337  	if read != 0 {
   338  		ex["read"] = DefaultTimeout(time.Duration(read) * time.Minute).Nanoseconds()
   339  	}
   340  	if update != 0 {
   341  		ex["update"] = DefaultTimeout(time.Duration(update) * time.Minute).Nanoseconds()
   342  	}
   343  	if del != 0 {
   344  		ex["delete"] = DefaultTimeout(time.Duration(del) * time.Minute).Nanoseconds()
   345  	}
   346  
   347  	if def != 0 {
   348  		defNano := DefaultTimeout(time.Duration(def) * time.Minute).Nanoseconds()
   349  		ex["default"] = defNano
   350  
   351  		for _, k := range timeoutKeys() {
   352  			if _, ok := ex[k]; !ok {
   353  				ex[k] = defNano
   354  			}
   355  		}
   356  	}
   357  
   358  	return ex
   359  }
   360  
   361  func expectedConfigForValues(create, read, update, delete, def int) map[string]interface{} {
   362  	ex := make(map[string]interface{}, 0)
   363  
   364  	if create != 0 {
   365  		ex["create"] = fmt.Sprintf("%dm", create)
   366  	}
   367  	if read != 0 {
   368  		ex["read"] = fmt.Sprintf("%dm", read)
   369  	}
   370  	if update != 0 {
   371  		ex["update"] = fmt.Sprintf("%dm", update)
   372  	}
   373  	if delete != 0 {
   374  		ex["delete"] = fmt.Sprintf("%dm", delete)
   375  	}
   376  
   377  	if def != 0 {
   378  		ex["default"] = fmt.Sprintf("%dm", def)
   379  	}
   380  	return ex
   381  }