add tests for ForceUnlock (#330)

* add tests for ForceUnlock

* add compatibility code for dir arg

* generate lock id error for all backend types
diff --git a/tfexec/exit_errors.go b/tfexec/exit_errors.go
index ea25b2a..9fc152d 100644
--- a/tfexec/exit_errors.go
+++ b/tfexec/exit_errors.go
@@ -46,6 +46,7 @@
 	statePlanReadErrRegexp = regexp.MustCompile(
 		`Terraform couldn't read the given file as a state or plan file.|` +
 			`Error: Failed to read the given file as a state or plan file`)
+	lockIdInvalidErrRegexp = regexp.MustCompile(`Failed to unlock state: `)
 )
 
 func (tf *Terraform) wrapExitError(ctx context.Context, err error, stderr string) error {
@@ -160,6 +161,8 @@
 		}
 	case statePlanReadErrRegexp.MatchString(stderr):
 		return &ErrStatePlanRead{stderr: stderr}
+	case lockIdInvalidErrRegexp.MatchString(stderr):
+		return &ErrLockIdInvalid{stderr: stderr}
 	}
 
 	return fmt.Errorf("%w\n%s", &unwrapper{exitErr, ctxErr}, stderr)
@@ -256,6 +259,16 @@
 	return e.stderr
 }
 
+type ErrLockIdInvalid struct {
+	unwrapper
+
+	stderr string
+}
+
+func (e *ErrLockIdInvalid) Error() string {
+	return e.stderr
+}
+
 // ErrCLIUsage is returned when the combination of flags or arguments is incorrect.
 //
 //  CLI indicates usage errors in three different ways: either
diff --git a/tfexec/force_unlock.go b/tfexec/force_unlock.go
index e501baf..de95f54 100644
--- a/tfexec/force_unlock.go
+++ b/tfexec/force_unlock.go
@@ -2,6 +2,7 @@
 
 import (
 	"context"
+	"fmt"
 	"os/exec"
 )
 
@@ -21,7 +22,10 @@
 
 // ForceUnlock represents the `terraform force-unlock` command
 func (tf *Terraform) ForceUnlock(ctx context.Context, lockID string, opts ...ForceUnlockOption) error {
-	unlockCmd := tf.forceUnlockCmd(ctx, lockID, opts...)
+	unlockCmd, err := tf.forceUnlockCmd(ctx, lockID, opts...)
+	if err != nil {
+		return err
+	}
 
 	if err := tf.runTerraformCmd(ctx, unlockCmd); err != nil {
 		return err
@@ -30,7 +34,7 @@
 	return nil
 }
 
-func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ...ForceUnlockOption) *exec.Cmd {
+func (tf *Terraform) forceUnlockCmd(ctx context.Context, lockID string, opts ...ForceUnlockOption) (*exec.Cmd, error) {
 	c := defaultForceUnlockOptions
 
 	for _, o := range opts {
@@ -43,8 +47,12 @@
 
 	// optional positional arguments
 	if c.dir != "" {
+		err := tf.compatible(ctx, nil, tf0_15_0)
+		if err != nil {
+			return nil, fmt.Errorf("[DIR] option was removed in Terraform v0.15.0")
+		}
 		args = append(args, c.dir)
 	}
 
-	return tf.buildTerraformCmd(ctx, nil, args...)
+	return tf.buildTerraformCmd(ctx, nil, args...), nil
 }
diff --git a/tfexec/force_unlock_test.go b/tfexec/force_unlock_test.go
new file mode 100644
index 0000000..b817979
--- /dev/null
+++ b/tfexec/force_unlock_test.go
@@ -0,0 +1,63 @@
+package tfexec
+
+import (
+	"context"
+	"testing"
+
+	"github.com/hashicorp/terraform-exec/tfexec/internal/testutil"
+)
+
+func TestForceUnlockCmd(t *testing.T) {
+	td := t.TempDir()
+
+	tf, err := NewTerraform(td, tfVersion(t, testutil.Latest_v1_1))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// empty env, to avoid environ mismatch in testing
+	tf.SetEnv(map[string]string{})
+
+	t.Run("defaults", func(t *testing.T) {
+		forceUnlockCmd, err := tf.forceUnlockCmd(context.Background(), "12345")
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertCmd(t, []string{
+			"force-unlock",
+			"-no-color",
+			"-force",
+			"12345",
+		}, nil, forceUnlockCmd)
+	})
+}
+
+// The optional final positional [DIR] argument is available
+// until v0.15.0.
+func TestForceUnlockCmd_pre015(t *testing.T) {
+	td := t.TempDir()
+
+	tf, err := NewTerraform(td, tfVersion(t, testutil.Latest014))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// empty env, to avoid environ mismatch in testing
+	tf.SetEnv(map[string]string{})
+
+	t.Run("override all defaults", func(t *testing.T) {
+		forceUnlockCmd, err := tf.forceUnlockCmd(context.Background(), "12345", Dir("mydir"))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assertCmd(t, []string{
+			"force-unlock",
+			"-no-color",
+			"-force",
+			"12345",
+			"mydir",
+		}, nil, forceUnlockCmd)
+	})
+}
diff --git a/tfexec/internal/e2etest/force_unlock_test.go b/tfexec/internal/e2etest/force_unlock_test.go
index 4665727..c5d626a 100644
--- a/tfexec/internal/e2etest/force_unlock_test.go
+++ b/tfexec/internal/e2etest/force_unlock_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"context"
+	"errors"
 	"testing"
 
 	"github.com/hashicorp/go-version"
@@ -40,4 +41,19 @@
 			t.Fatalf("error running ForceUnlock: %v", err)
 		}
 	})
+	runTest(t, "inmem_backend_locked", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) {
+		err := tf.Init(context.Background())
+		if err != nil {
+			t.Fatalf("error running Init: %v", err)
+		}
+
+		err = tf.ForceUnlock(context.Background(), "badlockid")
+		if err == nil {
+			t.Fatalf("expected error when running ForceUnlock with invalid lock id")
+		}
+		var foErr *tfexec.ErrLockIdInvalid
+		if !errors.As(err, &foErr) {
+			t.Fatalf("expected ErrLockIdInvalid, %T returned: %s", err, err)
+		}
+	})
 }