test: common: associations

Test software associations in common code.
Write necessary wrappers in example device to use the protected members.

Change-Id: If7c38f12472699672ed8a4c1b3e1c99c398cdba5
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/test/common/exampledevice/example_device.cpp b/test/common/exampledevice/example_device.cpp
index 86606fb..9200bb2 100644
--- a/test/common/exampledevice/example_device.cpp
+++ b/test/common/exampledevice/example_device.cpp
@@ -41,13 +41,45 @@
     SoftwareManager(ctx, "ExampleUpdater" + std::to_string(uniqueSuffix))
 {}
 
+ExampleCodeUpdater::ExampleCodeUpdater(sdbusplus::async::context& ctx,
+                                       const char* swVersion) :
+    ExampleCodeUpdater(ctx)
+{
+    const std::string exampleInvObjPath =
+        "/xyz/openbmc_project/inventory/system/board/ExampleBoard/ExampleDevice";
+    auto exampleDevice = std::make_unique<ExampleDevice>(ctx, &(*this));
+
+    devices.insert({exampleInvObjPath, std::move(exampleDevice)});
+
+    if (swVersion)
+    {
+        auto& device = getDevice();
+        device->softwareCurrent =
+            std::make_unique<ExampleSoftware>(ctx, *device);
+        device->softwareCurrent->setVersion(swVersion);
+    }
+}
+
+std::unique_ptr<ExampleDevice>& ExampleCodeUpdater::getDevice()
+{
+    if (devices.empty())
+    {
+        throw std::invalid_argument(
+            "could not find any device, example CU wrongly initialized");
+    }
+
+    auto& deviceRef = devices.begin()->second;
+
+    return reinterpret_cast<std::unique_ptr<ExampleDevice>&>(deviceRef);
+}
+
 sdbusplus::async::task<bool> ExampleCodeUpdater::initDevice(
     const std::string& /*unused*/, const std::string& /*unused*/,
     SoftwareConfig& /*unused*/)
 {
     auto device = std::make_unique<ExampleDevice>(ctx, this);
 
-    device->softwareCurrent = std::make_unique<Software>(ctx, *device);
+    device->softwareCurrent = std::make_unique<ExampleSoftware>(ctx, *device);
 
     device->softwareCurrent->setVersion("v1.0",
                                         SoftwareVersion::VersionPurpose::Other);
@@ -91,3 +123,7 @@
 
     co_return true;
 }
+
+ExampleSoftware::ExampleSoftware(sdbusplus::async::context& ctx,
+                                 ExampleDevice& parent) : Software(ctx, parent)
+{}
diff --git a/test/common/exampledevice/example_device.hpp b/test/common/exampledevice/example_device.hpp
index 247fb07..52da92d 100644
--- a/test/common/exampledevice/example_device.hpp
+++ b/test/common/exampledevice/example_device.hpp
@@ -12,16 +12,26 @@
 namespace phosphor::software::example_device
 {
 
+class ExampleDevice;
+
 class ExampleCodeUpdater : public phosphor::software::manager::SoftwareManager
 {
   public:
     ExampleCodeUpdater(sdbusplus::async::context& ctx,
                        long uniqueSuffix = getRandomId());
 
+    // @param swVersion     if this is nullptr, do not create the software
+    // version.
+    ExampleCodeUpdater(sdbusplus::async::context& ctx, const char* swVersion);
+
+    std::unique_ptr<ExampleDevice>& getDevice();
+
     sdbusplus::async::task<bool> initDevice(const std::string& service,
                                             const std::string& path,
                                             SoftwareConfig& config) final;
 
+    using SoftwareManager::getBusName;
+
   private:
     static long getRandomId();
 };
@@ -34,11 +44,19 @@
 const std::string exampleInvObjPath =
     "/xyz/openbmc_project/inventory/system/board/ExampleBoard/ExampleDevice";
 
+class ExampleSoftware : public Software
+{
+  public:
+    using Software::createInventoryAssociation;
+    using Software::objectPath;
+    ExampleSoftware(sdbusplus::async::context& ctx, ExampleDevice& parent);
+};
+
 class ExampleDevice : public Device
 {
   public:
+    using Device::softwareCurrent;
     using Device::softwarePending;
-    using phosphor::software::device::Device::softwareCurrent;
 
     static SoftwareConfig defaultConfig;
 
diff --git a/test/common/software/meson.build b/test/common/software/meson.build
index e67ee78..62c97de 100644
--- a/test/common/software/meson.build
+++ b/test/common/software/meson.build
@@ -1,5 +1,9 @@
 
-testcases = ['software_get_random_softwareid', 'software_config']
+testcases = [
+    'software_get_random_softwareid',
+    'software_config',
+    'software_association',
+]
 
 foreach t : testcases
     test(
diff --git a/test/common/software/software_association.cpp b/test/common/software/software_association.cpp
new file mode 100644
index 0000000..c8da550
--- /dev/null
+++ b/test/common/software/software_association.cpp
@@ -0,0 +1,124 @@
+#include "../exampledevice/example_device.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>
+
+PHOSPHOR_LOG2_USING;
+
+using namespace phosphor::software;
+using namespace phosphor::software::example_device;
+
+constexpr const char* exampleEndpoint = "/xyz/example_endpoint";
+
+class SoftwareAssocTest : public testing::Test
+{
+  protected:
+    SoftwareAssocTest() :
+        exampleUpdater(ctx, "swVersion"), device(exampleUpdater.getDevice()),
+        objPathCurrentSoftware(
+            reinterpret_cast<ExampleSoftware*>(device->softwareCurrent.get())
+                ->objectPath),
+        busName(exampleUpdater.getBusName())
+    {}
+    ~SoftwareAssocTest() noexcept override {}
+
+    sdbusplus::async::context ctx;
+    ExampleCodeUpdater exampleUpdater;
+    std::unique_ptr<ExampleDevice>& device;
+
+    std::string objPathCurrentSoftware;
+
+    std::string busName;
+
+  public:
+    SoftwareAssocTest(const SoftwareAssocTest&) = delete;
+    SoftwareAssocTest(SoftwareAssocTest&&) = delete;
+    SoftwareAssocTest& operator=(const SoftwareAssocTest&) = delete;
+    SoftwareAssocTest& operator=(SoftwareAssocTest&&) = delete;
+};
+
+sdbusplus::async::task<> testSoftwareAssociationMissing(
+    sdbusplus::async::context& ctx, const std::string& objPathCurrentSoftware,
+    const std::string& busName)
+{
+    auto client =
+        sdbusplus::client::xyz::openbmc_project::association::Definitions<>(ctx)
+            .service(busName)
+            .path(objPathCurrentSoftware);
+
+    // by default there is no association on the software
+    try
+    {
+        co_await client.associations();
+
+        EXPECT_TRUE(false);
+    }
+    catch (std::exception& e)
+    {
+        error(e.what());
+    }
+
+    ctx.request_stop();
+    co_return;
+}
+
+TEST_F(SoftwareAssocTest, TestSoftwareAssociationMissing)
+{
+    ctx.spawn(
+        testSoftwareAssociationMissing(ctx, objPathCurrentSoftware, busName));
+    ctx.run();
+}
+
+sdbusplus::async::task<> testSoftwareAssociation(
+    sdbusplus::async::context& ctx, std::unique_ptr<ExampleDevice>& device,
+    const std::string& objPathCurrentSoftware, const std::string& busName,
+    bool createRunningAssoc, std::string expectAssociation)
+{
+    auto client =
+        sdbusplus::client::xyz::openbmc_project::association::Definitions<>(ctx)
+            .service(busName)
+            .path(objPathCurrentSoftware);
+
+    reinterpret_cast<ExampleSoftware*>(device->softwareCurrent.get())
+        ->createInventoryAssociation(createRunningAssoc, exampleEndpoint);
+
+    try
+    {
+        auto res = co_await client.associations();
+
+        EXPECT_EQ(res.size(), 1);
+        EXPECT_EQ(std::get<0>(res[0]), expectAssociation);
+        EXPECT_EQ(std::get<2>(res[0]), exampleEndpoint);
+    }
+    catch (std::exception& e)
+    {
+        error(e.what());
+        EXPECT_TRUE(false);
+    }
+
+    ctx.request_stop();
+    co_return;
+}
+
+TEST_F(SoftwareAssocTest, TestSoftwareAssociationRunning)
+{
+    ctx.spawn(testSoftwareAssociation(ctx, device, objPathCurrentSoftware,
+                                      busName, true, "running"));
+    ctx.run();
+}
+
+TEST_F(SoftwareAssocTest, TestSoftwareAssociationActivating)
+{
+    // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+    ctx.spawn(testSoftwareAssociation(ctx, device, objPathCurrentSoftware,
+                                      busName, false, "activating"));
+    ctx.run();
+}