blob: b4f28addc3cc6991742dced6d483967115e33176 [file] [log] [blame]
/*
*
* Copyright (c) 2025 Project CHIP Authors
* All rights reserved.
*
* 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 <vector>
#include "lib/support/CHIPMem.h"
#include <app-common/zap-generated/cluster-objects.h>
#include <app/clusters/actions-server/actions-server.h>
#include <app/tests/test-ember-api.h>
#include <app/util/attribute-storage.h>
#include <lib/core/ErrorStr.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/core/TLV.h>
#include <lib/core/TLVDebug.h>
#include <lib/core/TLVUtilities.h>
#include <lib/support/CHIPCounter.h>
#include <messaging/ExchangeContext.h>
#include <messaging/Flags.h>
#include <protocols/interaction_model/Constants.h>
#include <pw_unit_test/framework.h>
namespace chip {
namespace app {
using namespace Clusters::Actions;
using namespace chip::Protocols::InteractionModel;
class TestActionsDelegateImpl : public Clusters::Actions::Delegate
{
public:
static constexpr uint8_t kMaxActionNameLength = 128u;
static constexpr uint8_t kMaxEndpointListNameLength = 128u;
static constexpr uint16_t kEndpointListMaxSize = 256u;
static constexpr uint8_t kMaxActions = 2;
static constexpr uint8_t kMaxEndpointLists = 2;
ActionStructStorage mActions[kMaxActions];
uint16_t mNumActions = 0;
EndpointListStorage mEndpointLists[kMaxEndpointLists];
uint16_t mNumEndpointLists = 0;
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;
}
Protocols::InteractionModel::Status HandleInstantAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleInstantActionWithTransition(uint16_t actionId, uint16_t transitionTime,
Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleStartAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleStartActionWithDuration(uint16_t actionId, uint32_t duration,
Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleStopAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandlePauseAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandlePauseActionWithDuration(uint16_t actionId, uint32_t duration,
Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleResumeAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleEnableAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleEnableActionWithDuration(uint16_t actionId, uint32_t duration,
Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleDisableAction(uint16_t actionId, Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
Protocols::InteractionModel::Status HandleDisableActionWithDuration(uint16_t actionId, uint32_t duration,
Optional<uint32_t> invokeId) override
{
return Status::NotFound;
}
};
static TestActionsDelegateImpl * sActionsDelegateImpl = nullptr;
static ActionsServer * sActionsServer = nullptr;
class TestActionsCluster : public ::testing::Test
{
public:
static void SetUpTestSuite()
{
ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR);
sActionsDelegateImpl = new TestActionsDelegateImpl;
sActionsServer = new ActionsServer(1, *sActionsDelegateImpl);
sActionsServer->Init();
}
static void TearDownTestSuite()
{
sActionsServer->Shutdown();
delete sActionsDelegateImpl;
delete sActionsServer;
chip::Platform::MemoryShutdown();
}
};
TEST_F(TestActionsCluster, TestActionListConstraints)
{
// Test 1: Action name length constraint
TestActionsDelegateImpl delegate = *sActionsDelegateImpl;
delegate.mNumActions = 0;
char longName[kActionNameMaxSize + 10];
memset(longName, 'A', sizeof(longName));
longName[sizeof(longName) - 1] = '\0';
ActionStructStorage actionWithLongName(1, CharSpan::fromCharString(longName), ActionTypeEnum::kScene, 0, BitMask<CommandBits>(),
ActionStateEnum::kInactive);
// Add action and verify it was added successfully
EXPECT_EQ(delegate.AddTestAction(actionWithLongName), CHIP_NO_ERROR);
// Verify the name was truncated by reading it back
ActionStructStorage readAction;
EXPECT_EQ(delegate.ReadActionAtIndex(0, readAction), CHIP_NO_ERROR);
EXPECT_EQ(readAction.name.size(), kActionNameMaxSize);
// Test 2: Maximum action list size
delegate.mNumActions = 0;
for (uint16_t i = 0; i < delegate.kMaxActions; i++)
{
ActionStructStorage action(i, CharSpan::fromCharString("Action"), ActionTypeEnum::kScene, 0, BitMask<CommandBits>(),
ActionStateEnum::kInactive);
EXPECT_EQ(delegate.AddTestAction(action), CHIP_NO_ERROR);
}
// Try to add one more action beyond the limit
ActionStructStorage extraAction(99, CharSpan::fromCharString("Extra"), ActionTypeEnum::kScene, 0, BitMask<CommandBits>(),
ActionStateEnum::kInactive);
EXPECT_EQ(delegate.AddTestAction(extraAction), CHIP_ERROR_BUFFER_TOO_SMALL);
}
TEST_F(TestActionsCluster, TestEndpointListConstraints)
{
// Test 1: Endpoint list name length constraint
TestActionsDelegateImpl delegate = *sActionsDelegateImpl;
delegate.mNumEndpointLists = 0;
char longName[kEndpointListNameMaxSize + 10];
memset(longName, 'B', sizeof(longName));
longName[sizeof(longName) - 1] = '\0';
const EndpointId endpoints[] = { 1, 2, 3 };
EndpointListStorage epListWithLongName(1, CharSpan::fromCharString(longName), EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints));
// Add endpoint list and verify it was added successfully
EXPECT_EQ(delegate.AddTestEndpointList(epListWithLongName), CHIP_NO_ERROR);
// Verify the name was truncated by reading it back
EndpointListStorage readEpList;
EXPECT_EQ(delegate.ReadEndpointListAtIndex(0, readEpList), CHIP_NO_ERROR);
EXPECT_EQ(readEpList.name.size(), kEndpointListNameMaxSize);
// Test 2: Maximum endpoint list size
delegate.mNumEndpointLists = 0;
for (uint16_t i = 0; i < delegate.kMaxEndpointLists; i++)
{
EndpointListStorage epList(i, CharSpan::fromCharString("List"), EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints));
EXPECT_EQ(delegate.AddTestEndpointList(epList), CHIP_NO_ERROR);
}
// Try to add one more endpoint list beyond the limit
EndpointListStorage extraEpList(99, CharSpan::fromCharString("Extra"), EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints));
EXPECT_EQ(delegate.AddTestEndpointList(extraEpList), CHIP_ERROR_BUFFER_TOO_SMALL);
// Test 3: Maximum endpoints per list
delegate.mNumEndpointLists = 0;
EndpointId tooManyEndpoints[kEndpointListMaxSize + 5];
for (uint16_t i = 0; i < kEndpointListMaxSize + 5; i++)
{
tooManyEndpoints[i] = i;
}
EndpointListStorage epListWithTooManyEndpoints(1, CharSpan::fromCharString("List"), EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(tooManyEndpoints));
// The list should be added but truncated to kEndpointListMaxSize
EXPECT_EQ(delegate.AddTestEndpointList(epListWithTooManyEndpoints), CHIP_NO_ERROR);
// Verify the endpoint list was truncated
EXPECT_EQ(delegate.ReadEndpointListAtIndex(0, readEpList), CHIP_NO_ERROR);
EXPECT_EQ(readEpList.endpoints.size(), kEndpointListMaxSize);
}
TEST_F(TestActionsCluster, TestActionListAttributeAccess)
{
TestActionsDelegateImpl * delegate = sActionsDelegateImpl;
delegate->mNumActions = 0;
// Add test actions
ActionStructStorage action1(1, CharSpan::fromCharString("FirstAction"), ActionTypeEnum::kScene, 0, BitMask<CommandBits>(),
ActionStateEnum::kInactive);
ActionStructStorage action2(2, CharSpan::fromCharString("SecondAction"), ActionTypeEnum::kScene, 1, BitMask<CommandBits>(),
ActionStateEnum::kActive);
EXPECT_EQ(delegate->AddTestAction(action1), CHIP_NO_ERROR);
EXPECT_EQ(delegate->AddTestAction(action2), CHIP_NO_ERROR);
// Test reading actions through attribute reader
uint8_t buf[1024];
// Create the TLV writer
TLV::TLVWriter tlvWriter;
tlvWriter.Init(buf);
AttributeReportIBs::Builder builder;
builder.Init(&tlvWriter);
ConcreteAttributePath path(1, Clusters::Actions::Id, Clusters::Actions::Attributes::ActionList::Id);
ConcreteReadAttributePath readPath(path);
chip::DataVersion dataVersion(0);
Access::SubjectDescriptor subjectDescriptor;
AttributeValueEncoder encoder(builder, subjectDescriptor, path, dataVersion);
// Read the action list using the Actions cluster's Read function
EXPECT_EQ(sActionsServer->Read(readPath, encoder), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buf);
TLV::TLVReader attrReportsReader;
TLV::TLVReader attrReportReader;
TLV::TLVReader attrDataReader;
reader.Next();
reader.OpenContainer(attrReportsReader);
attrReportsReader.Next();
attrReportsReader.OpenContainer(attrReportReader);
attrReportReader.Next();
attrReportReader.OpenContainer(attrDataReader);
// We're now in the attribute data IB, skip to the desired tag, we want TagNum = 2
attrDataReader.Next();
for (int i = 0; i < 3 && !(IsContextTag(attrDataReader.GetTag()) && TagNumFromTag(attrDataReader.GetTag()) == 2); ++i)
{
attrDataReader.Next();
}
EXPECT_TRUE(IsContextTag(attrDataReader.GetTag()));
EXPECT_EQ(TagNumFromTag(attrDataReader.GetTag()), 2u);
Clusters::Actions::Attributes::ActionList::TypeInfo::DecodableType list;
CHIP_ERROR err = list.Decode(attrDataReader);
EXPECT_EQ(err, CHIP_NO_ERROR);
auto iter = list.begin();
EXPECT_TRUE(iter.Next());
Clusters::Actions::Structs::ActionStruct::Type action = iter.GetValue();
EXPECT_EQ(action.actionID, 1);
EXPECT_TRUE(strncmp(action.name.data(), "FirstAction", action.name.size()) == 0);
EXPECT_EQ(action.type, ActionTypeEnum::kScene);
EXPECT_EQ(action.endpointListID, 0);
EXPECT_EQ(action.supportedCommands.Raw(), 0);
EXPECT_EQ(action.state, ActionStateEnum::kInactive);
EXPECT_TRUE(iter.Next());
action = iter.GetValue();
EXPECT_EQ(action.actionID, 2);
EXPECT_TRUE(strncmp(action.name.data(), "SecondAction", action.name.size()) == 0);
EXPECT_EQ(action.type, ActionTypeEnum::kScene);
EXPECT_EQ(action.endpointListID, 1);
EXPECT_EQ(action.supportedCommands.Raw(), 0);
EXPECT_EQ(action.state, ActionStateEnum::kActive);
}
TEST_F(TestActionsCluster, TestEndpointListAttributeAccess)
{
TestActionsDelegateImpl * delegate = sActionsDelegateImpl;
delegate->mNumEndpointLists = 0;
// Add test endpoint lists
const EndpointId endpoints1[] = { 1, 2 };
const EndpointId endpoints2[] = { 3, 4, 5 };
EndpointListStorage epList1(1, CharSpan::fromCharString("FirstList"), EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints1, 2));
EndpointListStorage epList2(2, CharSpan::fromCharString("SecondList"), EndpointListTypeEnum::kOther,
DataModel::List<const EndpointId>(endpoints2, 3));
EXPECT_EQ(delegate->AddTestEndpointList(epList1), CHIP_NO_ERROR);
EXPECT_EQ(delegate->AddTestEndpointList(epList2), CHIP_NO_ERROR);
// Test reading endpoint lists through attribute reader
TLV::TLVWriter writer;
uint8_t buf[1024];
writer.Init(buf);
// Create the builders
TLV::TLVWriter tlvWriter;
tlvWriter.Init(buf);
AttributeReportIBs::Builder builder;
builder.Init(&tlvWriter);
ConcreteAttributePath path(1, Clusters::Actions::Id, Clusters::Actions::Attributes::EndpointLists::Id);
ConcreteReadAttributePath readPath(path);
chip::DataVersion dataVersion(0);
Access::SubjectDescriptor subjectDescriptor;
AttributeValueEncoder encoder(builder, subjectDescriptor, path, dataVersion);
// Read the endpoint lists using the Actions cluster's Read function
EXPECT_EQ(sActionsServer->Read(path, encoder), CHIP_NO_ERROR);
TLV::TLVReader reader;
reader.Init(buf);
TLV::TLVReader attrReportsReader;
TLV::TLVReader attrReportReader;
TLV::TLVReader attrDataReader;
reader.Next();
reader.OpenContainer(attrReportsReader);
attrReportsReader.Next();
attrReportsReader.OpenContainer(attrReportReader);
attrReportReader.Next();
attrReportReader.OpenContainer(attrDataReader);
// We're now in the attribute data IB, skip to the desired tag, we want TagNum = 2
attrDataReader.Next();
for (int i = 0; i < 3 && !(IsContextTag(attrDataReader.GetTag()) && TagNumFromTag(attrDataReader.GetTag()) == 2); ++i)
{
attrDataReader.Next();
}
EXPECT_TRUE(IsContextTag(attrDataReader.GetTag()));
EXPECT_EQ(TagNumFromTag(attrDataReader.GetTag()), 2u);
Clusters::Actions::Attributes::EndpointLists::TypeInfo::DecodableType list;
CHIP_ERROR err = list.Decode(attrDataReader);
EXPECT_EQ(err, CHIP_NO_ERROR);
auto iter = list.begin();
EXPECT_TRUE(iter.Next());
Clusters::Actions::Structs::EndpointListStruct::DecodableType endpointList = iter.GetValue();
EXPECT_EQ(endpointList.endpointListID, 1);
EXPECT_TRUE(strncmp(endpointList.name.data(), "FirstList", endpointList.name.size()) == 0);
EXPECT_EQ(endpointList.type, EndpointListTypeEnum::kOther);
auto it = endpointList.endpoints.begin();
uint8_t i = 0;
while (it.Next())
{
if (i < 2)
{
EXPECT_EQ(endpoints1[i++], it.GetValue());
}
}
EXPECT_TRUE(iter.Next());
endpointList = iter.GetValue();
EXPECT_EQ(endpointList.endpointListID, 2);
EXPECT_TRUE(strncmp(endpointList.name.data(), "SecondList", endpointList.name.size()) == 0);
EXPECT_EQ(endpointList.type, EndpointListTypeEnum::kOther);
it = endpointList.endpoints.begin();
i = 0;
while (it.Next())
{
if (i < 3)
{
EXPECT_EQ(endpoints2[i++], it.GetValue());
}
}
}
} // namespace app
} // namespace chip