move files around to create a common convenience library

Problem: plan is to add another blob handler into ipmi-flash
(ipmi-flash-version). This new handler will re-use much of the
ipmi-flash (firmware-handler) code. The common code should be presented
as a convenience library to reduce code duplication.

Solution: move anticipated firmware-handler specific code into the
subdirectory bmc/firmware-handler and leave common code in bmc/.

The end goal is to have version-handler re-use as
much code as possible.

Tested:
rebuilt everything and ran unit tests.

Signed-off-by: Jason Ling <jasonling@google.com>
Change-Id: I2128da629b0ddf27b89f1faee358d1941f1dff38
diff --git a/bmc/firmware-handler/test/Makefile.am b/bmc/firmware-handler/test/Makefile.am
new file mode 100644
index 0000000..bf818fd
--- /dev/null
+++ b/bmc/firmware-handler/test/Makefile.am
@@ -0,0 +1,115 @@
+@VALGRIND_CHECK_RULES@
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/ \
+	-I$(top_srcdir)/tools/ \
+	-I$(top_srcdir)/bmc/ \
+	-I$(top_srcdir)/bmc/firmware-handler \
+	$(GTEST_CFLAGS) \
+	$(GMOCK_CFLAGS) \
+	$(CODE_COVERAGE_CPPFLAGS)
+AM_CXXFLAGS = \
+	$(SDBUSPLUS_CFLAGS) \
+	$(PHOSPHOR_LOGGING_CFLAGS) \
+	$(CODE_COVERAGE_CXXFLAGS)
+AM_LDFLAGS = \
+	$(GTEST_LIBS) \
+	$(GMOCK_LIBS) \
+	-lgmock_main \
+	$(OESDK_TESTCASE_FLAGS) \
+	$(SDBUSPLUS_LIBS) \
+	$(PHOSPHOR_LOGGING_LIBS) \
+	$(CODE_COVERAGE_LIBS)
+
+# Run all 'check' test programs
+check_PROGRAMS = \
+	firmware_handler_unittest \
+	firmware_stat_unittest \
+	firmware_canhandle_unittest \
+	firmware_write_unittest \
+	firmware_writemeta_unittest \
+	firmware_open_unittest \
+	firmware_close_unittest \
+	firmware_sessionstat_unittest \
+	firmware_commit_unittest \
+	file_handler_unittest \
+	firmware_state_notyetstarted_unittest \
+	firmware_state_uploadinprogress_unittest \
+	firmware_state_verificationpending_unittest \
+	firmware_state_verificationstarted_unittest \
+	firmware_state_verificationcompleted_unittest \
+	firmware_state_updatepending_unittest \
+	firmware_state_updatestarted_unittest \
+	firmware_state_updatecompleted_unittest \
+	firmware_state_notyetstarted_tarball_unittest \
+	firmware_multiplebundle_unittest \
+	firmware_json_unittest \
+	firmware_skip_unittest
+
+TESTS = $(check_PROGRAMS)
+
+firmware_handler_unittest_SOURCES = firmware_handler_unittest.cpp
+firmware_handler_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_stat_unittest_SOURCES = firmware_stat_unittest.cpp
+firmware_stat_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_canhandle_unittest_SOURCES = firmware_canhandle_unittest.cpp
+firmware_canhandle_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_write_unittest_SOURCES = firmware_write_unittest.cpp
+firmware_write_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_writemeta_unittest_SOURCES = firmware_writemeta_unittest.cpp
+firmware_writemeta_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_open_unittest_SOURCES = firmware_open_unittest.cpp
+firmware_open_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_close_unittest_SOURCES = firmware_close_unittest.cpp
+firmware_close_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_sessionstat_unittest_SOURCES = firmware_sessionstat_unittest.cpp
+firmware_sessionstat_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_commit_unittest_SOURCES = firmware_commit_unittest.cpp
+firmware_commit_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+file_handler_unittest_SOURCES = file_handler_unittest.cpp
+file_handler_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la -lstdc++fs
+
+firmware_state_notyetstarted_unittest_SOURCES = firmware_state_notyetstarted_unittest.cpp
+firmware_state_notyetstarted_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_uploadinprogress_unittest_SOURCES = firmware_state_uploadinprogress_unittest.cpp
+firmware_state_uploadinprogress_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_verificationpending_unittest_SOURCES = firmware_state_verificationpending_unittest.cpp
+firmware_state_verificationpending_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_verificationstarted_unittest_SOURCES = firmware_state_verificationstarted_unittest.cpp
+firmware_state_verificationstarted_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_verificationcompleted_unittest_SOURCES = firmware_state_verificationcompleted_unittest.cpp
+firmware_state_verificationcompleted_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_updatepending_unittest_SOURCES = firmware_state_updatepending_unittest.cpp
+firmware_state_updatepending_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_updatestarted_unittest_SOURCES = firmware_state_updatestarted_unittest.cpp
+firmware_state_updatestarted_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_updatecompleted_unittest_SOURCES = firmware_state_updatecompleted_unittest.cpp
+firmware_state_updatecompleted_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_state_notyetstarted_tarball_unittest_SOURCES = firmware_state_notyetstarted_tarball_unittest.cpp
+firmware_state_notyetstarted_tarball_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_multiplebundle_unittest_SOURCES = firmware_multiplebundle_unittest.cpp
+firmware_multiplebundle_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_json_unittest_SOURCES = firmware_json_unittest.cpp
+firmware_json_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
+
+firmware_skip_unittest_SOURCES = firmware_skip_unittest.cpp
+firmware_skip_unittest_LDADD = $(top_builddir)/bmc/firmware-handler/libfirmwareblob_common.la
diff --git a/bmc/firmware-handler/test/crc_mock.hpp b/bmc/firmware-handler/test/crc_mock.hpp
new file mode 100644
index 0000000..293ec24
--- /dev/null
+++ b/bmc/firmware-handler/test/crc_mock.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <cstdint>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+class CrcInterface
+{
+  public:
+    virtual ~CrcInterface() = default;
+
+    virtual std::uint16_t
+        generateCrc(const std::vector<std::uint8_t>& data) const = 0;
+};
+
+class CrcMock : public CrcInterface
+{
+  public:
+    virtual ~CrcMock() = default;
+    MOCK_CONST_METHOD1(generateCrc,
+                       std::uint16_t(const std::vector<std::uint8_t>&));
+};
diff --git a/bmc/firmware-handler/test/data_mock.hpp b/bmc/firmware-handler/test/data_mock.hpp
new file mode 100644
index 0000000..c8109ad
--- /dev/null
+++ b/bmc/firmware-handler/test/data_mock.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "data_handler.hpp"
+
+#include <gmock/gmock.h>
+
+namespace ipmi_flash
+{
+
+class DataHandlerMock : public DataInterface
+{
+  public:
+    virtual ~DataHandlerMock() = default;
+
+    MOCK_METHOD0(open, bool());
+    MOCK_METHOD0(close, bool());
+    MOCK_METHOD1(copyFrom, std::vector<std::uint8_t>(std::uint32_t));
+    MOCK_METHOD1(writeMeta, bool(const std::vector<std::uint8_t>&));
+    MOCK_METHOD0(readMeta, std::vector<std::uint8_t>());
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/file_handler_unittest.cpp b/bmc/firmware-handler/test/file_handler_unittest.cpp
new file mode 100644
index 0000000..ab12125
--- /dev/null
+++ b/bmc/firmware-handler/test/file_handler_unittest.cpp
@@ -0,0 +1,58 @@
+#include "file_handler.hpp"
+
+#include <cstdint>
+#include <cstdio>
+#include <fstream>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+static constexpr auto TESTPATH = "test.output";
+
+class FileHandlerOpenTest : public ::testing::Test
+{
+  protected:
+    void TearDown() override
+    {
+        (void)std::remove(TESTPATH);
+    }
+};
+
+TEST_F(FileHandlerOpenTest, VerifyItIsHappy)
+{
+    /* Opening a fail may create it? */
+
+    FileHandler handler(TESTPATH);
+    EXPECT_TRUE(handler.open(""));
+
+    /* Calling open twice fails the second time. */
+    EXPECT_FALSE(handler.open(""));
+}
+
+TEST_F(FileHandlerOpenTest, VerifyWriteDataWrites)
+{
+    /* Verify writing bytes writes them... flushing data can be an issue here,
+     * so we close first.
+     */
+    FileHandler handler(TESTPATH);
+    EXPECT_TRUE(handler.open(""));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    std::uint32_t offset = 0;
+
+    EXPECT_TRUE(handler.write(offset, bytes));
+    handler.close();
+
+    std::ifstream data;
+    data.open(TESTPATH, std::ios::binary);
+    char expectedBytes[2];
+    data.read(&expectedBytes[0], sizeof(expectedBytes));
+    EXPECT_EQ(expectedBytes[0], bytes[0]);
+    EXPECT_EQ(expectedBytes[1], bytes[1]);
+    /* annoyingly the memcmp was failing... but it's the same data. */
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_canhandle_unittest.cpp b/bmc/firmware-handler/test/firmware_canhandle_unittest.cpp
new file mode 100644
index 0000000..34c4e51
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_canhandle_unittest.cpp
@@ -0,0 +1,43 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+TEST(FirmwareHandlerCanHandleTest, VerifyItemsInListAreOk)
+{
+    struct ListItem
+    {
+        std::string name;
+        bool expected;
+    };
+
+    std::vector<ListItem> items = {
+        {"asdf", true}, {"nope", false}, {"123123", false}, {"bcdf", true}};
+
+    ImageHandlerMock imageMock;
+
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+    blobs.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+    blobs.emplace_back("bcdf", std::make_unique<ImageHandlerMock>());
+
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+
+    for (const auto& item : items)
+    {
+        EXPECT_EQ(item.expected, handler->canHandleBlob(item.name));
+    }
+}
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_close_unittest.cpp b/bmc/firmware-handler/test/firmware_close_unittest.cpp
new file mode 100644
index 0000000..6d1fe55
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_close_unittest.cpp
@@ -0,0 +1,76 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+using ::testing::StrEq;
+
+class FirmwareHandlerCloseTest : public FakeLpcFirmwareTest
+{};
+
+TEST_F(FirmwareHandlerCloseTest, CloseSucceedsWithDataHandler)
+{
+    /* Boring test where you open a blob_id, then verify that when it's closed
+     * everything looks right.
+     */
+    EXPECT_CALL(*dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(*hashImageMock, open(StrEq(hashBlobId))).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::lpc,
+        hashBlobId));
+
+    /* The active hash blob_id was added. */
+    auto currentBlobs = handler->getBlobIds();
+    EXPECT_EQ(3, currentBlobs.size());
+    EXPECT_EQ(1, std::count(currentBlobs.begin(), currentBlobs.end(),
+                            activeHashBlobId));
+
+    /* Set up close() expectations. */
+    EXPECT_CALL(*dataMock, close());
+    EXPECT_CALL(*hashImageMock, close());
+    EXPECT_TRUE(handler->close(0));
+
+    /* Close does not delete the active blob id.  This indicates that there is
+     * data queued.
+     */
+}
+
+TEST_F(FirmwareHandlerCloseTest, CloseSucceedsWithoutDataHandler)
+{
+    /* Boring test where you open a blob_id using ipmi, so there's no data
+     * handler, and it's closed and everything looks right.
+     */
+    EXPECT_CALL(*hashImageMock, open(StrEq(hashBlobId))).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi,
+        hashBlobId));
+
+    /* The active hash blob_id was added. */
+    auto currentBlobs = handler->getBlobIds();
+    EXPECT_EQ(3, currentBlobs.size());
+    EXPECT_EQ(1, std::count(currentBlobs.begin(), currentBlobs.end(),
+                            activeHashBlobId));
+
+    /* Set up close() expectations. */
+    EXPECT_CALL(*hashImageMock, close());
+    EXPECT_TRUE(handler->close(0));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_commit_unittest.cpp b/bmc/firmware-handler/test/firmware_commit_unittest.cpp
new file mode 100644
index 0000000..cb50b8e
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_commit_unittest.cpp
@@ -0,0 +1,77 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+using ::testing::StrEq;
+
+class FirmwareHandlerCommitTest : public ::testing::Test
+{
+  protected:
+    ImageHandlerMock *imageMock1, *imageMock2;
+    std::vector<HandlerPack> blobs;
+    std::vector<DataHandlerPack> data;
+
+    void SetUp() override
+    {
+        std::unique_ptr<ImageHandlerInterface> image =
+            std::make_unique<ImageHandlerMock>();
+        imageMock1 = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(hashBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        imageMock2 = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back("asdf", std::move(image));
+
+        data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+    }
+};
+
+TEST_F(FirmwareHandlerCommitTest, VerifyCannotCommitOnFlashImage)
+{
+    /* Verify the flash image returns failure on this command.  It's a fairly
+     * artificial test.
+     */
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+
+    EXPECT_CALL(*imageMock2, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi, "asdf"));
+
+    EXPECT_FALSE(handler->commit(0, {}));
+}
+
+TEST_F(FirmwareHandlerCommitTest, VerifyCannotCommitOnHashFile)
+{
+    /* Verify the hash file returns failure on this command.  It's a fairly
+     * artificial test.
+     */
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+
+    EXPECT_CALL(*imageMock1, open(StrEq(hashBlobId))).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi,
+        hashBlobId));
+
+    EXPECT_FALSE(handler->commit(0, {}));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_handler_unittest.cpp b/bmc/firmware-handler/test/firmware_handler_unittest.cpp
new file mode 100644
index 0000000..1c3cc58
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_handler_unittest.cpp
@@ -0,0 +1,122 @@
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::UnorderedElementsAreArray;
+
+TEST(FirmwareHandlerTest, CreateEmptyHandlerListVerifyFails)
+{
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        {}, std::move(data), CreateActionMap("abcd"));
+    EXPECT_EQ(handler, nullptr);
+}
+TEST(FirmwareHandlerTest, CreateEmptyDataHandlerListFails)
+{
+    ImageHandlerMock imageMock;
+
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+    blobs.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::vector<DataHandlerPack>(),
+        CreateActionMap("asdf"));
+    EXPECT_EQ(handler, nullptr);
+}
+TEST(FirmwareHandlerTest, CreateEmptyActionPackVerifyFails)
+{
+    /* The ActionPack map corresponds to the firmware list passed in, but
+     * they're not checked against each other yet.
+     */
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+    blobs.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+
+    ActionMap emptyMap;
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), std::move(emptyMap));
+    EXPECT_EQ(handler, nullptr);
+}
+TEST(FirmwareHandlerTest, FirmwareHandlerListRequiresAtLeastTwoEntries)
+{
+    /* The hashblob handler must be one of the entries, but it cannot be the
+     * only entry.
+     */
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    /* Provide a firmware list that has the hash blob, which is the required one
+     * -- tested in a different test.
+     */
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+    EXPECT_EQ(handler, nullptr);
+
+    /* Add second firmware and it'll now work. */
+    std::vector<HandlerPack> blobs2;
+    blobs2.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+    blobs2.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+
+    std::vector<DataHandlerPack> data2;
+    data2.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs2), std::move(data2), CreateActionMap("asdf"));
+
+    auto result = handler->getBlobIds();
+    std::vector<std::string> expectedBlobs = {"asdf", hashBlobId};
+    EXPECT_THAT(result, UnorderedElementsAreArray(expectedBlobs));
+}
+TEST(FirmwareHandlerTest, VerifyHashRequiredForHappiness)
+{
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    /* This works fine only if you also pass in the hash handler. */
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+    EXPECT_EQ(handler, nullptr);
+
+    std::vector<HandlerPack> blobs2;
+    blobs2.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+    blobs2.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+
+    std::vector<DataHandlerPack> data2;
+    data2.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs2), std::move(data2), CreateActionMap("asdf"));
+
+    auto result = handler->getBlobIds();
+    std::vector<std::string> expectedBlobs = {"asdf", hashBlobId};
+    EXPECT_THAT(result, UnorderedElementsAreArray(expectedBlobs));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_json_unittest.cpp b/bmc/firmware-handler/test/firmware_json_unittest.cpp
new file mode 100644
index 0000000..8f3ea30
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_json_unittest.cpp
@@ -0,0 +1,620 @@
+#include "buildjson.hpp"
+#include "general_systemd.hpp"
+#include "skip_action.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+using ::testing::IsEmpty;
+
+using json = nlohmann::json;
+
+TEST(FirmwareJsonTest, InvalidHandlerType)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "unsupported",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, InvalidPreparationType)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "superfun",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, InvalidVerificationType)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "funtimes",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, InvalidUpdateType)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "systemd"
+                }
+            }
+         }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, MissingHandler)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+        }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, MissingActions)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            }
+        }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, MissingActionPreparation)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+        }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, MissingActionVerification)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+        }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, MissingActionUpdate)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                }
+            }
+        }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, TwoConfigsOneInvalidReturnsValid)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         },
+         {
+            "blob" : "/flash/image2",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+        }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h[0].blobId, "/flash/image2");
+    EXPECT_EQ(h.size(), 1);
+}
+
+/*
+ * TODO: It may be worth individually using builders per type, and testing
+ * those.
+ *
+ * TODO: Only allow unique handler blob paths (tested at a higher level).
+ */
+
+TEST(FirmwareJsonTest, VerifyBlobNameMatches)
+{
+    /* A perfect configuration except the blob name doesn't start with "/flash/"
+     */
+    auto j2 = R"(
+        [{
+            "blob" : "bmc-image-flash",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, VerifyMinimumBlobNameLength)
+{
+    /* A perfect configuration except the blob name is effectively zero length.
+     */
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         }]
+    )"_json;
+
+    EXPECT_THAT(buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(FirmwareJsonTest, VerifySystemdWithReboot)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "reboot"
+                }
+            }
+         }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h[0].blobId, "/flash/image");
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->preparation == nullptr);
+    EXPECT_FALSE(h[0].actions->verification == nullptr);
+    EXPECT_FALSE(h[0].actions->update == nullptr);
+}
+
+TEST(FirmwareJsonTest, VerifyMultipleHandlersReturned)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-update.target"
+                }
+            }
+        },
+        {
+            "blob" : "/flash/bios",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-update.target"
+                }
+            }
+        }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h.size(), 2);
+    EXPECT_EQ(h[0].blobId, "/flash/image");
+    EXPECT_EQ(h[1].blobId, "/flash/bios");
+}
+
+TEST(FirmwareJsonTest, VerifyValidSingleNonReboot)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify"
+                },
+                "update" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-update.target"
+                }
+            }
+         }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h[0].blobId, "/flash/image");
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->preparation == nullptr);
+    EXPECT_FALSE(h[0].actions->verification == nullptr);
+    auto verifier = reinterpret_cast<SystemdWithStatusFile*>(
+        h[0].actions->verification.get());
+    EXPECT_THAT(verifier->getMode(), "replace");
+    EXPECT_FALSE(h[0].actions->update == nullptr);
+    auto updater = reinterpret_cast<SystemdNoFile*>(h[0].actions->update.get());
+    EXPECT_THAT(updater->getMode(), "replace");
+}
+
+TEST(FirmwareJsonTest, VerifyValidWithModes)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify",
+                    "mode" : "replace-nope"
+                },
+                "update" : {
+                    "type" : "systemd",
+                    "mode" : "replace-fake",
+                    "unit" : "phosphor-ipmi-flash-bmc-update.target"
+                }
+            }
+         }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h[0].blobId, "/flash/image");
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->preparation == nullptr);
+    EXPECT_FALSE(h[0].actions->verification == nullptr);
+    auto verifier = reinterpret_cast<SystemdWithStatusFile*>(
+        h[0].actions->verification.get());
+    EXPECT_THAT(verifier->getMode(), "replace-nope");
+    EXPECT_FALSE(h[0].actions->update == nullptr);
+    auto updater = reinterpret_cast<SystemdNoFile*>(h[0].actions->update.get());
+    EXPECT_THAT(updater->getMode(), "replace-fake");
+}
+
+TEST(FirmwareJsonTest, VerifyValidUpdateWithFilePath)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-bmc-prepare.target"
+                },
+                "verification" : {
+                    "type" : "fileSystemdVerify",
+                    "unit" : "phosphor-ipmi-flash-bmc-verify.target",
+                    "path" : "/tmp/bmc.verify",
+                    "mode" : "replace-nope"
+                },
+                "update" : {
+                    "type" : "fileSystemdUpdate",
+                    "mode" : "replace-fake",
+                    "unit" : "phosphor-ipmi-flash-bmc-update.target",
+                    "path" : "/tmp/update.verify"
+                }
+            }
+         }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h[0].blobId, "/flash/image");
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->preparation == nullptr);
+    EXPECT_FALSE(h[0].actions->verification == nullptr);
+    auto verifier = reinterpret_cast<SystemdWithStatusFile*>(
+        h[0].actions->verification.get());
+    EXPECT_THAT(verifier->getMode(), "replace-nope");
+    EXPECT_FALSE(h[0].actions->update == nullptr);
+    auto updater =
+        reinterpret_cast<SystemdWithStatusFile*>(h[0].actions->update.get());
+    EXPECT_THAT(updater->getMode(), "replace-fake");
+}
+
+TEST(FirmwareJsonTest, VerifySkipFields)
+{
+    // In this configuration, nothing happens because all actions are set to
+    // skip.
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "handler" : {
+                "type" : "file",
+                "path" : "/run/initramfs/bmc-image"
+            },
+            "actions" : {
+                "preparation" : {
+                    "type" : "skip"
+                },
+                "verification" : {
+                    "type" : "skip"
+                },
+                "update" : {
+                    "type" : "skip"
+                }
+            }
+         }]
+    )"_json;
+
+    auto h = buildHandlerFromJson(j2);
+    EXPECT_EQ(h[0].blobId, "/flash/image");
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->preparation == nullptr);
+    EXPECT_FALSE(h[0].actions->verification == nullptr);
+    EXPECT_FALSE(h[0].actions->update == nullptr);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_multiplebundle_unittest.cpp b/bmc/firmware-handler/test/firmware_multiplebundle_unittest.cpp
new file mode 100644
index 0000000..20c63a9
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_multiplebundle_unittest.cpp
@@ -0,0 +1,161 @@
+/* The goal of these tests is to verify that once a host-client has started the
+ * process with one blob bundle, they cannot pivot to upload data to another.
+ *
+ * This prevent someone from starting to upload a BMC firmware, and then midway
+ * through start uploading a BIOS image.
+ */
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "status.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+
+class IpmiOnlyTwoFirmwaresTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        std::unique_ptr<ImageHandlerInterface> image =
+            std::make_unique<ImageHandlerMock>();
+        hashImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(hashBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        staticImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(staticLayoutBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        biosImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(biosBlobId, std::move(image));
+
+        std::unique_ptr<TriggerableActionInterface> bmcPrepareMock =
+            std::make_unique<TriggerMock>();
+        bmcPrepareMockPtr =
+            reinterpret_cast<TriggerMock*>(bmcPrepareMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> bmcVerifyMock =
+            std::make_unique<TriggerMock>();
+        bmcVerifyMockPtr = reinterpret_cast<TriggerMock*>(bmcVerifyMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> bmcUpdateMock =
+            std::make_unique<TriggerMock>();
+        bmcUpdateMockPtr = reinterpret_cast<TriggerMock*>(bmcUpdateMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> biosPrepareMock =
+            std::make_unique<TriggerMock>();
+        biosPrepareMockPtr =
+            reinterpret_cast<TriggerMock*>(biosPrepareMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> biosVerifyMock =
+            std::make_unique<TriggerMock>();
+        biosVerifyMockPtr =
+            reinterpret_cast<TriggerMock*>(biosVerifyMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> biosUpdateMock =
+            std::make_unique<TriggerMock>();
+        biosUpdateMockPtr =
+            reinterpret_cast<TriggerMock*>(biosUpdateMock.get());
+
+        ActionMap packs;
+
+        std::unique_ptr<ActionPack> bmcPack = std::make_unique<ActionPack>();
+        bmcPack->preparation = std::move(bmcPrepareMock);
+        bmcPack->verification = std::move(bmcVerifyMock);
+        bmcPack->update = std::move(bmcUpdateMock);
+
+        std::unique_ptr<ActionPack> biosPack = std::make_unique<ActionPack>();
+        biosPack->preparation = std::move(biosPrepareMock);
+        biosPack->verification = std::move(biosVerifyMock);
+        biosPack->update = std::move(biosUpdateMock);
+
+        packs[staticLayoutBlobId] = std::move(bmcPack);
+        packs[biosBlobId] = std::move(biosPack);
+
+        std::vector<DataHandlerPack> data;
+        data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            std::move(blobs), std::move(data), std::move(packs));
+    }
+
+    void expectedState(FirmwareBlobHandler::UpdateState state)
+    {
+        auto realHandler = dynamic_cast<FirmwareBlobHandler*>(handler.get());
+        EXPECT_EQ(state, realHandler->getCurrentState());
+    }
+
+    ImageHandlerMock *hashImageMock, *staticImageMock, *biosImageMock;
+
+    std::vector<HandlerPack> blobs;
+
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+
+    TriggerMock *bmcPrepareMockPtr, *bmcVerifyMockPtr, *bmcUpdateMockPtr;
+    TriggerMock *biosPrepareMockPtr, *biosVerifyMockPtr, *biosUpdateMockPtr;
+
+    std::uint16_t session = 1;
+    std::uint16_t flags =
+        blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi;
+};
+
+TEST_F(IpmiOnlyTwoFirmwaresTest, OpeningBiosAfterBlobFails)
+{
+    /* You can only have one file open at a time, and the first firmware file
+     * you open locks it down
+     */
+    EXPECT_CALL(*staticImageMock, open(staticLayoutBlobId))
+        .WillOnce(Return(true));
+    EXPECT_CALL(*bmcPrepareMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(session, flags, staticLayoutBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_CALL(*staticImageMock, close()).WillOnce(Return());
+    handler->close(session);
+
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    EXPECT_CALL(*biosImageMock, open(biosBlobId)).Times(0);
+    EXPECT_FALSE(handler->open(session, flags, biosBlobId));
+}
+
+TEST_F(IpmiOnlyTwoFirmwaresTest, OpeningHashBeforeBiosSucceeds)
+{
+    /* Opening the hash blob does nothing special in this regard. */
+    EXPECT_CALL(*hashImageMock, open(hashBlobId)).WillOnce(Return(true));
+    EXPECT_TRUE(handler->open(session, flags, hashBlobId));
+
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_CALL(*hashImageMock, close()).WillOnce(Return());
+    handler->close(session);
+
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+    ASSERT_FALSE(handler->canHandleBlob(verifyBlobId));
+
+    EXPECT_CALL(*biosImageMock, open(biosBlobId)).WillOnce(Return(true));
+    EXPECT_TRUE(handler->open(session, flags, biosBlobId));
+
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_CALL(*biosImageMock, close()).WillOnce(Return());
+    handler->close(session);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_open_unittest.cpp b/bmc/firmware-handler/test/firmware_open_unittest.cpp
new file mode 100644
index 0000000..274ea14
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_open_unittest.cpp
@@ -0,0 +1,59 @@
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+class FirmwareOpenFailTest : public ::testing::TestWithParam<std::uint16_t>
+{};
+
+TEST_P(FirmwareOpenFailTest, WithFlags)
+{
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+    data.emplace_back(FirmwareFlags::UpdateFlags::p2a, nullptr);
+    data.emplace_back(FirmwareFlags::UpdateFlags::lpc, nullptr);
+
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+    blobs.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+
+    EXPECT_FALSE(handler->open(0, GetParam(), "asdf"));
+}
+
+const std::vector<std::uint16_t> OpenFailParams{
+    /* These first 4 fail because they don't have the "write" flag */
+    0b000 << 8,
+    0b110 << 8,
+    0b101 << 8,
+    0b011 << 8,
+    /* Next 1 doesn't specify any transport */
+    blobs::OpenFlags::write | 0b000 << 8,
+    /* Next 3 specify 2 reserved transport bits at the same time. This isn't
+     * allowed because older code expects these first 3 bits to be mutually
+     * exclusive.
+     */
+    blobs::OpenFlags::write | 0b110 << 8,
+    blobs::OpenFlags::write | 0b101 << 8,
+    blobs::OpenFlags::write | 0b011 << 8,
+};
+
+INSTANTIATE_TEST_CASE_P(WithFlags, FirmwareOpenFailTest,
+                        ::testing::ValuesIn(OpenFailParams));
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_sessionstat_unittest.cpp b/bmc/firmware-handler/test/firmware_sessionstat_unittest.cpp
new file mode 100644
index 0000000..0736293
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_sessionstat_unittest.cpp
@@ -0,0 +1,71 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+using ::testing::Return;
+
+class FirmwareSessionStateTestIpmiOnly : public IpmiOnlyFirmwareTest
+{};
+
+class FirmwareSessionStateTestLpc : public FakeLpcFirmwareTest
+{};
+
+TEST_F(FirmwareSessionStateTestIpmiOnly, DataTypeIpmiNoMetadata)
+{
+    /* Verifying running stat if the type of data session is IPMI returns no
+     * metadata.
+     */
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi, "asdf"));
+
+    int size = 512;
+    EXPECT_CALL(*imageMock, getSize()).WillOnce(Return(size));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(handler->stat(0, &meta));
+    EXPECT_EQ(meta.blobState,
+              blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi);
+    EXPECT_EQ(meta.size, size);
+    EXPECT_EQ(meta.metadata.size(), 0);
+}
+
+TEST_F(FirmwareSessionStateTestLpc, DataTypeP2AReturnsMetadata)
+{
+    /* Really any type that isn't IPMI can return metadata, but we only expect
+     * P2A to for now.  Later, LPC may have reason to provide data, and can by
+     * simply implementing read().
+     */
+    EXPECT_CALL(*dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::lpc, "asdf"));
+
+    int size = 512;
+    EXPECT_CALL(*imageMock, getSize()).WillOnce(Return(size));
+    std::vector<std::uint8_t> mBytes = {0x01, 0x02};
+    EXPECT_CALL(*dataMock, readMeta()).WillOnce(Return(mBytes));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(handler->stat(0, &meta));
+    EXPECT_EQ(meta.blobState,
+              blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::lpc);
+    EXPECT_EQ(meta.size, size);
+    EXPECT_EQ(meta.metadata.size(), mBytes.size());
+    EXPECT_EQ(meta.metadata[0], mBytes[0]);
+    EXPECT_EQ(meta.metadata[1], mBytes[1]);
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_skip_unittest.cpp b/bmc/firmware-handler/test/firmware_skip_unittest.cpp
new file mode 100644
index 0000000..a549582
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_skip_unittest.cpp
@@ -0,0 +1,36 @@
+#include "skip_action.hpp"
+#include "status.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+TEST(SkipActionTest, ValidateTriggerReturnsTrue)
+{
+    SkipAction skip;
+    EXPECT_TRUE(skip.trigger());
+    EXPECT_TRUE(skip.trigger());
+}
+
+TEST(SkipActionTest, ValidateStatusAlwaysSuccess)
+{
+    SkipAction skip;
+    EXPECT_EQ(ActionStatus::success, skip.status());
+    EXPECT_TRUE(skip.trigger());
+    EXPECT_EQ(ActionStatus::success, skip.status());
+}
+
+TEST(SkipActionTest, AbortHasNoImpactOnStatus)
+{
+    SkipAction skip;
+    EXPECT_EQ(ActionStatus::success, skip.status());
+    skip.abort();
+    EXPECT_EQ(ActionStatus::success, skip.status());
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_stat_unittest.cpp b/bmc/firmware-handler/test/firmware_stat_unittest.cpp
new file mode 100644
index 0000000..fcb2d34
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_stat_unittest.cpp
@@ -0,0 +1,45 @@
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+/* This test ensures the stat() method preserves compatibility with older host
+ * tools by reporting that all transports are supported. */
+TEST(FirmwareHandlerStatTest, StatOnInactiveBlobIDReturnsAllTransports)
+{
+    /* Test that the metadata information returned matches expectations for this
+     * case.
+     *
+     * canHandle has already been called at this point, so we don't need to test
+     * the input for this function.
+     */
+
+    std::vector<HandlerPack> blobs;
+    blobs.emplace_back(hashBlobId, std::make_unique<ImageHandlerMock>());
+    blobs.emplace_back("asdf", std::make_unique<ImageHandlerMock>());
+
+    std::vector<DataHandlerPack> data;
+    data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+    auto handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+        std::move(blobs), std::move(data), CreateActionMap("asdf"));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(handler->stat("asdf", &meta));
+    /* All transport flags are set */
+    EXPECT_EQ(0xff00, meta.blobState);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_notyetstarted_tarball_unittest.cpp b/bmc/firmware-handler/test/firmware_state_notyetstarted_tarball_unittest.cpp
new file mode 100644
index 0000000..e666395
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_notyetstarted_tarball_unittest.cpp
@@ -0,0 +1,104 @@
+/**
+ * The goal of these tests is to verify opening the ubi tarball changes state
+ * as expected and does not regress.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::UnorderedElementsAreArray;
+
+class FirmwareHandlerNotYetStartedUbitTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        std::unique_ptr<ImageHandlerInterface> image =
+            std::make_unique<ImageHandlerMock>();
+        hashImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(hashBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        imageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(ubiTarballBlobId, std::move(image));
+
+        std::unique_ptr<TriggerableActionInterface> verifyMock =
+            std::make_unique<TriggerMock>();
+        verifyMockPtr = reinterpret_cast<TriggerMock*>(verifyMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> updateMock =
+            std::make_unique<TriggerMock>();
+        updateMockPtr = reinterpret_cast<TriggerMock*>(updateMock.get());
+
+        std::unique_ptr<ActionPack> actionPack = std::make_unique<ActionPack>();
+        actionPack->preparation = CreateTriggerMock();
+        actionPack->verification = std::move(verifyMock);
+        actionPack->update = std::move(updateMock);
+
+        ActionMap packs;
+        packs[ubiTarballBlobId] = std::move(actionPack);
+
+        std::vector<DataHandlerPack> data;
+        data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            std::move(blobs), std::move(data), std::move(packs));
+    }
+
+    void expectedState(FirmwareBlobHandler::UpdateState state)
+    {
+        auto realHandler = dynamic_cast<FirmwareBlobHandler*>(handler.get());
+        EXPECT_EQ(state, realHandler->getCurrentState());
+    }
+
+    void openToInProgress(const std::string& blobId)
+    {
+        if (blobId == hashBlobId)
+        {
+            EXPECT_CALL(*hashImageMock, open(blobId)).WillOnce(Return(true));
+        }
+        else
+        {
+            EXPECT_CALL(*imageMock, open(blobId)).WillOnce(Return(true));
+        }
+        EXPECT_TRUE(handler->open(session, flags, blobId));
+        expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+    }
+
+    ImageHandlerMock *hashImageMock, *imageMock;
+    std::vector<HandlerPack> blobs;
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+    TriggerMock* verifyMockPtr;
+    TriggerMock* updateMockPtr;
+
+    std::uint16_t session = 1;
+    std::uint16_t flags =
+        blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi;
+};
+
+TEST_F(FirmwareHandlerNotYetStartedUbitTest,
+       OpeningTarballMovesToUploadInProgress)
+{
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray({hashBlobId, ubiTarballBlobId}));
+
+    openToInProgress(ubiTarballBlobId);
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(
+                    {hashBlobId, ubiTarballBlobId, activeImageBlobId}));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_notyetstarted_unittest.cpp b/bmc/firmware-handler/test/firmware_state_notyetstarted_unittest.cpp
new file mode 100644
index 0000000..970ae2f
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_notyetstarted_unittest.cpp
@@ -0,0 +1,130 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is notYetStarted.  The initial state.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+class FirmwareHandlerNotYetStartedTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * There are the following calls (parameters may vary):
+ * Note: you cannot have a session yet, so only commands that don't take a
+ * session parameter are valid. Once you open() from this state, we will vary
+ * you transition out of this state (ensuring the above is true). Technically
+ * the firmware handler receives the session number with open(), but the blob
+ * manager is providing this normally.
+ *
+ * canHandleBlob
+ * getBlobIds
+ * deleteBlob
+ * stat
+ * open
+ *
+ * canHandleBlob is just a count check (or something similar) against what is
+ * returned by getBlobIds.  It is tested in firmware_canhandle_unittest
+ */
+
+/*
+ * deleteBlob()
+ */
+TEST_F(FirmwareHandlerNotYetStartedTest, DeleteBlobInStateReturnsFalse)
+{
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/* canHandleBlob, getBlobIds */
+TEST_F(FirmwareHandlerNotYetStartedTest, GetBlobListValidateListContents)
+{
+    /* By only checking that the hash and static blob ids are present to start
+     * with, we're also verifying others aren't.
+     */
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    /* Verify canHandleBlob is reading from the same list (basically) */
+    for (const auto& blob : startingBlobs)
+    {
+        EXPECT_TRUE(handler->canHandleBlob(blob));
+    }
+}
+
+/* stat(blob_id) */
+TEST_F(FirmwareHandlerNotYetStartedTest, StatEachBlobIdVerifyResults)
+{
+    /* In this original state, calling stat() on the blob ids will return the
+     * idle status
+     */
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/* open(each blob id) (verifyblobid will no longer be available at this state.
+ */
+TEST_F(FirmwareHandlerNotYetStartedTest, OpenStaticImageFileVerifyStateChange)
+{
+    EXPECT_CALL(*imageMock2, open(staticLayoutBlobId)).WillOnce(Return(true));
+    EXPECT_CALL(*prepareMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(session, flags, staticLayoutBlobId));
+
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_TRUE(handler->canHandleBlob(activeImageBlobId));
+}
+
+TEST_F(FirmwareHandlerNotYetStartedTest, OpenHashFileVerifyStateChange)
+{
+    EXPECT_CALL(*hashImageMock, open(hashBlobId)).WillOnce(Return(true));
+    /* Opening the hash blob id doesn't trigger a preparation, only a firmware
+     * blob.
+     */
+    EXPECT_CALL(*prepareMockPtr, trigger()).Times(0);
+
+    EXPECT_TRUE(handler->open(session, flags, hashBlobId));
+
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    EXPECT_TRUE(handler->canHandleBlob(activeHashBlobId));
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerNotYetStartedTest, ExpireOnNotYetStartedAbortsProcess)
+{
+    ASSERT_TRUE(handler->expire(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_updatecompleted_unittest.cpp b/bmc/firmware-handler/test/firmware_state_updatecompleted_unittest.cpp
new file mode 100644
index 0000000..d002499
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_updatecompleted_unittest.cpp
@@ -0,0 +1,263 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is UpdateCompleted.  This state is achieved as an exit from
+ * updateStarted.
+ *
+ * This can be reached with success or failure from an update, and is reached
+ * via a stat() call from updatedStarted.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ */
+
+class FirmwareHandlerUpdateCompletedTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * open(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       AttemptToOpenFilesReturnsFailureAfterSuccess)
+{
+    /* In state updateCompleted a file is open, which means no others can be. */
+    getToUpdateCompleted(ActionStatus::success);
+
+    auto blobsToOpen = handler->getBlobIds();
+    for (const auto& blob : blobsToOpen)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/*
+ * stat(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CallingStatSessionAfterCompletedSuccessReturnsStateWithoutRechecking)
+{
+    getToUpdateCompleted(ActionStatus::success);
+    EXPECT_CALL(*updateMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CallingStatSessionAfterCompletedFailureReturnsStateWithoutRechecking)
+{
+    getToUpdateCompleted(ActionStatus::failed);
+    EXPECT_CALL(*updateMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, StatOnActiveImageReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest, StatOnUpdateBlobReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(updateBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, WriteMetaToUpdateBlobReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_FALSE(handler->writeMeta(session, 0, {0x01}));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, WriteToUpdateBlobReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_FALSE(handler->write(session, 0, {0x01}));
+}
+
+/*
+ * commit(session) - returns failure
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CommitOnUpdateBlobAfterSuccessReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).Times(0);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       CommitOnUpdateBlobAfterFailureReturnsFailure)
+{
+    getToUpdateCompleted(ActionStatus::failed);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).Times(0);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+/*
+ * read(session) - nothing to read here.
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, ReadingFromUpdateBlobReturnsNothing)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * getBlobIds
+ * canHandleBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, GetBlobListProvidesExpectedBlobs)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    EXPECT_THAT(
+        handler->getBlobIds(),
+        UnorderedElementsAreArray(
+            {updateBlobId, hashBlobId, activeImageBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * close(session) - closes everything out and returns to state not-yet-started.
+ * It doesn't go and clean out any update artifacts that may be present on the
+ * system.  It's up to the update implementation to deal with this before
+ * marking complete.
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       ClosingOnUpdateBlobIdAfterSuccessReturnsToNotYetStartedAndCleansBlobList)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+TEST_F(FirmwareHandlerUpdateCompletedTest,
+       ClosingOnUpdateBlobIdAfterFailureReturnsToNotYetStartedAndCleansBlobList)
+{
+    getToUpdateCompleted(ActionStatus::failed);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToUpdateCompleted(ActionStatus::success);
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerUpdateCompletedTest, ExpireOnUpdateCompletedAbortsProcess)
+{
+    getToUpdateCompleted(ActionStatus::success);
+
+    ASSERT_TRUE(handler->expire(session));
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_updatepending_unittest.cpp b/bmc/firmware-handler/test/firmware_state_updatepending_unittest.cpp
new file mode 100644
index 0000000..5f43259
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_updatepending_unittest.cpp
@@ -0,0 +1,309 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is updatePending.  This state is achieved as an exit from
+ * verificationCompleted.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+
+class FirmwareHandlerUpdatePendingTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, GetBlobsListHasExpectedValues)
+{
+    getToUpdatePending();
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray({updateBlobId, activeImageBlobId,
+                                           hashBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * open(blob) - because updatePending is in a fileOpen==false state, one can
+ * then open blobs. However, because we're in a special state, we will restrict
+ * them s.t. they can only open the updateBlobId.
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       OpenUpdateBlobIdIsSuccessfulAndDoesNotChangeState)
+{
+    getToUpdatePending();
+
+    /* Opening the update blob isn't interesting, except it's required for
+     * commit() which triggers the update process.
+     */
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, OpenAnyBlobOtherThanUpdateFails)
+{
+    getToUpdatePending();
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        if (blob == updateBlobId)
+        {
+            continue;
+        }
+        EXPECT_FALSE(handler->open(session, flags, blob));
+    }
+}
+
+/*
+ * close(session) - close from this state is uninteresting.
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, CloseUpdateBlobDoesNotChangeState)
+{
+    /* Verify nothing changes when one just opens, then closes the updateBlobId.
+     */
+    getToUpdatePending();
+
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+
+    handler->close(session);
+
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+    EXPECT_TRUE(handler->canHandleBlob(updateBlobId));
+}
+
+/*
+ * writemeta(session) - this will return failure.
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, WriteMetaToUpdateBlobReturnsFailure)
+{
+    getToUpdatePending();
+
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    EXPECT_FALSE(handler->writeMeta(session, 0, {0x01}));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, WriteToUpdateBlobReturnsFailure)
+{
+    getToUpdatePending();
+
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    EXPECT_FALSE(handler->write(session, 0, {0x01}));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, ReadFromUpdateBlobIdReturnsEmpty)
+{
+    getToUpdatePending();
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, StatOnActiveImageReturnsFailure)
+{
+    getToUpdatePending();
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, StatOnUpdateBlobReturnsFailure)
+{
+    getToUpdatePending();
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(updateBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToUpdatePending();
+
+    for (const auto& blob : startingBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * stat(session)
+ * In this case, you can open updateBlobId without changing state, therefore,
+ * let's call stat() against a session against this file. This done, ahead of
+ * commit() should report the state as "other."
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       SessionStatOnUpdateBlobIdReturnsFailure)
+{
+    getToUpdatePending();
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::unknown));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       CommitOnUpdateBlobTriggersUpdateAndChangesState)
+{
+    /* Commit triggers the update mechanism (similarly for the verifyBlobId) and
+     * changes state to updateStarted.
+     */
+    getToUpdatePending();
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest,
+       CommitOnUpdateBlobTriggersUpdateAndReturnsFailureDoesNotChangeState)
+{
+    getToUpdatePending();
+    EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+
+    EXPECT_CALL(*updateMockPtr, trigger()).WillOnce(Return(false));
+
+    EXPECT_FALSE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteUpdateAbortsProcess)
+{
+    /* It doesn't matter what blob id is used to delete in the design, so just
+     * delete the update blob id
+     */
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+    EXPECT_TRUE(handler->deleteBlob(updateBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteActiveImageAbortsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+    EXPECT_TRUE(handler->deleteBlob(activeImageBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteStaticLayoutAbortsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(staticLayoutBlobId));
+    EXPECT_TRUE(handler->deleteBlob(staticLayoutBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerUpdatePendingTest, DeleteHashAbortsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(hashBlobId));
+    EXPECT_TRUE(handler->deleteBlob(hashBlobId));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerUpdatePendingTest, ExpireOnUpdatePendingAborstsProcess)
+{
+    getToUpdatePending();
+
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->expire(session));
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_updatestarted_unittest.cpp b/bmc/firmware-handler/test/firmware_state_updatestarted_unittest.cpp
new file mode 100644
index 0000000..f187a8e
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_updatestarted_unittest.cpp
@@ -0,0 +1,257 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is updateStarted.  This state is achieved as an exit from
+ * updatePending.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ */
+
+class FirmwareHandlerUpdateStartedTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * open(blob)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, AttemptToOpenFilesReturnsFailure)
+{
+    /* In state updateStarted a file is open, which means no others can be. */
+    getToUpdateStarted();
+
+    auto blobsToOpen = handler->getBlobIds();
+    for (const auto& blob : blobsToOpen)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/* canHandleBlob(blob)
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, VerifyListOfBlobs)
+{
+    getToUpdateStarted();
+
+    EXPECT_THAT(
+        handler->getBlobIds(),
+        UnorderedElementsAreArray(
+            {updateBlobId, hashBlobId, activeImageBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToUpdateStarted();
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, StatOnActiveImageReturnsFailure)
+{
+    getToUpdateStarted();
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest, StatOnUpdateBlobReturnsFailure)
+{
+    getToUpdateStarted();
+
+    ASSERT_TRUE(handler->canHandleBlob(updateBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(updateBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToUpdateStarted();
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, WriteMetaToUpdateBlobReturnsFailure)
+{
+    getToUpdateStarted();
+    EXPECT_FALSE(handler->writeMeta(session, 0, {0x01}));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, WriteToUpdateBlobReturnsFailure)
+{
+    getToUpdateStarted();
+    EXPECT_FALSE(handler->write(session, 0, {0x01}));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, ReadFromUpdateBlobReturnsEmpty)
+{
+    getToUpdateStarted();
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallingCommitShouldReturnTrueAndHaveNoEffect)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, trigger()).Times(0);
+
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+}
+
+/*
+ * stat(session) - this will trigger a check, and the state may change.
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallStatChecksActionStatusReturnsRunningDoesNotChangeState)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, status())
+        .WillOnce(Return(ActionStatus::running));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committing;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::running));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallStatChecksActionStatusReturnsFailedChangesStateToCompleted)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, status())
+        .WillOnce(Return(ActionStatus::failed));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       CallStatChecksActionStatusReturnsSuccessChangesStateToCompleted)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, status())
+        .WillOnce(Return(ActionStatus::success));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+}
+
+/*
+ * close(session) - this will abort.
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest, CloseOnUpdateDuringUpdateAbortsProcess)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, abort()).Times(1);
+
+    EXPECT_TRUE(handler->close(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerUpdateStartedTest,
+       ExpireOnUpdateDuringUpdateAbortsProcess)
+{
+    getToUpdateStarted();
+    EXPECT_CALL(*updateMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->expire(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_uploadinprogress_unittest.cpp b/bmc/firmware-handler/test/firmware_state_uploadinprogress_unittest.cpp
new file mode 100644
index 0000000..c1a27fe
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_uploadinprogress_unittest.cpp
@@ -0,0 +1,295 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is uploadInProgress.  This state is achieved when an image
+ * or hash blob is opened and the handler is expected to receive bytes.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::ContainerEq;
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+class FirmwareHandlerUploadInProgressTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+TEST_F(FirmwareHandlerUploadInProgressTest, GetBlobIdsVerifyOutputActiveImage)
+{
+    /* Opening the image file will add the active image blob id */
+    openToInProgress(staticLayoutBlobId);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(
+                    {staticLayoutBlobId, hashBlobId, activeImageBlobId}));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, GetBlobIdsVerifyOutputActiveHash)
+{
+    /* Opening the image file will add the active image blob id */
+    openToInProgress(hashBlobId);
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(
+                    {staticLayoutBlobId, hashBlobId, activeHashBlobId}));
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, StatOnActiveImageReturnsFailure)
+{
+    /* you cannot call stat() on the active image or the active hash or the
+     * verify blob.
+     */
+    openToInProgress(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, StatOnActiveHashReturnsFailure)
+{
+    /* this test is separate from the active image one so that the state doesn't
+     * change from close.
+     */
+    openToInProgress(hashBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeHashBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeHashBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, StatOnNormalBlobsReturnsSuccess)
+{
+    /* Calling stat() on the normal blobs (not the active) ones will work and
+     * return the same information as in the notYetStarted state.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * stat(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       CallingStatOnActiveImageOrHashSessionReturnsDetails)
+{
+    /* This test will verify that the underlying image handler is called with
+     * this stat, in addition to the normal information.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    EXPECT_CALL(*imageMock2, getSize()).WillOnce(Return(32));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 32;
+    expectedMeta.blobState =
+        blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi;
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+/*
+ * open(blob) - While any blob is open, all other fail.
+ *
+ * The fullBlobsList is all the blob_ids present if both /flash/image and
+ * /flash/hash are opened, and one is left open (so there's no verify blob). if
+ * left closed, we'd be in verificationPending, not uploadInProgress.
+ */
+const std::vector<std::string> fullBlobsList = {
+    activeHashBlobId, activeImageBlobId, hashBlobId, staticLayoutBlobId};
+
+TEST_F(FirmwareHandlerUploadInProgressTest, OpeningHashFileWhileImageOpenFails)
+{
+    /* To be in this state, something must be open (and specifically either an
+     * active image (or tarball) or the hash file. Also verifies you can't just
+     * re-open the currently open file.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    for (const auto& blob : fullBlobsList)
+    {
+        EXPECT_FALSE(handler->open(2, flags, blob));
+    }
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, OpeningImageFileWhileHashOpenFails)
+{
+    openToInProgress(hashBlobId);
+
+    for (const auto& blob : fullBlobsList)
+    {
+        EXPECT_FALSE(handler->open(2, flags, blob));
+    }
+}
+
+/*
+ * close(session) - closing the hash or image will trigger a state transition to
+ * verificationPending.
+ *
+ * NOTE: Re-opening /flash/image will transition back to uploadInProgress, but
+ * that is verified in the verificationPending::open tests.
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       ClosingImageFileTransitionsToVerificationPending)
+{
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+    openToInProgress(staticLayoutBlobId);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    EXPECT_TRUE(handler->canHandleBlob(verifyBlobId));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       ClosingHashFileTransitionsToVerificationPending)
+{
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+    openToInProgress(hashBlobId);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       WriteMetaAgainstImageReturnsFailureIfNoDataHandler)
+{
+    /* Calling write/read/writeMeta are uninteresting against the open blob in
+     * this case because the blob will just pass the call along.  Whereas
+     * calling against the verify or update blob may be more interesting.
+     */
+    openToInProgress(staticLayoutBlobId);
+
+    /* TODO: Consider adding a test that has a data handler, but that test
+     * already exists under the general writeMeta test suite.
+     */
+    /* Note: with IPMI as the transport there's no data handler, so this should
+     * fail nicely. */
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, WriteToImageReturnsSuccess)
+{
+    openToInProgress(staticLayoutBlobId);
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_CALL(*imageMock2, write(0, ContainerEq(bytes)))
+        .WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(session, 0, bytes));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, WriteToHashReturnsSuccess)
+{
+    openToInProgress(hashBlobId);
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_CALL(*hashImageMock, write(0, ContainerEq(bytes)))
+        .WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(session, 0, bytes));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, ReadImageFileReturnsFailure)
+{
+    /* Read is not supported. */
+    openToInProgress(staticLayoutBlobId);
+    EXPECT_THAT(handler->read(session, 0, 32), IsEmpty());
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest,
+       CommitAgainstImageFileReturnsFailure)
+{
+    /* Commit is only valid against specific blobs. */
+    openToInProgress(staticLayoutBlobId);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+TEST_F(FirmwareHandlerUploadInProgressTest, CommitAgainstHashFileReturnsFailure)
+{
+    openToInProgress(hashBlobId);
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    openToInProgress(staticLayoutBlobId);
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerUploadInProgressTest, ExpireAbortsProcess)
+{
+    openToInProgress(staticLayoutBlobId);
+
+    ASSERT_TRUE(handler->expire(session));
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_verificationcompleted_unittest.cpp b/bmc/firmware-handler/test/firmware_state_verificationcompleted_unittest.cpp
new file mode 100644
index 0000000..bdac123
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_verificationcompleted_unittest.cpp
@@ -0,0 +1,324 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is verificationCompleted.  This state is achieved as a out
+ * of verificationStarted.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Like the state verificationStarted, there is a file open in
+ * verificationCompleted.  This state is transitioned to after a stat() command
+ * indicates a successful verification.
+ */
+
+class FirmwareHandlerVerificationCompletedTest :
+    public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * canHandleBlob
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OnVerificationCompleteSuccessUpdateBlobIdNotPresent)
+{
+    /* the uploadBlobId is only added on close() of the verifyBlobId.  This is a
+     * consistent behavior with verifyBlobId only added when closing the image
+     * or hash.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_FALSE(handler->canHandleBlob(updateBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OnVerificationCompleteFailureUpdateBlobIdNotPresent)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    EXPECT_FALSE(handler->canHandleBlob(updateBlobId));
+}
+
+/*
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, GetBlobIdsReturnsExpectedList)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_THAT(
+        handler->getBlobIds(),
+        UnorderedElementsAreArray(
+            {verifyBlobId, hashBlobId, activeImageBlobId, staticLayoutBlobId}));
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       StatOnActiveImageReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       VerifyActiveHashIdMissingInThisCase)
+{
+    /* The path taken to get to this state never opened the hash blob Id, which
+     * is fine.  But let's verify it behaved as intended.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_FALSE(handler->canHandleBlob(activeHashBlobId));
+}
+
+/* TODO: Add sufficient warning that you can get to verificationCompleted
+ * without ever opening the image blob id (or the tarball one).
+ *
+ * Although in this case, it's expected that any verification triggered would
+ * certainly fail.  So, although it's possible, it's uninteresting.
+ */
+
+TEST_F(FirmwareHandlerVerificationCompletedTest, StatOnVerifyBlobReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(verifyBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       StatOnNormalBlobsReturnsSuccess)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * stat(session) - the verify blobid is open in this state, so stat on that once
+ * completed should have no effect.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       SessionStatOnVerifyAfterSuccessDoesNothing)
+{
+    /* Every time you stat() once it's triggered, it checks the state again
+     * until it's completed.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_CALL(*verifyMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       SessionStatOnVerifyAfterFailureDoesNothing)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    EXPECT_CALL(*verifyMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+/*
+ * open(blob) - all open should fail
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OpeningAnyBlobAvailableFailsAfterSuccess)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       OpeningAnyBlobAvailableFailsAfterFailure)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+
+    auto blobs = handler->getBlobIds();
+    for (const auto& blob : blobs)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/*
+ * writemeta(session) - write meta should fail.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       WriteMetaToVerifyBlobReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session) - write should fail.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       WriteToVerifyBlobReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::success);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->write(session, 0, bytes));
+}
+
+/*
+ * read(session) - read returns empty.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, ReadOfVerifyBlobReturnsEmpty)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * commit(session) - returns failure
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       CommitOnVerifyBlobAfterSuccessReturnsFailure)
+{
+    /* If you've started this'll return success, but if it's finished, it won't
+     * let you try-again.
+     */
+    getToVerificationCompleted(ActionStatus::success);
+    EXPECT_CALL(*verifyMockPtr, trigger()).Times(0);
+
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       CommitOnVerifyBlobAfterFailureReturnsFailure)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    EXPECT_CALL(*verifyMockPtr, trigger()).Times(0);
+
+    EXPECT_FALSE(handler->commit(session, {}));
+}
+
+/*
+ * close(session) - close on the verify blobid:
+ *   1. if successful adds update blob id, changes state to UpdatePending
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       CloseAfterSuccessChangesStateAddsUpdateBlob)
+{
+    getToVerificationCompleted(ActionStatus::success);
+    ASSERT_FALSE(handler->canHandleBlob(updateBlobId));
+
+    handler->close(session);
+    EXPECT_TRUE(handler->canHandleBlob(updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+}
+
+/*
+ * close(session) - close on the verify blobid:
+ *   2. if unsuccessful it aborts.
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest, CloseAfterFailureAborts)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+    ASSERT_FALSE(handler->canHandleBlob(updateBlobId));
+
+    handler->close(session);
+    ASSERT_FALSE(handler->canHandleBlob(updateBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerVerificationCompletedTest,
+       ExpireAfterVerificationCompletedAborts)
+{
+    getToVerificationCompleted(ActionStatus::failed);
+
+    ASSERT_TRUE(handler->expire(session));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_verificationpending_unittest.cpp b/bmc/firmware-handler/test/firmware_state_verificationpending_unittest.cpp
new file mode 100644
index 0000000..dc6f3d3
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_verificationpending_unittest.cpp
@@ -0,0 +1,353 @@
+/**
+ * The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is verificationPending.  This state is achieved as a
+ * transition out of uploadInProgress.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <algorithm>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+
+class FirmwareHandlerVerificationPendingTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * getBlobIds
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, VerifyBlobIdAvailableInState)
+{
+    /* Only in the verificationPending state (and later), should the
+     * verifyBlobId be present.
+     */
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->canHandleBlob(verifyBlobId));
+    EXPECT_TRUE(handler->canHandleBlob(activeImageBlobId));
+    EXPECT_FALSE(handler->canHandleBlob(updateBlobId));
+}
+
+/*
+ * delete(blob)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteVerifyPendingAbortsProcess)
+{
+    /* It doesn't matter what blob id is used to delete in the design, so just
+     * delete the verify blob id
+     */
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+    EXPECT_TRUE(handler->deleteBlob(verifyBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteActiveImageAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+    EXPECT_TRUE(handler->deleteBlob(activeImageBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteStaticLayoutAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(staticLayoutBlobId));
+    EXPECT_TRUE(handler->deleteBlob(staticLayoutBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, DeleteHashAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    ASSERT_TRUE(handler->canHandleBlob(hashBlobId));
+    EXPECT_TRUE(handler->deleteBlob(hashBlobId));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       ExpireVerificationPendingAbortsProcess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    EXPECT_TRUE(handler->expire(session));
+
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId};
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnActiveImageReturnsFailure)
+{
+    getToVerificationPending(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnActiveHashReturnsFailure)
+{
+    getToVerificationPending(hashBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeHashBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeHashBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       StatOnVerificationBlobReturnsFailure)
+{
+    getToVerificationPending(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(verifyBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       VerificationBlobNotFoundWithoutStaticDataAsWell)
+{
+    /* If you only ever open the hash blob id, and never the firmware blob id,
+     * the verify blob isn't added.
+     */
+    getToVerificationPending(hashBlobId);
+    EXPECT_FALSE(handler->canHandleBlob(verifyBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * open(blob)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, OpenVerifyBlobSucceeds)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    /* the session is safe because it was already closed to get to this state.
+     */
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest, OpenActiveBlobsFail)
+{
+    /* Try opening the active blob Id.  This test is equivalent to trying to
+     * open the active hash blob id, in that neither are ever allowed.
+     */
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_FALSE(handler->open(session, flags, activeImageBlobId));
+    EXPECT_FALSE(handler->open(session, flags, activeHashBlobId));
+}
+
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       OpenImageBlobTransitionsToUploadInProgress)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    /* Verify the active blob for the image is in the list once to start.
+     * Note: This is truly tested under the notYetStarted::open() test.
+     */
+    std::vector<std::string> expectedBlobs = {staticLayoutBlobId, hashBlobId,
+                                              verifyBlobId, activeImageBlobId};
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+
+    /* Verifies it isn't triggered again. */
+    EXPECT_CALL(*prepareMockPtr, trigger()).Times(0);
+
+    EXPECT_CALL(*imageMock2, open(staticLayoutBlobId)).WillOnce(Return(true));
+    EXPECT_TRUE(handler->open(session, flags, staticLayoutBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+
+    expectedBlobs.erase(
+        std::remove(expectedBlobs.begin(), expectedBlobs.end(), verifyBlobId),
+        expectedBlobs.end());
+
+    /* Verify the active blob ID was not added to the list twice and
+     * verifyBlobId is removed
+     */
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(expectedBlobs));
+}
+
+/*
+ * close(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       ClosingVerifyBlobWithoutCommitDoesNotChangeState)
+{
+    /* commit() will change the state, closing post-commit is part of
+     * verificationStarted testing.
+     */
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+    handler->close(session);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       CommitOnVerifyBlobTriggersVerificationAndStateTransition)
+{
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    EXPECT_CALL(*verifyMockPtr, trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+}
+
+/*
+ * stat(session) - in this state, you can only open(verifyBlobId) without
+ * changing state.
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, StatOnVerifyBlobIdReturnsState)
+{
+    /* If this is called before commit(), it's still verificationPending, so it
+     * just returns the state is other
+     */
+    getToVerificationPending(staticLayoutBlobId);
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    EXPECT_CALL(*verifyMockPtr, trigger()).Times(0);
+    EXPECT_CALL(*verifyMockPtr, status()).Times(0);
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::unknown));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, WriteMetaAgainstVerifyFails)
+{
+    /* The verifyBlobId has no data handler, which means write meta fails. */
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest, WriteAgainstVerifyBlobIdFails)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->write(session, 0, bytes));
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerVerificationPendingTest,
+       ReadAgainstVerifyBlobIdReturnsEmpty)
+{
+    getToVerificationPending(staticLayoutBlobId);
+
+    EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_state_verificationstarted_unittest.cpp b/bmc/firmware-handler/test/firmware_state_verificationstarted_unittest.cpp
new file mode 100644
index 0000000..4f69c89
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_state_verificationstarted_unittest.cpp
@@ -0,0 +1,305 @@
+/* The goal of these tests is to verify the behavior of all blob commands given
+ * the current state is verificationStarted.  This state is achieved as a out of
+ * verificationPending.
+ */
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::IsEmpty;
+using ::testing::Return;
+using ::testing::UnorderedElementsAreArray;
+
+/*
+ * There are the following calls (parameters may vary):
+ * canHandleBlob(blob)
+ * getBlobIds
+ * deleteBlob(blob)
+ * stat(blob)
+ * stat(session)
+ * open(blob)
+ * close(session)
+ * writemeta(session)
+ * write(session)
+ * read(session)
+ * commit(session)
+ *
+ * Testing canHandleBlob is uninteresting in this state.  Getting the BlobIDs
+ * will inform what canHandleBlob will return.
+ */
+
+class FirmwareHandlerVerificationStartedTest : public IpmiOnlyFirmwareStaticTest
+{};
+
+/*
+ * canHandleBlob(blob)
+ * getBlobIds()
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, GetBlobIdsReturnsExpectedList)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    auto blobs = handler->getBlobIds();
+    EXPECT_THAT(
+        blobs, UnorderedElementsAreArray({activeImageBlobId, staticLayoutBlobId,
+                                          hashBlobId, verifyBlobId}));
+
+    for (const auto& blob : blobs)
+    {
+        EXPECT_TRUE(handler->canHandleBlob(blob));
+    }
+}
+
+/*
+ * stat(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitChecksStateAndReturnsRunning)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::running));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committing;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::running));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitChecksStateAndReturnsOther)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::unknown));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committing;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::unknown));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitCheckStateAndReturnsFailed)
+{
+    /* If the returned state from the verification handler is failed, it sets
+     * commit_error and transitions to verificationCompleted.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::failed));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::commit_error;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::failed));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       StatOnVerifyBlobIdAfterCommitCheckStateAndReturnsSuccess)
+{
+    /* If the returned state from the verification handler is success, it sets
+     * committed and transitions to verificationCompleted.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, status())
+        .WillOnce(Return(ActionStatus::success));
+
+    blobs::BlobMeta meta, expectedMeta = {};
+    expectedMeta.size = 0;
+    expectedMeta.blobState = flags | blobs::StateFlags::committed;
+    expectedMeta.metadata.push_back(
+        static_cast<std::uint8_t>(ActionStatus::success));
+
+    EXPECT_TRUE(handler->stat(session, &meta));
+    EXPECT_EQ(expectedMeta, meta);
+    expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+}
+
+/*
+ * deleteBlob(blob)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, DeleteBlobReturnsFalse)
+{
+    /* Try deleting all blobs, it doesn't really matter which though because you
+     * cannot close out an open session, therefore you must fail to delete
+     * anything unless everything is closed.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+    auto blobs = handler->getBlobIds();
+    for (const auto& b : blobs)
+    {
+        EXPECT_FALSE(handler->deleteBlob(b));
+    }
+}
+
+/*
+ * stat(blob)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnActiveImageReturnsFailure)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(activeImageBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeImageBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnActiveHashReturnsFailure)
+{
+    getToVerificationStartedWitHashBlob();
+    ASSERT_TRUE(handler->canHandleBlob(activeHashBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(activeHashBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnVerifyBlobReturnsFailure)
+{
+    /* the verifyBlobId is available starting at verificationPending. */
+    getToVerificationStarted(staticLayoutBlobId);
+    ASSERT_TRUE(handler->canHandleBlob(verifyBlobId));
+
+    blobs::BlobMeta meta;
+    EXPECT_FALSE(handler->stat(verifyBlobId, &meta));
+}
+
+TEST_F(FirmwareHandlerVerificationStartedTest, StatOnNormalBlobsReturnsSuccess)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    std::vector<std::string> testBlobs = {staticLayoutBlobId, hashBlobId};
+    for (const auto& blob : testBlobs)
+    {
+        ASSERT_TRUE(handler->canHandleBlob(blob));
+
+        blobs::BlobMeta meta = {};
+        EXPECT_TRUE(handler->stat(blob, &meta));
+        EXPECT_EQ(expectedIdleMeta, meta);
+    }
+}
+
+/*
+ * writemeta(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       WriteMetaOnVerifySessionReturnsFailure)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->writeMeta(session, 0, bytes));
+}
+
+/*
+ * write(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       WriteOnVerifySessionReturnsFailure)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02};
+    EXPECT_FALSE(handler->write(session, 0, bytes));
+}
+
+/*
+ * open(blob) - there is nothing you can open, this state has an open file.
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       AttemptToOpenImageFileReturnsFailure)
+{
+    /* Attempt to open a file one normally can open, however, as there is
+     * already a file open, this will fail.
+     */
+    getToVerificationStarted(staticLayoutBlobId);
+
+    auto blobsToOpen = handler->getBlobIds();
+    for (const auto& blob : blobsToOpen)
+    {
+        EXPECT_FALSE(handler->open(session + 1, flags, blob));
+    }
+}
+
+/*
+ * read(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest, ReadOfVerifyBlobReturnsEmpty)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_THAT(handler->read(session, 0, 1), IsEmpty());
+}
+
+/*
+ * commit(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       CommitOnVerifyDuringVerificationHasNoImpact)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_TRUE(handler->commit(session, {}));
+    expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+}
+
+/*
+ * close(session) - close while state if verificationStarted without calling
+ * stat first will abort.
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       CloseOnVerifyDuringVerificationAbortsProcess)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(1);
+
+    EXPECT_TRUE(handler->close(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+/*
+ * expire(session)
+ */
+TEST_F(FirmwareHandlerVerificationStartedTest,
+       ExpireOnSessionDuringVerificationAbortsProcess)
+{
+    getToVerificationStarted(staticLayoutBlobId);
+    EXPECT_CALL(*verifyMockPtr, abort()).Times(0);
+
+    EXPECT_TRUE(handler->expire(session));
+
+    EXPECT_THAT(handler->getBlobIds(),
+                UnorderedElementsAreArray(startingBlobs));
+
+    expectedState(FirmwareBlobHandler::UpdateState::notYetStarted);
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_unittest.hpp b/bmc/firmware-handler/test/firmware_unittest.hpp
new file mode 100644
index 0000000..e9db343
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_unittest.hpp
@@ -0,0 +1,254 @@
+#pragma once
+
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "flags.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Return;
+
+class IpmiOnlyFirmwareStaticTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        /* Unfortunately, since the FirmwareHandler object ends up owning the
+         * handlers, we can't just share handlers.
+         */
+        std::unique_ptr<ImageHandlerInterface> image =
+            std::make_unique<ImageHandlerMock>();
+        hashImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(hashBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        imageMock2 = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(staticLayoutBlobId, std::move(image));
+
+        std::unique_ptr<TriggerableActionInterface> prepareMock =
+            std::make_unique<TriggerMock>();
+        prepareMockPtr = reinterpret_cast<TriggerMock*>(prepareMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> verifyMock =
+            std::make_unique<TriggerMock>();
+        verifyMockPtr = reinterpret_cast<TriggerMock*>(verifyMock.get());
+
+        std::unique_ptr<TriggerableActionInterface> updateMock =
+            std::make_unique<TriggerMock>();
+        updateMockPtr = reinterpret_cast<TriggerMock*>(updateMock.get());
+
+        std::unique_ptr<ActionPack> actionPack = std::make_unique<ActionPack>();
+        actionPack->preparation = std::move(prepareMock);
+        actionPack->verification = std::move(verifyMock);
+        actionPack->update = std::move(updateMock);
+
+        ActionMap packs;
+        packs[staticLayoutBlobId] = std::move(actionPack);
+
+        std::vector<DataHandlerPack> data;
+        data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            std::move(blobs), std::move(data), std::move(packs));
+    }
+
+    void expectedState(FirmwareBlobHandler::UpdateState state)
+    {
+        auto realHandler = dynamic_cast<FirmwareBlobHandler*>(handler.get());
+        EXPECT_EQ(state, realHandler->getCurrentState());
+    }
+
+    void openToInProgress(const std::string& blobId)
+    {
+        if (blobId == hashBlobId)
+        {
+            EXPECT_CALL(*hashImageMock, open(blobId)).WillOnce(Return(true));
+        }
+        else
+        {
+            EXPECT_CALL(*imageMock2, open(blobId)).WillOnce(Return(true));
+        }
+
+        if (blobId != hashBlobId)
+        {
+            EXPECT_CALL(*prepareMockPtr, trigger()).WillOnce(Return(true));
+        }
+        EXPECT_TRUE(handler->open(session, flags, blobId));
+        expectedState(FirmwareBlobHandler::UpdateState::uploadInProgress);
+    }
+
+    void getToVerificationPending(const std::string& blobId)
+    {
+        openToInProgress(blobId);
+
+        if (blobId == hashBlobId)
+        {
+            EXPECT_CALL(*hashImageMock, close()).WillRepeatedly(Return());
+        }
+        else
+        {
+            EXPECT_CALL(*imageMock2, close()).WillRepeatedly(Return());
+        }
+        handler->close(session);
+        expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+    }
+
+    void getToVerificationStarted(const std::string& blobId)
+    {
+        getToVerificationPending(blobId);
+
+        EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+        EXPECT_CALL(*verifyMockPtr, trigger()).WillOnce(Return(true));
+
+        EXPECT_TRUE(handler->commit(session, {}));
+        expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+    }
+
+    void getToVerificationStartedWitHashBlob()
+    {
+        /* Open both static and hash to check for activeHashBlobId. */
+        getToVerificationPending(staticLayoutBlobId);
+
+        openToInProgress(hashBlobId);
+        EXPECT_CALL(*hashImageMock, close()).WillRepeatedly(Return());
+        handler->close(session);
+        expectedState(FirmwareBlobHandler::UpdateState::verificationPending);
+
+        /* Now the hash is active AND the static image is active. */
+        EXPECT_TRUE(handler->open(session, flags, verifyBlobId));
+        EXPECT_CALL(*verifyMockPtr, trigger()).WillOnce(Return(true));
+
+        EXPECT_TRUE(handler->commit(session, {}));
+        expectedState(FirmwareBlobHandler::UpdateState::verificationStarted);
+    }
+
+    void getToVerificationCompleted(ActionStatus checkResponse)
+    {
+        getToVerificationStarted(staticLayoutBlobId);
+
+        EXPECT_CALL(*verifyMockPtr, status()).WillOnce(Return(checkResponse));
+        blobs::BlobMeta meta;
+        EXPECT_TRUE(handler->stat(session, &meta));
+        expectedState(FirmwareBlobHandler::UpdateState::verificationCompleted);
+    }
+
+    void getToUpdatePending()
+    {
+        getToVerificationCompleted(ActionStatus::success);
+
+        handler->close(session);
+        expectedState(FirmwareBlobHandler::UpdateState::updatePending);
+    }
+
+    void getToUpdateStarted()
+    {
+        getToUpdatePending();
+        EXPECT_TRUE(handler->open(session, flags, updateBlobId));
+
+        EXPECT_CALL(*updateMockPtr, trigger()).WillOnce(Return(true));
+        EXPECT_TRUE(handler->commit(session, {}));
+        expectedState(FirmwareBlobHandler::UpdateState::updateStarted);
+    }
+
+    void getToUpdateCompleted(ActionStatus result)
+    {
+        getToUpdateStarted();
+        EXPECT_CALL(*updateMockPtr, status()).WillOnce(Return(result));
+
+        blobs::BlobMeta meta;
+        EXPECT_TRUE(handler->stat(session, &meta));
+        expectedState(FirmwareBlobHandler::UpdateState::updateCompleted);
+    }
+
+    ImageHandlerMock *hashImageMock, *imageMock2;
+
+    std::vector<HandlerPack> blobs;
+
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+
+    TriggerMock* prepareMockPtr;
+    TriggerMock* verifyMockPtr;
+    TriggerMock* updateMockPtr;
+
+    std::uint16_t session = 1;
+    std::uint16_t flags =
+        blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi;
+
+    blobs::BlobMeta expectedIdleMeta = {0xff00, 0, {}};
+
+    std::vector<std::string> startingBlobs = {staticLayoutBlobId, hashBlobId};
+};
+
+class IpmiOnlyFirmwareTest : public ::testing::Test
+{
+  protected:
+    ImageHandlerMock *hashImageMock, *imageMock;
+    std::vector<HandlerPack> blobs;
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+
+    void SetUp() override
+    {
+        std::unique_ptr<ImageHandlerInterface> image =
+            std::make_unique<ImageHandlerMock>();
+        hashImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(hashBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        imageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back("asdf", std::move(image));
+
+        std::vector<DataHandlerPack> data;
+        data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            std::move(blobs), std::move(data), CreateActionMap("asdf"));
+    }
+};
+
+class FakeLpcFirmwareTest : public ::testing::Test
+{
+  protected:
+    DataHandlerMock* dataMock;
+    ImageHandlerMock *hashImageMock, *imageMock;
+    std::vector<HandlerPack> blobs;
+    std::unique_ptr<blobs::GenericBlobInterface> handler;
+
+    void SetUp() override
+    {
+        std::unique_ptr<ImageHandlerInterface> image =
+            std::make_unique<ImageHandlerMock>();
+        hashImageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back(hashBlobId, std::move(image));
+
+        image = std::make_unique<ImageHandlerMock>();
+        imageMock = reinterpret_cast<ImageHandlerMock*>(image.get());
+        blobs.emplace_back("asdf", std::move(image));
+
+        auto dataMockInstance = std::make_unique<DataHandlerMock>();
+        dataMock = dataMockInstance.get();
+
+        std::vector<DataHandlerPack> data;
+        data.emplace_back(FirmwareFlags::UpdateFlags::ipmi, nullptr);
+        data.emplace_back(FirmwareFlags::UpdateFlags::lpc,
+                          std::move(dataMockInstance));
+        handler = FirmwareBlobHandler::CreateFirmwareBlobHandler(
+            std::move(blobs), std::move(data), CreateActionMap("asdf"));
+    }
+};
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_write_unittest.cpp b/bmc/firmware-handler/test/firmware_write_unittest.cpp
new file mode 100644
index 0000000..e9b70ba
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_write_unittest.cpp
@@ -0,0 +1,88 @@
+#include "data.hpp"
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Eq;
+using ::testing::Return;
+
+class FirmwareHandlerWriteTestIpmiOnly : public IpmiOnlyFirmwareTest
+{};
+
+class FirmwareHandlerWriteTestLpc : public FakeLpcFirmwareTest
+{};
+
+TEST_F(FirmwareHandlerWriteTestIpmiOnly, DataTypeIpmiWriteSuccess)
+{
+    /* Verify if data type ipmi, it calls write with the bytes. */
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi, "asdf"));
+
+    std::vector<std::uint8_t> bytes = {0xaa, 0x55};
+
+    EXPECT_CALL(*imageMock, write(0, Eq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(0, 0, bytes));
+}
+
+TEST_F(FirmwareHandlerWriteTestLpc, DataTypeNonIpmiWriteSuccess)
+{
+    /* Verify if data type non-ipmi, it calls write with the length. */
+    EXPECT_CALL(*dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::lpc, "asdf"));
+
+    struct ExtChunkHdr request;
+    request.length = 4; /* number of bytes to read. */
+    std::vector<std::uint8_t> ipmiRequest;
+    ipmiRequest.resize(sizeof(request));
+    std::memcpy(ipmiRequest.data(), &request, sizeof(request));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(*dataMock, copyFrom(request.length)).WillOnce(Return(bytes));
+    EXPECT_CALL(*imageMock, write(0, Eq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->write(0, 0, ipmiRequest));
+}
+
+TEST_F(FirmwareHandlerWriteTestLpc, DataTypeNonIpmiWriteFailsBadRequest)
+{
+    /* Verify the data type non-ipmi, if the request's structure doesn't match,
+     * return failure. */
+    EXPECT_CALL(*dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::lpc, "asdf"));
+
+    struct ExtChunkHdr request;
+    request.length = 4; /* number of bytes to read. */
+
+    std::vector<std::uint8_t> ipmiRequest;
+    ipmiRequest.resize(sizeof(request));
+    std::memcpy(ipmiRequest.data(), &request, sizeof(request));
+    ipmiRequest.push_back(1);
+
+    /* ipmiRequest is too large by one byte. */
+    EXPECT_FALSE(handler->write(0, 0, ipmiRequest));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/firmware_writemeta_unittest.cpp b/bmc/firmware-handler/test/firmware_writemeta_unittest.cpp
new file mode 100644
index 0000000..512754a
--- /dev/null
+++ b/bmc/firmware-handler/test/firmware_writemeta_unittest.cpp
@@ -0,0 +1,51 @@
+#include "data_mock.hpp"
+#include "firmware_handler.hpp"
+#include "firmware_unittest.hpp"
+#include "image_mock.hpp"
+#include "triggerable_mock.hpp"
+#include "util.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+namespace
+{
+
+using ::testing::Eq;
+using ::testing::Return;
+
+class FirmwareHandlerWriteMetaTest : public FakeLpcFirmwareTest
+{};
+
+TEST_F(FirmwareHandlerWriteMetaTest, WriteConfigParametersFailIfOverIPMI)
+{
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::ipmi, "asdf"));
+
+    std::vector<std::uint8_t> bytes = {0xaa, 0x55};
+
+    EXPECT_FALSE(handler->writeMeta(0, 0, bytes));
+}
+
+TEST_F(FirmwareHandlerWriteMetaTest, WriteConfigParametersPassedThrough)
+{
+    EXPECT_CALL(*dataMock, open()).WillOnce(Return(true));
+    EXPECT_CALL(*imageMock, open("asdf")).WillOnce(Return(true));
+
+    EXPECT_TRUE(handler->open(
+        0, blobs::OpenFlags::write | FirmwareFlags::UpdateFlags::lpc, "asdf"));
+
+    std::vector<std::uint8_t> bytes = {0x01, 0x02, 0x03, 0x04};
+
+    EXPECT_CALL(*dataMock, writeMeta(Eq(bytes))).WillOnce(Return(true));
+    EXPECT_TRUE(handler->writeMeta(0, 0, bytes));
+}
+
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/image_mock.hpp b/bmc/firmware-handler/test/image_mock.hpp
new file mode 100644
index 0000000..fb10c39
--- /dev/null
+++ b/bmc/firmware-handler/test/image_mock.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "image_handler.hpp"
+
+#include <gmock/gmock.h>
+
+namespace ipmi_flash
+{
+
+class ImageHandlerMock : public ImageHandlerInterface
+{
+  public:
+    virtual ~ImageHandlerMock() = default;
+
+    MOCK_METHOD1(open, bool(const std::string&));
+    MOCK_METHOD0(close, void());
+    MOCK_METHOD2(write, bool(std::uint32_t, const std::vector<std::uint8_t>&));
+    MOCK_METHOD0(getSize, int());
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/triggerable_mock.hpp b/bmc/firmware-handler/test/triggerable_mock.hpp
new file mode 100644
index 0000000..3469127
--- /dev/null
+++ b/bmc/firmware-handler/test/triggerable_mock.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "firmware_handler.hpp"
+#include "status.hpp"
+
+#include <memory>
+#include <string>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+// TriggerableActionInterface
+
+class TriggerMock : public TriggerableActionInterface
+{
+  public:
+    MOCK_METHOD0(trigger, bool());
+    MOCK_METHOD0(abort, void());
+    MOCK_METHOD0(status, ActionStatus());
+};
+
+std::unique_ptr<TriggerableActionInterface> CreateTriggerMock()
+{
+    return std::make_unique<TriggerMock>();
+}
+
+ActionMap CreateActionMap(const std::string& blobPath)
+{
+    std::unique_ptr<ActionPack> actionPack = std::make_unique<ActionPack>();
+    actionPack->preparation = CreateTriggerMock();
+    actionPack->verification = CreateTriggerMock();
+    actionPack->update = CreateTriggerMock();
+
+    ActionMap map;
+    map[blobPath] = std::move(actionPack);
+    return map;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/firmware-handler/test/window_mapper_mock.hpp b/bmc/firmware-handler/test/window_mapper_mock.hpp
new file mode 100644
index 0000000..dd3a89b
--- /dev/null
+++ b/bmc/firmware-handler/test/window_mapper_mock.hpp
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "window_hw_interface.hpp"
+
+#include <cstdint>
+#include <utility>
+#include <vector>
+
+#include <gmock/gmock.h>
+
+namespace ipmi_flash
+{
+
+class HardwareInterfaceMock : public HardwareMapperInterface
+{
+  public:
+    virtual ~HardwareInterfaceMock() = default;
+
+    MOCK_METHOD0(open, MemorySet());
+    MOCK_METHOD0(close, ());
+    MOCK_METHOD2(mapWindow, WindowMapResult(std::uint32_t, std::uint32_t));
+};
+
+} // namespace ipmi_flash