terraform-provider-file/internal/provider/file_local_resource_test.go

708 lines
22 KiB
Go

// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"os"
"path/filepath"
"slices"
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
const (
defaultId = ""
defaultDirectory = "."
defaultMode = "0600"
defaultProtected = "false"
defaultHmacSecretKey = ""
)
var booleanFields = []string{"protected", "fake"}
func TestLocalResourceMetadata(t *testing.T) {
t.Run("Metadata function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
want resource.MetadataResponse
}{
{"Basic test", LocalResource{}, resource.MetadataResponse{TypeName: "file_local"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := resource.MetadataResponse{}
tc.fit.Metadata(context.Background(), resource.MetadataRequest{ProviderTypeName: "file"}, &res)
got := res
if got != tc.want {
t.Errorf("%#v.Metadata() is %v; want %v", tc.fit, got, tc.want)
}
})
}
})
}
func TestLocalSchema(t *testing.T) {
t.Run("Schema function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
want resource.SchemaResponse
}{
{"Basic test", LocalResource{}, *getLocalResourceSchema()},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := resource.SchemaResponse{}
tc.fit.Schema(context.Background(), resource.SchemaRequest{}, &r)
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Schema() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func TestLocalResourceCreate(t *testing.T) {
t.Run("Create function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.CreateRequest
want resource.CreateResponse
tearDownPath string
}{
{
"Basic",
LocalResource{},
// have
getCreateRequest(t, map[string]string{
"id": defaultId,
"name": "test_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a basic test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey, // this should use the hard coded hmac secret key for unprotected files
}),
// want
getCreateResponse(t, map[string]string{
"id": "3de642fb91d2fb0ce02fe66c3d19ebdf44cbc6a2ebcc2dad22f1950b67c1217f",
"name": "test_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a basic test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
filepath.Join(defaultDirectory, "test_basic.tmp"),
},
{
"Protected",
LocalResource{},
// have
getCreateRequest(t, map[string]string{
"id": "4ccd8ec7ea24e0524c8aba459fbf3a2649ec3cd96a1c8f9dfb326cc57a9d3127",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getCreateResponse(t, map[string]string{
"id": "4ccd8ec7ea24e0524c8aba459fbf3a2649ec3cd96a1c8f9dfb326cc57a9d3127",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
filepath.Join(defaultDirectory, "test_protected.tmp"),
},
{
"Protected using key from environment",
LocalResource{},
// have
getCreateRequest(t, map[string]string{
"id": "59fed8691a76c7693fc9dcd4fda28390a1fd3090114bc64f3e5a3abe312a92f5",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": defaultHmacSecretKey, // this relies on TF_FILE_HMAC_SECRET_KEY=thisisasupersecretkey in your environment
}),
// want
getCreateResponse(t, map[string]string{
"id": "59fed8691a76c7693fc9dcd4fda28390a1fd3090114bc64f3e5a3abe312a92f5",
"name": "test_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a test",
"protected": "true",
"hmac_secret_key": defaultHmacSecretKey,
}),
filepath.Join(defaultDirectory, "test_protected.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var plannedState LocalResourceModel
if diags := tc.have.Plan.Get(context.Background(), &plannedState); diags.HasError() {
t.Errorf("Failed to get planned state: %v", diags)
}
plannedProtected := plannedState.Protected.ValueBool()
plannedHmacSecretKey := plannedState.HmacSecretKey.ValueString()
if plannedProtected && plannedHmacSecretKey == "" {
t.Setenv("TF_FILE_HMAC_SECRET_KEY", "thisisasupersecretkey")
}
r := getCreateResponseContainer()
tc.fit.Create(context.Background(), tc.have, &r)
defer teardown(tc.tearDownPath)
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Create() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func TestLocalResourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.ReadRequest
want resource.ReadResponse
setup map[string]string
tearDownPath string
}{
{
"Unprotected",
LocalResource{},
// have
getReadRequest(t, map[string]string{
"id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2",
"name": "read.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is an unprotected read test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
// want
getReadResponse(t, map[string]string{
"id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2",
"name": "read.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is an unprotected read test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "read.tmp"),
"contents": "this is an unprotected read test",
},
filepath.Join(defaultDirectory, "read.tmp"),
},
{
"Protected",
LocalResource{},
// have
getReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getReadResponse(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "read_protected.tmp"),
"contents": "this is a protected read test",
},
filepath.Join(defaultDirectory, "read_protected.tmp"),
},
{
"Protected with content update",
LocalResource{},
// have
getReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_content.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getReadResponse(t, map[string]string{
"id": "84326116e261654e44ca3cb73fa026580853794062d472bc817b7ec2c82ff648",
"name": "read_protected_content.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a change in contents in the real file",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "read_protected_content.tmp"),
"contents": "this is a change in contents in the real file",
},
filepath.Join(defaultDirectory, "read_protected_content.tmp"),
},
{
"Protected with mode update",
LocalResource{},
// have
getReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_mode.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getReadResponse(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_mode.tmp",
"directory": defaultDirectory,
"mode": "0755",
"contents": "this is a protected read test",
"protected": "true",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": "0755",
"path": filepath.Join(defaultDirectory, "read_protected_mode.tmp"),
"contents": "this is a protected read test",
},
filepath.Join(defaultDirectory, "read_protected_mode.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setup(tc.setup)
defer teardown(tc.tearDownPath)
r := getReadResponseContainer()
tc.fit.Read(context.Background(), tc.have, &r)
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Read() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func TestLocalResourceUpdate(t *testing.T) {
t.Run("Update function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.UpdateRequest
want resource.UpdateResponse
setup map[string]string
tearDownPath string
}{
{
"Basic test",
LocalResource{},
// have
getUpdateRequest(t, map[string]map[string]string{
"priorState": {
"id": defaultId,
"name": "update_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is an update test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
},
"plan": {
"id": defaultId,
"name": "update_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a basic update test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
},
}),
// want
getUpdateResponse(t, map[string]string{
"id": "0ec41eee6c157a3f7e50b78d586ee2ddb4d6e93b6de8bdf6d9354cf720e89549",
"name": "update_basic.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a basic update test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
// setup
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "update_basic.tmp"),
"contents": "this is an update test",
},
filepath.Join(defaultDirectory, "update_basic.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setup(tc.setup)
defer teardown(tc.tearDownPath)
r := getUpdateResponseContainer()
tc.fit.Update(context.Background(), tc.have, &r)
got := r
var plannedState LocalResourceModel
if diags := tc.have.Plan.Get(context.Background(), &plannedState); diags.HasError() {
t.Errorf("Failed to get planned state: %v", diags)
}
plannedContents := plannedState.Contents.ValueString()
plannedFilePath := filepath.Join(plannedState.Directory.ValueString(), plannedState.Name.ValueString())
contentsAfterUpdate, err := os.ReadFile(plannedFilePath)
if err != nil {
t.Errorf("Failed to read file for update verification: %s", err)
}
if string(contentsAfterUpdate) != plannedContents {
t.Errorf("File content was not updated correctly. Got %q, want %q", string(contentsAfterUpdate), plannedContents)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Update() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
func TestLocalResourceDelete(t *testing.T) {
t.Run("Delete function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalResource
have resource.DeleteRequest
want resource.DeleteResponse
setup map[string]string
tearDownPath string
}{
{
"Basic test",
LocalResource{},
// have
getDeleteRequest(t, map[string]string{
"id": "fd6fb8621c4850c228190f4d448ce30881a32609d6b4c7341d48d0027e597567",
"name": "delete.tmp",
"directory": defaultDirectory,
"mode": defaultMode,
"contents": "this is a delete test",
"protected": defaultProtected,
"hmac_secret_key": defaultHmacSecretKey,
}),
// want
getDeleteResponse(),
// setup
map[string]string{
"mode": defaultMode,
"path": filepath.Join(defaultDirectory, "delete.tmp"),
"contents": "this is a delete test",
},
filepath.Join(defaultDirectory, "delete.tmp"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setup(tc.setup)
r := getDeleteResponseContainer()
tc.fit.Delete(context.Background(), tc.have, &r)
got := r
// Verify the file was actually deleted from disk
if _, err := os.Stat(tc.setup["path"]); !os.IsNotExist(err) {
t.Errorf("Expected file to be deleted, but it still exists.")
}
// verify that the file was removed from state
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Update() mismatch (-want +got):\n%s", diff)
}
})
}
})
}
// *** Test Helper Functions *** //
func getCreateRequest(t *testing.T, data map[string]string) resource.CreateRequest {
planMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue)
} else {
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
planMap[key] = tftypes.NewValue(tftypes.Bool, v)
}
} else {
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
} else {
planMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
}
planValue := tftypes.NewValue(getObjectAttributeTypes(), planMap)
return resource.CreateRequest{
Plan: tfsdk.Plan{
Raw: planValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getCreateResponseContainer() resource.CreateResponse {
return resource.CreateResponse{
State: tfsdk.State{Schema: getLocalResourceSchema().Schema},
}
}
func getCreateResponse(t *testing.T, data map[string]string) resource.CreateResponse {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.CreateResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getReadRequest(t *testing.T, data map[string]string) resource.ReadRequest {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.ReadRequest{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getReadResponseContainer() resource.ReadResponse {
return resource.ReadResponse{
State: tfsdk.State{Schema: getLocalResourceSchema().Schema},
}
}
func getReadResponse(t *testing.T, data map[string]string) resource.ReadResponse {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.ReadResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getUpdateRequest(t *testing.T, data map[string]map[string]string) resource.UpdateRequest {
stateMap := make(map[string]tftypes.Value)
for key, value := range data["priorState"] {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
priorStateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
planMap := make(map[string]tftypes.Value)
for key, value := range data["plan"] {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue)
} else {
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
planMap[key] = tftypes.NewValue(tftypes.Bool, v)
}
} else {
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
} else {
planMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
}
planValue := tftypes.NewValue(getObjectAttributeTypes(), planMap)
return resource.UpdateRequest{
State: tfsdk.State{
Raw: priorStateValue,
Schema: getLocalResourceSchema().Schema,
},
Plan: tfsdk.Plan{
Raw: planValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getUpdateResponseContainer() resource.UpdateResponse {
return resource.UpdateResponse{
State: tfsdk.State{Schema: getLocalResourceSchema().Schema},
}
}
func getUpdateResponse(t *testing.T, data map[string]string) resource.UpdateResponse {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.UpdateResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getDeleteRequest(t *testing.T, data map[string]string) resource.DeleteRequest {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.DeleteRequest{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalResourceSchema().Schema,
},
}
}
func getDeleteResponseContainer() resource.DeleteResponse {
// A delete response does not need a schema as it results in a null state.
return resource.DeleteResponse{}
}
func getDeleteResponse() resource.DeleteResponse {
return resource.DeleteResponse{
State: tfsdk.State{
Raw: tftypes.Value{},
Schema: nil,
},
}
}
func getObjectAttributeTypes() tftypes.Object {
return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
"name": tftypes.String,
"directory": tftypes.String,
"mode": tftypes.String,
"contents": tftypes.String,
"hmac_secret_key": tftypes.String,
"protected": tftypes.Bool,
},
}
}
func getLocalResourceSchema() *resource.SchemaResponse {
var testResource LocalResource
r := &resource.SchemaResponse{}
testResource.Schema(context.Background(), resource.SchemaRequest{}, r)
return r
}
func setup(data map[string]string) {
modeInt, _ := strconv.ParseUint(data["mode"], 8, 32)
_ = os.WriteFile(data["path"], []byte(data["contents"]), os.FileMode(modeInt))
}
func teardown(path string) {
os.Remove(path)
}