github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/resource/testing_new.go (about) 1 package resource 2 3 import ( 4 "fmt" 5 "log" 6 "reflect" 7 "strings" 8 "testing" 9 10 "github.com/davecgh/go-spew/spew" 11 tfjson "github.com/hashicorp/terraform-json" 12 "github.com/hashicorp/terraform-plugin-sdk/acctest" 13 "github.com/hashicorp/terraform-plugin-sdk/terraform" 14 tftest "github.com/hashicorp/terraform-plugin-test/v2" 15 ) 16 17 func runPostTestDestroy(t *testing.T, c TestCase, wd *tftest.WorkingDir, factories map[string]terraform.ResourceProviderFactory, statePreDestroy *terraform.State) error { 18 t.Helper() 19 20 err := runProviderCommand(t, func() error { 21 wd.RequireDestroy(t) 22 return nil 23 }, wd, factories) 24 if err != nil { 25 return err 26 } 27 28 if c.CheckDestroy != nil { 29 if err := c.CheckDestroy(statePreDestroy); err != nil { 30 return err 31 } 32 } 33 34 return nil 35 } 36 37 func RunNewTest(t *testing.T, c TestCase, providers map[string]terraform.ResourceProvider) { 38 t.Helper() 39 40 spewConf := spew.NewDefaultConfig() 41 spewConf.SortKeys = true 42 wd := acctest.TestHelper.RequireNewWorkingDir(t) 43 44 defer func() { 45 var statePreDestroy *terraform.State 46 err := runProviderCommand(t, func() error { 47 statePreDestroy = getState(t, wd) 48 return nil 49 }, wd, c.ProviderFactories) 50 if err != nil { 51 t.Fatalf("Error retrieving state, there may be dangling resources: %s", err.Error()) 52 return 53 } 54 55 if !stateIsEmpty(statePreDestroy) { 56 err := runPostTestDestroy(t, c, wd, c.ProviderFactories, statePreDestroy) 57 if err != nil { 58 t.Fatalf("Error running post-test destroy, there may be dangling resources: %s", err.Error()) 59 } 60 } 61 62 wd.Close() 63 }() 64 65 providerCfg, err := testProviderConfig(c) 66 if err != nil { 67 t.Fatal(err) 68 } 69 70 wd.RequireSetConfig(t, providerCfg) 71 72 err = runProviderCommand(t, func() error { 73 return wd.Init() 74 }, wd, c.ProviderFactories) 75 if err != nil { 76 t.Fatalf("Error running init: %s", err.Error()) 77 return 78 } 79 80 // use this to track last step succesfully applied 81 // acts as default for import tests 82 var appliedCfg string 83 84 for i, step := range c.Steps { 85 86 if step.PreConfig != nil { 87 step.PreConfig() 88 } 89 90 if step.SkipFunc != nil { 91 skip, err := step.SkipFunc() 92 if err != nil { 93 t.Fatal(err) 94 } 95 if skip { 96 log.Printf("[WARN] Skipping step %d/%d", i+1, len(c.Steps)) 97 continue 98 } 99 } 100 101 if step.ImportState { 102 step.providers = providers 103 err := testStepNewImportState(t, c, wd, step, appliedCfg) 104 if step.ExpectError != nil { 105 if err == nil { 106 t.Fatalf("Step %d/%d error running import: expected an error but got none", i+1, len(c.Steps)) 107 } 108 if !step.ExpectError.MatchString(err.Error()) { 109 t.Fatalf("Step %d/%d error running import, expected an error with pattern (%s), no match on: %s", i+1, len(c.Steps), step.ExpectError.String(), err) 110 } 111 } else { 112 if err != nil { 113 t.Fatalf("Step %d/%d error running import: %s", i+1, len(c.Steps), err) 114 } 115 } 116 continue 117 } 118 119 if step.Config != "" { 120 err := testStepNewConfig(t, c, wd, step) 121 if step.ExpectError != nil { 122 if err == nil { 123 t.Fatalf("Step %d/%d, expected an error but got none", i+1, len(c.Steps)) 124 } 125 if !step.ExpectError.MatchString(err.Error()) { 126 t.Fatalf("Step %d/%d, expected an error with pattern, no match on: %s", i+1, len(c.Steps), err) 127 } 128 } else { 129 if err != nil { 130 t.Fatalf("Step %d/%d error: %s", i+1, len(c.Steps), err) 131 } 132 } 133 appliedCfg = step.Config 134 continue 135 } 136 137 t.Fatal("Unsupported test mode") 138 } 139 } 140 141 func getState(t *testing.T, wd *tftest.WorkingDir) *terraform.State { 142 t.Helper() 143 144 jsonState := wd.RequireState(t) 145 state, err := shimStateFromJson(jsonState) 146 if err != nil { 147 t.Fatal(err) 148 } 149 return state 150 } 151 152 func stateIsEmpty(state *terraform.State) bool { 153 return state.Empty() || !state.HasResources() 154 } 155 156 func planIsEmpty(plan *tfjson.Plan) bool { 157 for _, rc := range plan.ResourceChanges { 158 if rc.Mode == tfjson.DataResourceMode { 159 // Skip data sources as the current implementation ignores 160 // existing state and they are all re-read every time 161 continue 162 } 163 164 for _, a := range rc.Change.Actions { 165 if a != tfjson.ActionNoop { 166 return false 167 } 168 } 169 } 170 return true 171 } 172 173 func testIDRefresh(c TestCase, t *testing.T, wd *tftest.WorkingDir, step TestStep, r *terraform.ResourceState) error { 174 t.Helper() 175 176 spewConf := spew.NewDefaultConfig() 177 spewConf.SortKeys = true 178 179 // Build the state. The state is just the resource with an ID. There 180 // are no attributes. We only set what is needed to perform a refresh. 181 state := terraform.NewState() 182 state.RootModule().Resources = make(map[string]*terraform.ResourceState) 183 state.RootModule().Resources[c.IDRefreshName] = &terraform.ResourceState{} 184 185 // Temporarily set the config to a minimal provider config for the refresh 186 // test. After the refresh we can reset it. 187 cfg, err := testProviderConfig(c) 188 if err != nil { 189 return err 190 } 191 wd.RequireSetConfig(t, cfg) 192 defer wd.RequireSetConfig(t, step.Config) 193 194 // Refresh! 195 err = runProviderCommand(t, func() error { 196 wd.RequireRefresh(t) 197 state = getState(t, wd) 198 return nil 199 }, wd, c.ProviderFactories) 200 if err != nil { 201 return err 202 } 203 204 // Verify attribute equivalence. 205 actualR := state.RootModule().Resources[c.IDRefreshName] 206 if actualR == nil { 207 return fmt.Errorf("Resource gone!") 208 } 209 if actualR.Primary == nil { 210 return fmt.Errorf("Resource has no primary instance") 211 } 212 actual := actualR.Primary.Attributes 213 expected := r.Primary.Attributes 214 // Remove fields we're ignoring 215 for _, v := range c.IDRefreshIgnore { 216 for k := range actual { 217 if strings.HasPrefix(k, v) { 218 delete(actual, k) 219 } 220 } 221 for k := range expected { 222 if strings.HasPrefix(k, v) { 223 delete(expected, k) 224 } 225 } 226 } 227 228 if !reflect.DeepEqual(actual, expected) { 229 // Determine only the different attributes 230 for k, v := range expected { 231 if av, ok := actual[k]; ok && v == av { 232 delete(expected, k) 233 delete(actual, k) 234 } 235 } 236 237 spewConf := spew.NewDefaultConfig() 238 spewConf.SortKeys = true 239 return fmt.Errorf( 240 "Attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+ 241 "\n\n%s\n\n%s", 242 spewConf.Sdump(actual), spewConf.Sdump(expected)) 243 } 244 245 return nil 246 }