Format LUKS encrypted device

This commit adds the functionality to format a new LUKS device, create a
filesystem, and mount it. Unit tests are included.

Currently, the D-Bus interface to format the LUKS device is synchronous,
but it may need to become asynchronous, since it can take some time. The
format operation took about 20 seconds when testing it.

Tested: Ran eStoraged on a machine with an eMMC, using the following
commands:
$ /usr/bin/eStoraged -b /dev/mmcblk0 &
$ busctl call xyz.openbmc_project.eStoraged.mmcblk0 \
  /xyz/openbmc_project/storage/mmcblk0 xyz.openbmc_project.eStoraged \
  Format ay 3 1 2 3
$ busctl call xyz.openbmc_project.eStoraged.mmcblk0 \
  /xyz/openbmc_project/storage/mmcblk0 xyz.openbmc_project.eStoraged \
  Lock ay 3 1 2 3
$ busctl call xyz.openbmc_project.eStoraged.mmcblk0 \
  /xyz/openbmc_project/storage/mmcblk0 xyz.openbmc_project.eStoraged \
  Unlock ay 3 1 2 3

Signed-off-by: John Wedig <johnwedig@google.com>
Change-Id: Ib5d0b8bb201b43a60238bfd4f13a29a6519a9f7d
diff --git a/include/cryptsetupInterface.hpp b/include/cryptsetupInterface.hpp
new file mode 100644
index 0000000..9e26da9
--- /dev/null
+++ b/include/cryptsetupInterface.hpp
@@ -0,0 +1,193 @@
+#pragma once
+
+#include <libcryptsetup.h>
+
+#include <stdplus/handle/managed.hpp>
+
+namespace estoraged
+{
+
+/** @class CryptsetupInterface
+ *  @brief Interface to the cryptsetup functions used to manage a LUKS device.
+ *  @details This class is used to mock out the cryptsetup functions.
+ */
+class CryptsetupInterface
+{
+  public:
+    virtual ~CryptsetupInterface() = default;
+
+    /** @brief Wrapper around crypt_format.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] cd - crypt device handle.
+     *  @param[in] type - type of device (optional params struct must be of
+     *    this type).
+     *  @param[in] cipher - (e.g. "aes").
+     *  @params[in cipher_mode - including IV specification (e.g. "xts-plain").
+     *  @params[in] uuid - requested UUID or NULL if it should be generated.
+     *  @params[in] volume_key - pre-generated volume key or NULL if it should
+     *    be generated (only for LUKS).
+     *  @params[in] volume_key_size - size of volume key in bytes.
+     *  @params[in] params - crypt type specific parameters.
+     *
+     *  @returns 0 on success or negative errno value otherwise.
+     */
+    virtual int cryptFormat(struct crypt_device* cd, const char* type,
+                            const char* cipher, const char* cipherMode,
+                            const char* uuid, const char* volumeKey,
+                            size_t volumeKeySize, void* params) = 0;
+
+    /** @brief Wrapper around crypt_keyslot_add_by_volume_key.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] cd - crypt device handle.
+     *  @param[in] keyslot - requested keyslot or CRYPT_ANY_SLOT.
+     *  @param[in] volume_key - provided volume key or NULL if used after
+     *    crypt_format.
+     *  @param[in] volume_key_size - size of volume_key.
+     *  @param[in] passphrase - passphrase for new keyslot.
+     *  @param[in] passphrase_size - size of passphrase.
+     *
+     *  @returns allocated key slot number or negative errno otherwise.
+     */
+    virtual int cryptKeyslotAddByVolumeKey(struct crypt_device* cd, int keyslot,
+                                           const char* volumeKey,
+                                           size_t volumeKeySize,
+                                           const char* passphrase,
+                                           size_t passphraseSize) = 0;
+
+    /** @brief Wrapper around crypt_load.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] cd - crypt device handle.
+     *  @param[in] requested_type - crypt-type or NULL for all known.
+     *  @param[in] params - crypt type specific parameters (see crypt-type).
+     *
+     *  @returns 0 on success or negative errno value otherwise.
+     */
+    virtual int cryptLoad(struct crypt_device* cd, const char* requestedType,
+                          void* params) = 0;
+
+    /** @brief Wrapper around crypt_activate_by_passphrase.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] cd - crypt device handle.
+     *  @param[in] name - name of device to create, if NULL only check
+     *    passphrase.
+     *  @param[in] keyslot - requested keyslot to check or CRYPT_ANY_SLOT.
+     *  @param[in] passphrase - passphrase used to unlock volume key.
+     *  @param[in] passphrase_size - size of passphrase.
+     *  @param[in] flags - activation flags.
+     *
+     *  @returns unlocked key slot number or negative errno otherwise.
+     */
+    virtual int cryptActivateByPassphrase(struct crypt_device* cd,
+                                          const char* name, int keyslot,
+                                          const char* passphrase,
+                                          size_t passphraseSize,
+                                          uint32_t flags) = 0;
+
+    /** @brief Wrapper around crypt_deactivate.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] cd - crypt device handle, can be NULL.
+     *  @param[in] name - name of device to deactivate.
+     *
+     *  @returns 0 on success or negative errno value otherwise.
+     */
+    virtual int cryptDeactivate(struct crypt_device* cd, const char* name) = 0;
+};
+
+/** @class Cryptsetup
+ *  @brief Implements CryptsetupInterface.
+ */
+class Cryptsetup : public CryptsetupInterface
+{
+  public:
+    ~Cryptsetup() = default;
+
+    int cryptFormat(struct crypt_device* cd, const char* type,
+                    const char* cipher, const char* cipherMode,
+                    const char* uuid, const char* volumeKey,
+                    size_t volumeKeySize, void* params) override
+    {
+        return crypt_format(cd, type, cipher, cipherMode, uuid, volumeKey,
+                            volumeKeySize, params);
+    }
+
+    int cryptKeyslotAddByVolumeKey(struct crypt_device* cd, int keyslot,
+                                   const char* volumeKey, size_t volumeKeySize,
+                                   const char* passphrase,
+                                   size_t passphraseSize) override
+    {
+        return crypt_keyslot_add_by_volume_key(
+            cd, keyslot, volumeKey, volumeKeySize, passphrase, passphraseSize);
+    }
+
+    int cryptLoad(struct crypt_device* cd, const char* requestedType,
+                  void* params) override
+    {
+        return crypt_load(cd, requestedType, params);
+    }
+
+    int cryptActivateByPassphrase(struct crypt_device* cd, const char* name,
+                                  int keyslot, const char* passphrase,
+                                  size_t passphraseSize,
+                                  uint32_t flags) override
+    {
+        return crypt_activate_by_passphrase(cd, name, keyslot, passphrase,
+                                            passphraseSize, flags);
+    }
+
+    int cryptDeactivate(struct crypt_device* cd, const char* name) override
+    {
+        return crypt_deactivate(cd, name);
+    }
+};
+
+/** @class CryptHandle
+ *  @brief This manages a crypt_device struct and automatically frees it when
+ *  this handle exits the current scope.
+ */
+class CryptHandle
+{
+  public:
+    /** @brief Constructor for CryptHandle
+     *
+     *  @param[out] cd - pointer to crypt_device*, to be allocated
+     *  @param[in] device - path to device file
+     */
+    CryptHandle(struct crypt_device** cd, const char* device) :
+        handle(init(cd, device))
+    {}
+
+    /** @brief Allocate and initialize the crypt_device struct
+     *
+     *  @param[out] cd - pointer to crypt_device*, to be allocated
+     *  @param[in] device - path to device file
+     */
+    struct crypt_device* init(struct crypt_device** cd, const char* device)
+    {
+        int retval = crypt_init(cd, device);
+        if (retval < 0)
+        {
+            return nullptr;
+        }
+
+        return *cd;
+    }
+
+    /** @brief Free the crypt_device struct
+     *
+     *  @param[in] cd - pointer to crypt_device*, to be freed
+     */
+    static void cryptFree(struct crypt_device*&& cd)
+    {
+        crypt_free(cd);
+    }
+
+    /** @brief Managed handle to crypt_device struct */
+    stdplus::Managed<struct crypt_device*>::Handle<cryptFree> handle;
+};
+
+} // namespace estoraged
diff --git a/include/estoraged.hpp b/include/estoraged.hpp
index 557c490..3a18416 100644
--- a/include/estoraged.hpp
+++ b/include/estoraged.hpp
@@ -1,17 +1,27 @@
 #pragma once
 
+#include "cryptsetupInterface.hpp"
+#include "filesystemInterface.hpp"
+
+#include <libcryptsetup.h>
+
 #include <sdbusplus/bus.hpp>
 #include <sdbusplus/exception.hpp>
 #include <sdbusplus/server/object.hpp>
 #include <xyz/openbmc_project/eStoraged/server.hpp>
 
+#include <filesystem>
+#include <memory>
 #include <string>
+#include <string_view>
 #include <vector>
 
 namespace estoraged
 {
 using eStoragedInherit = sdbusplus::server::object_t<
     sdbusplus::xyz::openbmc_project::server::eStoraged>;
+using estoraged::Cryptsetup;
+using estoraged::Filesystem;
 
 /** @class eStoraged
  *  @brief eStoraged object to manage a LUKS encrypted storage device.
@@ -19,10 +29,27 @@
 class eStoraged : eStoragedInherit
 {
   public:
+    /** @brief Constructor for eStoraged
+     *
+     *  @param[in] bus - sdbusplus dbus object
+     *  @param[in] path - DBus object path
+     *  @param[in] devPath - path to device file, e.g. /dev/mmcblk0
+     *  @param[in] luksName - name for the LUKS container
+     *  @param[in] cryptInterface - (optional) pointer to CryptsetupInterface
+     *    object
+     *  @param[in] fsInterface - (optional) pointer to FilesystemInterface
+     *    object
+     */
     eStoraged(sdbusplus::bus::bus& bus, const char* path,
-              const std::string& devPath, const std::string& containerName) :
+              const std::string& devPath, const std::string& luksName,
+              std::unique_ptr<CryptsetupInterface> cryptInterface =
+                  std::make_unique<Cryptsetup>(),
+              std::unique_ptr<FilesystemInterface> fsInterface =
+                  std::make_unique<Filesystem>()) :
         eStoragedInherit(bus, path),
-        devPath(devPath), containerName(containerName)
+        devPath(devPath), containerName(luksName),
+        mountPoint("/mnt/" + luksName + "_fs"),
+        cryptIface(std::move(cryptInterface)), fsIface(std::move(fsInterface))
     {}
 
     /** @brief Format the LUKS encrypted device and create empty filesystem.
@@ -58,12 +85,65 @@
     void changePassword(std::vector<uint8_t> oldPassword,
                         std::vector<uint8_t> newPassword) override;
 
+    /** @brief Check if the LUKS device is currently locked. */
+    bool isLocked() const;
+
+    /** @brief Get the mount point for the filesystem on the LUKS device. */
+    std::string_view getMountPoint() const;
+
   private:
-    /* Full path of the device file, e.g. /dev/mmcblk0 */
+    /** @brief Full path of the device file, e.g. /dev/mmcblk0. */
     std::string devPath;
 
-    /* Name of the LUKS container. */
+    /** @brief Name of the LUKS container. */
     std::string containerName;
+
+    /** @brief Mount point for the filesystem. */
+    std::string mountPoint;
+
+    /** @brief Pointer to cryptsetup interface object.
+     *  @details This is used to mock out the cryptsetup functions.
+     */
+    std::unique_ptr<CryptsetupInterface> cryptIface;
+
+    /** @brief Pointer to filesystem interface object.
+     *  @details This is used to mock out filesystem operations.
+     */
+    std::unique_ptr<FilesystemInterface> fsIface;
+
+    /** @brief Format LUKS encrypted device.
+     *
+     *  @param[in] cd - initialized crypt_device struct for the device.
+     *  @param[in] password - password to set for the LUKS device.
+     */
+    void formatLuksDev(struct crypt_device* cd, std::vector<uint8_t> password);
+
+    /** @brief Unlock the device.
+     *
+     *  @param[in] cd - initialized crypt_device struct for the device.
+     *  @param[in] password - password to activate the LUKS device.
+     */
+    void activateLuksDev(struct crypt_device* cd,
+                         std::vector<uint8_t> password);
+
+    /** @brief Create the filesystem on the LUKS device.
+     *  @details The LUKS device should already be activated, i.e. unlocked.
+     */
+    void createFilesystem();
+
+    /** @brief Deactivate the LUKS device.
+     *  @details The filesystem is assumed to be unmounted already.
+     */
+    void deactivateLuksDev();
+
+    /** @brief Mount the filesystem.
+     *  @details The filesystem should already exist and the LUKS device should
+     *  be unlocked already.
+     */
+    void mountFilesystem();
+
+    /** @brief Unmount the filesystem. */
+    void unmountFilesystem();
 };
 
 } // namespace estoraged
diff --git a/include/filesystemInterface.hpp b/include/filesystemInterface.hpp
new file mode 100644
index 0000000..d34ff06
--- /dev/null
+++ b/include/filesystemInterface.hpp
@@ -0,0 +1,111 @@
+#pragma once
+
+#include <sys/mount.h>
+
+#include <filesystem>
+#include <string>
+
+namespace estoraged
+{
+
+/** @class FilesystemInterface
+ *  @brief Interface to the filesystem operations that eStoraged needs.
+ *  @details This class is used to mock out the filesystem operations.
+ */
+class FilesystemInterface
+{
+  public:
+    virtual ~FilesystemInterface() = default;
+
+    /** @brief Runs the mkfs command to create the filesystem.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] logicalVolume - name of the mapped LUKS device.
+     *
+     *  @returns 0 on success, nonzero on failure.
+     */
+    virtual int runMkfs(const std::string& logicalVolume) = 0;
+
+    /** @brief Wrapper around mount().
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] source - device where the filesystem is located.
+     *  @param[in] target - path to where the filesystem should be mounted.
+     *  @param[in] filesystemType - (e.g. "ext4").
+     *  @param[in] mountflags - flags bit mask (see mount() documentation).
+     *  @param[in] data - options for specific filesystem type, can be NULL
+     *    (see mount() documentation).
+     *
+     *  @returns On success, zero is returned.  On error, -1 is returned, and
+     *    errno is set to indicate the error.
+     */
+    virtual int doMount(const char* source, const char* target,
+                        const char* filesystemtype, unsigned long mountflags,
+                        const void* data) = 0;
+
+    /** @brief Wrapper around umount().
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] target - path location where the filesystem is mounted.
+     *
+     *  @returns On success, zero is returned.  On error, -1 is returned, and
+     *    errno is set to indicate the error.
+     */
+    virtual int doUnmount(const char* target) = 0;
+
+    /** @brief Wrapper around std::filesystem::create_directory.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] p - path to directory that should be created.
+     *
+     *  @returns true on success, false otherwise.
+     */
+    virtual bool createDirectory(const std::filesystem::path& p) = 0;
+
+    /** @brief Wrapper around std::filesystem::remove.
+     *  @details Used for mocking purposes.
+     *
+     *  @param[in] p - path to directory that should be removed.
+     *
+     *  @returns true on success, false otherwise.
+     */
+    virtual bool removeDirectory(const std::filesystem::path& p) = 0;
+};
+
+/** @class Filesystem
+ *  @brief Implements FilesystemInterface
+ */
+class Filesystem : public FilesystemInterface
+{
+  public:
+    ~Filesystem() = default;
+
+    int runMkfs(const std::string& logicalVolume) override
+    {
+        std::string mkfsCommand("mkfs.ext4 /dev/mapper/" + logicalVolume);
+        return system(mkfsCommand.c_str());
+    }
+
+    int doMount(const char* source, const char* target,
+                const char* filesystemtype, unsigned long mountflags,
+                const void* data) override
+    {
+        return mount(source, target, filesystemtype, mountflags, data);
+    }
+
+    int doUnmount(const char* target) override
+    {
+        return umount(target);
+    }
+
+    bool createDirectory(const std::filesystem::path& p) override
+    {
+        return std::filesystem::create_directory(p);
+    }
+
+    bool removeDirectory(const std::filesystem::path& p) override
+    {
+        return std::filesystem::remove(p);
+    }
+};
+} // namespace estoraged
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..0fc2767
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1 @@
+option('tests', type: 'feature', description: 'Build tests')
diff --git a/src/estoraged.cpp b/src/estoraged.cpp
index 8e49562..dcbb524 100644
--- a/src/estoraged.cpp
+++ b/src/estoraged.cpp
@@ -1,19 +1,45 @@
 
 #include "estoraged.hpp"
 
-#include <phosphor-logging/lg2.hpp>
+#include "cryptsetupInterface.hpp"
 
+#include <libcryptsetup.h>
+#include <openssl/rand.h>
+#include <stdlib.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/eStoraged/error.hpp>
+
+#include <filesystem>
 #include <iostream>
+#include <string_view>
 #include <vector>
 
 namespace estoraged
 {
 
-void eStoraged::format(std::vector<uint8_t>)
+using sdbusplus::xyz::openbmc_project::eStoraged::Error::EncryptionError;
+using sdbusplus::xyz::openbmc_project::eStoraged::Error::FilesystemError;
+
+void eStoraged::format(std::vector<uint8_t> password)
 {
-    std::cerr << "Formatting encrypted eMMC" << std::endl;
     std::string msg = "OpenBMC.0.1.DriveFormat";
     lg2::info("Starting format", "REDFISH_MESSAGE_ID", msg);
+
+    struct crypt_device* cryptDev;
+    CryptHandle cryptHandle(&cryptDev, devPath.c_str());
+    if (*cryptHandle.handle == nullptr)
+    {
+        lg2::error("Failed to initialize crypt device", "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.FormatFail"));
+        throw EncryptionError();
+    }
+
+    formatLuksDev(cryptDev, password);
+    activateLuksDev(cryptDev, password);
+
+    createFilesystem();
+    mountFilesystem();
 }
 
 void eStoraged::erase(std::vector<uint8_t>, EraseMethod)
@@ -25,16 +51,29 @@
 
 void eStoraged::lock(std::vector<uint8_t>)
 {
-    std::cerr << "Locking encrypted eMMC" << std::endl;
     std::string msg = "OpenBMC.0.1.DriveLock";
     lg2::info("Starting lock", "REDFISH_MESSAGE_ID", msg);
+
+    unmountFilesystem();
+    deactivateLuksDev();
 }
 
-void eStoraged::unlock(std::vector<uint8_t>)
+void eStoraged::unlock(std::vector<uint8_t> password)
 {
-    std::cerr << "Unlocking encrypted eMMC" << std::endl;
     std::string msg = "OpenBMC.0.1.DriveUnlock";
     lg2::info("Starting unlock", "REDFISH_MESSAGE_ID", msg);
+
+    struct crypt_device* cryptDev;
+    CryptHandle cryptHandle(&cryptDev, devPath.c_str());
+    if (*cryptHandle.handle == nullptr)
+    {
+        lg2::error("Failed to initialize crypt device", "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.UnlockFail"));
+        throw EncryptionError();
+    }
+
+    activateLuksDev(cryptDev, password);
+    mountFilesystem();
 }
 
 void eStoraged::changePassword(std::vector<uint8_t>, std::vector<uint8_t>)
@@ -44,4 +83,199 @@
     lg2::info("Starting change password", "REDFISH_MESSAGE_ID", msg);
 }
 
+bool eStoraged::isLocked() const
+{
+    return locked();
+}
+
+std::string_view eStoraged::getMountPoint() const
+{
+    return mountPoint;
+}
+
+void eStoraged::formatLuksDev(struct crypt_device* cd,
+                              std::vector<uint8_t> password)
+{
+    lg2::info("Formatting device {DEV}", "DEV", devPath, "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.FormatLuksDev"));
+
+    /* Generate the volume key. */
+    const std::size_t keySize = 64;
+    std::vector<uint8_t> volumeKey(keySize);
+    if (RAND_bytes(volumeKey.data(), keySize) != 1)
+    {
+        lg2::error("Failed to create volume key", "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.FormatLuksDevFail"));
+        throw EncryptionError();
+    }
+    /* Format the LUKS encrypted device. */
+    int retval =
+        cryptIface->cryptFormat(cd, CRYPT_LUKS2, "aes", "xts-plain64", nullptr,
+                                reinterpret_cast<const char*>(volumeKey.data()),
+                                volumeKey.size(), nullptr);
+    if (retval < 0)
+    {
+        lg2::error("Failed to format encrypted device: {RETVAL}", "RETVAL",
+                   retval, "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.FormatLuksDevFail"));
+        throw EncryptionError();
+    }
+
+    /* Device is now encrypted. */
+    locked(true);
+
+    /* Set the password. */
+    retval = cryptIface->cryptKeyslotAddByVolumeKey(
+        cd, CRYPT_ANY_SLOT, nullptr, 0,
+        reinterpret_cast<const char*>(password.data()), password.size());
+
+    if (retval < 0)
+    {
+        lg2::error("Failed to set encryption password", "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.FormatLuksDevFail"));
+        throw EncryptionError();
+    }
+
+    lg2::info("Encrypted device {DEV} successfully formatted", "DEV", devPath,
+              "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.FormatLuksDevSuccess"));
+}
+
+void eStoraged::activateLuksDev(struct crypt_device* cd,
+                                std::vector<uint8_t> password)
+{
+    lg2::info("Activating LUKS dev {DEV}", "DEV", devPath, "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.ActivateLuksDev"));
+
+    int retval = cryptIface->cryptLoad(cd, CRYPT_LUKS2, nullptr);
+    if (retval < 0)
+    {
+        lg2::error("Failed to load LUKS header: {RETVAL}", "RETVAL", retval,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.ActivateLuksDevFail"));
+        throw EncryptionError();
+    }
+
+    retval = cryptIface->cryptActivateByPassphrase(
+        cd, containerName.c_str(), CRYPT_ANY_SLOT,
+        reinterpret_cast<const char*>(password.data()), password.size(), 0);
+
+    if (retval < 0)
+    {
+        lg2::error("Failed to activate LUKS dev: {RETVAL}", "RETVAL", retval,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.ActivateLuksDevFail"));
+        throw EncryptionError();
+    }
+
+    /* Device is now unlocked. */
+    locked(false);
+
+    lg2::info("Successfully activated LUKS dev {DEV}", "DEV", devPath,
+              "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.ActivateLuksDevSuccess"));
+}
+
+void eStoraged::createFilesystem()
+{
+    /* Run the command to create the filesystem. */
+    int retval = fsIface->runMkfs(containerName);
+    if (retval)
+    {
+        lg2::error("Failed to create filesystem: {RETVAL}", "RETVAL", retval,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.CreateFilesystemFail"));
+        throw FilesystemError();
+    }
+    lg2::info("Successfully created filesystem for /dev/mapper/{CONTAINER}",
+              "CONTAINER", containerName, "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.CreateFilesystemSuccess"));
+}
+
+void eStoraged::mountFilesystem()
+{
+    /* Create directory for the filesystem. */
+    bool success = fsIface->createDirectory(std::filesystem::path(mountPoint));
+    if (!success)
+    {
+        lg2::error("Failed to create mount point: {DIR}", "DIR", mountPoint,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.MountFilesystemFail"));
+        throw FilesystemError();
+    }
+
+    /* Run the command to mount the filesystem. */
+    std::string luksContainer("/dev/mapper/" + containerName);
+    int retval = fsIface->doMount(luksContainer.c_str(), mountPoint.c_str(),
+                                  "ext4", 0, nullptr);
+    if (retval)
+    {
+        lg2::error("Failed to mount filesystem: {RETVAL}", "RETVAL", retval,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.MountFilesystemFail"));
+        bool removeSuccess =
+            fsIface->removeDirectory(std::filesystem::path(mountPoint));
+        if (!removeSuccess)
+        {
+            lg2::error("Failed to remove mount point: {DIR}", "DIR", mountPoint,
+                       "REDFISH_MESSAGE_ID",
+                       std::string("OpenBMC.0.1.MountFilesystemFail"));
+        }
+        throw FilesystemError();
+    }
+
+    lg2::info("Successfully mounted filesystem at {DIR}", "DIR", mountPoint,
+              "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.MountFilesystemSuccess"));
+}
+
+void eStoraged::unmountFilesystem()
+{
+    int retval = fsIface->doUnmount(mountPoint.c_str());
+    if (retval)
+    {
+        lg2::error("Failed to unmount filesystem: {RETVAL}", "RETVAL", retval,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.UnmountFilesystemFail"));
+        throw FilesystemError();
+    }
+
+    /* Remove the mount point. */
+    bool success = fsIface->removeDirectory(std::filesystem::path(mountPoint));
+    if (!success)
+    {
+        lg2::error("Failed to remove mount point {DIR}", "DIR", mountPoint,
+                   "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.UnmountFilesystemFail"));
+        throw FilesystemError();
+    }
+
+    lg2::info("Successfully unmounted filesystem at {DIR}", "DIR", mountPoint,
+              "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.MountFilesystemSuccess"));
+}
+
+void eStoraged::deactivateLuksDev()
+{
+    lg2::info("Deactivating LUKS device {DEV}", "DEV", devPath,
+              "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.DeactivateLuksDev"));
+
+    int retval = cryptIface->cryptDeactivate(nullptr, containerName.c_str());
+    if (retval < 0)
+    {
+        lg2::error("Failed to deactivate crypt device: {RETVAL}", "RETVAL",
+                   retval, "REDFISH_MESSAGE_ID",
+                   std::string("OpenBMC.0.1.DeactivateLuksDevFail"));
+        throw EncryptionError();
+    }
+
+    /* Device is now locked. */
+    locked(true);
+
+    lg2::info("Successfully deactivated LUKS device {DEV}", "DEV", devPath,
+              "REDFISH_MESSAGE_ID",
+              std::string("OpenBMC.0.1.DeactivateLuksDevSuccess"));
+}
+
 } // namespace estoraged
diff --git a/src/meson.build b/src/meson.build
index 958fa0a..50a5c7c 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -6,6 +6,9 @@
                'phosphor_logging_dep'],
     ),
   eStoraged_dbus,
+  dependency('openssl'),
+  dependency('libcryptsetup'),
+  dependency('stdplus'),
 ]
 
 libeStoraged_lib = static_library(
@@ -29,3 +32,9 @@
   install: true,
   install_dir: get_option('bindir')
 )
+
+build_tests = get_option('tests')
+if not build_tests.disabled()
+  subdir('test')
+endif
+
diff --git a/src/test/estoraged_test.cpp b/src/test/estoraged_test.cpp
new file mode 100644
index 0000000..f293ffc
--- /dev/null
+++ b/src/test/estoraged_test.cpp
@@ -0,0 +1,491 @@
+
+#include "cryptsetupInterface.hpp"
+#include "estoraged.hpp"
+#include "filesystemInterface.hpp"
+
+#include <unistd.h>
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <xyz/openbmc_project/eStoraged/error.hpp>
+
+#include <exception>
+#include <filesystem>
+#include <fstream>
+#include <iterator>
+#include <string>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace estoraged_test
+{
+
+class MockFilesystemInterface : public estoraged::FilesystemInterface
+{
+  public:
+    MOCK_METHOD(int, runMkfs, (const std::string& logicalVolume), (override));
+
+    MOCK_METHOD(int, doMount,
+                (const char* source, const char* target,
+                 const char* filesystemtype, unsigned long mountflags,
+                 const void* data),
+                (override));
+
+    MOCK_METHOD(int, doUnmount, (const char* target), (override));
+
+    MOCK_METHOD(bool, createDirectory, (const std::filesystem::path& p),
+                (override));
+
+    MOCK_METHOD(bool, removeDirectory, (const std::filesystem::path& p),
+                (override));
+};
+
+class MockCryptsetupInterface : public estoraged::CryptsetupInterface
+{
+  public:
+    MOCK_METHOD(int, cryptFormat,
+                (struct crypt_device * cd, const char* type, const char* cipher,
+                 const char* cipher_mode, const char* uuid,
+                 const char* volume_key, size_t volume_key_size, void* params),
+                (override));
+
+    MOCK_METHOD(int, cryptKeyslotAddByVolumeKey,
+                (struct crypt_device * cd, int keyslot, const char* volume_key,
+                 size_t volume_key_size, const char* passphrase,
+                 size_t passphrase_size),
+                (override));
+
+    MOCK_METHOD(int, cryptLoad,
+                (struct crypt_device * cd, const char* requested_type,
+                 void* params),
+                (override));
+
+    MOCK_METHOD(int, cryptActivateByPassphrase,
+                (struct crypt_device * cd, const char* name, int keyslot,
+                 const char* passphrase, size_t passphrase_size,
+                 uint32_t flags),
+                (override));
+
+    MOCK_METHOD(int, cryptDeactivate,
+                (struct crypt_device * cd, const char* name), (override));
+};
+
+using sdbusplus::xyz::openbmc_project::eStoraged::Error::EncryptionError;
+using sdbusplus::xyz::openbmc_project::eStoraged::Error::FilesystemError;
+using std::filesystem::path;
+using ::testing::_;
+using ::testing::ContainsRegex;
+using ::testing::IsNull;
+using ::testing::Return;
+using ::testing::StrEq;
+
+/*
+ * This sdbus mock object gets used in the destructor of one of the parent
+ * classes for the MockeStoraged object, so this can't be part of the
+ * eStoragedTest class.
+ */
+sdbusplus::SdBusMock sdbusMock;
+
+class eStoragedTest : public testing::Test
+{
+  public:
+    static constexpr char testFileName[] = "testfile";
+    static constexpr char testLuksDevName[] = "testfile_luksDev";
+    std::ofstream testFile;
+    std::unique_ptr<estoraged::eStoraged> esObject;
+    static constexpr auto TEST_PATH = "/test/openbmc_project/storage/test_dev";
+    static constexpr auto ESTORAGED_INTERFACE = "xyz.openbmc_project.eStoraged";
+    sdbusplus::bus::bus bus;
+    std::string passwordString;
+    std::vector<uint8_t> password;
+    MockCryptsetupInterface* mockCryptIface;
+    MockFilesystemInterface* mockFsIface;
+
+    eStoragedTest() :
+        bus(sdbusplus::get_mocked_new(&sdbusMock)), passwordString("password"),
+        password(passwordString.begin(), passwordString.end())
+    {}
+
+    void SetUp() override
+    {
+        /* Create an empty file that we'll pretend is a 'storage device'. */
+        testFile.open(testFileName,
+                      std::ios::out | std::ios::binary | std::ios::trunc);
+        testFile.close();
+        if (testFile.fail())
+        {
+            throw std::runtime_error("Failed to open test file");
+        }
+
+        EXPECT_CALL(sdbusMock,
+                    sd_bus_add_object_vtable(IsNull(), _, StrEq(TEST_PATH),
+                                             StrEq(ESTORAGED_INTERFACE), _, _))
+            .WillRepeatedly(Return(0));
+
+        EXPECT_CALL(sdbusMock,
+                    sd_bus_emit_object_added(IsNull(), StrEq(TEST_PATH)))
+            .WillRepeatedly(Return(0));
+
+        EXPECT_CALL(sdbusMock,
+                    sd_bus_emit_object_removed(IsNull(), StrEq(TEST_PATH)))
+            .WillRepeatedly(Return(0));
+
+        std::unique_ptr<MockCryptsetupInterface> cryptIface =
+            std::make_unique<MockCryptsetupInterface>();
+        mockCryptIface = cryptIface.get();
+        std::unique_ptr<MockFilesystemInterface> fsIface =
+            std::make_unique<MockFilesystemInterface>();
+        mockFsIface = fsIface.get();
+
+        esObject = std::make_unique<estoraged::eStoraged>(
+            bus, TEST_PATH, std::string(testFileName),
+            std::string(testLuksDevName), std::move(cryptIface),
+            std::move(fsIface));
+    }
+
+    void TearDown() override
+    {
+        EXPECT_EQ(0, unlink(testFileName));
+    }
+};
+
+/* Test case to format and then lock the LUKS device. */
+TEST_F(eStoragedTest, FormatPass)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, createDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockFsIface,
+                doMount(ContainsRegex("/dev/mapper/"),
+                        StrEq(esObject->getMountPoint()), _, _, _))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, doUnmount(StrEq(esObject->getMountPoint())))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, removeDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockCryptIface, cryptDeactivate(_, _)).Times(1);
+
+    /* Format the encrypted device. */
+    esObject->format(password);
+    EXPECT_FALSE(esObject->isLocked());
+
+    esObject->lock(password);
+    EXPECT_TRUE(esObject->isLocked());
+}
+
+/* Test case where the device/file doesn't exist. */
+TEST_F(eStoragedTest, FormatNoDeviceFail)
+{
+    /* Delete the test file. */
+    EXPECT_EQ(0, unlink(testFileName));
+
+    EXPECT_THROW(esObject->format(password), EncryptionError);
+    EXPECT_FALSE(esObject->isLocked());
+
+    /* Create the test file again, so that the TearDown function works. */
+    testFile.open(testFileName,
+                  std::ios::out | std::ios::binary | std::ios::trunc);
+    testFile.close();
+}
+
+/* Test case where we fail to format the LUKS device. */
+TEST_F(eStoragedTest, FormatFail)
+{
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _))
+        .WillOnce(Return(-1));
+
+    EXPECT_THROW(esObject->format(password), EncryptionError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+/* Test case where we fail to set the password for the LUKS device. */
+TEST_F(eStoragedTest, AddKeyslotFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .WillOnce(Return(-1));
+
+    EXPECT_THROW(esObject->format(password), EncryptionError);
+    EXPECT_TRUE(esObject->isLocked());
+}
+
+/* Test case where we fail to load the LUKS header. */
+TEST_F(eStoragedTest, LoadLuksHeaderFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).WillOnce(Return(-1));
+
+    EXPECT_THROW(esObject->format(password), EncryptionError);
+    EXPECT_TRUE(esObject->isLocked());
+}
+
+/* Test case where we fail to activate the LUKS device. */
+TEST_F(eStoragedTest, ActivateFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .WillOnce(Return(-1));
+
+    EXPECT_THROW(esObject->format(password), EncryptionError);
+    EXPECT_TRUE(esObject->isLocked());
+}
+
+/* Test case where we fail to create the filesystem. */
+TEST_F(eStoragedTest, CreateFilesystemFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(-1));
+
+    EXPECT_THROW(esObject->format(password), FilesystemError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+/* Test case where we fail to create the mount point. */
+TEST_F(eStoragedTest, CreateMountPointFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, createDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(false));
+
+    EXPECT_THROW(esObject->format(password), FilesystemError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+/* Test case where we fail to mount the filesystem. */
+TEST_F(eStoragedTest, MountFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, createDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockFsIface,
+                doMount(ContainsRegex("/dev/mapper/"),
+                        StrEq(esObject->getMountPoint()), _, _, _))
+        .WillOnce(Return(-1));
+
+    EXPECT_CALL(*mockFsIface, removeDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_THROW(esObject->format(password), FilesystemError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+/* Test case where we fail to unmount the filesystem. */
+TEST_F(eStoragedTest, UnmountFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, createDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockFsIface,
+                doMount(ContainsRegex("/dev/mapper/"),
+                        StrEq(esObject->getMountPoint()), _, _, _))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, doUnmount(StrEq(esObject->getMountPoint())))
+        .WillOnce(Return(-1));
+
+    esObject->format(password);
+    EXPECT_FALSE(esObject->isLocked());
+
+    EXPECT_THROW(esObject->lock(password), FilesystemError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+/* Test case where we fail to remove the mount point. */
+TEST_F(eStoragedTest, RemoveMountPointFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, createDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockFsIface,
+                doMount(ContainsRegex("/dev/mapper/"),
+                        StrEq(esObject->getMountPoint()), _, _, _))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, doUnmount(StrEq(esObject->getMountPoint())))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, removeDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(false));
+
+    esObject->format(password);
+    EXPECT_FALSE(esObject->isLocked());
+
+    /* This will fail to remove the mount point. */
+    EXPECT_THROW(esObject->lock(password), FilesystemError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+/* Test case where we fail to deactivate the LUKS device. */
+TEST_F(eStoragedTest, DeactivateFail)
+{
+    EXPECT_CALL(sdbusMock,
+                sd_bus_emit_properties_changed_strv(
+                    IsNull(), StrEq(TEST_PATH), StrEq(ESTORAGED_INTERFACE), _))
+        .WillRepeatedly(Return(0));
+
+    EXPECT_CALL(*mockCryptIface, cryptFormat(_, _, _, _, _, _, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptKeyslotAddByVolumeKey(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptLoad(_, _, _)).Times(1);
+
+    EXPECT_CALL(*mockCryptIface, cryptActivateByPassphrase(_, _, _, _, _, _))
+        .Times(1);
+
+    EXPECT_CALL(*mockFsIface, runMkfs(testLuksDevName)).WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, createDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockFsIface,
+                doMount(ContainsRegex("/dev/mapper/"),
+                        StrEq(esObject->getMountPoint()), _, _, _))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, doUnmount(StrEq(esObject->getMountPoint())))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(*mockFsIface, removeDirectory(path(esObject->getMountPoint())))
+        .WillOnce(Return(true));
+
+    EXPECT_CALL(*mockCryptIface, cryptDeactivate(_, _)).WillOnce(Return(-1));
+
+    /* Format the encrypted device. */
+    esObject->format(password);
+    EXPECT_FALSE(esObject->isLocked());
+
+    EXPECT_THROW(esObject->lock(password), EncryptionError);
+    EXPECT_FALSE(esObject->isLocked());
+}
+
+} // namespace estoraged_test
diff --git a/src/test/meson.build b/src/test/meson.build
new file mode 100644
index 0000000..a4c23f7
--- /dev/null
+++ b/src/test/meson.build
@@ -0,0 +1,17 @@
+gtest = dependency('gtest', main: true, disabler: true, required: build_tests)
+gmock = dependency('gmock', disabler: true, required: build_tests)
+
+tests = [
+  'estoraged_test',
+]
+
+foreach t : tests
+  test(t, executable(t.underscorify(), t + '.cpp',
+                     implicit_include_directories: false,
+                     dependencies: [
+                       gtest,
+                       gmock,
+                       libeStoraged,
+                     ],
+                     include_directories: eStoraged_headers))
+endforeach