github.com/opentofu/opentofu@v1.7.1/internal/encryption/method/compliancetest/compliance.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 compliancetest 7 8 import ( 9 "bytes" 10 "errors" 11 "reflect" 12 "testing" 13 14 "github.com/hashicorp/hcl/v2/gohcl" 15 "github.com/opentofu/opentofu/internal/encryption/compliancetest" 16 "github.com/opentofu/opentofu/internal/encryption/config" 17 "github.com/opentofu/opentofu/internal/encryption/method" 18 ) 19 20 // ComplianceTest tests the functionality of a method to make sure it conforms to the expectations of the method 21 // interface. 22 func ComplianceTest[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method]( 23 t *testing.T, 24 testConfig TestConfiguration[TDescriptor, TConfig, TMethod], 25 ) { 26 testConfig.execute(t) 27 } 28 29 type TestConfiguration[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method] struct { 30 Descriptor TDescriptor 31 // HCLParseTestCases contains the test cases of parsing HCL configuration and then validating it using the Build() 32 // function. 33 HCLParseTestCases map[string]HCLParseTestCase[TDescriptor, TConfig, TMethod] 34 35 // ConfigStructT validates that a certain config results or does not result in a valid Build() call. 36 ConfigStructTestCases map[string]ConfigStructTestCase[TConfig, TMethod] 37 38 // ProvideTestCase exercises the entire chain and generates two keys. 39 EncryptDecryptTestCase EncryptDecryptTestCase[TConfig, TMethod] 40 } 41 42 func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) execute(t *testing.T) { 43 t.Run("id", func(t *testing.T) { 44 cfg.testID(t) 45 }) 46 t.Run("hcl", func(t *testing.T) { 47 cfg.testHCL(t) 48 }) 49 t.Run("config-struct", func(t *testing.T) { 50 cfg.testConfigStruct(t) 51 }) 52 t.Run("encrypt-decrypt", func(t *testing.T) { 53 cfg.EncryptDecryptTestCase.execute(t) 54 }) 55 } 56 57 func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) testID(t *testing.T) { 58 id := cfg.Descriptor.ID() 59 if err := id.Validate(); err != nil { 60 compliancetest.Fail(t, "Invalid ID returned from method descriptor: %s (%v)", id, err) 61 } else { 62 compliancetest.Log(t, "The ID provided by the method descriptor is valid: %s", id) 63 } 64 } 65 66 func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) testHCL(t *testing.T) { 67 if cfg.HCLParseTestCases == nil { 68 compliancetest.Fail(t, "Please provide a map to HCLParseTestCases.") 69 } 70 hasInvalidHCL := false 71 hasValidHCLInvalidBuild := false 72 hasValidBuild := false 73 for name, tc := range cfg.HCLParseTestCases { 74 tc := tc 75 if !tc.ValidHCL { 76 hasInvalidHCL = true 77 } else { 78 if tc.ValidBuild { 79 hasValidBuild = true 80 } else { 81 hasValidHCLInvalidBuild = true 82 } 83 } 84 t.Run(name, func(t *testing.T) { 85 tc.execute(t, cfg.Descriptor) 86 }) 87 } 88 t.Run("completeness", func(t *testing.T) { 89 if !hasInvalidHCL { 90 compliancetest.Fail(t, "Please provide at least one test case with an invalid HCL.") 91 } 92 if !hasValidHCLInvalidBuild { 93 compliancetest.Fail(t, "Please provide at least one test case with a valid HCL that fails on Build()") 94 } 95 if !hasValidBuild { 96 compliancetest.Fail( 97 t, 98 "Please provide at least one test case with a valid HCL that succeeds on Build()", 99 ) 100 } 101 }) 102 } 103 104 func (cfg *TestConfiguration[TDescriptor, TConfig, TMethod]) testConfigStruct(t *testing.T) { 105 compliancetest.ConfigStruct[TConfig](t, cfg.Descriptor.ConfigStruct()) 106 107 if cfg.ConfigStructTestCases == nil { 108 compliancetest.Fail(t, "Please provide a map to ConfigStructTestCases.") 109 } 110 111 for name, tc := range cfg.ConfigStructTestCases { 112 tc := tc 113 t.Run(name, func(t *testing.T) { 114 tc.execute(t) 115 }) 116 } 117 } 118 119 // HCLParseTestCase contains a test case that parses HCL into a configuration. 120 type HCLParseTestCase[TDescriptor method.Descriptor, TConfig method.Config, TMethod method.Method] struct { 121 // HCL contains the code that should be parsed into the configuration structure. 122 HCL string 123 // ValidHCL indicates that the HCL block should be parsable into the configuration structure, but not necessarily 124 // result in a valid Build() call. 125 ValidHCL bool 126 // ValidBuild indicates that calling the Build() function should not result in an error. 127 ValidBuild bool 128 // Validate is an extra optional validation function that can check if the configuration contains the correct 129 // values parsed from HCL. If ValidBuild is true, the method will be passed as well. 130 Validate func(config TConfig, method TMethod) error 131 } 132 133 func (h *HCLParseTestCase[TDescriptor, TConfig, TMethod]) execute(t *testing.T, descriptor TDescriptor) { 134 parseError := false 135 parsedConfig, diags := config.LoadConfigFromString("config.hcl", h.HCL) 136 if h.ValidHCL { 137 if diags.HasErrors() { 138 compliancetest.Fail(t, "Unexpected HCL error (%v).", diags) 139 } else { 140 compliancetest.Log(t, "HCL successfully parsed.") 141 } 142 } else { 143 if diags.HasErrors() { 144 parseError = true 145 } 146 } 147 148 configStruct := descriptor.ConfigStruct() 149 diags = gohcl.DecodeBody( 150 parsedConfig.MethodConfigs[0].Body, 151 nil, 152 configStruct, 153 ) 154 var m TMethod 155 if h.ValidHCL { 156 if diags.HasErrors() { 157 compliancetest.Fail(t, "Failed to parse empty HCL block into config struct (%v).", diags) 158 } else { 159 compliancetest.Log(t, "HCL successfully loaded into config struct.") 160 } 161 162 m = buildConfigAndValidate[TMethod](t, configStruct, h.ValidBuild) 163 } else { 164 if !parseError && !diags.HasErrors() { 165 compliancetest.Fail(t, "Expected error during HCL parsing, but no error was returned.") 166 } else { 167 compliancetest.Log(t, "HCL loading errored correctly (%v).", diags) 168 } 169 } 170 171 if h.Validate != nil { 172 if err := h.Validate(configStruct.(TConfig), m); err != nil { 173 compliancetest.Fail(t, "Error during validation and configuration (%v).", err) 174 } else { 175 compliancetest.Log(t, "Successfully validated parsed HCL config and applied modifications.") 176 } 177 } else { 178 compliancetest.Log(t, "No ValidateAndConfigure provided, skipping HCL parse validation.") 179 } 180 } 181 182 // ConfigStructTestCase validates that the config struct is behaving correctly when Build() is called. 183 type ConfigStructTestCase[TConfig method.Config, TMethod method.Method] struct { 184 Config TConfig 185 ValidBuild bool 186 Validate func(method TMethod) error 187 } 188 189 func (m ConfigStructTestCase[TConfig, TMethod]) execute(t *testing.T) { 190 newMethod := buildConfigAndValidate[TMethod, TConfig](t, m.Config, m.ValidBuild) 191 if m.Validate != nil { 192 if err := m.Validate(newMethod); err != nil { 193 compliancetest.Fail(t, "method validation failed (%v)", err) 194 } 195 } 196 } 197 198 // EncryptDecryptTestCase handles a full encryption-decryption cycle. 199 type EncryptDecryptTestCase[TConfig method.Config, TMethod method.Method] struct { 200 // ValidEncryptOnlyConfig is a configuration that has no decryption key and can only be used for encryption. The 201 // key must match ValidFullConfig. 202 ValidEncryptOnlyConfig TConfig 203 // ValidFullConfig is a configuration that contains both an encryption and decryption key. 204 ValidFullConfig TConfig 205 } 206 207 func (m EncryptDecryptTestCase[TConfig, TMethod]) execute(t *testing.T) { 208 if reflect.ValueOf(m.ValidEncryptOnlyConfig).IsNil() { 209 compliancetest.Fail(t, "Please provide a ValidEncryptOnlyConfig to EncryptDecryptTestCase.") 210 } 211 if reflect.ValueOf(m.ValidFullConfig).IsNil() { 212 compliancetest.Fail(t, "Please provide a ValidFullConfig to EncryptDecryptTestCase.") 213 } 214 215 encryptMethod := buildConfigAndValidate[TMethod, TConfig](t, m.ValidEncryptOnlyConfig, true) 216 decryptMethod := buildConfigAndValidate[TMethod, TConfig](t, m.ValidFullConfig, true) 217 218 plainData := []byte("Hello world!") 219 encryptedData, err := encryptMethod.Encrypt(plainData) 220 if err != nil { 221 compliancetest.Fail(t, "Unexpected error after Encrypt() on the encrypt-only method (%v).", err) 222 } 223 224 _, err = encryptMethod.Decrypt(encryptedData) 225 if err == nil { 226 compliancetest.Fail(t, "Decrypt() did not fail without a decryption key.") 227 } else { 228 compliancetest.Log(t, "Decrypt() returned an error with a decryption key.") 229 } 230 var noDecryptionKeyError *method.ErrDecryptionKeyUnavailable 231 if !errors.As(err, &noDecryptionKeyError) { 232 compliancetest.Fail(t, "Decrypt() returned a %T instead of a %T without a decryption key. Please use the correct typed errors.", err, noDecryptionKeyError) 233 } else { 234 compliancetest.Log(t, "Decrypt() returned the correct error type of %T without a decryption key.", noDecryptionKeyError) 235 } 236 237 _, err = decryptMethod.Decrypt([]byte{}) 238 if err == nil { 239 compliancetest.Fail(t, "Decrypt() must return an error when decrypting empty data, no error returned.") 240 } else { 241 compliancetest.Log(t, "Decrypt() correctly returned an error when decrypting empty data.") 242 } 243 var typedDecryptError *method.ErrDecryptionFailed 244 if !errors.As(err, &typedDecryptError) { 245 compliancetest.Fail(t, "Decrypt() returned a %T instead of a %T when decrypting empty data. Please use the correct typed errors.", err, typedDecryptError) 246 } else { 247 compliancetest.Log(t, "Decrypt() returned the correct error type of %T when decrypting empty data.", typedDecryptError) 248 } 249 typedDecryptError = nil 250 251 _, err = decryptMethod.Decrypt(plainData) 252 if err == nil { 253 compliancetest.Fail(t, "Decrypt() must return an error when decrypting unencrypted data, no error returned.") 254 } else { 255 compliancetest.Log(t, "Decrypt() correctly returned an error when decrypting unencrypted data.") 256 } 257 if !errors.As(err, &typedDecryptError) { 258 compliancetest.Fail(t, "Decrypt() returned a %T instead of a %T when decrypting unencrypted data. Please use the correct typed errors.", err, typedDecryptError) 259 } else { 260 compliancetest.Log(t, "Decrypt() returned the correct error type of %T when decrypting unencrypted data.", typedDecryptError) 261 } 262 263 decryptedData, err := decryptMethod.Decrypt(encryptedData) 264 if err != nil { 265 compliancetest.Fail(t, "Decrypt() failed to decrypt previously-encrypted data (%v).", err) 266 } else { 267 compliancetest.Log(t, "Decrypt() succeeded.") 268 } 269 270 if !bytes.Equal(decryptedData, plainData) { 271 compliancetest.Fail(t, "Decrypt() returned incorrect plain text data:\n%v\nexpected:\n%v", decryptedData, plainData) 272 } else { 273 compliancetest.Log(t, "Decrypt() returned the correct plain text data.") 274 } 275 } 276 277 func buildConfigAndValidate[TMethod method.Method, TConfig method.Config]( 278 t *testing.T, 279 configStruct TConfig, 280 validBuild bool, 281 ) TMethod { 282 if reflect.ValueOf(configStruct).IsNil() { 283 compliancetest.Fail(t, "Nil struct passed!") 284 } 285 286 var typedMethod TMethod 287 var ok bool 288 kp, err := configStruct.Build() 289 if validBuild { 290 if err != nil { 291 compliancetest.Fail(t, "Build() returned an unexpected error: %v.", err) 292 } else { 293 compliancetest.Log(t, "Build() did not return an error.") 294 } 295 typedMethod, ok = kp.(TMethod) 296 if !ok { 297 compliancetest.Fail(t, "Build() returned an invalid method type of %T, expected %T", kp, typedMethod) 298 } else { 299 compliancetest.Log(t, "Build() returned the correct method type of %T.", typedMethod) 300 } 301 } else { 302 if err == nil { 303 compliancetest.Fail(t, "Build() did not return an error.") 304 } else { 305 compliancetest.Log(t, "Build() correctly returned an error: %v", err) 306 } 307 308 var typedError *method.ErrInvalidConfiguration 309 if !errors.As(err, &typedError) { 310 compliancetest.Fail( 311 t, 312 "Build() did not return the correct error type, got %T but expected %T", 313 err, 314 typedError, 315 ) 316 } else { 317 compliancetest.Log(t, "Build() returned the correct error type of %T", typedError) 318 } 319 } 320 return typedMethod 321 }