fw update: tests for common code

Tests for the common code introduced in another commit.

These tests should check that the common code behaves as outlined in the
design [1]

References:
[1] https://github.com/openbmc/docs/blob/master/designs/code-update.md

Tested: unit tests pass

Change-Id: I8f12839afd47ef3403a80439af54fedcc00f10be
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/test/common/device/meson.build b/test/common/device/meson.build
new file mode 100644
index 0000000..b0b09c2
--- /dev/null
+++ b/test/common/device/meson.build
@@ -0,0 +1,27 @@
+
+testcases = [
+  'test_device_specific_update_function',
+  'test_device_start_update_async_success',
+  'test_device_start_update_async_invalid_fd',
+]
+
+foreach t : testcases
+  test(
+    t,
+    executable(
+      t,
+      f'@t@.cpp',
+      include_directories: [
+        common_include,
+      ],
+      dependencies: [
+        libpldm_dep,
+        sdbusplus_dep,
+        phosphor_logging_dep,
+        gtest,
+      ],
+      link_with: [libpldmutil, libpldmcreatepkg, software_common_lib, libnopdevice]
+    )
+  )
+endforeach
+
diff --git a/test/common/device/test_device_specific_update_function.cpp b/test/common/device/test_device_specific_update_function.cpp
new file mode 100644
index 0000000..903e10b
--- /dev/null
+++ b/test/common/device/test_device_specific_update_function.cpp
@@ -0,0 +1,73 @@
+#include "../nopdevice/nopdevice.hpp"
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+#include <gtest/gtest.h>
+
+class DeviceTest : public testing::Test
+{
+    void SetUp() override {}
+    void TearDown() override {}
+};
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testDeviceSpecificUpdateFunction(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    // NOLINTBEGIN
+    std::unique_ptr<SoftwareActivationProgress> sap =
+        std::make_unique<SoftwareActivationProgress>(ctx, "/");
+    uint8_t buffer[10];
+    size_t buffer_size = 10;
+    bool success =
+        co_await device->updateDevice((const uint8_t*)buffer, buffer_size, sap);
+
+    assert(success);
+
+    // NOLINTEND
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST_F(DeviceTest, TestDeviceConstructor)
+{
+    sdbusplus::async::context ctx;
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    assert(device->getEMConfigType().starts_with("Nop"));
+
+    // the software version is currently unknown
+    assert(device->softwareCurrent == nullptr);
+}
+
+TEST_F(DeviceTest, TestDeviceSpecificUpdateFunction)
+{
+    sdbusplus::async::context ctx;
+
+    // NOLINTBEGIN
+    ctx.spawn(testDeviceSpecificUpdateFunction(ctx));
+    // NOLINTEND
+
+    ctx.run();
+}
diff --git a/test/common/device/test_device_start_update_async_invalid_fd.cpp b/test/common/device/test_device_start_update_async_invalid_fd.cpp
new file mode 100644
index 0000000..c71f962
--- /dev/null
+++ b/test/common/device/test_device_start_update_async_invalid_fd.cpp
@@ -0,0 +1,71 @@
+#include "../nopdevice/nopdevice.hpp"
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+#include "test/create_package/create_pldm_fw_package.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+#include <gtest/gtest.h>
+
+class DeviceTest : public testing::Test
+{
+    void SetUp() override {}
+    void TearDown() override {}
+};
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testDeviceStartUpdateInvalidFD(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent =
+        std::make_unique<Software>(ctx, "swid_invalid_fd", *device);
+
+    device->softwareCurrent->setVersion("vUnknown");
+
+    std::unique_ptr<SoftwareActivationProgress> sap =
+        std::make_unique<SoftwareActivationProgress>(ctx, "/");
+
+    sdbusplus::message::unix_fd image;
+    image.fd = -1;
+
+    const auto applyTimeImmediate = sdbusplus::common::xyz::openbmc_project::
+        software::ApplyTime::RequestedApplyTimes::Immediate;
+
+    assert(!device->deviceSpecificUpdateFunctionCalled);
+
+    std::unique_ptr<Software> softwareUpdate =
+        std::make_unique<Software>(ctx, "new_swid", *device);
+
+    co_await device->startUpdateAsync(image, applyTimeImmediate,
+                                      std::move(softwareUpdate));
+
+    // assert the bad file descriptor was caught and we did not proceed
+    assert(!device->deviceSpecificUpdateFunctionCalled);
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST_F(DeviceTest, TestDeviceStartUpdateInvalidFD)
+{
+    sdbusplus::async::context ctx;
+
+    ctx.spawn(testDeviceStartUpdateInvalidFD(ctx));
+
+    ctx.run();
+}
diff --git a/test/common/device/test_device_start_update_async_success.cpp b/test/common/device/test_device_start_update_async_success.cpp
new file mode 100644
index 0000000..4682596
--- /dev/null
+++ b/test/common/device/test_device_start_update_async_success.cpp
@@ -0,0 +1,97 @@
+#include "../nopdevice/nopdevice.hpp"
+#include "test/create_package/create_pldm_fw_package.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+#include <gtest/gtest.h>
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testDeviceStartUpdateSuccess(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent = std::make_unique<Software>(ctx, "swid1", *device);
+
+    device->softwareCurrent->setVersion("vUnknown");
+
+    std::unique_ptr<SoftwareActivationProgress> sap =
+        std::make_unique<SoftwareActivationProgress>(ctx, "/");
+
+    const int fd = memfd_create("test_memfd", 0);
+
+    assert(fd >= 0);
+
+    lg2::debug("create fd {FD}", "FD", fd);
+
+    uint8_t component_image[] = {0x12, 0x34, 0x83, 0x21};
+
+    size_t size_out = 0;
+    std::unique_ptr<uint8_t[]> buf = create_pldm_package_buffer(
+        component_image, sizeof(component_image),
+        std::optional<uint32_t>(exampleVendorIANA),
+        std::optional<std::string>(exampleCompatible), size_out);
+
+    ssize_t bytes_written = write(fd, (void*)buf.get(), size_out);
+    if (bytes_written == -1)
+    {
+        std::cerr << "Failed to write to memfd: " << strerror(errno)
+                  << std::endl;
+        close(fd);
+        assert(false);
+    }
+    if (lseek(fd, 0, SEEK_SET) != 0)
+    {
+        lg2::error("could not seek to the beginning of the file");
+        assert(false);
+    }
+
+    int fd2 = dup(fd);
+    sdbusplus::message::unix_fd image2 = fd2;
+
+    if (fd2 < 0)
+    {
+        lg2::error("ERROR calling dup on fd: {ERR}", "ERR", strerror(errno));
+        assert(false);
+    }
+
+    lg2::debug("dup fd: {FD}", "FD", fd2);
+
+    const auto applyTimeImmediate = sdbusplus::common::xyz::openbmc_project::
+        software::ApplyTime::RequestedApplyTimes::Immediate;
+
+    std::unique_ptr<Software> softwareUpdate =
+        std::make_unique<Software>(ctx, "myotherswid", *device);
+
+    co_await device->startUpdateAsync(image2, applyTimeImmediate,
+                                      std::move(softwareUpdate));
+
+    assert(device->deviceSpecificUpdateFunctionCalled);
+
+    close(fd);
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(DeviceTest, TestDeviceStartUpdateSuccess)
+{
+    sdbusplus::async::context ctx;
+
+    ctx.spawn(testDeviceStartUpdateSuccess(ctx));
+
+    ctx.run();
+}
diff --git a/test/common/meson.build b/test/common/meson.build
index f808149..fa29507 100644
--- a/test/common/meson.build
+++ b/test/common/meson.build
@@ -1 +1,34 @@
 subdir('pldm')
+subdir('nopdevice')
+subdir('device')
+subdir('software')
+
+testcases = [
+  'test_device_config',
+  'test_software_update',
+]
+
+foreach t : testcases
+  test(
+    t,
+    executable(
+      t,
+      f'@t@.cpp',
+      include_directories: [
+        common_include,
+      ],
+      dependencies: [
+        libpldm_dep,
+        sdbusplus_dep,
+        phosphor_logging_dep,
+        gtest_main,
+      ],
+      link_with: [
+        libpldmutil,
+        libpldmcreatepkg,
+        software_common_lib,
+        libnopdevice,
+      ]
+    )
+  )
+endforeach
diff --git a/test/common/nopdevice/meson.build b/test/common/nopdevice/meson.build
new file mode 100644
index 0000000..be943f5
--- /dev/null
+++ b/test/common/nopdevice/meson.build
@@ -0,0 +1,15 @@
+libnopdevice = static_library('nopdevice',
+  'nopdevice.cpp',
+  include_directories: ['.', common_include],
+  dependencies: [
+    pdi_dep,
+    phosphor_logging_dep,
+    sdbusplus_dep,
+    libpldm_dep,
+  ],
+  link_with: [
+    software_common_lib,
+  ],
+  install: false,
+)
+
diff --git a/test/common/nopdevice/nopdevice.cpp b/test/common/nopdevice/nopdevice.cpp
new file mode 100644
index 0000000..ddf6f6e
--- /dev/null
+++ b/test/common/nopdevice/nopdevice.cpp
@@ -0,0 +1,91 @@
+#include "nopdevice.hpp"
+
+#include "common/include/device.hpp"
+#include "common/include/device_config.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+static long getRandomId()
+{
+    struct timespec ts;
+    clock_gettime(CLOCK_REALTIME, &ts);
+    unsigned int seed = ts.tv_nsec ^ getpid();
+    srandom(seed);
+    return random() % 10000;
+}
+
+// nop code updater needs unique suffix on dbus for parallel unit testing
+NopCodeUpdater::NopCodeUpdater(sdbusplus::async::context& ctx) :
+    NopCodeUpdater(ctx, getRandomId())
+{}
+
+NopCodeUpdater::NopCodeUpdater(sdbusplus::async::context& ctx,
+                               long uniqueSuffix) :
+    SoftwareManager(ctx, "Nop" + std::to_string(uniqueSuffix), true)
+{}
+
+// NOLINTBEGIN
+sdbusplus::async::task<> NopCodeUpdater::getInitialConfigurationSingleDevice(
+    const std::string& /*unused*/, const std::string& /*unused*/,
+    DeviceConfig& /*unused*/)
+// NOLINTEND
+{
+    co_return;
+}
+
+NopDevice::NopDevice(sdbusplus::async::context& ctx, const DeviceConfig& config,
+                     SoftwareManager* parent) :
+    Device(ctx, true, config, parent)
+{}
+NopDevice::NopDevice(sdbusplus::async::context& ctx, SoftwareManager* parent) :
+    Device(ctx, true,
+           DeviceConfig(exampleVendorIANA, exampleCompatible, "Nop",
+                        exampleName),
+           parent)
+{}
+
+// NOLINTBEGIN
+sdbusplus::async::task<bool> NopDevice::updateDevice(
+    const uint8_t* /*unused*/, size_t compImageSize,
+    std::unique_ptr<SoftwareActivationProgress>& activationProgress)
+// NOLINTEND
+{
+    lg2::debug(
+        "[nop device] called device specific update function with image size {SIZE}",
+        "SIZE", compImageSize);
+
+    deviceSpecificUpdateFunctionCalled = true;
+
+    // Setting this property for demonstration purpose.
+    // For a real device, this could represent the
+    // percentage completion of writing the firmware,
+    // and any progress made in the update process within this function.
+    activationProgress->progress(30);
+
+    // There is no hard constraint on the values here,
+    // we do not have to reach any specific percentage.
+    // The percentage should be monotonic and increasing.
+    activationProgress->progress(90);
+
+    co_return true;
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<std::string> NopDevice::getInventoryItemObjectPath()
+// NOLINTEND
+{
+    // for a real device, it could find its inventory item via associations and
+    // the configuration interface, or go via the mapper to find the
+    // specific inventory item interface and then get the path from there.
+
+    co_return "/xyz/openbmc_project/inventory/item/my_nop_item";
+}
diff --git a/test/common/nopdevice/nopdevice.hpp b/test/common/nopdevice/nopdevice.hpp
new file mode 100644
index 0000000..0f3b24e
--- /dev/null
+++ b/test/common/nopdevice/nopdevice.hpp
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "common/include/device.hpp"
+#include "common/include/software_manager.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/server.hpp>
+#include <xyz/openbmc_project/Software/Update/server.hpp>
+
+#include <memory>
+
+class NopCodeUpdater : public SoftwareManager
+{
+  public:
+    NopCodeUpdater(sdbusplus::async::context& ctx);
+    NopCodeUpdater(sdbusplus::async::context& ctx, long uniqueSuffix);
+
+    sdbusplus::async::task<> getInitialConfigurationSingleDevice(
+        const std::string& service, const std::string& path,
+        DeviceConfig& config) final;
+};
+
+const std::string exampleConfigObjPath =
+    "/xyz/openbmc_project/inventory/system/board/Tyan_S5549_Baseboard/HostSPIFlash";
+const std::string exampleName = "HostSPIFlash";
+
+const uint32_t exampleVendorIANA = 0x0000a015;
+const std::string exampleCompatible = "com.example.compatible";
+
+class NopDevice : public Device
+{
+  public:
+    NopDevice(sdbusplus::async::context& ctx, const DeviceConfig& config,
+              SoftwareManager* parent);
+    NopDevice(sdbusplus::async::context& ctx, SoftwareManager* parent);
+
+    // NOLINTBEGIN
+    sdbusplus::async::task<bool> updateDevice(
+        const uint8_t* image, size_t image_size,
+        std::unique_ptr<SoftwareActivationProgress>& activationProgress)
+        override;
+    // NOLINTEND
+
+    sdbusplus::async::task<std::string> getInventoryItemObjectPath() override;
+
+    bool deviceSpecificUpdateFunctionCalled = false;
+};
diff --git a/test/common/software/meson.build b/test/common/software/meson.build
new file mode 100644
index 0000000..76813c5
--- /dev/null
+++ b/test/common/software/meson.build
@@ -0,0 +1,33 @@
+
+testcases = [
+  'test_software',
+  'test_software_association',
+  'test_software_version',
+  'test_software_get_random_softwareid',
+]
+
+foreach t : testcases
+  test(
+    t,
+    executable(
+      t,
+      f'@t@.cpp',
+      include_directories: [
+        common_include,
+        '../device/',
+      ],
+      dependencies: [
+        libpldm_dep,
+        sdbusplus_dep,
+        phosphor_logging_dep,
+        gtest,
+      ],
+      link_with: [
+        libpldmutil,
+        software_common_lib,
+        libnopdevice,
+      ]
+    )
+  )
+endforeach
+
diff --git a/test/common/software/test_software.cpp b/test/common/software/test_software.cpp
new file mode 100644
index 0000000..5eae56d
--- /dev/null
+++ b/test/common/software/test_software.cpp
@@ -0,0 +1,79 @@
+#include "../nopdevice/nopdevice.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+
+#include <memory>
+#include <regex>
+
+#include <gtest/gtest.h>
+
+// NOLINTBEGIN
+sdbusplus::async::task<> endContext(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(SoftwareTest, TestSoftwareConstructor)
+{
+    sdbusplus::async::context ctx;
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    // the software version is currently unknown
+    assert(device->softwareCurrent == nullptr);
+
+    auto sw = std::make_unique<Software>(ctx, "swid_test_constructor", *device);
+
+    // since that software is not an update, there is no progress
+    assert(sw->optSoftwareActivationProgress == nullptr);
+
+    // test succeeds if we construct without any exception
+    // and can run the context
+
+    ctx.spawn(endContext(ctx));
+    ctx.run();
+}
+
+TEST(SoftwareTest, TestSoftwareId)
+{
+    sdbusplus::async::context ctx;
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    // the software version is currently unknown
+    assert(device->softwareCurrent == nullptr);
+
+    auto sw = std::make_unique<Software>(ctx, "Nop_swid_test_swid", *device);
+
+    // design: Swid = <DeviceX>_<RandomId>
+    assert(sw->swid.starts_with("Nop"));
+}
+
+TEST(SoftwareTest, TestSoftwareObjectPath)
+{
+    sdbusplus::async::context ctx;
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    auto sw = std::make_unique<Software>(ctx, "swid_test_obj_path", *device);
+
+    lg2::debug("{PATH}", "PATH", sw->getObjectPath());
+
+    // assert that the object path is as per the design
+    // design: /xyz/openbmc_project/Software/<SwId>
+    assert(std::string(sw->getObjectPath())
+               .starts_with("/xyz/openbmc_project/software/"));
+}
diff --git a/test/common/software/test_software_association.cpp b/test/common/software/test_software_association.cpp
new file mode 100644
index 0000000..f845af8
--- /dev/null
+++ b/test/common/software/test_software_association.cpp
@@ -0,0 +1,176 @@
+#include "../nopdevice/nopdevice.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/client.hpp>
+
+#include <memory>
+
+#include <gtest/gtest.h>
+
+// const std::string objPathInventory = "/xyz/openbmc_project/inventory/item9";
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testSoftwareAssociationMissing(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    std::string service = nopcu.setupBusName();
+
+    std::unique_ptr<NopDevice> device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent =
+        std::make_unique<Software>(ctx, "myswid", *device);
+
+    std::string objPathCurrentSoftware =
+        device->softwareCurrent->getObjectPath();
+
+    auto client =
+        sdbusplus::client::xyz::openbmc_project::association::Definitions<>(ctx)
+            .service(service)
+            .path(objPathCurrentSoftware);
+
+    // by default there is no association on the software
+    try
+    {
+        co_await client.associations();
+
+        assert(false);
+    }
+    catch (std::exception& e)
+    {
+        lg2::debug(e.what());
+    }
+
+    co_await device->softwareCurrent
+        ->setAssociationDefinitionsRunningActivating(false, false);
+
+    assert((co_await client.associations()).empty());
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(SoftwareTest, TestSoftwareAssociationMissing)
+{
+    sdbusplus::async::context ctx;
+
+    ctx.spawn(testSoftwareAssociationMissing(ctx));
+
+    ctx.run();
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testSoftwareAssociationRunning(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    std::string service = nopcu.setupBusName();
+
+    std::unique_ptr<NopDevice> device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent =
+        std::make_unique<Software>(ctx, "myotherswid", *device);
+
+    std::string objPathCurrentSoftware =
+        device->softwareCurrent->getObjectPath();
+
+    auto client =
+        sdbusplus::client::xyz::openbmc_project::association::Definitions<>(ctx)
+            .service(service)
+            .path(objPathCurrentSoftware);
+
+    co_await device->softwareCurrent
+        ->setAssociationDefinitionsRunningActivating(true, false);
+
+    auto res = co_await client.associations();
+
+    assert(res.size() == 1);
+
+    auto assoc = res[0];
+
+    std::string forward = std::get<0>(assoc);
+    std::string reverse = std::get<1>(assoc);
+    std::string endpoint = std::get<2>(assoc);
+
+    assert(forward == "running");
+    assert(reverse == "ran_on");
+    assert(endpoint == (co_await device->getInventoryItemObjectPath()));
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(SoftwareTest, TestSoftwareAssociationRunning)
+{
+    sdbusplus::async::context ctx;
+
+    ctx.spawn(testSoftwareAssociationRunning(ctx));
+
+    ctx.run();
+}
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testSoftwareAssociationActivating(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    std::string service = nopcu.setupBusName();
+
+    std::unique_ptr<NopDevice> device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent =
+        std::make_unique<Software>(ctx, "newswid", *device);
+
+    std::string objPathCurrentSoftware =
+        device->softwareCurrent->getObjectPath();
+
+    auto client =
+        sdbusplus::client::xyz::openbmc_project::association::Definitions<>(ctx)
+            .service(service)
+            .path(objPathCurrentSoftware);
+
+    co_await device->softwareCurrent
+        ->setAssociationDefinitionsRunningActivating(false, true);
+
+    auto res = co_await client.associations();
+
+    assert(res.size() == 1);
+
+    auto assoc = res[0];
+
+    std::string forward = std::get<0>(assoc);
+    std::string reverse = std::get<1>(assoc);
+    std::string endpoint = std::get<2>(assoc);
+
+    assert(forward == "activating");
+    assert(reverse == "activated_on");
+    assert(endpoint == (co_await device->getInventoryItemObjectPath()));
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(SoftwareTest, TestSoftwareAssociationActivating)
+{
+    sdbusplus::async::context ctx;
+
+    ctx.spawn(testSoftwareAssociationActivating(ctx));
+
+    ctx.run();
+}
diff --git a/test/common/software/test_software_get_random_softwareid.cpp b/test/common/software/test_software_get_random_softwareid.cpp
new file mode 100644
index 0000000..89b8efe
--- /dev/null
+++ b/test/common/software/test_software_get_random_softwareid.cpp
@@ -0,0 +1,43 @@
+#include "../nopdevice/nopdevice.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/server.hpp>
+
+#include <memory>
+#include <regex>
+
+#include <gtest/gtest.h>
+
+class WrapSoftware : public Software
+{
+  public:
+    static std::string wrapGetRandomSoftwareId(Device& parent)
+    {
+        return Software::getRandomSoftwareId(parent);
+    };
+};
+
+TEST(SoftwareTest, TestSoftwareGetRandomSoftwareId)
+{
+    sdbusplus::async::context ctx;
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    DeviceConfig config(0x1234, "my.example.compatible", "Nop",
+                        "MB1NopComponent");
+
+    auto device = std::make_unique<NopDevice>(ctx, config, cu);
+
+    std::string swid = WrapSoftware::wrapGetRandomSoftwareId(*device);
+    lg2::debug("{SWID}", "SWID", swid);
+
+    std::regex re("[a-zA-Z0-9]+_[a-zA-Z0-9]+_[0-9]+");
+    std::cmatch m;
+
+    EXPECT_TRUE(std::regex_match(swid.c_str(), m, re));
+
+    EXPECT_TRUE(swid.starts_with("Nop_MB1NopComponent_"));
+}
diff --git a/test/common/software/test_software_version.cpp b/test/common/software/test_software_version.cpp
new file mode 100644
index 0000000..0fc216f
--- /dev/null
+++ b/test/common/software/test_software_version.cpp
@@ -0,0 +1,77 @@
+#include "../nopdevice/nopdevice.hpp"
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <unistd.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Software/Update/client.hpp>
+#include <xyz/openbmc_project/Software/Version/client.hpp>
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+
+#include <gtest/gtest.h>
+
+// NOLINTBEGIN
+sdbusplus::async::task<> testSoftwareVersion(sdbusplus::async::context& ctx)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    const std::string service = nopcu.setupBusName();
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent =
+        std::make_unique<Software>(ctx, "swid_sw_version", *device);
+
+    std::string objPathCurrentSoftware =
+        device->softwareCurrent->getObjectPath();
+
+    auto clientVersion =
+        sdbusplus::client::xyz::openbmc_project::software::Version<>(ctx)
+            .service(service)
+            .path(objPathCurrentSoftware);
+
+    // the version is unavailable at this point
+    try
+    {
+        co_await clientVersion.version();
+        assert(false);
+    }
+    catch (std::exception& e)
+    {
+        lg2::debug(e.what());
+    }
+
+    // now the version is available
+    {
+        device->softwareCurrent->setVersion("v12.6");
+
+        assert((co_await clientVersion.version()) == "v12.6");
+    }
+
+    // we cannot set the version twice
+    {
+        device->softwareCurrent->setVersion("v20");
+
+        assert((co_await clientVersion.version()) == "v12.6");
+    }
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(SoftwareUpdate, TestSoftwareUpdate)
+{
+    sdbusplus::async::context ctx;
+
+    ctx.spawn(testSoftwareVersion(ctx));
+
+    ctx.run();
+}
diff --git a/test/common/test_device_config.cpp b/test/common/test_device_config.cpp
new file mode 100644
index 0000000..a4f0422
--- /dev/null
+++ b/test/common/test_device_config.cpp
@@ -0,0 +1,47 @@
+
+#include "common/include/device_config.hpp"
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <unistd.h>
+
+#include <phosphor-logging/lg2.hpp>
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+
+#include <gtest/gtest.h>
+
+TEST(DeviceConfig, success)
+{
+    DeviceConfig config(0x0324, "com.example.SampleCorp", "ConfigType",
+                        "SampleComponent");
+
+    ASSERT_EQ(config.vendorIANA, 0x0324);
+    ASSERT_EQ(config.compatibleHardware, "com.example.SampleCorp");
+}
+
+TEST(DeviceConfig, failureCompatibleNoDot)
+{
+    try
+    {
+        DeviceConfig config(0x0324, "comexamplesamplecorp", "ConfigType",
+                            "SampleComponent");
+        ASSERT_FALSE(true);
+    }
+    catch (std::exception& /*unused*/)
+    {}
+}
+
+TEST(DeviceConfig, failureCompatibleInvalidChar)
+{
+    try
+    {
+        DeviceConfig config(0x0324, "com-examplesamplecorp#", "ConfigType",
+                            "SampleComponent");
+        ASSERT_FALSE(true);
+    }
+    catch (std::exception& /*unused*/)
+    {}
+}
diff --git a/test/common/test_software_update.cpp b/test/common/test_software_update.cpp
new file mode 100644
index 0000000..1a2571a
--- /dev/null
+++ b/test/common/test_software_update.cpp
@@ -0,0 +1,140 @@
+
+#include "nopdevice/nopdevice.hpp"
+#include "test/create_package/create_pldm_fw_package.hpp"
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <unistd.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <xyz/openbmc_project/Association/Definitions/client.hpp>
+#include <xyz/openbmc_project/Software/Update/client.hpp>
+#include <xyz/openbmc_project/Software/Version/client.hpp>
+
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+
+#include <gtest/gtest.h>
+
+// NOLINTBEGIN
+sdbusplus::async::task<>
+    testSoftwareUpdateSuccess(sdbusplus::async::context& ctx, int fd)
+// NOLINTEND
+{
+    NopCodeUpdater nopcu(ctx);
+    NopCodeUpdater* cu = &nopcu;
+
+    const std::string service = nopcu.setupBusName();
+
+    auto device = std::make_unique<NopDevice>(ctx, cu);
+
+    device->softwareCurrent =
+        std::make_unique<Software>(ctx, "myswid", *device);
+
+    device->softwareCurrent->setVersion("v12.345");
+
+    std::unique_ptr<SoftwareActivationProgress> sap =
+        std::make_unique<SoftwareActivationProgress>(ctx, "/");
+
+    const auto applyTimeImmediate = sdbusplus::common::xyz::openbmc_project::
+        software::ApplyTime::RequestedApplyTimes::Immediate;
+
+    device->softwareCurrent->enableUpdate({applyTimeImmediate});
+
+    std::string objPathCurrentSoftware =
+        device->softwareCurrent->getObjectPath();
+
+    // go via dbus to call the dbus method to start the update
+    auto client =
+        sdbusplus::client::xyz::openbmc_project::software::Update<>(ctx)
+            .service(service)
+            .path(objPathCurrentSoftware);
+
+    sdbusplus::message::object_path objPathNewSoftware =
+        co_await client.start_update(fd, applyTimeImmediate);
+
+    // TODO: somehow remove this sleep
+    co_await sdbusplus::async::sleep_for(ctx, std::chrono::seconds(2));
+
+    assert(objPathNewSoftware != objPathCurrentSoftware);
+
+    // assert that update function was called
+    assert(device->deviceSpecificUpdateFunctionCalled);
+
+    auto clientNewVersion =
+        sdbusplus::client::xyz::openbmc_project::software::Version<>(ctx)
+            .service(service)
+            .path(objPathNewSoftware.str);
+
+    const std::string newVersion = co_await clientNewVersion.version();
+
+    // assert the new version is not the old version
+    assert(newVersion != "v12.345");
+    assert(newVersion == "VersionString1");
+
+    auto clientAssoc =
+        sdbusplus::client::xyz::openbmc_project::association::Definitions<>(ctx)
+            .service(service)
+            .path(objPathNewSoftware.str);
+
+    // assert that the new version is associated as the running version
+    {
+        auto res = co_await clientAssoc.associations();
+        assert(res.size() == 1);
+
+        auto assoc = res[0];
+
+        std::string forward = std::get<0>(assoc);
+        std::string reverse = std::get<1>(assoc);
+        std::string endpoint = std::get<2>(assoc);
+
+        assert(forward == "running");
+        assert(reverse == "ran_on");
+        assert(endpoint == (co_await device->getInventoryItemObjectPath()));
+    }
+
+    ctx.request_stop();
+
+    co_return;
+}
+
+TEST(SoftwareUpdate, TestSoftwareUpdate)
+{
+    sdbusplus::async::context ctx;
+
+    int fd = memfd_create("test_memfd", 0);
+
+    assert(fd >= 0);
+
+    lg2::debug("create fd {FD}", "FD", fd);
+
+    uint8_t component_image[] = {0x12, 0x34, 0x83, 0x21};
+
+    size_t size_out = 0;
+    std::unique_ptr<uint8_t[]> buf = create_pldm_package_buffer(
+        component_image, sizeof(component_image),
+        std::optional<uint32_t>(exampleVendorIANA),
+        std::optional<std::string>(exampleCompatible), size_out);
+
+    ssize_t bytes_written = write(fd, (void*)buf.get(), size_out);
+    if (bytes_written == -1)
+    {
+        std::cerr << "Failed to write to memfd: " << strerror(errno)
+                  << std::endl;
+        close(fd);
+        assert(false);
+    }
+    if (lseek(fd, 0, SEEK_SET) != 0)
+    {
+        lg2::error("could not seek to the beginning of the file");
+        assert(false);
+    }
+
+    ctx.spawn(testSoftwareUpdateSuccess(ctx, fd));
+
+    ctx.run();
+
+    close(fd);
+}