blob: 8bfe8c508cce9b5c89a8a63380d8feff09f72e6c [file]
/*
*
* Copyright (c) 2026 Project CHIP Authors
*
* 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 <pw_unit_test/framework.h>
#include <app/clusters/actions-server/ActionsCluster.h>
#include <app/clusters/actions-server/ActionsDelegate.h>
#include <app/clusters/actions-server/ActionsStructs.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <lib/core/CHIPError.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/CHIPMem.h>
namespace chip {
namespace app {
namespace Clusters {
namespace Actions {
using namespace chip::app::Clusters::Actions::Attributes;
using namespace chip::Protocols::InteractionModel;
// Mock ActionsDelegate for testing
class MockActionsDelegate : public Delegate
{
public:
static constexpr uint8_t kMaxActions = 10;
static constexpr uint8_t kMaxEndpointLists = 10;
ActionStructStorage mActions[kMaxActions];
uint16_t mNumActions = 0;
EndpointListStorage mEndpointLists[kMaxEndpointLists];
uint16_t mNumEndpointLists = 0;
// Track if HandleInstantAction was called
bool mHandleInstantActionCalled = false;
uint16_t mLastActionId = 0;
Optional<uint32_t> mLastInvokeId;
Status mReturnStatus = Status::Success;
CHIP_ERROR ReadActionAtIndex(uint16_t index, ActionStructStorage & action) override
{
if (index >= mNumActions)
{
return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED;
}
action = mActions[index];
return CHIP_NO_ERROR;
}
CHIP_ERROR ReadEndpointListAtIndex(uint16_t index, EndpointListStorage & epList) override
{
if (index >= mNumEndpointLists)
{
return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED;
}
epList = mEndpointLists[index];
return CHIP_NO_ERROR;
}
bool HaveActionWithId(uint16_t actionId, uint16_t & aActionIndex) override
{
for (uint16_t i = 0; i < mNumActions; i++)
{
if (mActions[i].actionID == actionId)
{
aActionIndex = i;
return true;
}
}
return false;
}
// Test helper methods
CHIP_ERROR AddTestAction(const ActionStructStorage & action)
{
if (mNumActions >= kMaxActions)
{
return CHIP_ERROR_BUFFER_TOO_SMALL;
}
mActions[mNumActions++] = action;
return CHIP_NO_ERROR;
}
CHIP_ERROR AddTestEndpointList(const EndpointListStorage & epList)
{
if (mNumEndpointLists >= kMaxEndpointLists)
{
return CHIP_ERROR_BUFFER_TOO_SMALL;
}
mEndpointLists[mNumEndpointLists++] = epList;
return CHIP_NO_ERROR;
}
void Reset()
{
mNumActions = 0;
mNumEndpointLists = 0;
mHandleInstantActionCalled = false;
mLastActionId = 0;
mLastInvokeId = Optional<uint32_t>();
mReturnStatus = Status::Success;
}
// Command handler implementations
Status HandleInstantAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
mHandleInstantActionCalled = true;
mLastActionId = actionId;
mLastInvokeId = invokeId;
return mReturnStatus;
}
Status HandleInstantActionWithTransition(uint16_t actionId, uint16_t transitionTime, Optional<uint32_t> invokeId) override
{
return Status::UnsupportedCommand;
}
Status HandleStartAction(uint16_t actionId, Optional<uint32_t> invokeId) override { return Status::UnsupportedCommand; }
Status HandleStartActionWithDuration(uint16_t actionId, uint32_t duration, Optional<uint32_t> invokeId) override
{
return Status::UnsupportedCommand;
}
Status HandleStopAction(uint16_t actionId, Optional<uint32_t> invokeId) override { return Status::UnsupportedCommand; }
Status HandlePauseAction(uint16_t actionId, Optional<uint32_t> invokeId) override { return Status::UnsupportedCommand; }
Status HandlePauseActionWithDuration(uint16_t actionId, uint32_t duration, Optional<uint32_t> invokeId) override
{
return Status::UnsupportedCommand;
}
Status HandleResumeAction(uint16_t actionId, Optional<uint32_t> invokeId) override { return Status::UnsupportedCommand; }
Status HandleEnableAction(uint16_t actionId, Optional<uint32_t> invokeId) override { return Status::UnsupportedCommand; }
Status HandleEnableActionWithDuration(uint16_t actionId, uint32_t duration, Optional<uint32_t> invokeId) override
{
return Status::UnsupportedCommand;
}
Status HandleDisableAction(uint16_t actionId, Optional<uint32_t> invokeId) override { return Status::UnsupportedCommand; }
Status HandleDisableActionWithDuration(uint16_t actionId, uint32_t duration, Optional<uint32_t> invokeId) override
{
return Status::UnsupportedCommand;
}
};
// Test fixture for ActionsCluster tests
class TestActionsCluster : public ::testing::Test
{
public:
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
void SetUp() override { mDelegate.Reset(); }
MockActionsDelegate mDelegate;
};
// Test reading ActionList attribute
TEST_F(TestActionsCluster, TestReadActionListAttribute)
{
// Add test actions
ActionStructStorage action1(1, "FirstAction"_span, ActionTypeEnum::kScene, 0, BitMask<CommandBits>(),
ActionStateEnum::kInactive);
ActionStructStorage action2(2, "SecondAction"_span, ActionTypeEnum::kScene, 1, BitMask<CommandBits>(),
ActionStateEnum::kActive);
ASSERT_EQ(mDelegate.AddTestAction(action1), CHIP_NO_ERROR);
ASSERT_EQ(mDelegate.AddTestAction(action2), CHIP_NO_ERROR);
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to read the attribute
chip::Testing::ClusterTester tester(cluster);
ActionList::TypeInfo::DecodableType outList;
auto status = tester.ReadAttribute(ActionList::Id, outList);
ASSERT_TRUE(status.IsSuccess());
// Verify the list contains the expected actions
auto iter = outList.begin();
ASSERT_TRUE(iter.Next());
Structs::ActionStruct::DecodableType action = iter.GetValue();
EXPECT_EQ(action.actionID, 1);
EXPECT_EQ(action.type, ActionTypeEnum::kScene);
EXPECT_EQ(action.endpointListID, 0);
EXPECT_EQ(action.state, ActionStateEnum::kInactive);
ASSERT_TRUE(iter.Next());
action = iter.GetValue();
EXPECT_EQ(action.actionID, 2);
EXPECT_EQ(action.type, ActionTypeEnum::kScene);
EXPECT_EQ(action.endpointListID, 1);
EXPECT_EQ(action.state, ActionStateEnum::kActive);
EXPECT_FALSE(iter.Next());
}
// Test reading EndpointLists attribute
TEST_F(TestActionsCluster, TestReadEndpointListsAttribute)
{
// Add test endpoint lists
const EndpointId endpoints1[] = { 1, 2 };
const EndpointId endpoints2[] = { 3, 4, 5 };
EndpointListStorage epList1(1, "FirstList"_span, EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints1, 2));
EndpointListStorage epList2(2, "SecondList"_span, EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints2, 3));
ASSERT_EQ(mDelegate.AddTestEndpointList(epList1), CHIP_NO_ERROR);
ASSERT_EQ(mDelegate.AddTestEndpointList(epList2), CHIP_NO_ERROR);
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to read the attribute
chip::Testing::ClusterTester tester(cluster);
EndpointLists::TypeInfo::DecodableType outList;
auto status = tester.ReadAttribute(EndpointLists::Id, outList);
ASSERT_TRUE(status.IsSuccess());
// Verify the list contains the expected endpoint lists
auto iter = outList.begin();
ASSERT_TRUE(iter.Next());
Structs::EndpointListStruct::DecodableType epList = iter.GetValue();
EXPECT_EQ(epList.endpointListID, 1);
EXPECT_EQ(epList.type, EndpointListTypeEnum::kOther);
ASSERT_TRUE(iter.Next());
epList = iter.GetValue();
EXPECT_EQ(epList.endpointListID, 2);
EXPECT_EQ(epList.type, EndpointListTypeEnum::kOther);
EXPECT_FALSE(iter.Next());
}
// Test invoking InstantAction command successfully
TEST_F(TestActionsCluster, TestInvokeInstantActionSuccess)
{
// Add a test action with InstantAction command supported.
// We must pass a bitmask that explicitly enables the InstantAction command (Bit 0).
ActionStructStorage action(1, "TestAction"_span, ActionTypeEnum::kScene, 0,
BitMask<CommandBits>(1 << Commands::InstantAction::Id), ActionStateEnum::kInactive);
ASSERT_EQ(mDelegate.AddTestAction(action), CHIP_NO_ERROR);
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to invoke the command
chip::Testing::ClusterTester tester(cluster);
Commands::InstantAction::Type request;
request.actionID = 1;
request.invokeID = Optional<uint32_t>(12345);
auto result = tester.Invoke(request);
// Verify the command was successful
ASSERT_TRUE(result.status.has_value());
EXPECT_TRUE(result.status.value().IsSuccess()); // NOLINT(bugprone-unchecked-optional-access)
// Verify the delegate was called
EXPECT_TRUE(mDelegate.mHandleInstantActionCalled);
EXPECT_EQ(mDelegate.mLastActionId, 1);
// ASSERT_TRUE forces the test to stop evaluating immediately if it fails, preventing the crash
ASSERT_TRUE(mDelegate.mLastInvokeId.HasValue());
EXPECT_EQ(mDelegate.mLastInvokeId.Value(), 12345u); // NOLINT(bugprone-unchecked-optional-access)
}
// Test invoking InstantAction command with invalid action ID
TEST_F(TestActionsCluster, TestInvokeInstantActionNotFound)
{
// Create cluster instance without adding any actions
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to invoke the command
chip::Testing::ClusterTester tester(cluster);
Commands::InstantAction::Type request;
request.actionID = 999; // Non-existent action ID
request.invokeID = Optional<uint32_t>(12345);
auto result = tester.Invoke(request);
// Verify the command returned NotFound status
ASSERT_TRUE(result.status.has_value());
EXPECT_FALSE(result.status.value().IsSuccess()); // NOLINT(bugprone-unchecked-optional-access)
}
// Test invoking InstantAction command when delegate returns failure
TEST_F(TestActionsCluster, TestInvokeInstantActionDelegateFailure)
{
// Add a test action with InstantAction command supported
// Note: For this test, we're using an empty bitmask. The command support check
// in ActionsCluster.cpp would normally reject this, but we're testing the basic
// command invocation flow here.
ActionStructStorage action(1, "TestAction"_span, ActionTypeEnum::kScene, 0,
BitMask<CommandBits>(1 << Commands::InstantAction::Id), ActionStateEnum::kInactive);
ASSERT_EQ(mDelegate.AddTestAction(action), CHIP_NO_ERROR);
// Set the delegate to return a failure status
mDelegate.mReturnStatus = Status::Failure;
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to invoke the command
chip::Testing::ClusterTester tester(cluster);
Commands::InstantAction::Type request;
request.actionID = 1;
request.invokeID = Optional<uint32_t>(12345);
auto result = tester.Invoke(request);
// Verify the command returned the failure status from delegate
ASSERT_TRUE(result.status.has_value());
EXPECT_FALSE(result.status.value().IsSuccess()); // NOLINT(bugprone-unchecked-optional-access)
// Verify the delegate was still called
EXPECT_TRUE(mDelegate.mHandleInstantActionCalled);
}
// Test invoking InstantAction command with unsupported command
TEST_F(TestActionsCluster, TestInvokeInstantActionUnsupportedCommand)
{
// Add a test action WITHOUT InstantAction command supported
// Note: The command support check in ActionsCluster.cpp would reject commands
// that aren't marked as supported. For this test, we're verifying the basic
// functionality with an empty bitmask.
ActionStructStorage action(1, "TestAction"_span, ActionTypeEnum::kScene, 0, BitMask<CommandBits>(), ActionStateEnum::kInactive);
ASSERT_EQ(mDelegate.AddTestAction(action), CHIP_NO_ERROR);
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to invoke the command
chip::Testing::ClusterTester tester(cluster);
Commands::InstantAction::Type request;
request.actionID = 1;
request.invokeID = Optional<uint32_t>(12345);
auto result = tester.Invoke(request);
// Verify the command returned InvalidCommand status
ASSERT_TRUE(result.status.has_value());
EXPECT_FALSE(result.status.value().IsSuccess()); // NOLINT(bugprone-unchecked-optional-access)
// Verify the delegate was NOT called (command was rejected before delegate)
EXPECT_FALSE(mDelegate.mHandleInstantActionCalled);
}
// Test reading empty ActionList
TEST_F(TestActionsCluster, TestReadEmptyActionList)
{
// Don't add any actions
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to read the attribute
chip::Testing::ClusterTester tester(cluster);
ActionList::TypeInfo::DecodableType outList;
auto status = tester.ReadAttribute(ActionList::Id, outList);
ASSERT_TRUE(status.IsSuccess());
// Verify the list is empty
auto iter = outList.begin();
EXPECT_FALSE(iter.Next());
}
// Test reading empty EndpointLists
TEST_F(TestActionsCluster, TestReadEmptyEndpointLists)
{
// Don't add any endpoint lists
// Create cluster instance
ActionsCluster cluster(1, mDelegate);
// Use ClusterTester to read the attribute
chip::Testing::ClusterTester tester(cluster);
EndpointLists::TypeInfo::DecodableType outList;
auto status = tester.ReadAttribute(EndpointLists::Id, outList);
ASSERT_TRUE(status.IsSuccess());
// Verify the list is empty
auto iter = outList.begin();
EXPECT_FALSE(iter.Next());
}
} // namespace Actions
} // namespace Clusters
} // namespace app
} // namespace chip