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  }