blob: d61be076f4ea22f10c105e65370a665db68fd9e8 [file] [log] [blame]
/**
* Copyright (c) 2025 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/scenes-server/SceneTableImpl.h>
#include <app/clusters/scenes-server/ScenesManagementCluster.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <app/server-cluster/testing/AttributeTesting.h>
#include <app/server-cluster/testing/ClusterTester.h>
#include <app/server-cluster/testing/TestServerClusterContext.h>
#include <app/server-cluster/testing/ValidateGlobalAttributes.h>
#include <clusters/ScenesManagement/Commands.h>
#include <clusters/ScenesManagement/Metadata.h>
#include <clusters/ScenesManagement/Structs.h>
#include <credentials/GroupDataProvider.h>
#include <credentials/PersistentStorageOpCertStore.h>
#include <credentials/tests/CHIPCert_test_vectors.h>
#include <credentials/tests/CHIPCert_unit_test_vectors.h>
#include <lib/core/StringBuilderAdapters.h>
#include <lib/support/TestPersistentStorageDelegate.h>
#include <lib/support/TypeTraits.h>
namespace {
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::ScenesManagement;
using namespace chip::Credentials;
using namespace chip::Protocols::InteractionModel;
using namespace chip::Testing;
using namespace chip::scenes;
using namespace chip::app::Clusters::ScenesManagement::Attributes;
using namespace chip::app::Clusters::ScenesManagement::Commands;
using namespace chip::app::Clusters::ScenesManagement::Structs;
using namespace chip::app::DataModel;
constexpr EndpointId kTestEndpointId = 123;
constexpr FabricIndex kFabricIndex = 1;
constexpr FabricIndex kFabricIndex2 = 2;
constexpr FabricIndex kFabricIndex3 = 3;
constexpr GroupId kTestGroupId = 123;
constexpr SceneId kTestSceneId = 111;
constexpr GroupId kTestOtherGroupId = 124;
constexpr SceneId kTestOtherSceneId = 112;
constexpr ClusterId kMockClusterId = 0xDEAD;
class MockSceneHandler : public scenes::SceneHandler
{
public:
// Test EFS
struct MockClusterEFS
{
bool on = false;
CHIP_ERROR Encode(TLV::TLVWriter & writer, TLV::Tag tag) const
{
TLV::TLVType outer;
ReturnErrorOnFailure(writer.StartContainer(tag, TLV::kTLVType_Structure, outer));
ReturnErrorOnFailure(writer.PutBoolean(TLV::ContextTag(1), on));
return writer.EndContainer(outer);
}
CHIP_ERROR Decode(TLV::TLVReader & reader)
{
TLV::TLVType outer;
ReturnErrorOnFailure(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()));
ReturnErrorOnFailure(reader.EnterContainer(outer));
ReturnErrorOnFailure(reader.Next(TLV::ContextTag(1)));
ReturnErrorOnFailure(reader.Get(on));
return reader.ExitContainer(outer);
}
};
MockClusterEFS mState;
AttributeValuePairStruct::Type mAttributeRead[1]; // Persistent storage for Deserialize
// SceneHandler Implementation
bool SupportsCluster(EndpointId endpoint, ClusterId cluster) override { return cluster == kMockClusterId; }
CHIP_ERROR
SerializeAdd(EndpointId endpoint,
const app::Clusters::ScenesManagement::Structs::ExtensionFieldSetStruct::DecodableType & extensionFieldSet,
MutableByteSpan & serialisedBytes) override
{
VerifyOrReturnError(extensionFieldSet.clusterID == kMockClusterId, CHIP_ERROR_INVALID_ARGUMENT);
MockClusterEFS efs_data;
auto iter = extensionFieldSet.attributeValueList.begin();
while (iter.Next())
{
auto & attr = iter.GetValue();
if (attr.attributeID == 0 && attr.valueUnsigned8.HasValue()) // Assuming Attribute ID 0 is the 'on' state
{
efs_data.on = attr.valueUnsigned8.Value();
}
}
ReturnErrorOnFailure(iter.GetStatus());
TLV::TLVWriter writer;
writer.Init(serialisedBytes);
ReturnErrorOnFailure(efs_data.Encode(writer, TLV::AnonymousTag()));
serialisedBytes.reduce_size(writer.GetLengthWritten());
return CHIP_NO_ERROR;
}
CHIP_ERROR SerializeSave(EndpointId endpoint, ClusterId cluster, MutableByteSpan & serializedBytes) override
{
VerifyOrReturnError(cluster == kMockClusterId, CHIP_ERROR_INVALID_ARGUMENT);
TLV::TLVWriter writer;
writer.Init(serializedBytes);
ReturnErrorOnFailure(mState.Encode(writer, TLV::AnonymousTag()));
serializedBytes.reduce_size(writer.GetLengthWritten());
return CHIP_NO_ERROR;
}
CHIP_ERROR Deserialize(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes,
app::Clusters::ScenesManagement::Structs::ExtensionFieldSetStruct::Type & extensionFieldSet) override
{
VerifyOrReturnError(cluster == kMockClusterId, CHIP_ERROR_INVALID_ARGUMENT);
MockClusterEFS deserializedEFS;
TLV::TLVReader reader;
reader.Init(serializedBytes);
ReturnErrorOnFailure(deserializedEFS.Decode(reader));
extensionFieldSet.clusterID = kMockClusterId;
mAttributeRead[0].attributeID = 0; // Assuming Attribute ID 0 is the 'on' state
mAttributeRead[0].valueUnsigned8.SetValue(deserializedEFS.on);
extensionFieldSet.attributeValueList = DataModel::List<AttributeValuePairStruct::Type>(mAttributeRead);
return CHIP_NO_ERROR;
}
CHIP_ERROR ApplyScene(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes,
TransitionTimeMs timeMs) override
{
VerifyOrReturnError(cluster == kMockClusterId, CHIP_ERROR_INVALID_ARGUMENT);
MockClusterEFS deserializedEFS;
TLV::TLVReader reader;
reader.Init(serializedBytes);
ReturnErrorOnFailure(deserializedEFS.Decode(reader));
mState.on = deserializedEFS.on;
return CHIP_NO_ERROR;
}
};
class MockGroupDataProvider : public GroupDataProvider
{
public:
MockGroupDataProvider() : GroupDataProvider(0, 0) {}
CHIP_ERROR Init() override { return CHIP_NO_ERROR; }
void Finish() override {}
CHIP_ERROR SetGroupInfo(FabricIndex, const GroupInfo &) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetGroupInfo(FabricIndex, GroupId, GroupInfo &) override { return CHIP_ERROR_NOT_FOUND; }
CHIP_ERROR RemoveGroupInfo(FabricIndex, GroupId) override { return CHIP_NO_ERROR; }
CHIP_ERROR SetGroupInfoAt(FabricIndex, size_t, const GroupInfo &) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetGroupInfoAt(FabricIndex, size_t, GroupInfo &) override { return CHIP_ERROR_NOT_FOUND; }
CHIP_ERROR RemoveGroupInfoAt(FabricIndex, size_t) override { return CHIP_NO_ERROR; }
bool HasEndpoint(FabricIndex fabric, GroupId group, EndpointId endpoint) override { return mHasEndpoint; }
CHIP_ERROR AddEndpoint(FabricIndex, GroupId, EndpointId) override { return CHIP_NO_ERROR; }
CHIP_ERROR RemoveEndpoint(FabricIndex, GroupId, EndpointId) override { return CHIP_NO_ERROR; }
CHIP_ERROR RemoveEndpoint(FabricIndex, EndpointId) override { return CHIP_NO_ERROR; }
CHIP_ERROR RemoveEndpoints(FabricIndex fabric_index, GroupId group_id) override { return CHIP_NO_ERROR; }
GroupInfoIterator * IterateGroupInfo(FabricIndex) override { return nullptr; }
EndpointIterator * IterateEndpoints(FabricIndex, std::optional<GroupId>) override { return nullptr; }
CHIP_ERROR SetGroupKey(FabricIndex fabric_index, GroupId group_id, KeysetId keyset_id) override { return CHIP_NO_ERROR; }
CHIP_ERROR SetGroupKeyAt(FabricIndex, size_t, const GroupKey &) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetGroupKey(FabricIndex fabric_index, GroupId group_id, KeysetId & keyset_id) override
{
return CHIP_ERROR_NOT_FOUND;
}
CHIP_ERROR GetGroupKeyAt(FabricIndex, size_t, GroupKey &) override { return CHIP_ERROR_NOT_FOUND; }
CHIP_ERROR RemoveGroupKeyAt(FabricIndex, size_t) override { return CHIP_NO_ERROR; }
CHIP_ERROR RemoveGroupKeys(FabricIndex) override { return CHIP_NO_ERROR; }
GroupKeyIterator * IterateGroupKeys(FabricIndex) override { return nullptr; }
CHIP_ERROR SetKeySet(FabricIndex, const ByteSpan &, const KeySet &) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetKeySet(FabricIndex, KeysetId, KeySet &) override { return CHIP_ERROR_NOT_FOUND; }
CHIP_ERROR RemoveKeySet(FabricIndex, KeysetId) override { return CHIP_NO_ERROR; }
CHIP_ERROR GetIpkKeySet(FabricIndex, KeySet &) override { return CHIP_ERROR_NOT_FOUND; }
KeySetIterator * IterateKeySets(FabricIndex) override { return nullptr; }
CHIP_ERROR RemoveFabric(FabricIndex) override { return CHIP_NO_ERROR; }
GroupSessionIterator * IterateGroupSessions(uint16_t) override { return nullptr; }
Crypto::SymmetricKeyContext * GetKeyContext(FabricIndex, GroupId) override { return nullptr; }
uint16_t getMaxMembershipCount() override { return 0; }
uint16_t getMaxMcastAddrCount() override { return 0; }
bool mHasEndpoint = true;
};
class TestSceneTable : public DefaultSceneTableImpl
{
public:
CHIP_ERROR SceneSaveEFS(SceneTableEntry & scene) override
{
if (HandlerListEmpty())
{
return CHIP_NO_ERROR;
}
ExtensionFieldSet EFS;
MutableByteSpan EFSSpan = MutableByteSpan(EFS.mBytesBuffer, kMaxFieldBytesPerCluster);
EFS.mID = kMockClusterId;
for (auto & handler : mHandlerList)
{
if (handler.SupportsCluster(mEndpointId, kMockClusterId))
{
ReturnErrorOnFailure(handler.SerializeSave(mEndpointId, EFS.mID, EFSSpan));
EFS.mUsedBytes = static_cast<uint8_t>(EFSSpan.size());
ReturnErrorOnFailure(scene.mStorageData.mExtensionFieldSets.InsertFieldSet(EFS));
break;
}
}
return CHIP_NO_ERROR;
}
};
class TestScenesManagementTableProvider : public ScenesManagementTableProvider
{
public:
TestScenesManagementTableProvider() = default;
~TestScenesManagementTableProvider() override = default;
void Init(PersistentStorageDelegate * storage, Provider * provider)
{
mSceneTable = Platform::MakeUnique<TestSceneTable>();
mSceneTable->SetEndpoint(kTestEndpointId);
ASSERT_EQ(mSceneTable->Init(*storage, *provider), CHIP_NO_ERROR);
}
ScenesManagementSceneTable * Take() override { return mSceneTable.get(); }
void Release(ScenesManagementSceneTable *) override {}
Platform::UniquePtr<TestSceneTable> mSceneTable;
};
class CustomDataModel : public EmptyProvider
{
public:
CHIP_ERROR Endpoints(ReadOnlyBufferBuilder<DataModel::EndpointEntry> & builder) override
{
static constexpr DataModel::EndpointEntry kEndpoints[] = { {
.id = kTestEndpointId,
.parentId = kInvalidEndpointId,
.compositionPattern = EndpointCompositionPattern::kTree,
}
};
return builder.ReferenceExisting(Span(kEndpoints));
}
};
CHIP_ERROR AddFakeFabric(FabricTable & fabricTable, FabricIndex expectedFabricIndex)
{
Crypto::P256SerializedKeypair opKeysSerialized;
static Crypto::P256Keypair opKey;
struct NocData
{
const ByteSpan & publicKey;
const ByteSpan & privateKey;
const ByteSpan & rcac;
const ByteSpan & icac;
const ByteSpan & noc;
};
static const NocData kNocItems[] = {
{
.publicKey = TestCerts::sTestCert_Node01_01_PublicKey,
.privateKey = TestCerts::sTestCert_Node01_01_PrivateKey,
.rcac = TestCerts::sTestCert_Root01_Chip,
.icac = TestCerts::sTestCert_ICA01_Chip,
.noc = TestCerts::sTestCert_Node01_01_Chip,
},
{
.publicKey = TestCerts::sTestCert_Node02_02_PublicKey,
.privateKey = TestCerts::sTestCert_Node02_02_PrivateKey,
.rcac = TestCerts::sTestCert_Root02_Chip,
.icac = TestCerts::sTestCert_ICA02_Chip,
.noc = TestCerts::sTestCert_Node02_02_Chip,
},
};
ssize_t nocDataIndex = -1;
switch (expectedFabricIndex)
{
case kFabricIndex:
nocDataIndex = 0;
break;
case kFabricIndex2:
nocDataIndex = 1;
break;
}
VerifyOrReturnError(nocDataIndex >= 0 && static_cast<size_t>(nocDataIndex) < MATTER_ARRAY_SIZE(kNocItems), CHIP_ERROR_INTERNAL);
const NocData & nocData = kNocItems[nocDataIndex];
FabricIndex fabricIndex;
memcpy(opKeysSerialized.Bytes(), nocData.publicKey.data(), nocData.publicKey.size());
memcpy(opKeysSerialized.Bytes() + nocData.publicKey.size(), nocData.privateKey.data(), nocData.privateKey.size());
ReturnErrorOnFailure(opKeysSerialized.SetLength(TestCerts::sTestCert_Node01_01_PublicKey.size() +
TestCerts::sTestCert_Node01_01_PrivateKey.size()));
ReturnErrorOnFailure(opKey.Deserialize(opKeysSerialized));
ReturnErrorOnFailure(fabricTable.AddNewPendingTrustedRootCert(nocData.rcac));
ReturnErrorOnFailure(fabricTable.AddNewPendingFabricWithProvidedOpKey(nocData.noc, nocData.icac, VendorId::TestVendor1, &opKey,
/*isExistingOpKeyExternallyOwned =*/true, &fabricIndex));
VerifyOrReturnError(fabricIndex == expectedFabricIndex, CHIP_ERROR_INTERNAL);
return fabricTable.CommitPendingFabricData();
}
struct TestScenesManagementCluster : public ::testing::Test
{
static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); }
static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); }
TestScenesManagementCluster() :
cluster(kTestEndpointId,
ScenesManagementCluster::Context{ .groupDataProvider = &mockGroupDataProvider,
.fabricTable = &fabricTable,
.features =
BitMask<ScenesManagement::Feature>(ScenesManagement::Feature::kSceneNames),
.sceneTableProvider = sceneTableProvider,
.supportsCopyScene = true })
{}
void SetUp() override
{
testContext.StorageDelegate().ClearStorage(); // Clear storage before each test
ASSERT_EQ(mOpCertStore.Init(&testContext.StorageDelegate()), CHIP_NO_ERROR);
FabricTable::InitParams initParams;
initParams.storage = &testContext.StorageDelegate();
initParams.opCertStore = &mOpCertStore;
ASSERT_EQ(fabricTable.Init(initParams), CHIP_NO_ERROR);
sceneTableProvider.Init(&testContext.StorageDelegate(), &testContext.Get().provider);
sceneTableProvider.mSceneTable->RegisterHandler(&mMockSceneHandler);
ServerClusterContext context = testContext.Get();
clusterContext = std::make_unique<ServerClusterContext>(ServerClusterContext{
.provider = customDataModel,
.storage = context.storage,
.attributeStorage = context.attributeStorage,
.interactionContext = context.interactionContext,
});
ASSERT_EQ(cluster.Startup(*clusterContext), CHIP_NO_ERROR);
}
void TearDown() override
{
sceneTableProvider.mSceneTable->UnregisterHandler(&mMockSceneHandler);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
clusterContext.reset();
fabricTable.Shutdown();
}
void AddSceneToTable(ClusterTester & tester, GroupId groupID, SceneId sceneID, const char * name = "TestScene",
uint16_t transitionTime = 100, const DataModel::List<ExtensionFieldSetStruct::Type> * efs = nullptr)
{
AddScene::Type request_data;
request_data.groupID = groupID;
request_data.sceneID = sceneID;
request_data.transitionTime = transitionTime;
request_data.sceneName = CharSpan::fromCharString(name);
if (efs)
{
request_data.extensionFieldSetStructs = *efs;
}
else
{
request_data.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
}
auto response = tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, request_data);
ASSERT_TRUE(response.IsSuccess());
ASSERT_TRUE(response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
ASSERT_EQ(response.response->status, to_underlying(Status::Success));
EXPECT_EQ(response.response->groupID, groupID);
EXPECT_EQ(response.response->sceneID, sceneID);
// NOLINTEND(bugprone-unchecked-optional-access)
}
void VerifySceneInfoCount(ClusterTester & tester, FabricIndex fabricIndex, uint16_t expectedCount)
{
Attributes::FabricSceneInfo::TypeInfo::DecodableType sceneInfoList;
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
auto it = sceneInfoList.begin();
bool found = false;
while (it.Next())
{
if (it.GetValue().fabricIndex == fabricIndex)
{
EXPECT_EQ(it.GetValue().sceneCount, expectedCount);
found = true;
break;
}
}
EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR);
EXPECT_TRUE(found);
}
template <typename DecodableType>
void ExpectCommandStatus(const ClusterTester::InvokeResult<DecodableType> & response, Status expectedStatus)
{
ASSERT_TRUE(response.IsSuccess());
ASSERT_TRUE(response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(response.response->status, to_underlying(expectedStatus));
}
CustomDataModel customDataModel;
TestServerClusterContext testContext;
MockGroupDataProvider mockGroupDataProvider;
PersistentStorageOpCertStore mOpCertStore;
FabricTable fabricTable;
TestScenesManagementTableProvider sceneTableProvider;
ScenesManagementCluster cluster;
MockSceneHandler mMockSceneHandler;
// Test context uses an empty data model provider, however we scenes relies on
// endpoint iteration. Create a DMP that returns some endpoints.
std::unique_ptr<ServerClusterContext> clusterContext;
};
TEST_F(TestScenesManagementCluster, AttributesTest)
{
ASSERT_TRUE(IsAttributesListEqualTo(cluster,
{
SceneTableSize::kMetadataEntry,
FabricSceneInfo::kMetadataEntry,
}));
}
TEST_F(TestScenesManagementCluster, AddSceneCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "TestScene", 100);
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry(sceneStorageId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_NO_ERROR);
SceneTable<ExtensionFieldSetsImpl>::SceneData expectedSceneData("TestScene"_span, 100);
EXPECT_EQ(sceneEntry.mStorageData, expectedSceneData);
VerifySceneInfoCount(tester, kFabricIndex, 1);
}
TEST_F(TestScenesManagementCluster, AddSceneCommandModifyExistingScene)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add an initial scene
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "InitialSceneName", 50);
// 2. Modify the scene using AddScene with the same ID but different data
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "ModifiedName", 150);
// 3. Verify the modification using ViewScene
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestSceneId;
auto view_response = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ASSERT_TRUE(view_response.IsSuccess());
ASSERT_TRUE(view_response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto & data = *view_response.response;
EXPECT_EQ(data.groupID, kTestGroupId);
EXPECT_EQ(data.sceneID, kTestSceneId);
ASSERT_TRUE(data.transitionTime.HasValue());
EXPECT_EQ(data.transitionTime.Value(), 150u);
ASSERT_TRUE(data.sceneName.HasValue());
EXPECT_TRUE(data.sceneName.Value().data_equal("ModifiedName"_span));
VerifySceneInfoCount(tester, kFabricIndex, 1);
}
TEST_F(TestScenesManagementCluster, AddSceneCommandInvalidGroupID)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Configure the mock to return false for HasEndpoint to simulate the group not existing for this endpoint.
mockGroupDataProvider.mHasEndpoint = false;
AddScene::Type request_data;
request_data.groupID = kTestOtherGroupId; // A group ID that doesn't "exist" for the endpoint.
request_data.sceneID = kTestSceneId;
request_data.transitionTime = 100;
request_data.sceneName = "InvalidGScene"_span;
request_data.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
ClusterTester::InvokeResult<AddSceneResponse::DecodableType> response =
tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, request_data);
ExpectCommandStatus(response, Status::InvalidCommand);
// Restore the mock's default behavior
mockGroupDataProvider.mHasEndpoint = true;
}
TEST_F(TestScenesManagementCluster, ViewSceneCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Prepare EFS data
AttributeValuePairStruct::Type attributeValue;
attributeValue.attributeID = 0; // Assuming Attribute ID 0 is the 'on' state
attributeValue.valueUnsigned8.SetValue(true);
AttributeValuePairStruct::Type attributeValueList[1];
attributeValueList[0] = attributeValue;
ExtensionFieldSetStruct::Type efsStruct;
efsStruct.clusterID = kMockClusterId;
efsStruct.attributeValueList = DataModel::List<AttributeValuePairStruct::Type>(attributeValueList);
ExtensionFieldSetStruct::Type efsList[1];
efsList[0] = efsStruct;
DataModel::List<ExtensionFieldSetStruct::Type> efs = DataModel::List<ExtensionFieldSetStruct::Type>(efsList);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "ViewSceneWithEFS", 100, &efs);
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestSceneId;
ClusterTester::InvokeResult<ViewSceneResponse::DecodableType> view_response =
tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ExpectCommandStatus(view_response, Status::Success);
ASSERT_TRUE(view_response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto & data = *view_response.response;
EXPECT_EQ(data.groupID, kTestGroupId);
EXPECT_EQ(data.sceneID, kTestSceneId);
ASSERT_TRUE(data.transitionTime.HasValue());
EXPECT_EQ(data.transitionTime.Value(), 100u);
ASSERT_TRUE(data.sceneName.HasValue());
EXPECT_TRUE(data.sceneName.Value().data_equal("ViewSceneWithEFS"_span));
// Verify EFS content
ASSERT_TRUE(data.extensionFieldSetStructs.HasValue());
auto responseEfsList = data.extensionFieldSetStructs.Value();
auto iterator = responseEfsList.begin();
size_t efs_count = 0;
bool found_mock_efs = false;
while (iterator.Next())
{
efs_count++;
auto const & received_efs = iterator.GetValue();
if (received_efs.clusterID == kMockClusterId)
{
found_mock_efs = true;
auto attr_iter = received_efs.attributeValueList.begin();
ASSERT_TRUE(attr_iter.Next());
auto const & attr = attr_iter.GetValue();
EXPECT_EQ(attr.attributeID, 0u);
ASSERT_TRUE(attr.valueUnsigned8.HasValue());
EXPECT_EQ(attr.valueUnsigned8.Value(), true);
ASSERT_FALSE(attr_iter.Next()); // Should only be one attribute
EXPECT_EQ(attr_iter.GetStatus(), CHIP_NO_ERROR);
}
}
EXPECT_EQ(iterator.GetStatus(), CHIP_NO_ERROR);
EXPECT_EQ(efs_count, 1u);
EXPECT_TRUE(found_mock_efs);
}
TEST_F(TestScenesManagementCluster, RemoveSceneCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
AddSceneToTable(tester, kTestGroupId, kTestSceneId);
RemoveScene::Type remove_request;
remove_request.groupID = kTestGroupId;
remove_request.sceneID = kTestSceneId;
ClusterTester::InvokeResult<RemoveSceneResponse::DecodableType> remove_response =
tester.Invoke<RemoveScene::Type, RemoveSceneResponse::DecodableType>(RemoveScene::Id, remove_request);
ExpectCommandStatus(remove_response, Status::Success);
ASSERT_TRUE(remove_response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_EQ(remove_response.response->groupID, remove_request.groupID);
EXPECT_EQ(remove_response.response->sceneID, remove_request.sceneID);
// NOLINTEND(bugprone-unchecked-optional-access)
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry(sceneStorageId);
EXPECT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_ERROR_NOT_FOUND);
VerifySceneInfoCount(tester, kFabricIndex, 0);
}
TEST_F(TestScenesManagementCluster, RemoveAllScenesCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
struct SceneDataForTest
{
GroupId groupID;
SceneId sceneID;
bool toBeRemoved;
};
SceneDataForTest scenesToAdd[] = {
{ kTestGroupId, kTestSceneId, true },
{ kTestGroupId, kTestOtherSceneId, true },
{ kTestOtherGroupId, kTestSceneId, false },
};
for (const auto & scene : scenesToAdd)
{
AddSceneToTable(tester, scene.groupID, scene.sceneID);
}
// Invoke RemoveAllScenes for kTestGroupId
RemoveAllScenes::Type remove_all_request;
remove_all_request.groupID = kTestGroupId;
ClusterTester::InvokeResult<RemoveAllScenesResponse::DecodableType> remove_all_response =
tester.Invoke<RemoveAllScenes::Type, RemoveAllScenesResponse::DecodableType>(RemoveAllScenes::Id, remove_all_request);
ExpectCommandStatus(remove_all_response, Status::Success);
ASSERT_TRUE(remove_all_response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(remove_all_response.response->groupID, kTestGroupId);
// Verify scenes based on toBeRemoved flag
for (const auto & scene : scenesToAdd)
{
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId(scene.sceneID, scene.groupID);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry(sceneStorageId);
if (scene.toBeRemoved)
{
EXPECT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry),
CHIP_ERROR_NOT_FOUND);
}
else
{
EXPECT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_NO_ERROR);
}
}
VerifySceneInfoCount(tester, kFabricIndex, 1);
}
TEST_F(TestScenesManagementCluster, RemoveAllScenesInvalidGroupID)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Configure the mock to return false for HasEndpoint to simulate the group not existing for this endpoint.
mockGroupDataProvider.mHasEndpoint = false;
RemoveAllScenes::Type remove_all_request;
remove_all_request.groupID = kTestOtherGroupId; // A group ID that doesn't "exist" for the endpoint.
ClusterTester::InvokeResult<RemoveAllScenesResponse::DecodableType> remove_all_response =
tester.Invoke<RemoveAllScenes::Type, RemoveAllScenesResponse::DecodableType>(RemoveAllScenes::Id, remove_all_request);
ExpectCommandStatus(remove_all_response, Status::InvalidCommand);
// Restore the mock's default behavior
mockGroupDataProvider.mHasEndpoint = true;
}
TEST_F(TestScenesManagementCluster, GetSceneMembershipCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
struct SceneDataForTest
{
GroupId groupID;
SceneId sceneID;
};
SceneDataForTest scenesToAdd[] = {
{ kTestGroupId, kTestSceneId },
{ kTestGroupId, kTestOtherSceneId },
{ kTestOtherGroupId, kTestSceneId },
};
for (const auto & scene : scenesToAdd)
{
AddSceneToTable(tester, scene.groupID, scene.sceneID);
}
GetSceneMembership::Type request;
request.groupID = kTestGroupId;
ClusterTester::InvokeResult<GetSceneMembershipResponse::DecodableType> response =
tester.Invoke<GetSceneMembership::Type, GetSceneMembershipResponse::DecodableType>(GetSceneMembership::Id, request);
ExpectCommandStatus(response, Status::Success);
ASSERT_TRUE(response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto & data = *response.response;
EXPECT_EQ(data.groupID, kTestGroupId);
ASSERT_FALSE(data.capacity.IsNull());
// We added 3 scenes total to the fabric, so capacity should be kMaxScenesPerFabric - 3
EXPECT_EQ(data.capacity.Value(), kMaxScenesPerFabric - (sizeof(scenesToAdd) / sizeof(scenesToAdd[0])));
// Verify the scene list
ASSERT_TRUE(data.sceneList.HasValue());
auto sceneList = data.sceneList.Value();
auto iterator = sceneList.begin();
size_t list_size = 0;
bool foundScene1 = false;
bool foundScene2 = false;
while (iterator.Next())
{
list_size++;
auto sceneId = iterator.GetValue();
if (sceneId == kTestSceneId)
{
foundScene1 = true;
}
else if (sceneId == kTestOtherSceneId)
{
foundScene2 = true;
}
}
EXPECT_EQ(iterator.GetStatus(), CHIP_NO_ERROR);
EXPECT_EQ(list_size, 2u);
EXPECT_TRUE(foundScene1);
EXPECT_TRUE(foundScene2);
}
TEST_F(TestScenesManagementCluster, GetSceneMembershipInvalidGroupID)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Configure the mock to return false for HasEndpoint to simulate the group not existing for this endpoint.
mockGroupDataProvider.mHasEndpoint = false;
GetSceneMembership::Type request;
request.groupID = kTestOtherGroupId;
ClusterTester::InvokeResult<GetSceneMembershipResponse::DecodableType> response =
tester.Invoke<GetSceneMembership::Type, GetSceneMembershipResponse::DecodableType>(GetSceneMembership::Id, request);
ExpectCommandStatus(response, Status::InvalidCommand);
// Restore the mock's default behavior
mockGroupDataProvider.mHasEndpoint = true;
}
TEST_F(TestScenesManagementCluster, StoreSceneCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// A scene must be added via AddScene before StoreScene can be used to modify it.
AddSceneToTable(tester, kTestGroupId, kTestSceneId);
mMockSceneHandler.mState.on = true;
StoreScene::Type request;
request.groupID = kTestGroupId;
request.sceneID = kTestSceneId;
ClusterTester::InvokeResult<StoreSceneResponse::DecodableType> response =
tester.Invoke<StoreScene::Type, StoreSceneResponse::DecodableType>(StoreScene::Id, request);
ExpectCommandStatus(response, Status::Success);
ASSERT_TRUE(response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_EQ(response.response->groupID, kTestGroupId);
EXPECT_EQ(response.response->sceneID, kTestSceneId);
// NOLINTEND(bugprone-unchecked-optional-access)
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry(sceneStorageId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_NO_ERROR);
EXPECT_EQ(sceneEntry.mStorageData.mExtensionFieldSets.GetFieldSetCount(), 1u);
ExtensionFieldSet efs;
ASSERT_EQ(sceneEntry.mStorageData.mExtensionFieldSets.GetFieldSetAtPosition(efs, 0), CHIP_NO_ERROR);
EXPECT_EQ(efs.mID, kMockClusterId);
MockSceneHandler::MockClusterEFS deserializedEFS;
TLV::TLVReader reader;
reader.Init(efs.mBytesBuffer, efs.mUsedBytes);
CHIP_ERROR err = deserializedEFS.Decode(reader);
ASSERT_EQ(err, CHIP_NO_ERROR);
EXPECT_EQ(deserializedEFS.on, mMockSceneHandler.mState.on);
}
TEST_F(TestScenesManagementCluster, StoreSceneCommandCreatesSceneIfNotFound)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Do not add the scene first. Per Matter Spec 1.4 - 1.10.6.9, StoreScene should create the scene if it does not exist.
StoreScene::Type request;
request.groupID = kTestGroupId;
request.sceneID = kTestSceneId;
ClusterTester::InvokeResult<StoreSceneResponse::DecodableType> response =
tester.Invoke<StoreScene::Type, StoreSceneResponse::DecodableType>(StoreScene::Id, request);
ASSERT_TRUE(response.IsSuccess());
ASSERT_TRUE(response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_EQ(response.response->status, to_underlying(Status::Success));
EXPECT_EQ(response.response->groupID, kTestGroupId);
EXPECT_EQ(response.response->sceneID, kTestSceneId);
// NOLINTEND(bugprone-unchecked-optional-access)
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestSceneId;
auto view_response = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ASSERT_TRUE(view_response.IsSuccess());
ASSERT_TRUE(view_response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_EQ(view_response.response->status, to_underlying(Status::Success));
auto & data = view_response.response.value();
// NOLINTEND(bugprone-unchecked-optional-access)
// Per Matter Spec 1.4 - 1.10.6.9, when creating a scene via StoreScene,
// the transition time is 0 and the scene name is an empty string.
ASSERT_TRUE(data.transitionTime.HasValue());
EXPECT_EQ(data.transitionTime.Value(), 0u);
ASSERT_TRUE(data.sceneName.HasValue());
EXPECT_TRUE(data.sceneName.Value().empty());
}
TEST_F(TestScenesManagementCluster, StoreSceneResourceExhausted)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Fill the scene table to its maximum capacity.
for (SceneId sceneId = 0; sceneId < kMaxScenesPerFabric; ++sceneId)
{
AddSceneToTable(tester, kTestGroupId, sceneId);
}
// Attempt to store a new scene, which should fail due to lack of space in the fabric scene table.
StoreScene::Type store_request_fail;
store_request_fail.groupID = kTestOtherGroupId;
store_request_fail.sceneID = kTestOtherSceneId;
ClusterTester::InvokeResult<StoreSceneResponse::DecodableType> store_response_fail =
tester.Invoke<StoreScene::Type, StoreSceneResponse::DecodableType>(StoreScene::Id, store_request_fail);
ExpectCommandStatus(store_response_fail, Status::ResourceExhausted);
}
TEST_F(TestScenesManagementCluster, RecallSceneCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Add and store a scene with the mock handler's state as "on".
AddSceneToTable(tester, kTestGroupId, kTestSceneId);
mMockSceneHandler.mState.on = true;
StoreScene::Type store_request;
store_request.groupID = kTestGroupId;
store_request.sceneID = kTestSceneId;
auto store_response = tester.Invoke<StoreScene::Type, StoreSceneResponse::DecodableType>(StoreScene::Id, store_request);
ExpectCommandStatus(store_response, Status::Success);
// Change the mock handler's state to "off" to ensure RecallScene changes it.
mMockSceneHandler.mState.on = false;
EXPECT_FALSE(mMockSceneHandler.mState.on);
RecallScene::Type recall_request;
recall_request.groupID = kTestGroupId;
recall_request.sceneID = kTestSceneId;
auto recall_response = tester.Invoke<RecallScene::Type, NullObjectType>(RecallScene::Id, recall_request);
ASSERT_TRUE(recall_response.IsSuccess());
// Verify the mock handler's state was restored to "on" by ApplyScene.
EXPECT_TRUE(mMockSceneHandler.mState.on);
}
TEST_F(TestScenesManagementCluster, RecallSceneNotFound)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
RecallScene::Type recall_request;
recall_request.groupID = kTestGroupId;
recall_request.sceneID = kTestSceneId;
auto recall_response = tester.Invoke<RecallScene::Type, NullObjectType>(RecallScene::Id, recall_request);
ASSERT_FALSE(recall_response.IsSuccess());
ASSERT_TRUE(recall_response.status.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(recall_response.status.value(), Status::NotFound);
}
TEST_F(TestScenesManagementCluster, CopySceneCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add and Store a scene in the source location.
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "SourceScene", 1234);
mMockSceneHandler.mState.on = true;
StoreScene::Type store_request;
store_request.groupID = kTestGroupId;
store_request.sceneID = kTestSceneId;
auto store_response = tester.Invoke<StoreScene::Type, StoreSceneResponse::DecodableType>(StoreScene::Id, store_request);
ExpectCommandStatus(store_response, Status::Success);
// 2. Invoke CopyScene to copy from {kTestGroupId, kTestSceneId} to {kTestOtherGroupId, kTestOtherSceneId}.
CopyScene::Type copy_request;
copy_request.mode.Set(CopyModeBitmap::kCopyAllScenes, false);
copy_request.groupIdentifierFrom = kTestGroupId;
copy_request.sceneIdentifierFrom = kTestSceneId;
copy_request.groupIdentifierTo = kTestOtherGroupId;
copy_request.sceneIdentifierTo = kTestOtherSceneId;
auto copy_response = tester.Invoke<CopyScene::Type, CopySceneResponse::DecodableType>(CopyScene::Id, copy_request);
ExpectCommandStatus(copy_response, Status::Success);
ASSERT_TRUE(copy_response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_EQ(copy_response.response->groupIdentifierFrom, kTestGroupId);
EXPECT_EQ(copy_response.response->sceneIdentifierFrom, kTestSceneId);
// NOLINTEND(bugprone-unchecked-optional-access)
// 3. Verify the copied scene exists and is identical to the source.
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId destSceneId(kTestOtherSceneId, kTestOtherGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry destSceneEntry(destSceneId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, destSceneId, destSceneEntry), CHIP_NO_ERROR);
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId srcSceneId(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry srcSceneEntry(srcSceneId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, srcSceneId, srcSceneEntry), CHIP_NO_ERROR);
EXPECT_EQ(destSceneEntry.mStorageData, srcSceneEntry.mStorageData);
}
TEST_F(TestScenesManagementCluster, CopySceneInvalidGroupID)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Configure the mock to return false for HasEndpoint to simulate the group not existing for this endpoint.
mockGroupDataProvider.mHasEndpoint = false;
CopyScene::Type copy_request;
copy_request.mode.Set(CopyModeBitmap::kCopyAllScenes, false);
copy_request.groupIdentifierFrom = kTestOtherGroupId; // Non-existent group
copy_request.sceneIdentifierFrom = kTestOtherSceneId;
copy_request.groupIdentifierTo = kTestGroupId;
copy_request.sceneIdentifierTo = kTestSceneId;
auto copy_response = tester.Invoke<CopyScene::Type, CopySceneResponse::DecodableType>(CopyScene::Id, copy_request);
ExpectCommandStatus(copy_response, Status::InvalidCommand);
// Restore the mock's default behavior
mockGroupDataProvider.mHasEndpoint = true;
}
TEST_F(TestScenesManagementCluster, AcceptedCommandsWithCopySceneTest)
{
// Test with supportsCopyScene = true
ScenesManagementCluster clusterWithCopy(
kTestEndpointId,
ScenesManagementCluster::Context{ .groupDataProvider = &mockGroupDataProvider,
.fabricTable = &fabricTable,
.features = BitMask<ScenesManagement::Feature>(ScenesManagement::Feature::kSceneNames),
.sceneTableProvider = sceneTableProvider,
.supportsCopyScene = true });
ASSERT_TRUE(IsAcceptedCommandsListEqualTo(clusterWithCopy,
{
AddScene::kMetadataEntry,
ViewScene::kMetadataEntry,
RemoveScene::kMetadataEntry,
RemoveAllScenes::kMetadataEntry,
StoreScene::kMetadataEntry,
RecallScene::kMetadataEntry,
GetSceneMembership::kMetadataEntry,
CopyScene::kMetadataEntry,
}));
}
TEST_F(TestScenesManagementCluster, AcceptedCommandsWithoutCopySceneTest)
{
// Test with supportsCopyScene = false
ScenesManagementCluster clusterWithoutCopy(
kTestEndpointId,
ScenesManagementCluster::Context{ .groupDataProvider = &mockGroupDataProvider,
.fabricTable = &fabricTable,
.features = BitMask<ScenesManagement::Feature>(ScenesManagement::Feature::kSceneNames),
.sceneTableProvider = sceneTableProvider,
.supportsCopyScene = false });
ASSERT_TRUE(IsAcceptedCommandsListEqualTo(clusterWithoutCopy,
{
AddScene::kMetadataEntry,
ViewScene::kMetadataEntry,
RemoveScene::kMetadataEntry,
RemoveAllScenes::kMetadataEntry,
StoreScene::kMetadataEntry,
RecallScene::kMetadataEntry,
GetSceneMembership::kMetadataEntry,
}));
}
TEST_F(TestScenesManagementCluster, ViewSceneCommandNotFound)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
ViewScene::Type view_request;
view_request.groupID = kTestOtherGroupId; // Use a group that has no scenes
view_request.sceneID = kTestOtherSceneId; // Use a scene that does not exist
ClusterTester::InvokeResult<ViewSceneResponse::DecodableType> view_response =
tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ExpectCommandStatus(view_response, Status::NotFound);
}
TEST_F(TestScenesManagementCluster, RemoveSceneCommandNotFound)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
RemoveScene::Type remove_request;
remove_request.groupID = kTestOtherGroupId; // Use a group that has no scenes
remove_request.sceneID = kTestOtherSceneId; // Use a scene that does not exist
ClusterTester::InvokeResult<RemoveSceneResponse::DecodableType> remove_response =
tester.Invoke<RemoveScene::Type, RemoveSceneResponse::DecodableType>(RemoveScene::Id, remove_request);
ExpectCommandStatus(remove_response, Status::NotFound);
}
TEST_F(TestScenesManagementCluster, AddSceneCommandResourceExhausted)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Fill the scene table to its maximum capacity.
for (SceneId sceneId = 0; sceneId < kMaxScenesPerFabric; ++sceneId)
{
AddSceneToTable(tester, kTestGroupId, sceneId);
}
// Attempt to add one more scene, exceeding the fabric's scene capacity.
AddScene::Type add_request_fail;
add_request_fail.groupID = kTestGroupId;
add_request_fail.sceneID = kMaxScenesPerFabric; // This sceneId will exceed the limit
add_request_fail.transitionTime = 100;
add_request_fail.sceneName = "OverflowScene"_span;
add_request_fail.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
ClusterTester::InvokeResult<AddSceneResponse::DecodableType> add_response_fail =
tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, add_request_fail);
ExpectCommandStatus(add_response_fail, Status::ResourceExhausted);
}
TEST_F(TestScenesManagementCluster, CopySceneCommandNotFound)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
CopyScene::Type copy_request;
copy_request.mode.Set(CopyModeBitmap::kCopyAllScenes, false);
copy_request.groupIdentifierFrom = kTestOtherGroupId; // Non-existent group
copy_request.sceneIdentifierFrom = kTestOtherSceneId; // Non-existent scene
copy_request.groupIdentifierTo = kTestGroupId;
copy_request.sceneIdentifierTo = kTestSceneId;
auto copy_response = tester.Invoke<CopyScene::Type, CopySceneResponse::DecodableType>(CopyScene::Id, copy_request);
ExpectCommandStatus(copy_response, Status::NotFound);
}
TEST_F(TestScenesManagementCluster, CopySceneCommandResourceExhausted)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Fill the scene table to its maximum capacity for the fabric.
for (SceneId sceneId = 0; sceneId < kMaxScenesPerFabric; ++sceneId)
{
AddSceneToTable(tester, kTestOtherGroupId, sceneId, "DestScene", 0);
}
// 2. Attempt to add a source scene to a different group. This will fail with RESOURCE_EXHAUSTED
// because the fabric's scene table is already full.
AddScene::Type add_source_request;
add_source_request.groupID = kTestGroupId; // Source group
add_source_request.sceneID = kTestSceneId;
add_source_request.transitionTime = 0;
add_source_request.sceneName = "SourceScene"_span;
add_source_request.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
auto add_source_response = tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, add_source_request);
ExpectCommandStatus(add_source_response, Status::ResourceExhausted);
// 3. Attempt to copy the non-existent source scene to the destination group.
// This is expected to fail with NOT_FOUND first, but we are testing RESOURCE_EXHAUSTED scenarios.
// In a real scenario, this would return NOT_FOUND. However, since the table is full, if the scene DID exist,
// it would return RESOURCE_EXHAUSTED. To force this, we can try copying a scene that DOES exist.
// Let's use scene {kTestOtherGroupId, 0} as the source, to copy to {kTestGroupId, kTestSceneId}.
CopyScene::Type copy_request;
copy_request.mode.Set(CopyModeBitmap::kCopyAllScenes, false);
copy_request.groupIdentifierFrom = kTestOtherGroupId;
copy_request.sceneIdentifierFrom = 0;
copy_request.groupIdentifierTo = kTestGroupId; // Different group
copy_request.sceneIdentifierTo = kTestSceneId; // A new, unused scene ID
auto copy_response = tester.Invoke<CopyScene::Type, CopySceneResponse::DecodableType>(CopyScene::Id, copy_request);
// Even though the destination GroupID/SceneID doesn't exist, the table is full, so RESOURCE_EXHAUSTED is returned first.
ExpectCommandStatus(copy_response, Status::ResourceExhausted);
}
TEST_F(TestScenesManagementCluster, SceneNamesFeatureEnabledTest)
{
// Scenario: kSceneNames feature ENABLED (default for fixture).
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "MySceneName");
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestSceneId;
auto view_response = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ExpectCommandStatus(view_response, Status::Success);
ASSERT_TRUE(view_response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
ASSERT_TRUE(view_response.response->sceneName.HasValue());
EXPECT_TRUE(view_response.response->sceneName.Value().data_equal("MySceneName"_span));
// NOLINTEND(bugprone-unchecked-optional-access)
}
TEST_F(TestScenesManagementCluster, SceneNamesFeatureDisabledTest)
{
// Scenario: kSceneNames feature DISABLED.
// Create a new cluster instance with kSceneNames feature disabled.
TestScenesManagementTableProvider disabledSceneTableProvider;
disabledSceneTableProvider.Init(&testContext.StorageDelegate(), &testContext.Get().provider);
ScenesManagementCluster clusterWithoutSceneNames(
kTestEndpointId,
ScenesManagementCluster::Context{ .groupDataProvider = &mockGroupDataProvider,
.fabricTable = &fabricTable,
.features = BitMask<ScenesManagement::Feature>(), // No features enabled
.sceneTableProvider = disabledSceneTableProvider,
.supportsCopyScene = true }); // supportsCopyScene doesn't affect scene names
ASSERT_EQ(clusterWithoutSceneNames.Startup(testContext.Get()), CHIP_NO_ERROR);
ClusterTester tester(clusterWithoutSceneNames);
tester.SetFabricIndex(kFabricIndex);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "IgnoredSceneName");
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestSceneId;
auto view_response = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ExpectCommandStatus(view_response, Status::Success);
ASSERT_TRUE(view_response.response.has_value());
// When kSceneNames is not supported, the Scene Name must be an empty string.
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(!view_response.response.value().sceneName.HasValue() || view_response.response.value().sceneName.Value().empty());
clusterWithoutSceneNames.Shutdown(ClusterShutdownType::kClusterShutdown);
}
TEST_F(TestScenesManagementCluster, CopyAllScenesCommandSuccess)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add multiple scenes to the source group kTestGroupId.
const SceneId sourceSceneIds[] = { kTestSceneId, kTestOtherSceneId };
for (const auto & sceneId : sourceSceneIds)
{
AddSceneToTable(tester, kTestGroupId, sceneId);
}
// 2. Invoke CopyScene with kCopyAllScenes mode to copy from kTestGroupId to kTestOtherGroupId.
CopyScene::Type copy_request;
copy_request.mode.Set(CopyModeBitmap::kCopyAllScenes, true);
copy_request.groupIdentifierFrom = kTestGroupId;
copy_request.sceneIdentifierFrom = 0; // Per Matter Spec 1.4 - 1.10.6.10, this field is ignored when copying all scenes.
copy_request.groupIdentifierTo = kTestOtherGroupId;
copy_request.sceneIdentifierTo = 0; // Per Matter Spec 1.4 - 1.10.6.10, this field is ignored when copying all scenes.
auto copy_response = tester.Invoke<CopyScene::Type, CopySceneResponse::DecodableType>(CopyScene::Id, copy_request);
ExpectCommandStatus(copy_response, Status::Success);
ASSERT_TRUE(copy_response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_EQ(copy_response.response->groupIdentifierFrom, kTestGroupId);
// 3. Verify that all scenes were copied to the destination group.
for (const auto & sceneId : sourceSceneIds)
{
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId destSceneId(sceneId, kTestOtherGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry destSceneEntry(destSceneId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, destSceneId, destSceneEntry), CHIP_NO_ERROR);
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId srcSceneId(sceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry srcSceneEntry(srcSceneId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, srcSceneId, srcSceneEntry), CHIP_NO_ERROR);
EXPECT_EQ(destSceneEntry.mStorageData, srcSceneEntry.mStorageData);
}
}
TEST_F(TestScenesManagementCluster, CopySceneOverwriteExisting)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add a source scene
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "SourceScene", 1234);
// 2. Add a destination scene with the same ID, but different content
AddSceneToTable(tester, kTestGroupId, kTestOtherSceneId, "OrigDestScene", 5678);
// 3. Copy from {kTestGroupId, kTestSceneId} to {kTestGroupId, kTestOtherSceneId}, overwriting the existing scene.
CopyScene::Type copy_request;
copy_request.mode.Set(CopyModeBitmap::kCopyAllScenes, false);
copy_request.groupIdentifierFrom = kTestGroupId;
copy_request.sceneIdentifierFrom = kTestSceneId;
copy_request.groupIdentifierTo = kTestGroupId;
copy_request.sceneIdentifierTo = kTestOtherSceneId;
auto copy_response = tester.Invoke<CopyScene::Type, CopySceneResponse::DecodableType>(CopyScene::Id, copy_request);
ExpectCommandStatus(copy_response, Status::Success);
// 4. Verify the destination scene is now a copy of the source scene.
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestOtherSceneId;
auto view_response = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ExpectCommandStatus(view_response, Status::Success);
ASSERT_TRUE(view_response.response.has_value());
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(view_response.response->sceneName.Value().data_equal("SourceScene"_span));
EXPECT_EQ(view_response.response->transitionTime.Value(), 1234u);
// NOLINTEND(bugprone-unchecked-optional-access)
}
TEST_F(TestScenesManagementCluster, FabricScopingAddScene)
{
ClusterTester tester(cluster);
// Add scene to fabric 1
tester.SetFabricIndex(kFabricIndex);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric1Scene");
// Add same scene ID to fabric 2 with a different name
tester.SetFabricIndex(kFabricIndex2);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric2Scene");
// Verify fabric 1 scene
tester.SetFabricIndex(kFabricIndex);
ViewScene::Type view_request1;
view_request1.groupID = kTestGroupId;
view_request1.sceneID = kTestSceneId;
auto view_response1 = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request1);
ExpectCommandStatus(view_response1, Status::Success);
ASSERT_TRUE(view_response1.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(view_response1.response->sceneName.Value().data_equal("Fabric1Scene"_span));
// Verify fabric 2 scene
tester.SetFabricIndex(kFabricIndex2);
ViewScene::Type view_request2;
view_request2.groupID = kTestGroupId;
view_request2.sceneID = kTestSceneId;
auto view_response2 = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request2);
ExpectCommandStatus(view_response2, Status::Success);
ASSERT_TRUE(view_response2.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
EXPECT_TRUE(view_response2.response->sceneName.Value().data_equal("Fabric2Scene"_span));
// Verify scene counts for both fabrics
tester.SetFabricIndex(kFabricIndex); // Reading attributes can be done from any fabric context
Attributes::FabricSceneInfo::TypeInfo::DecodableType sceneInfoList;
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
auto it = sceneInfoList.begin();
int found_fabrics = 0;
while (it.Next())
{
auto const & info = it.GetValue();
if (info.fabricIndex == kFabricIndex)
{
EXPECT_EQ(info.sceneCount, 1u);
found_fabrics++;
}
else if (info.fabricIndex == kFabricIndex2)
{
EXPECT_EQ(info.sceneCount, 1u);
found_fabrics++;
}
}
EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR);
EXPECT_EQ(found_fabrics, 2);
}
TEST_F(TestScenesManagementCluster, FabricRemovalRemovesScenes)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add a scene for Fabric 1
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric1Scene");
// 2. Add a scene for Fabric 2
tester.SetFabricIndex(kFabricIndex2);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric2Scene");
// Verify scenes exist
VerifySceneInfoCount(tester, kFabricIndex, 1);
VerifySceneInfoCount(tester, kFabricIndex2, 1);
// 3. Simulate Fabric 1 removal
cluster.OnFabricRemoved(fabricTable, kFabricIndex);
// 4. Verify Fabric 1 scenes are gone
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId1(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry1(sceneStorageId1);
EXPECT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId1, sceneEntry1), CHIP_ERROR_NOT_FOUND);
// 5. Verify Fabric 2 scenes are still there
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId2(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry2(sceneStorageId2);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex2, sceneStorageId2, sceneEntry2), CHIP_NO_ERROR);
// 6. Verify FabricSceneInfo list is updated
Attributes::FabricSceneInfo::TypeInfo::DecodableType sceneInfoList;
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
auto it = sceneInfoList.begin();
bool foundFabric1 = false;
bool foundFabric2 = false;
while (it.Next())
{
if (it.GetValue().fabricIndex == kFabricIndex)
{
foundFabric1 = true;
}
else if (it.GetValue().fabricIndex == kFabricIndex2)
{
foundFabric2 = true;
EXPECT_EQ(it.GetValue().sceneCount, 1u);
}
}
EXPECT_EQ(it.GetStatus(), CHIP_NO_ERROR);
EXPECT_FALSE(foundFabric1); // Fabric 1 should not be in the list anymore
EXPECT_TRUE(foundFabric2);
}
TEST_F(TestScenesManagementCluster, GroupRemovalRemovesScenes)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add scenes for kTestGroupId and kTestOtherGroupId
AddSceneToTable(tester, kTestGroupId, kTestSceneId);
AddSceneToTable(tester, kTestOtherGroupId, kTestSceneId);
// Verify scenes exist
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneId1(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry entry1(sceneId1);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneId1, entry1), CHIP_NO_ERROR);
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneId2(kTestSceneId, kTestOtherGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry entry2(sceneId2);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneId2, entry2), CHIP_NO_ERROR);
// 2. Call GroupWillBeRemoved for kTestGroupId
ASSERT_EQ(cluster.GroupWillBeRemoved(kFabricIndex, kTestGroupId), CHIP_NO_ERROR);
// 3. Verify scenes for kTestGroupId are gone
EXPECT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneId1, entry1), CHIP_ERROR_NOT_FOUND);
// 4. Verify scenes for kTestOtherGroupId are still there
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneId2, entry2), CHIP_NO_ERROR);
}
TEST_F(TestScenesManagementCluster, ShutdownPermanentRemoveWipesData)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add a scene
AddSceneToTable(tester, kTestGroupId, kTestSceneId);
// Verify it exists in storage
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry(sceneStorageId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_NO_ERROR);
// 2. Shutdown with kPermanentRemove
cluster.Shutdown(ClusterShutdownType::kPermanentRemove);
// 3. Verify data is gone from storage
// Re-init provider and table
sceneTableProvider.Init(&testContext.StorageDelegate(), &testContext.Get().provider);
// Check entry
EXPECT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_ERROR_NOT_FOUND);
// Restore cluster for TearDown to be happy
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
}
TEST_F(TestScenesManagementCluster, PersistenceAfterPowerCycle)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add a scene
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "PersistentScene");
// 2. Simulate Power Cycle: Shutdown cluster and provider, but KEEP storage.
sceneTableProvider.mSceneTable->UnregisterHandler(&mMockSceneHandler);
cluster.Shutdown(ClusterShutdownType::kClusterShutdown);
// 3. Re-initialize
sceneTableProvider.Init(&testContext.StorageDelegate(), &testContext.Get().provider);
sceneTableProvider.mSceneTable->RegisterHandler(&mMockSceneHandler);
ASSERT_EQ(cluster.Startup(testContext.Get()), CHIP_NO_ERROR);
// 4. Verify scene still exists
SceneTable<ExtensionFieldSetsImpl>::SceneStorageId sceneStorageId(kTestSceneId, kTestGroupId);
SceneTable<ExtensionFieldSetsImpl>::SceneTableEntry sceneEntry(sceneStorageId);
ASSERT_EQ(sceneTableProvider.mSceneTable->GetSceneTableEntry(kFabricIndex, sceneStorageId, sceneEntry), CHIP_NO_ERROR);
EXPECT_EQ(sceneEntry.mStorageData.mNameLength, 15u); // "PersistentScene" length
CharSpan nameSpan(sceneEntry.mStorageData.mName, sceneEntry.mStorageData.mNameLength);
EXPECT_TRUE(nameSpan.data_equal("PersistentScene"_span));
}
TEST_F(TestScenesManagementCluster, RecallSceneInvalidatesOtherFabrics)
{
// Pretend that we have some fabrics since this is what our tests expect
// Note I could only find mock data for 2 fabrics even though fabric index 3 is defined.
// This is just sufficient here for our own tests (adding fabric entries is rough!)
ASSERT_EQ(AddFakeFabric(fabricTable, kFabricIndex), CHIP_NO_ERROR);
ASSERT_EQ(AddFakeFabric(fabricTable, kFabricIndex2), CHIP_NO_ERROR);
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// 1. Add scenes for Fabric 1 and Fabric 2
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric1Scene");
tester.SetFabricIndex(kFabricIndex2);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric2Scene");
// 2. Recall Scene on Fabric 1
tester.SetFabricIndex(kFabricIndex);
RecallScene::Type recall_request;
recall_request.groupID = kTestGroupId;
recall_request.sceneID = kTestSceneId;
auto recall_response = tester.Invoke<RecallScene::Type, NullObjectType>(RecallScene::Id, recall_request);
ASSERT_TRUE(recall_response.IsSuccess());
// 3. Verify SceneValid is TRUE for Fabric 1 and FALSE for Fabric 2
// Make sure read is using fabric 1 (so that we can read fabric 1 data) - that allows us to read fabric sensitive fields
// like currentScene and currentGroup
tester.SetFabricIndex(kFabricIndex);
Attributes::FabricSceneInfo::TypeInfo::DecodableType sceneInfoList;
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
auto it = sceneInfoList.begin();
while (it.Next())
{
auto val = it.GetValue();
if (val.fabricIndex == kFabricIndex)
{
EXPECT_EQ(val.currentScene, kTestSceneId);
EXPECT_EQ(val.currentGroup, kTestGroupId);
EXPECT_TRUE(val.sceneValid);
}
else if (val.fabricIndex == kFabricIndex2)
{
// For Fabric 2, if sceneValid is initialized, it should be false (or effectively treated as invalid)
EXPECT_FALSE(val.sceneValid);
}
}
// 4. Recall Scene on Fabric 2
tester.SetFabricIndex(kFabricIndex2);
auto recall_response2 = tester.Invoke<RecallScene::Type, NullObjectType>(RecallScene::Id, recall_request);
ASSERT_TRUE(recall_response2.IsSuccess());
// 5. Verify SceneValid is TRUE for Fabric 2 and FALSE for Fabric 1
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
auto it2 = sceneInfoList.begin();
while (it2.Next())
{
auto val = it2.GetValue();
if (val.fabricIndex == kFabricIndex)
{
// Fabric 1 should now be invalid
EXPECT_FALSE(val.sceneValid);
}
else if (val.fabricIndex == kFabricIndex2)
{
EXPECT_TRUE(val.sceneValid);
}
}
}
TEST_F(TestScenesManagementCluster, TransitionTimeLimit)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Try adding a scene with TransitionTime > 60,000,000 ms
// 60,000,001
AddScene::Type request_data;
request_data.groupID = kTestGroupId;
request_data.sceneID = kTestSceneId;
request_data.transitionTime = 60000001;
request_data.sceneName = "TooLong"_span;
request_data.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
auto response = tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, request_data);
// Expect ConstraintError
ExpectCommandStatus(response, Status::ConstraintError);
}
TEST_F(TestScenesManagementCluster, SceneNameLength)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Try adding a scene with Name length > 16
// "12345678901234567" (17 chars)
AddScene::Type request_data;
request_data.groupID = kTestGroupId;
request_data.sceneID = kTestSceneId;
request_data.transitionTime = 100;
request_data.sceneName = "12345678901234567"_span;
request_data.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
auto response = tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, request_data);
// Expect ConstraintError
ExpectCommandStatus(response, Status::ConstraintError);
}
TEST_F(TestScenesManagementCluster, DuplicateFieldSets)
{
ClusterTester tester(cluster);
tester.SetFabricIndex(kFabricIndex);
// Create EFS with duplicate cluster IDs
AttributeValuePairStruct::Type attributeValue1;
attributeValue1.attributeID = 0;
attributeValue1.valueUnsigned8.SetValue(true); // First value
AttributeValuePairStruct::Type attributeValue2;
attributeValue2.attributeID = 0;
attributeValue2.valueUnsigned8.SetValue(false); // Second value (should overwrite)
AttributeValuePairStruct::Type avList1[] = { attributeValue1 };
AttributeValuePairStruct::Type avList2[] = { attributeValue2 };
ExtensionFieldSetStruct::Type efs1;
efs1.clusterID = kMockClusterId;
efs1.attributeValueList = DataModel::List<AttributeValuePairStruct::Type>(avList1);
ExtensionFieldSetStruct::Type efs2;
efs2.clusterID = kMockClusterId;
efs2.attributeValueList = DataModel::List<AttributeValuePairStruct::Type>(avList2);
ExtensionFieldSetStruct::Type efsList[] = { efs1, efs2 };
DataModel::List<ExtensionFieldSetStruct::Type> efs(efsList);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "DuplicateEFS", 100, &efs);
// Verify only the last one is recorded (value should be false)
ViewScene::Type view_request;
view_request.groupID = kTestGroupId;
view_request.sceneID = kTestSceneId;
auto view_response = tester.Invoke<ViewScene::Type, ViewSceneResponse::DecodableType>(ViewScene::Id, view_request);
ExpectCommandStatus(view_response, Status::Success);
ASSERT_TRUE(view_response.response.has_value());
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto & data = *view_response.response;
ASSERT_TRUE(data.extensionFieldSetStructs.HasValue());
auto responseEfsList = data.extensionFieldSetStructs.Value();
int count = 0;
auto iter = responseEfsList.begin();
while (iter.Next())
{
count++;
auto const & received_efs = iter.GetValue();
if (received_efs.clusterID == kMockClusterId)
{
auto attr_iter = received_efs.attributeValueList.begin();
ASSERT_TRUE(attr_iter.Next());
auto const & attr = attr_iter.GetValue();
EXPECT_EQ(attr.valueUnsigned8.Value(), false); // Should be the second value
}
}
EXPECT_EQ(count, 1); // Should only have 1 entry for kMockClusterId
}
TEST_F(TestScenesManagementCluster, RemainingCapacityUpdatesAcrossFabrics)
{
ClusterTester tester(cluster);
// 1. Add a scene on Fabric 1 to populate FabricSceneInfo and consume some capacity
tester.SetFabricIndex(kFabricIndex);
AddSceneToTable(tester, kTestGroupId, kTestSceneId, "Fabric1Scene");
// 2. Get initial capacity on Fabric 1
Attributes::FabricSceneInfo::TypeInfo::DecodableType sceneInfoList;
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
uint8_t capacity1_initial = 0;
bool found1 = false;
auto it = sceneInfoList.begin();
while (it.Next())
{
if (it.GetValue().fabricIndex == kFabricIndex)
{
capacity1_initial = it.GetValue().remainingCapacity;
found1 = true;
}
}
ASSERT_TRUE(found1);
// 3. Add scenes on Fabric 2 and 3 until we hit global limit
// We simply try to add more scenes than what a single fabric can hold,
// and do this for multiple fabrics to exhaust the endpoint limit.
// Fill Fabric 2
tester.SetFabricIndex(kFabricIndex2);
for (int i = 0; i < kMaxScenesPerFabric + 5; i++)
{
AddScene::Type request_data;
request_data.groupID = kTestGroupId;
request_data.sceneID = static_cast<SceneId>(kTestSceneId + i + 1);
request_data.transitionTime = 100;
request_data.sceneName = "Fabric2Scene"_span;
request_data.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
// We ignore the result here because we expect failure eventually
auto response = tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, request_data);
(void) response;
}
// Fill Fabric 3
tester.SetFabricIndex(kFabricIndex3);
for (int i = 0; i < kMaxScenesPerFabric + 5; i++)
{
AddScene::Type request_data;
request_data.groupID = kTestGroupId;
request_data.sceneID = static_cast<SceneId>(kTestSceneId + i + 1);
request_data.transitionTime = 100;
request_data.sceneName = "Fabric3Scene"_span;
request_data.extensionFieldSetStructs = List<ExtensionFieldSetStruct::Type>();
// We ignore the result here because we expect failure eventually
auto response = tester.Invoke<AddScene::Type, AddSceneResponse::DecodableType>(AddScene::Id, request_data);
(void) response;
}
// 4. Get capacity on Fabric 1 again
tester.SetFabricIndex(kFabricIndex);
ASSERT_EQ(tester.ReadAttribute(Attributes::FabricSceneInfo::Id, sceneInfoList), CHIP_NO_ERROR);
uint8_t capacity1_new = 0;
found1 = false;
auto it2 = sceneInfoList.begin();
while (it2.Next())
{
if (it2.GetValue().fabricIndex == kFabricIndex)
{
capacity1_new = it2.GetValue().remainingCapacity;
found1 = true;
}
}
ASSERT_TRUE(found1);
// 5. Verify capacity decreased
EXPECT_LT(capacity1_new, capacity1_initial);
}
} // namespace