github.com/opentofu/opentofu@v1.7.1/internal/backend/local/testing.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 local 7 8 import ( 9 "path/filepath" 10 "testing" 11 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/opentofu/opentofu/internal/addrs" 15 "github.com/opentofu/opentofu/internal/backend" 16 "github.com/opentofu/opentofu/internal/configs/configschema" 17 "github.com/opentofu/opentofu/internal/encryption" 18 "github.com/opentofu/opentofu/internal/providers" 19 "github.com/opentofu/opentofu/internal/states" 20 "github.com/opentofu/opentofu/internal/states/statemgr" 21 "github.com/opentofu/opentofu/internal/tofu" 22 ) 23 24 // TestLocal returns a configured Local struct with temporary paths and 25 // in-memory ContextOpts. 26 // 27 // No operations will be called on the returned value, so you can still set 28 // public fields without any locks. 29 func TestLocal(t *testing.T) *Local { 30 t.Helper() 31 tempDir, err := filepath.EvalSymlinks(t.TempDir()) 32 if err != nil { 33 t.Fatal(err) 34 } 35 36 local := New(encryption.StateEncryptionDisabled()) 37 local.StatePath = filepath.Join(tempDir, "state.tfstate") 38 local.StateOutPath = filepath.Join(tempDir, "state.tfstate") 39 local.StateBackupPath = filepath.Join(tempDir, "state.tfstate.bak") 40 local.StateWorkspaceDir = filepath.Join(tempDir, "state.tfstate.d") 41 local.ContextOpts = &tofu.ContextOpts{} 42 43 return local 44 } 45 46 // TestLocalProvider modifies the ContextOpts of the *Local parameter to 47 // have a provider with the given name. 48 func TestLocalProvider(t *testing.T, b *Local, name string, schema providers.ProviderSchema) *tofu.MockProvider { 49 // Build a mock resource provider for in-memory operations 50 p := new(tofu.MockProvider) 51 52 p.GetProviderSchemaResponse = &schema 53 54 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 55 // this is a destroy plan, 56 if req.ProposedNewState.IsNull() { 57 resp.PlannedState = req.ProposedNewState 58 resp.PlannedPrivate = req.PriorPrivate 59 return resp 60 } 61 62 rSchema, _ := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName) 63 if rSchema == nil { 64 rSchema = &configschema.Block{} // default schema is empty 65 } 66 plannedVals := map[string]cty.Value{} 67 for name, attrS := range rSchema.Attributes { 68 val := req.ProposedNewState.GetAttr(name) 69 if attrS.Computed && val.IsNull() { 70 val = cty.UnknownVal(attrS.Type) 71 } 72 plannedVals[name] = val 73 } 74 for name := range rSchema.BlockTypes { 75 // For simplicity's sake we just copy the block attributes over 76 // verbatim, since this package's mock providers are all relatively 77 // simple -- we're testing the backend, not esoteric provider features. 78 plannedVals[name] = req.ProposedNewState.GetAttr(name) 79 } 80 81 return providers.PlanResourceChangeResponse{ 82 PlannedState: cty.ObjectVal(plannedVals), 83 PlannedPrivate: req.PriorPrivate, 84 } 85 } 86 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 87 return providers.ReadResourceResponse{NewState: req.PriorState} 88 } 89 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 90 return providers.ReadDataSourceResponse{State: req.Config} 91 } 92 93 // Initialize the opts 94 if b.ContextOpts == nil { 95 b.ContextOpts = &tofu.ContextOpts{} 96 } 97 98 // Set up our provider 99 b.ContextOpts.Providers = map[addrs.Provider]providers.Factory{ 100 addrs.NewDefaultProvider(name): providers.FactoryFixed(p), 101 } 102 103 return p 104 105 } 106 107 // TestLocalSingleState is a backend implementation that wraps Local 108 // and modifies it to only support single states (returns 109 // ErrWorkspacesNotSupported for multi-state operations). 110 // 111 // This isn't an actual use case, this is exported just to provide a 112 // easy way to test that behavior. 113 type TestLocalSingleState struct { 114 *Local 115 } 116 117 // TestNewLocalSingle is a factory for creating a TestLocalSingleState. 118 // This function matches the signature required for backend/init. 119 func TestNewLocalSingle(enc encryption.StateEncryption) backend.Backend { 120 return &TestLocalSingleState{Local: New(encryption.StateEncryptionDisabled())} 121 } 122 123 func (b *TestLocalSingleState) Workspaces() ([]string, error) { 124 return nil, backend.ErrWorkspacesNotSupported 125 } 126 127 func (b *TestLocalSingleState) DeleteWorkspace(string, bool) error { 128 return backend.ErrWorkspacesNotSupported 129 } 130 131 func (b *TestLocalSingleState) StateMgr(name string) (statemgr.Full, error) { 132 if name != backend.DefaultStateName { 133 return nil, backend.ErrWorkspacesNotSupported 134 } 135 136 return b.Local.StateMgr(name) 137 } 138 139 // TestLocalNoDefaultState is a backend implementation that wraps 140 // Local and modifies it to support named states, but not the 141 // default state. It returns ErrDefaultWorkspaceNotSupported when 142 // the DefaultStateName is used. 143 type TestLocalNoDefaultState struct { 144 *Local 145 } 146 147 // TestNewLocalNoDefault is a factory for creating a TestLocalNoDefaultState. 148 // This function matches the signature required for backend/init. 149 func TestNewLocalNoDefault(enc encryption.StateEncryption) backend.Backend { 150 return &TestLocalNoDefaultState{Local: New(encryption.StateEncryptionDisabled())} 151 } 152 153 func (b *TestLocalNoDefaultState) Workspaces() ([]string, error) { 154 workspaces, err := b.Local.Workspaces() 155 if err != nil { 156 return nil, err 157 } 158 159 filtered := workspaces[:0] 160 for _, name := range workspaces { 161 if name != backend.DefaultStateName { 162 filtered = append(filtered, name) 163 } 164 } 165 166 return filtered, nil 167 } 168 169 func (b *TestLocalNoDefaultState) DeleteWorkspace(name string, force bool) error { 170 if name == backend.DefaultStateName { 171 return backend.ErrDefaultWorkspaceNotSupported 172 } 173 return b.Local.DeleteWorkspace(name, force) 174 } 175 176 func (b *TestLocalNoDefaultState) StateMgr(name string) (statemgr.Full, error) { 177 if name == backend.DefaultStateName { 178 return nil, backend.ErrDefaultWorkspaceNotSupported 179 } 180 return b.Local.StateMgr(name) 181 } 182 183 func testStateFile(t *testing.T, path string, s *states.State) { 184 t.Helper() 185 186 if err := statemgr.WriteAndPersist(statemgr.NewFilesystem(path, encryption.StateEncryptionDisabled()), s, nil); err != nil { 187 t.Fatal(err) 188 } 189 } 190 191 func mustProviderConfig(s string) addrs.AbsProviderConfig { 192 p, diags := addrs.ParseAbsProviderConfigStr(s) 193 if diags.HasErrors() { 194 panic(diags.Err()) 195 } 196 return p 197 } 198 199 func mustResourceInstanceAddr(s string) addrs.AbsResourceInstance { 200 addr, diags := addrs.ParseAbsResourceInstanceStr(s) 201 if diags.HasErrors() { 202 panic(diags.Err()) 203 } 204 return addr 205 } 206 207 // assertBackendStateUnlocked attempts to lock the backend state for a test. 208 // Failure indicates that the state was locked and false is returned. 209 // True is returned if a lock was obtained. 210 func assertBackendStateUnlocked(t *testing.T, b *Local) bool { 211 t.Helper() 212 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 213 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 214 t.Errorf("state is already locked: %s", err.Error()) 215 // lock was obtained 216 return false 217 } 218 // lock was not obtained 219 return true 220 } 221 222 // assertBackendStateLocked attempts to lock the backend state for a test. 223 // Failure indicates that the state was not locked and false is returned. 224 // True is returned if a lock was not obtained. 225 func assertBackendStateLocked(t *testing.T, b *Local) bool { 226 t.Helper() 227 stateMgr, _ := b.StateMgr(backend.DefaultStateName) 228 if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { 229 // lock was not obtained 230 return true 231 } 232 t.Error("unexpected success locking state") 233 // lock was obtained 234 return false 235 }