Add support for log-handler

This handler is based off of version handler with major differences in
handler_builder

Tested:
created the blackbox blob successfully using this lib.
Read the data from the blob using a host side tool

Signed-off-by: Gaurav Gandhi <gauravgandhi@google.com>
Change-Id: I9ef775af752156a1647453ff3831ef4c0449d546
diff --git a/bmc/log-handler/log_handler.cpp b/bmc/log-handler/log_handler.cpp
new file mode 100644
index 0000000..1883128
--- /dev/null
+++ b/bmc/log-handler/log_handler.cpp
@@ -0,0 +1,227 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+
+#include <algorithm>
+#include <cstring>
+#include <ios>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+LogBlobHandler::LogBlobHandler(std::vector<HandlerConfig<ActionPack>>&& configs)
+{
+    for (auto& config : configs)
+    {
+        auto info = std::make_unique<BlobInfo>();
+        info->blobId = std::move(config.blobId);
+        info->actions = std::move(config.actions);
+        info->handler = std::move(config.handler);
+        info->actions->onOpen->setCallback(
+            [infoP = info.get()](TriggerableActionInterface& tai) {
+                auto data =
+                    std::make_shared<std::optional<std::vector<uint8_t>>>();
+                do
+                {
+                    if (tai.status() != ActionStatus::success)
+                    {
+                        fprintf(stderr,
+                                "LogBlobHandler: Log file unit failed for %s\n",
+                                infoP->blobId.c_str());
+                        continue;
+                    }
+                    if (!infoP->handler->open("", std::ios::in))
+                    {
+                        fprintf(
+                            stderr,
+                            "LogBlobHandler: Opening log file failed for %s\n",
+                            infoP->blobId.c_str());
+                        continue;
+                    }
+                    auto d = infoP->handler->read(
+                        0, std::numeric_limits<uint32_t>::max());
+                    infoP->handler->close();
+                    if (!d)
+                    {
+                        fprintf(
+                            stderr,
+                            "LogBlobHandler: Reading log file failed for %s\n",
+                            infoP->blobId.c_str());
+                        continue;
+                    }
+                    *data = std::move(d);
+                } while (false);
+                for (auto sessionP : infoP->sessionsToUpdate)
+                {
+                    sessionP->data = data;
+                }
+                infoP->sessionsToUpdate.clear();
+            });
+        if (!blobInfoMap.try_emplace(info->blobId, std::move(info)).second)
+        {
+            fprintf(stderr,
+                    "LogBlobHandler: Ignoring duplicate config for %s\n",
+                    info->blobId.c_str());
+        }
+    }
+}
+
+bool LogBlobHandler::canHandleBlob(const std::string& path)
+{
+    return blobInfoMap.find(path) != blobInfoMap.end();
+}
+
+std::vector<std::string> LogBlobHandler::getBlobIds()
+{
+    std::vector<std::string> ret;
+    for (const auto& [key, _] : blobInfoMap)
+    {
+        ret.emplace_back(key);
+    }
+    return ret;
+}
+
+/**
+ * deleteBlob - does nothing, always fails
+ */
+bool LogBlobHandler::deleteBlob(const std::string& path)
+{
+    for (const auto& [sessionId, sessionInfo] : sessionInfoMap)
+    {
+        if (sessionInfo->blob->blobId == path)
+        {
+            fprintf(stderr,
+                    "LogBlobHandler: delete %s fail: there is an open session "
+                    "for this blob\n",
+                    path.c_str());
+            return false;
+        }
+    }
+
+    auto* blob = blobInfoMap.at(path).get();
+    if (!blob->actions->onDelete->trigger())
+    {
+        fprintf(stderr,
+                "LogBlobHandler: delete %s fail: onDelete trigger failed\n",
+                path.c_str());
+        return false;
+    }
+    return true;
+}
+
+bool LogBlobHandler::stat(const std::string&, blobs::BlobMeta*)
+{
+    return false;
+}
+
+bool LogBlobHandler::open(uint16_t session, uint16_t flags,
+                          const std::string& path)
+{
+    /* only reads are supported, check if blob is handled and make sure
+     * the blob isn't already opened
+     */
+    if (flags != blobs::read)
+    {
+        fprintf(stderr,
+                "LogBlobHandler: open %s fail: unsupported flags(0x%04X.)\n",
+                path.c_str(), flags);
+        return false;
+    }
+
+    auto info = std::make_unique<SessionInfo>();
+    info->blob = blobInfoMap.at(path).get();
+    info->blob->sessionsToUpdate.emplace(info.get());
+    if (info->blob->sessionsToUpdate.size() == 1 &&
+        !info->blob->actions->onOpen->trigger())
+    {
+        fprintf(stderr, "LogBlobHandler: open %s fail: onOpen trigger failed\n",
+                path.c_str());
+        info->blob->sessionsToUpdate.erase(info.get());
+        return false;
+    }
+
+    sessionInfoMap[session] = std::move(info);
+    return true;
+}
+
+std::vector<uint8_t> LogBlobHandler::read(uint16_t session, uint32_t offset,
+                                          uint32_t requestedSize)
+{
+    auto& data = sessionInfoMap.at(session)->data;
+    if (data == nullptr || !*data)
+    {
+        throw std::runtime_error("LogBlobHandler: Log data not ready for read");
+    }
+    if ((*data)->size() < offset)
+    {
+        return {};
+    }
+    std::vector<uint8_t> ret(
+        std::min<size_t>(requestedSize, (*data)->size() - offset));
+    std::memcpy(&ret[0], &(**data)[offset], ret.size());
+    return ret;
+}
+
+bool LogBlobHandler::close(uint16_t session)
+{
+    auto it = sessionInfoMap.find(session);
+    if (it == sessionInfoMap.end())
+    {
+        return false;
+    }
+    auto& info = *it->second;
+    info.blob->sessionsToUpdate.erase(&info);
+    if (info.blob->sessionsToUpdate.empty())
+    {
+        info.blob->actions->onOpen->abort();
+    }
+    sessionInfoMap.erase(it);
+    return true;
+}
+
+bool LogBlobHandler::stat(uint16_t session, blobs::BlobMeta* meta)
+{
+    const auto& data = sessionInfoMap.at(session)->data;
+    if (data == nullptr)
+    {
+        meta->blobState = blobs::StateFlags::committing;
+        meta->size = 0;
+    }
+    else if (!*data)
+    {
+        meta->blobState = blobs::StateFlags::commit_error;
+        meta->size = 0;
+    }
+    else
+    {
+        meta->blobState =
+            blobs::StateFlags::committed | blobs::StateFlags::open_read;
+        meta->size = (*data)->size();
+    }
+    return true;
+}
+
+bool LogBlobHandler::expire(uint16_t session)
+{
+    close(session);
+    return true;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/log_handler.hpp b/bmc/log-handler/log_handler.hpp
new file mode 100644
index 0000000..32aebaa
--- /dev/null
+++ b/bmc/log-handler/log_handler.hpp
@@ -0,0 +1,110 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+#include "handler_config.hpp"
+#include "image_handler.hpp"
+#include "status.hpp"
+#include "util.hpp"
+
+#include <blobs-ipmid/blobs.hpp>
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <set>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+class LogBlobHandler : public blobs::GenericBlobInterface
+{
+  public:
+    struct ActionPack
+    {
+        /** Only file operation action supported currently */
+        std::unique_ptr<TriggerableActionInterface> onOpen;
+        std::unique_ptr<TriggerableActionInterface> onDelete;
+    };
+
+    /**
+     * Create a LogBlobHandler.
+     *
+     * @param[in] configs - list of blob configurations to support
+     */
+    LogBlobHandler(std::vector<HandlerConfig<ActionPack>>&& configs);
+
+    ~LogBlobHandler() = default;
+    LogBlobHandler(const LogBlobHandler&) = delete;
+    LogBlobHandler& operator=(const LogBlobHandler&) = delete;
+    LogBlobHandler(LogBlobHandler&&) = default;
+    LogBlobHandler& operator=(LogBlobHandler&&) = default;
+
+    bool canHandleBlob(const std::string& path) override;
+    std::vector<std::string> getBlobIds() override;
+    bool deleteBlob(const std::string& path) override;
+    bool stat(const std::string&, blobs::BlobMeta* meta) override;
+    bool open(uint16_t session, uint16_t flags,
+              const std::string& path) override;
+    std::vector<uint8_t> read(uint16_t session, uint32_t offset,
+                              uint32_t requestedSize) override;
+    bool write(uint16_t, uint32_t, const std::vector<uint8_t>&) override
+    {
+        return false; /* not supported */
+    };
+    bool writeMeta(uint16_t, uint32_t, const std::vector<uint8_t>&) override
+    {
+        return false; /* not supported */
+    }
+    bool commit(uint16_t, const std::vector<uint8_t>&) override
+    {
+        return false; // not supported
+    }
+    bool close(uint16_t session) override;
+    bool stat(uint16_t session, blobs::BlobMeta* meta) override;
+    bool expire(uint16_t session) override;
+
+  private:
+    struct SessionInfo;
+
+    struct BlobInfo
+    {
+        Pinned<std::string> blobId;
+        std::unique_ptr<ActionPack> actions;
+        std::unique_ptr<ImageHandlerInterface> handler;
+        std::set<SessionInfo*> sessionsToUpdate;
+    };
+
+    struct SessionInfo
+    {
+        BlobInfo* blob;
+
+        // A cached copy of the version data shared by all clients for a single
+        // execution of the version retrieval action. This is is null until the
+        // TriggerableAction has completed. If the action is an error, the
+        // shared object is nullopt. Otherwise, contains a vector of the version
+        // data when successfully read.
+        std::shared_ptr<const std::optional<std::vector<uint8_t>>> data;
+    };
+
+    std::unordered_map<std::string_view, std::unique_ptr<BlobInfo>> blobInfoMap;
+    std::unordered_map<uint16_t, std::unique_ptr<SessionInfo>> sessionInfoMap;
+};
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/log_handlers_builder.cpp b/bmc/log-handler/log_handlers_builder.cpp
new file mode 100644
index 0000000..2781534
--- /dev/null
+++ b/bmc/log-handler/log_handlers_builder.cpp
@@ -0,0 +1,125 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handlers_builder.hpp"
+
+#include "file_handler.hpp"
+#include "fs.hpp"
+#include "skip_action.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <algorithm>
+#include <cstdio>
+#include <exception>
+#include <fstream>
+#include <memory>
+#include <regex>
+#include <string>
+#include <vector>
+
+namespace ipmi_flash
+{
+
+std::vector<HandlerConfig<LogBlobHandler::ActionPack>>
+    LogHandlersBuilder::buildHandlerFromJson(const nlohmann::json& data)
+{
+    std::vector<HandlerConfig<LogBlobHandler::ActionPack>> handlers;
+
+    for (const auto& item : data)
+    {
+        try
+        {
+            HandlerConfig<LogBlobHandler::ActionPack> output;
+
+            /* at() throws an exception when the key is not present. */
+            item.at("blob").get_to(output.blobId);
+
+            /* name must be: /log/ */
+            std::regex regexpr("^\\/(?:log)\\/(.+)");
+            std::smatch matches;
+            if (!std::regex_search(output.blobId, matches, regexpr))
+            {
+                continue;
+            }
+            /* log must have handler */
+            const auto& h = item.at("handler");
+
+            const std::string& handlerType = h.at("type");
+            if (handlerType == "file")
+            {
+                const auto& path = h.at("path");
+                output.handler = std::make_unique<FileHandler>(path);
+            }
+            else
+            {
+                throw std::runtime_error("Invalid handler type: " +
+                                         handlerType);
+            }
+
+            /* actions are required (presently). */
+            const auto& a = item.at("actions");
+            auto pack = std::make_unique<LogBlobHandler::ActionPack>();
+
+            /* to make an action optional, assign type "skip" */
+            const auto& onOpen = a.at("open");
+            const std::string& onOpenType = onOpen.at("type");
+            if (onOpenType == "systemd")
+            {
+                pack->onOpen = std::move(buildSystemd(onOpen));
+            }
+            else if (onOpenType == "skip")
+            {
+                pack->onOpen = SkipAction::CreateSkipAction();
+            }
+            else
+            {
+                throw std::runtime_error("Invalid preparation type: " +
+                                         onOpenType);
+            }
+
+            const auto& onDelete = a.at("delete");
+            const std::string& onDeleteType = onOpen.at("type");
+            if (onOpenType == "systemd")
+            {
+                pack->onDelete = std::move(buildSystemd(onDelete));
+            }
+            else if (onOpenType == "skip")
+            {
+                pack->onDelete = SkipAction::CreateSkipAction();
+            }
+            else
+            {
+                throw std::runtime_error("Invalid preparation type: " +
+                                         onDeleteType);
+            }
+
+            output.actions = std::move(pack);
+            handlers.push_back(std::move(output));
+        }
+        catch (const std::exception& e)
+        {
+            /* TODO: Once phosphor-logging supports unit-test injection, fix
+             * this to log.
+             */
+            std::fprintf(stderr,
+                         "Excepted building HandlerConfig from json: %s\n",
+                         e.what());
+        }
+    }
+
+    return handlers;
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/log_handlers_builder.hpp b/bmc/log-handler/log_handlers_builder.hpp
new file mode 100644
index 0000000..552758d
--- /dev/null
+++ b/bmc/log-handler/log_handlers_builder.hpp
@@ -0,0 +1,35 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+#include "buildjson.hpp"
+#include "log_handler.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <vector>
+
+namespace ipmi_flash
+{
+/**
+ * provide the method to parse and validate blob entries from json and produce
+ * something that is usable by the log handler.
+ */
+class LogHandlersBuilder : public HandlersBuilderIfc<LogBlobHandler::ActionPack>
+{
+  public:
+    std::vector<HandlerConfig<LogBlobHandler::ActionPack>>
+        buildHandlerFromJson(const nlohmann::json& data) override;
+};
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/main.cpp b/bmc/log-handler/main.cpp
new file mode 100644
index 0000000..d860ca7
--- /dev/null
+++ b/bmc/log-handler/main.cpp
@@ -0,0 +1,24 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_handlers_builder.hpp"
+
+#include <memory>
+
+extern "C" std::unique_ptr<blobs::GenericBlobInterface> createHandler()
+{
+    return std::make_unique<ipmi_flash::LogBlobHandler>(
+        ipmi_flash::LogHandlersBuilder().buildHandlerConfigsFromDefaultPaths());
+}
diff --git a/bmc/log-handler/meson.build b/bmc/log-handler/meson.build
new file mode 100644
index 0000000..50c4bed
--- /dev/null
+++ b/bmc/log-handler/meson.build
@@ -0,0 +1,35 @@
+log_inc = include_directories('.')
+
+log_pre = declare_dependency(
+  include_directories: [root_inc, log_inc],
+  dependencies : [
+    common_dep,
+    firmware_dep,
+  ])
+
+log_lib = static_library(
+  'logblob',
+  'log_handler.cpp',
+  'log_handlers_builder.cpp',
+  implicit_include_directories: false,
+  dependencies: log_pre)
+
+
+log_dep = declare_dependency(
+  link_with: log_lib,
+  dependencies: common_pre)
+
+shared_module(
+  'logblob',
+  'main.cpp',
+  implicit_include_directories: false,
+  dependencies: [
+    log_dep,
+    dependency('libipmid'),
+  ],
+  install: true,
+  install_dir: get_option('libdir') / 'blob-ipmid')
+
+if not get_option('tests').disabled()
+  subdir('test')
+endif
\ No newline at end of file
diff --git a/bmc/log-handler/test/log_canhandle_enumerate_unittest.cpp b/bmc/log-handler/test/log_canhandle_enumerate_unittest.cpp
new file mode 100644
index 0000000..f27737d
--- /dev/null
+++ b/bmc/log-handler/test/log_canhandle_enumerate_unittest.cpp
@@ -0,0 +1,43 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <array>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+TEST(LogHandlerCanHandleTest, VerifyGoodInfoMap)
+{
+    constexpr std::array blobNames{"blob0", "blob1", "blob2", "blob3"};
+    LogBlobHandler handler(createMockLogConfigs(blobNames));
+    for (const auto& blobName : blobNames)
+    {
+        EXPECT_TRUE(handler.canHandleBlob(blobName));
+    }
+}
+
+TEST(LogHandlerEnumerateTest, VerifyGoodInfoMap)
+{
+    constexpr std::array blobNames{"blob0", "blob1", "blob2", "blob3"};
+    LogBlobHandler handler(createMockLogConfigs(blobNames));
+    EXPECT_THAT(handler.getBlobIds(),
+                ::testing::UnorderedElementsAreArray(blobNames));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_close_unittest.cpp b/bmc/log-handler/test/log_close_unittest.cpp
new file mode 100644
index 0000000..8755541
--- /dev/null
+++ b/bmc/log-handler/test/log_close_unittest.cpp
@@ -0,0 +1,94 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogCloseExpireBlobTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        h = std::make_unique<LogBlobHandler>(
+            createMockLogConfigs(blobNames, &im, &tm));
+    }
+
+    std::unique_ptr<blobs::GenericBlobInterface> h;
+    std::vector<std::string> blobNames{"blob0", "blob1", "blob2", "blob3"};
+    std::unordered_map<std::string, TriggerMock*> tm;
+    std::unordered_map<std::string, ImageHandlerMock*> im;
+};
+
+TEST_F(LogCloseExpireBlobTest, VerifyOpenThenClose)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+    EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+    EXPECT_TRUE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifySingleAbort)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+    EXPECT_TRUE(h->open(1, blobs::read, "blob0"));
+    EXPECT_TRUE(h->close(0));
+    EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+    EXPECT_TRUE(h->close(1));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyUnopenedBlobCloseFails)
+{
+    EXPECT_FALSE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyDoubleCloseFails)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+    EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+    EXPECT_TRUE(h->close(0));
+    EXPECT_FALSE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyBadSessionNumberCloseFails)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+    EXPECT_FALSE(h->close(1));
+    EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+    EXPECT_TRUE(h->close(0));
+}
+
+TEST_F(LogCloseExpireBlobTest, VerifyRunningActionIsAborted)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+    EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+    EXPECT_TRUE(h->close(0));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_createhandler_unittest.cpp b/bmc/log-handler/test/log_createhandler_unittest.cpp
new file mode 100644
index 0000000..99d5d3e
--- /dev/null
+++ b/bmc/log-handler/test/log_createhandler_unittest.cpp
@@ -0,0 +1,44 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <array>
+#include <utility>
+
+#include <gtest/gtest.h>
+
+namespace ipmi_flash
+{
+
+TEST(LogHandlerCanHandleTest, VerifyGoodInfoMapPasses)
+{
+    constexpr std::array blobs{"blob0", "blob1"};
+    LogBlobHandler handler(createMockLogConfigs(blobs));
+    EXPECT_THAT(handler.getBlobIds(),
+                testing::UnorderedElementsAreArray(blobs));
+}
+
+TEST(LogHandlerCanHandleTest, VerifyDuplicatesIgnored)
+{
+    constexpr std::array blobs{"blob0"};
+    auto configs = createMockLogConfigs(blobs);
+    configs.push_back(createMockLogConfig(blobs[0]));
+    LogBlobHandler handler(std::move(configs));
+    EXPECT_THAT(handler.getBlobIds(),
+                testing::UnorderedElementsAreArray(blobs));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_json_unittest.cpp b/bmc/log-handler/test/log_json_unittest.cpp
new file mode 100644
index 0000000..5759c25
--- /dev/null
+++ b/bmc/log-handler/test/log_json_unittest.cpp
@@ -0,0 +1,314 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "general_systemd.hpp"
+#include "log_handlers_builder.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(LogJsonTest, ValidConfigurationNoLogHandler)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/sink_seq",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(1));
+    EXPECT_THAT(h[0].blobId, "/log/sink_seq");
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].handler == nullptr);
+}
+
+TEST(LogJsonTest, ValidConfigurationLogBlobName)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/log/sink_seq",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions": {
+                    "open" : {
+                    "type" : "systemd",
+                    "unit" : "phosphor-ipmi-flash-log-sink-sequencer.target"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(1));
+    EXPECT_THAT(h[0].blobId, "/log/sink_seq");
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].handler == nullptr);
+}
+
+TEST(LogJsonTest, MissingHandlerType)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "log":{
+                "handler": {
+                   "path" : "/tmp/log_info"
+                 },
+                "actions": {
+                  "open" : {
+                  "type" : "systemd",
+                  "unit" : "absolute"}
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, BadBlobName)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/bad/image",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions": {
+                  "open" : {
+                  "type" : "systemd",
+                  "unit" : "absolute"}
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, MissingActions)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, MissingOpenAction)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/image",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions": {}
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, OneInvalidTwoValidSucceeds)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/sink_seq0",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         },
+         {
+            "blob" : "/log/sink_seq1",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         },
+         {
+            "blob" : "/bad/sink_seq",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         }
+         ]
+    )"_json;
+    auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(2));
+    EXPECT_THAT(h[0].blobId, "/log/sink_seq0");
+    EXPECT_THAT(h[1].blobId, "/log/sink_seq1");
+}
+
+TEST(LogJsonTest, BlobNameIsTooShort)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    EXPECT_THAT(LogHandlersBuilder().buildHandlerFromJson(j2), IsEmpty());
+}
+
+TEST(LogJsonTest, OpenSkipAction)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/sink_seqs",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "skip"
+                    }
+                 }
+            }
+         }]
+    )"_json;
+    auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+    EXPECT_THAT(h, ::testing::SizeIs(1));
+    EXPECT_TRUE(h[0].blobId == "/log/sink_seqs");
+    ASSERT_FALSE(h[0].actions == nullptr);
+    EXPECT_FALSE(h[0].actions->onOpen == nullptr);
+}
+
+TEST(LogJsonTest, OpenActionsWithDifferentModes)
+{
+    auto j2 = R"(
+        [{
+            "blob" : "/flash/blob1",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute",
+                    "mode" : "replace-nope"
+                    }
+                 }
+            }
+         },
+         {
+            "blob" : "/flash/blob2",
+            "log":{
+                "handler": {
+                   "type" : "file",
+                   "path" : "/tmp/log_info"
+                 },
+                "actions":{
+                    "open" :{
+                    "type" : "systemd",
+                    "unit" : "absolute",
+                    "mode" : "replace-fake"
+                    }
+                 }
+            }
+         }
+         ]
+    )"_json;
+    auto h = LogHandlersBuilder().buildHandlerFromJson(j2);
+    ASSERT_THAT(h, ::testing::SizeIs(2));
+
+    EXPECT_FALSE(h[0].handler == nullptr);
+    EXPECT_FALSE(h[0].actions == nullptr);
+    EXPECT_THAT(h[0].blobId, "/log/blob1");
+    auto onOpen0 = reinterpret_cast<SystemdNoFile*>(h[0].actions->onOpen.get());
+    EXPECT_THAT(onOpen0->getMode(), "replace-nope");
+
+    EXPECT_FALSE(h[1].handler == nullptr);
+    EXPECT_FALSE(h[1].actions == nullptr);
+    EXPECT_THAT(h[1].blobId, "/log/blob2");
+    auto onOpen1 = reinterpret_cast<SystemdNoFile*>(h[1].actions->onOpen.get());
+    EXPECT_THAT(onOpen1->getMode(), "replace-fake");
+}
+} // namespace
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_mock.hpp b/bmc/log-handler/test/log_mock.hpp
new file mode 100644
index 0000000..e9628eb
--- /dev/null
+++ b/bmc/log-handler/test/log_mock.hpp
@@ -0,0 +1,61 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "handler_config.hpp"
+#include "image_mock.hpp"
+#include "log_handler.hpp"
+#include "triggerable_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+namespace ipmi_flash
+{
+
+auto createMockLogConfig(const std::string& id, ImageHandlerMock** im = nullptr,
+                         TriggerMock** tm = nullptr)
+{
+    HandlerConfig<LogBlobHandler::ActionPack> ret;
+    ret.blobId = id;
+    auto handler = std::make_unique<testing::StrictMock<ImageHandlerMock>>();
+    if (im != nullptr)
+    {
+        *im = handler.get();
+    }
+    ret.handler = std::move(handler);
+    ret.actions = std::make_unique<LogBlobHandler::ActionPack>();
+    auto trigger = std::make_unique<testing::StrictMock<TriggerMock>>();
+    if (tm != nullptr)
+    {
+        *tm = trigger.get();
+    }
+    ret.actions->onOpen = std::move(trigger);
+    return ret;
+}
+
+template <typename C, typename Im = std::map<std::string, ImageHandlerMock*>,
+          typename Tm = std::map<std::string, TriggerMock*>>
+auto createMockLogConfigs(const C& ids, Im* im = nullptr, Tm* tm = nullptr)
+{
+    std::vector<HandlerConfig<LogBlobHandler::ActionPack>> ret;
+    for (const auto& id : ids)
+    {
+        ret.push_back(
+            createMockLogConfig(id, im == nullptr ? nullptr : &(*im)[id],
+                                tm == nullptr ? nullptr : &(*tm)[id]));
+    }
+    return ret;
+}
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_open_unittest.cpp b/bmc/log-handler/test/log_open_unittest.cpp
new file mode 100644
index 0000000..301da86
--- /dev/null
+++ b/bmc/log-handler/test/log_open_unittest.cpp
@@ -0,0 +1,101 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogOpenBlobTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        h = std::make_unique<LogBlobHandler>(
+            createMockLogConfigs(blobNames, &im, &tm));
+    }
+
+    std::unique_ptr<blobs::GenericBlobInterface> h;
+    std::vector<std::string> blobNames{"blob0", "blob1", "blob2", "blob3"};
+    std::unordered_map<std::string, TriggerMock*> tm;
+    std::unordered_map<std::string, ImageHandlerMock*> im;
+    const std::uint16_t defaultSessionNumber{0};
+};
+
+TEST_F(LogOpenBlobTest, VerifySingleBlobOpen)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).Times(1).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyMultipleBlobOpens)
+{
+    for (const auto& [_, val] : tm)
+    {
+        /* set the expectation that every onOpen will be triggered */
+        EXPECT_CALL(*val, trigger()).WillOnce(Return(true));
+    }
+    int i{defaultSessionNumber};
+    for (const auto& blob : blobNames)
+    {
+        EXPECT_TRUE(h->open(i++, blobs::read, blob));
+    }
+}
+
+TEST_F(LogOpenBlobTest, VerifyOpenAfterClose)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+
+    EXPECT_CALL(*tm.at("blob0"), abort()).Times(1);
+    EXPECT_TRUE(h->close(defaultSessionNumber));
+
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyMultiOpenWorks)
+{
+    EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob1"));
+    EXPECT_TRUE(h->open(1, blobs::read, "blob1"));
+    EXPECT_TRUE(h->open(2, blobs::read, "blob1"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyFailedTriggerFails)
+{
+    EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(false));
+    EXPECT_FALSE(h->open(0, blobs::read, "blob1"));
+    EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob1"));
+}
+
+TEST_F(LogOpenBlobTest, VerifyUnsupportedOpenFlagsFails)
+{
+    EXPECT_FALSE(h->open(0, blobs::write, "blob1"));
+    EXPECT_CALL(*tm.at("blob1"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob1"));
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_read_unittest.cpp b/bmc/log-handler/test/log_read_unittest.cpp
new file mode 100644
index 0000000..ab952e6
--- /dev/null
+++ b/bmc/log-handler/test/log_read_unittest.cpp
@@ -0,0 +1,153 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::ElementsAreArray;
+using ::testing::Ge;
+using ::testing::IsEmpty;
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogReadBlobTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        h = std::make_unique<LogBlobHandler>(
+            createMockLogConfigs(blobNames, &im, &tm));
+    }
+    std::unique_ptr<blobs::GenericBlobInterface> h;
+    std::vector<std::string> blobNames{"blob0", "blob1", "blob2", "blob3"};
+    std::unordered_map<std::string, TriggerMock*> tm;
+    std::unordered_map<std::string, ImageHandlerMock*> im;
+    const std::uint16_t defaultSessionNumber{200};
+    std::vector<uint8_t> vector1{0xDE, 0xAD, 0xBE, 0xEF,
+                                 0xBA, 0xDF, 0xEE, 0x0D};
+    std::vector<uint8_t> vector2{0xCE, 0xAD, 0xDE, 0xFF};
+};
+
+TEST_F(LogReadBlobTest, VerifyValidRead)
+{
+    testing::InSequence seq;
+    EXPECT_CALL(*tm.at("blob0"), trigger())
+        .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+                        Return(true)));
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::success));
+    EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+    EXPECT_CALL(*im.at("blob0"), read(0, Ge(vector1.size())))
+        .WillOnce(Return(vector1));
+    EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+
+    std::basic_string_view<uint8_t> vectorS(vector1.data(), vector1.size());
+    EXPECT_THAT(h->read(defaultSessionNumber, 0, 7),
+                ElementsAreArray(vectorS.substr(0, 7)));
+    EXPECT_THAT(h->read(defaultSessionNumber, 2, 10),
+                ElementsAreArray(vectorS.substr(2, 6)));
+    EXPECT_THAT(h->read(defaultSessionNumber, 10, 0), IsEmpty());
+}
+
+TEST_F(LogReadBlobTest, VerifyMultipleSession)
+{
+    testing::InSequence seq;
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+    EXPECT_TRUE(h->open(1, blobs::read, "blob0"));
+
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::success));
+    EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+    EXPECT_CALL(*im.at("blob0"), read(0, Ge(vector1.size())))
+        .WillOnce(Return(vector1));
+    EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+    tm.at("blob0")->cb(*tm.at("blob0"));
+
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+    EXPECT_TRUE(h->open(2, blobs::read, "blob0"));
+
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::success));
+    EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+    EXPECT_CALL(*im.at("blob0"), read(0, Ge(vector2.size())))
+        .WillOnce(Return(vector2));
+    EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+    tm.at("blob0")->cb(*tm.at("blob0"));
+
+    EXPECT_THAT(h->read(0, 0, 10), ElementsAreArray(vector1));
+    EXPECT_THAT(h->read(1, 0, 10), ElementsAreArray(vector1));
+    EXPECT_THAT(h->read(2, 0, 10), ElementsAreArray(vector2));
+}
+
+TEST_F(LogReadBlobTest, VerifyReadEarlyFails)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+    EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+TEST_F(LogReadBlobTest, VerifyTriggerFailureReadFails)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger())
+        .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+                        Return(true)));
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::failed));
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+    EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+TEST_F(LogReadBlobTest, VerifyReadFailsOnFileOpenFailure)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger())
+        .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+                        Return(true)));
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::success));
+    EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(false));
+
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+    EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+TEST_F(LogReadBlobTest, VerifyReadFailsOnFileReadFailure)
+{
+    EXPECT_CALL(*tm.at("blob0"), trigger())
+        .WillOnce(DoAll([&]() { tm.at("blob0")->cb(*tm.at("blob0")); },
+                        Return(true)));
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::success));
+    EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+    EXPECT_CALL(*im.at("blob0"), read(_, _)).WillOnce(Return(std::nullopt));
+    EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+
+    EXPECT_TRUE(h->open(defaultSessionNumber, blobs::read, "blob0"));
+    EXPECT_THROW(h->read(defaultSessionNumber, 0, 10), std::runtime_error);
+}
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/log_stat_unittest.cpp b/bmc/log-handler/test/log_stat_unittest.cpp
new file mode 100644
index 0000000..6eb91b5
--- /dev/null
+++ b/bmc/log-handler/test/log_stat_unittest.cpp
@@ -0,0 +1,95 @@
+// Copyright 2021 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "log_handler.hpp"
+#include "log_mock.hpp"
+
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::Return;
+
+namespace ipmi_flash
+{
+
+class LogStatBlobTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        h = std::make_unique<LogBlobHandler>(
+            createMockLogConfigs(blobNames, &im, &tm));
+
+        EXPECT_CALL(*tm.at("blob0"), trigger()).WillOnce(Return(true));
+        EXPECT_TRUE(h->open(0, blobs::read, "blob0"));
+
+        blobs::BlobMeta meta;
+        EXPECT_TRUE(h->stat(0, &meta));
+        EXPECT_EQ(blobs::StateFlags::committing, meta.blobState);
+    }
+
+    std::unique_ptr<blobs::GenericBlobInterface> h;
+    std::vector<std::string> blobNames{"blob0"};
+    std::unordered_map<std::string, TriggerMock*> tm;
+    std::unordered_map<std::string, ImageHandlerMock*> im;
+};
+
+TEST_F(LogStatBlobTest, CreateError)
+{
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::failed));
+    tm.at("blob0")->cb(*tm.at("blob0"));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(h->stat(0, &meta));
+    EXPECT_EQ(blobs::StateFlags::commit_error, meta.blobState);
+}
+
+class LogStatSizeBlobTest :
+    public LogStatBlobTest,
+    public ::testing::WithParamInterface<std::vector<uint8_t>>
+{};
+
+TEST_P(LogStatSizeBlobTest, StatWithSize)
+{
+    const std::vector<uint8_t> data = GetParam();
+    EXPECT_CALL(*tm.at("blob0"), status())
+        .WillOnce(Return(ActionStatus::success));
+    EXPECT_CALL(*im.at("blob0"), open(_, std::ios::in)).WillOnce(Return(true));
+    EXPECT_CALL(*im.at("blob0"), read(0, ::testing::Ge(data.size())))
+        .WillOnce(Return(data));
+    EXPECT_CALL(*im.at("blob0"), close()).Times(1);
+    tm.at("blob0")->cb(*tm.at("blob0"));
+
+    blobs::BlobMeta meta;
+    EXPECT_TRUE(h->stat(0, &meta));
+    EXPECT_EQ(blobs::StateFlags::committed | blobs::StateFlags::open_read,
+              meta.blobState);
+    EXPECT_EQ(data.size(), meta.size);
+}
+
+const std::vector<std::vector<uint8_t>> datas = {
+    {},
+    {0, 1, 2, 3, 4, 5, 6},
+};
+
+INSTANTIATE_TEST_SUITE_P(DifferentData, LogStatSizeBlobTest,
+                         testing::ValuesIn(datas));
+
+} // namespace ipmi_flash
diff --git a/bmc/log-handler/test/meson.build b/bmc/log-handler/test/meson.build
new file mode 100644
index 0000000..36b4917
--- /dev/null
+++ b/bmc/log-handler/test/meson.build
@@ -0,0 +1,14 @@
+log_tests = [
+  'canhandle_enumerate',
+  'createhandler']
+
+foreach t : log_tests
+  test(
+    t,
+    executable(
+      t.underscorify(), 'log_' + t + '_unittest.cpp',
+      build_by_default: false,
+      implicit_include_directories: false,
+      include_directories: [root_inc, bmc_test_inc, log_inc],
+      dependencies: [log_dep, blobs_dep, gtest, gmock]))
+endforeach