blob: 479267daf2cedf09c5a873f733ec75820f3c9bf6 [file]
// 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_updater_cli.hpp"
#include <systemd/sd-bus.h>
#include <sdbusplus/exception.hpp>
#include <cstdio>
#include <filesystem>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace google::hoth::tools
{
namespace
{
using ::testing::_;
using ::testing::Return;
class HothUpdaterCLIMock : public HothUpdaterCLI
{
public:
MOCK_METHOD(HothVersionStringsRsp, getHothVersion,
(sdbusplus::bus::bus & bus, std::string_view hoth_id));
MOCK_METHOD(std::optional<std::string>, getTransportType,
(sdbusplus::bus::bus & bus, std::string_view hoth_id));
MOCK_METHOD(HothActivationStatistics, getHothActivationStatistics,
(sdbusplus::bus::bus & bus, std::string_view hoth_id));
MOCK_METHOD(void, spiWrite,
(sdbusplus::bus::bus&, std::string_view,
const std::span<const uint8_t>, std::optional<uint32_t>));
MOCK_METHOD(std::vector<uint8_t>, readFileIntoByteArray,
(std::string_view));
MOCK_METHOD(FirmwareUpdateStatus, getFirmwareUpdateStatus,
(sdbusplus::bus::bus & bus, std::string_view hoth_id));
MOCK_METHOD(std::vector<uint8_t>, sendHostCommand,
(sdbusplus::bus::bus & bus, std::string_view hoth_id,
const std::span<const uint8_t> command));
};
TEST(PayloadUpdateCLITest, splitStringTest)
{
std::vector<std::string> sp = splitString("", ',');
EXPECT_EQ(sp.size(), 0);
sp = splitString("product_3_1", ',');
EXPECT_EQ(sp.size(), 1);
EXPECT_EQ(sp[0], "product_3_1");
sp = splitString("product_3_1,product_3_2", ',');
EXPECT_EQ(sp.size(), 2);
EXPECT_EQ(sp[0], "product_3_1");
EXPECT_EQ(sp[1], "product_3_2");
}
class HothUpdaterCLITest : public ::testing::Test
{
protected:
HothUpdaterCLIMock cli;
};
// Test for making sure doActivationCheck does not error out
TEST_F(HothUpdaterCLITest, doActivationCheckTest)
{
HothVersionStringsRsp rsp = {
.header =
{
.struct_version = 0,
.checksum = 0,
.result = 0,
.data_len = 0,
.reserved = 0,
},
.version = {
.version_string_ro = "1.2.345",
.version_string_rw = "2.3.456",
.reserved = "",
.current_image = 0,
}};
Args args;
args.expectedRwVersion = "2.3.456";
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, getHothVersion(_, _)).Times(1).WillOnce(Return(rsp));
EXPECT_CALL(cli, getHothActivationStatistics(_, _)).Times(0);
cli.doActivationCheckLogic(bus, args);
}
// Test doActivationCheck when version mismatch.
TEST_F(HothUpdaterCLITest, doActivationCheckWrongVersion)
{
HothVersionStringsRsp rsp = {
.header =
{
.struct_version = 0,
.checksum = 0,
.result = 0,
.data_len = 0,
.reserved = 0,
},
.version = {
.version_string_ro = "1.2.345",
.version_string_rw = "2.3.456",
.reserved = "",
.current_image = 0,
}};
HothActivationStatistics stats = {
.rw_failure_code = 1,
.rw_failed_minor = 2,
.ro_failure_code = 0xffff,
.reset_flags = 3,
.uptime_us = 0xdeadbeeffeedbead,
};
Args args;
args.expectedRwVersion = "3.4.567";
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, getHothVersion(_, _)).Times(1).WillOnce(Return(rsp));
EXPECT_CALL(cli, getHothActivationStatistics(_, _))
.Times(1)
.WillOnce(Return(stats));
EXPECT_THROW(cli.doActivationCheckLogic(bus, args), std::runtime_error);
}
// Test for making sure the doUpdate loop works correctly
TEST_F(HothUpdaterCLITest, getFirmwareUpdateStatusTest)
{
Args args;
args.updateMethod = "spi";
args.imageFilename = "dummy";
args.address = 0x01ef0000;
std::vector<uint8_t> dummy_image = {0, 1, 2, 3};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, readFileIntoByteArray(_))
.Times(1)
.WillOnce(Return(dummy_image));
EXPECT_CALL(cli, spiWrite(_, _, _, _)).Times(1);
EXPECT_CALL(cli, getFirmwareUpdateStatus(_, _))
.Times(3)
.WillOnce(Return(FirmwareUpdateStatus::InProgress))
.WillOnce(Return(FirmwareUpdateStatus::InProgress))
.WillOnce(Return(FirmwareUpdateStatus::Done));
cli.doUpdateLogic(bus, args);
}
// Test for making sure doUpdate logic works correctly with multiple hoth
// instances
TEST_F(HothUpdaterCLITest, MultipleHothUpdateTest)
{
Args args;
args.hothId = "inst0,inst1";
args.updateMethod = "spi";
args.imageFilename = "dummy";
args.address = 0x01ef0000;
std::vector<uint8_t> dummy_image = {0, 1, 2, 3};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, readFileIntoByteArray(_))
.Times(1)
.WillOnce(Return(dummy_image));
EXPECT_CALL(cli, spiWrite(_, "inst0", _, _)).Times(1);
EXPECT_CALL(cli, spiWrite(_, "inst1", _, _)).Times(1);
EXPECT_CALL(cli, getFirmwareUpdateStatus(_, "inst0"))
.Times(1)
.WillOnce(Return(FirmwareUpdateStatus::Done));
EXPECT_CALL(cli, getFirmwareUpdateStatus(_, "inst1"))
.Times(1)
.WillOnce(Return(FirmwareUpdateStatus::Done));
cli.doUpdateLogic(bus, args);
}
// Test for enabling secureboot enforcement successfully
TEST_F(HothUpdaterCLITest, EnableSecurebootEnforcementSuccess)
{
Args args;
args.hothId = "inst0";
const std::vector<uint8_t> successResponse = {0x03, 0xfc, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, sendHostCommand(_, args.hothId, _))
.WillOnce(Return(successResponse));
EXPECT_NO_THROW(cli.doEnableSecurebootEnforcementLogic(bus, args));
}
// Test for enabling secureboot enforcement failure
TEST_F(HothUpdaterCLITest, EnableSecurebootEnforcementFailure)
{
Args args;
args.hothId = "inst0";
const std::vector<uint8_t> failureResponse = {0x03, 0xfc, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, sendHostCommand(_, args.hothId, _))
.WillOnce(Return(failureResponse));
EXPECT_THROW(cli.doEnableSecurebootEnforcementLogic(bus, args),
std::runtime_error);
}
TEST_F(HothUpdaterCLITest, getFailureSymptomKeySuccess)
{
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, getHothVersion(_, _))
.Times(1)
.WillOnce(Return(HothVersionStringsRsp{}));
EXPECT_CALL(cli, getTransportType(_, _)).Times(0);
EXPECT_EQ(cli.getFailureSymptomKey(bus, "inst0"),
"rot-install-generic-failure");
}
TEST_F(HothUpdaterCLITest, getFailureSymptomKeyInterfaceError)
{
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, getHothVersion(_, _))
.Times(1)
.WillOnce(testing::Invoke([](sdbusplus::bus::bus&, std::string_view)
-> HothVersionStringsRsp {
sd_bus_error err{};
sd_bus_error_set(
&err, "xyz.openbmc_project.Control.Hoth.Error.InterfaceError",
"some error");
throw sdbusplus::exception::SdBusError(&err, "prefix");
}));
EXPECT_CALL(cli, getTransportType(_, _)).Times(1).WillOnce(Return("usb"));
EXPECT_EQ(cli.getFailureSymptomKey(bus, "inst0"),
"rot-install-usb-failure");
}
TEST_F(HothUpdaterCLITest, getFailureSymptomKeyOtherError)
{
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, getHothVersion(_, _))
.Times(1)
.WillOnce(
testing::Invoke([](sdbusplus::bus::bus&,
std::string_view) -> HothVersionStringsRsp {
sd_bus_error err{};
sd_bus_error_set(
&err, "xyz.openbmc_project.Control.Hoth.Error.OtherError",
"some error");
throw sdbusplus::exception::SdBusError(&err, "prefix");
}));
EXPECT_CALL(cli, getTransportType(_, _)).Times(0);
EXPECT_EQ(cli.getFailureSymptomKey(bus, "inst0"),
"rot-install-generic-failure");
}
TEST_F(HothUpdaterCLITest, getFailureSymptomKeyGenericException)
{
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, getHothVersion(_, _))
.Times(1)
.WillOnce(
testing::Invoke([](sdbusplus::bus::bus&,
std::string_view) -> HothVersionStringsRsp {
throw std::runtime_error("generic error");
}));
EXPECT_CALL(cli, getTransportType(_, _)).Times(0);
EXPECT_EQ(cli.getFailureSymptomKey(bus, "inst0"),
"rot-install-generic-failure");
}
// Test retry logic: update fails initially, retries, and then succeeds
TEST_F(HothUpdaterCLITest, RetrySuccessTest)
{
Args args;
args.updateMethod = "spi";
args.imageFilename = "dummy";
args.address = 0x01ef0000;
std::vector<uint8_t> dummy_image = {0, 1, 2, 3};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, readFileIntoByteArray(_))
.Times(1)
.WillOnce(Return(dummy_image));
// Initial startUpdate calls spiWrite once; 1st retry calls spiWrite a 2nd
// time.
EXPECT_CALL(cli, spiWrite(_, _, _, _)).Times(2);
// Attempt 1 returns Error, causing a retry. Attempt 2 returns Done.
EXPECT_CALL(cli, getFirmwareUpdateStatus(_, _))
.Times(2)
.WillOnce(Return(FirmwareUpdateStatus::Error))
.WillOnce(Return(FirmwareUpdateStatus::Done));
cli.doUpdateLogic(bus, args);
}
// Test retry logic: update fails initially and on all retries, exhausting
// kAttemptLimit (3 attempts total: 1 initial + 2 retries)
TEST_F(HothUpdaterCLITest, RetryExhaustedTest)
{
Args args;
args.updateMethod = "spi";
args.imageFilename = "dummy";
args.address = 0x01ef0000;
std::vector<uint8_t> dummy_image = {0, 1, 2, 3};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, readFileIntoByteArray(_))
.Times(1)
.WillOnce(Return(dummy_image));
// Initial startUpdate calls spiWrite (1), plus 2 retries = 3 spiWrite calls
// total (matching kAttemptLimit = 3).
EXPECT_CALL(cli, spiWrite(_, _, _, _)).Times(3);
EXPECT_CALL(cli, getFirmwareUpdateStatus(_, _))
.Times(3)
.WillRepeatedly(Return(FirmwareUpdateStatus::Error));
EXPECT_CALL(cli, getHothVersion(_, _))
.Times(1)
.WillOnce(Return(HothVersionStringsRsp{}));
EXPECT_THROW(cli.doUpdateLogic(bus, args), std::runtime_error);
}
// Test retry logic: update fails initially, and when attempting to restart
// update for retry, startUpdate fails
TEST_F(HothUpdaterCLITest, RetryStartFailureTest)
{
Args args;
args.updateMethod = "spi";
args.imageFilename = "dummy";
args.address = 0x01ef0000;
std::vector<uint8_t> dummy_image = {0, 1, 2, 3};
sdbusplus::bus::bus bus{nullptr};
EXPECT_CALL(cli, readFileIntoByteArray(_))
.Times(1)
.WillOnce(Return(dummy_image));
// Initial spiWrite succeeds; 2nd spiWrite (retry 1) throws D-Bus exception
EXPECT_CALL(cli, spiWrite(_, _, _, _))
.Times(2)
.WillOnce(Return())
.WillOnce(testing::Invoke(
[](sdbusplus::bus::bus&, std::string_view,
const std::span<const uint8_t>, std::optional<uint32_t>) {
throw std::runtime_error("D-Bus connection error");
}));
EXPECT_CALL(cli, getFirmwareUpdateStatus(_, _))
.Times(1)
.WillOnce(Return(FirmwareUpdateStatus::Error));
EXPECT_THROW(cli.doUpdateLogic(bus, args), std::runtime_error);
}
} // namespace
} // namespace google::hoth::tools