| // Copyright 2024 Google LLC |
| // |
| // 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 "hoth_update_unittest.hpp" |
| |
| #include <cstdint> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include <gmock/gmock.h> |
| |
| using ::testing::_; |
| using ::testing::ContainerEq; |
| using ::testing::IsEmpty; |
| using ::testing::NotNull; |
| using ::testing::Return; |
| |
| using namespace std::literals; |
| |
| namespace ipmi_hoth |
| { |
| |
| using Cb = internal::DbusUpdate::Cb; |
| using FirmwareUpdateStatus = internal::DbusUpdate::FirmwareUpdateStatus; |
| |
| class HothUpdateCommitTest : public HothUpdateTest |
| { |
| protected: |
| template <typename Req> |
| void expectUpdate(std::string_view hothId, Req&& req) |
| { |
| EXPECT_CALL(dbus, UpdateFirmware(hothId, req, _)) |
| .WillOnce( |
| [&](std::string_view, const std::vector<uint8_t>&, Cb&& icb) { |
| cb = std::move(icb); |
| return stdplus::Cancel(std::nullopt); |
| }); |
| } |
| |
| Cb cb; |
| }; |
| |
| const auto static test_str = "Hello,\0 world!"s; |
| const std::vector<uint8_t> static test_buf(test_str.begin(), test_str.end()); |
| |
| TEST_F(HothUpdateCommitTest, InvalidSessionCommitIsRejected) |
| { |
| // Verify the hoth update handler checks for a valid session. |
| |
| EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>())); |
| } |
| |
| TEST_F(HothUpdateCommitTest, NonEmptyDataParamFails) |
| { |
| // Verify that we do not accept any data passed into the commit call. |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true)); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath)); |
| |
| EXPECT_FALSE(hvn.commit(session_, std::vector<uint8_t>({1, 2, 3}))); |
| } |
| |
| TEST_F(HothUpdateCommitTest, DbusExceptionSetsCommitErrorStateAndFails) |
| { |
| // Verify that when the dbus call hits an exception, commit method fails |
| // and that the state is set to blobs::StateFlags::commit_error |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true)); |
| expectUpdate(/*hothId=*/"", ContainerEq(test_buf)); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath)); |
| // session, offset, data |
| EXPECT_TRUE(hvn.write(session_, 0, test_buf)); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Error); |
| |
| blobs::BlobMeta meta; |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| EXPECT_EQ(meta.blobState, |
| blobs::StateFlags::open_write | blobs::StateFlags::commit_error); |
| } |
| |
| TEST_F(HothUpdateCommitTest, EmptyBufferCommitLegacyHothSuccessful) |
| { |
| // Verify that we are able to succssfully commit an empty buffer |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true)); |
| expectUpdate(/*hothId=*/"", IsEmpty()); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath)); |
| // No write call to populate the buffer here |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Done); |
| |
| blobs::BlobMeta meta; |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| EXPECT_EQ(meta.size, 0); |
| EXPECT_EQ(meta.blobState, |
| blobs::StateFlags::open_write | blobs::StateFlags::committed); |
| } |
| |
| TEST_F(HothUpdateCommitTest, EmptyBufferCommitNamedHothSuccessful) |
| { |
| // Verify that we are able to succssfully commit an empty buffer |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(name))).WillOnce(Return(true)); |
| expectUpdate(/*hothId=*/name, IsEmpty()); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), namedPath)); |
| // No write call to populate the buffer here |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Done); |
| |
| blobs::BlobMeta meta; |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| EXPECT_EQ(meta.size, 0); |
| EXPECT_EQ(meta.blobState, |
| blobs::StateFlags::open_write | blobs::StateFlags::committed); |
| } |
| |
| TEST_F(HothUpdateCommitTest, NonEmptyBufferCommitSuccessful) |
| { |
| // Verify that we are able to succssfully commit a buffer with valid data |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true)); |
| expectUpdate(/*hothId=*/"", ContainerEq(test_buf)); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath)); |
| // session, offset, data |
| EXPECT_TRUE(hvn.write(session_, 0, test_buf)); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Done); |
| |
| blobs::BlobMeta meta; |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| EXPECT_EQ(meta.size, test_buf.size()); |
| EXPECT_EQ(meta.blobState, |
| blobs::StateFlags::open_write | blobs::StateFlags::committed); |
| } |
| |
| TEST_F(HothUpdateCommitTest, IdempotentSuccess) |
| { |
| // Verify that repeated successful commits only result in one D-Bus call |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true)); |
| expectUpdate(/*hothId=*/"", IsEmpty()); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath)); |
| // No write call to populate the buffer here |
| |
| // These calls will return due to the state being "committing". Calling |
| // stat is the only way to check if the state has changed to "committed" |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Done); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| |
| blobs::BlobMeta meta; |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| // These calls will return due to the state being "committed" |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| |
| // Stat is also idempotent, no need to mock the D-Bus call here |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| EXPECT_EQ(meta.size, 0); |
| EXPECT_EQ(meta.blobState, |
| blobs::StateFlags::open_write | blobs::StateFlags::committed); |
| } |
| |
| TEST_F(HothUpdateCommitTest, ErrorRetry) |
| { |
| // Verify that commit after error retries the update D-Bus call |
| |
| EXPECT_CALL(dbus, pingHothd(std::string_view(""))).WillOnce(Return(true)); |
| |
| EXPECT_TRUE(hvn.open(session_, hvn.requiredFlags(), legacyPath)); |
| |
| // These calls will return due to the state being "commit_error" |
| expectUpdate(/*hothId=*/"", IsEmpty()); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Error); |
| expectUpdate(/*hothId=*/"", IsEmpty()); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Error); |
| expectUpdate(/*hothId=*/"", IsEmpty()); |
| EXPECT_TRUE(hvn.commit(session_, std::vector<uint8_t>())); |
| cb(FirmwareUpdateStatus::Error); |
| |
| blobs::BlobMeta meta; |
| EXPECT_TRUE(hvn.stat(session_, &meta)); |
| |
| EXPECT_EQ(meta.blobState, |
| blobs::StateFlags::open_write | blobs::StateFlags::commit_error); |
| } |
| |
| } // namespace ipmi_hoth |