| /* |
| * 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/group-key-mgmt-server/GroupKeyManagementCluster.h> |
| #include <app/server-cluster/DefaultServerCluster.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/GroupKeyManagement/ClusterId.h> |
| #include <clusters/GroupKeyManagement/Enums.h> |
| #include <clusters/GroupKeyManagement/Ids.h> |
| #include <clusters/GroupKeyManagement/Metadata.h> |
| #include <clusters/GroupKeyManagement/Structs.h> |
| #include <credentials/GroupDataProvider.h> |
| #include <lib/core/CHIPError.h> |
| #include <lib/support/ReadOnlyBuffer.h> |
| namespace { |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters; |
| using namespace chip::app::DataModel; |
| using namespace chip::Testing; |
| using chip::Testing::IsAcceptedCommandsListEqualTo; |
| using chip::Testing::IsGeneratedCommandsListEqualTo; |
| |
| chip::FabricIndex kTestFabricIndex = Testing::kTestFabricIndex; |
| const chip::GroupId kTestGroupId = 0x1234; |
| constexpr uint16_t kTestKeySetId = 1; |
| |
| namespace TestHelpers { |
| |
| GroupKeyManagement::Structs::GroupKeyMapStruct::Type CreateKey(GroupId groupId, uint16_t keySetId, FabricIndex fabricIndex) |
| { |
| GroupKeyManagement::Structs::GroupKeyMapStruct::Type key; |
| key.groupId = groupId; |
| key.groupKeySetID = keySetId; |
| key.fabricIndex = fabricIndex; |
| return key; |
| } |
| std::vector<GroupKeyManagement::Structs::GroupKeyMapStruct::Type> |
| CreateGroupKeyMapList(size_t count, FabricIndex fabricIndex, GroupId startGroupId = kTestGroupId, |
| uint16_t startKeySetId = kTestKeySetId, uint16_t groupIdIncrement = 1, uint16_t keySetIdIncrement = 1) |
| { |
| std::vector<GroupKeyManagement::Structs::GroupKeyMapStruct::Type> list; |
| for (size_t i = 0; i < count; ++i) |
| { |
| GroupId currentGroupId = static_cast<GroupId>(startGroupId + (i * groupIdIncrement)); |
| uint16_t currentKeySetId = static_cast<uint16_t>(startKeySetId + (i * keySetIdIncrement)); |
| list.push_back(CreateKey(currentGroupId, currentKeySetId, fabricIndex)); |
| } |
| return list; |
| } |
| |
| } // namespace TestHelpers |
| |
| struct TestGroupKeyManagementCluster : public ::testing::Test |
| { |
| |
| static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } |
| static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } |
| |
| TestServerClusterContext mTestContext; |
| Credentials::GroupDataProviderImpl mRealProvider; |
| Crypto::DefaultSessionKeystore mMockKeystore; |
| FabricTestFixture fabricHelper{ &mTestContext.StorageDelegate() }; |
| |
| GroupKeyManagementCluster mCluster{ { fabricHelper.GetFabricTable(), mRealProvider } }; |
| |
| ClusterTester tester{ mCluster }; |
| |
| void SetUp() override |
| { |
| auto * storage = &mTestContext.StorageDelegate(); |
| |
| mRealProvider.SetStorageDelegate(storage); |
| mRealProvider.SetSessionKeystore(&mMockKeystore); |
| ASSERT_EQ(mRealProvider.Init(), CHIP_NO_ERROR); |
| ASSERT_EQ(mCluster.Startup(mTestContext.Get()), CHIP_NO_ERROR); |
| CHIP_ERROR err = fabricHelper.SetUpTestFabric(kTestFabricIndex); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| Credentials::SetGroupDataProvider(&mRealProvider); |
| tester.SetFabricIndex(kTestFabricIndex); |
| } |
| |
| void TearDown() override |
| { |
| tester.SetFabricIndex(kUndefinedFabricIndex); |
| mCluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| Credentials::SetGroupDataProvider(nullptr); |
| CHIP_ERROR err = fabricHelper.TearDownTestFabric(kTestFabricIndex); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| mRealProvider.Finish(); |
| } |
| // Writes a list of group keys to the GroupKeyMap attribute for a given fabric. |
| // Used to set up test scenarios with pre-existing keys. |
| void PrepopulateGroupKeyMap(const std::vector<GroupKeyManagement::Structs::GroupKeyMapStruct::Type> & keys, |
| ListWritingPattern listWritingPattern) |
| { |
| auto listToWrite = |
| app::DataModel::List<const GroupKeyManagement::Structs::GroupKeyMapStruct::Type>(keys.data(), keys.size()); |
| |
| CHIP_ERROR err = tester.WriteAttribute(GroupKeyManagement::Attributes::GroupKeyMap::Id, listToWrite, listWritingPattern) |
| .GetUnderlyingError(); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| } |
| |
| // Checks that the stored group keys for a fabric match the expected list. |
| // Validates group IDs and keyset IDs to ensure attribute writes succeeded. |
| void VerifyGroupKeysMatch(const FabricIndex fabricIndex, |
| const std::vector<GroupKeyManagement::Structs::GroupKeyMapStruct::Type> & expectedKeys) |
| { |
| auto * iterator = mRealProvider.IterateGroupKeys(fabricIndex); |
| ASSERT_NE(iterator, nullptr); |
| |
| ASSERT_EQ(iterator->Count(), expectedKeys.size()); |
| |
| size_t i = 0; |
| Credentials::GroupDataProvider::GroupKey storedKey; |
| while (iterator->Next(storedKey)) |
| { |
| ASSERT_LT(i, expectedKeys.size()); |
| EXPECT_EQ(storedKey.group_id, expectedKeys[i].groupId); |
| EXPECT_EQ(storedKey.keyset_id, expectedKeys[i].groupKeySetID); |
| ++i; |
| } |
| |
| ASSERT_EQ(i, expectedKeys.size()); |
| iterator->Release(); // ensure this frees memory or returns to pool |
| } |
| }; |
| |
| TEST_F(TestGroupKeyManagementCluster, CompileTest) |
| { |
| ASSERT_EQ(mCluster.GetClusterFlags({ kRootEndpointId, GroupKeyManagement::Id }), BitFlags<ClusterQualityFlags>()); |
| } |
| |
| TEST_F(TestGroupKeyManagementCluster, CommandsTest) |
| { |
| // Check required accepted commands are present |
| ASSERT_TRUE(IsAcceptedCommandsListEqualTo(mCluster, |
| { |
| GroupKeyManagement::Commands::KeySetWrite::kMetadataEntry, |
| GroupKeyManagement::Commands::KeySetRead::kMetadataEntry, |
| GroupKeyManagement::Commands::KeySetRemove::kMetadataEntry, |
| GroupKeyManagement::Commands::KeySetReadAllIndices::kMetadataEntry, |
| })); |
| |
| // Check required generated commands are present |
| ASSERT_TRUE(IsGeneratedCommandsListEqualTo(mCluster, |
| { |
| GroupKeyManagement::Commands::KeySetReadAllIndicesResponse::Id, |
| GroupKeyManagement::Commands::KeySetReadResponse::Id, |
| })); |
| } |
| TEST_F(TestGroupKeyManagementCluster, AttributesTest) |
| { |
| std::vector<app::DataModel::AttributeEntry> expectedAttributes = { |
| DataModel::AttributeEntry(GroupKeyManagement::Attributes::GroupKeyMap::Id, |
| BitFlags<DataModel::AttributeQualityFlags>(DataModel::AttributeQualityFlags::kListAttribute, |
| DataModel::AttributeQualityFlags::kChangesOmitted), |
| Access::Privilege::kView, Access::Privilege::kManage), |
| GroupKeyManagement::Attributes::GroupTable::kMetadataEntry, |
| GroupKeyManagement::Attributes::MaxGroupsPerFabric::kMetadataEntry, |
| GroupKeyManagement::Attributes::MaxGroupKeysPerFabric::kMetadataEntry, |
| }; |
| |
| ASSERT_TRUE(chip::Testing::IsAttributesListEqualTo(mCluster, expectedAttributes)); |
| } |
| |
| // Cluster should accept writing multiple group keys with the same KeySetID but different Group IDs |
| TEST_F(TestGroupKeyManagementCluster, TestWriteGroupKeyMapAttributeSameKeySetDifferentGroup) |
| { |
| for (ListWritingPattern listWritingPattern : { ListWritingPattern::ReplaceAll, ListWritingPattern::ClearAllThenAppendItems }) |
| { |
| auto keys = TestHelpers::CreateGroupKeyMapList(2, kTestFabricIndex, kTestGroupId, kTestKeySetId, 1, 0); |
| PrepopulateGroupKeyMap(keys, listWritingPattern); |
| VerifyGroupKeysMatch(kTestFabricIndex, keys); |
| } |
| } |
| |
| // Cluster should reject a write containing duplicate keys for the same group/keyset combination. |
| TEST_F(TestGroupKeyManagementCluster, TestWriteGroupKeyMapAttributeDuplicateKey) |
| { |
| for (ListWritingPattern listWritingPattern : { ListWritingPattern::ReplaceAll, ListWritingPattern::ClearAllThenAppendItems }) |
| { |
| // Intentionally creates two identical entries (duplicate group/keyset combination). |
| // by setting increments to 0 (groupIdIncrement = 0, keySetIdIncrement = 0) |
| auto keys = TestHelpers::CreateGroupKeyMapList(2, kTestFabricIndex, kTestGroupId, kTestKeySetId, 0, 0); |
| |
| auto listToWrite = |
| app::DataModel::List<const GroupKeyManagement::Structs::GroupKeyMapStruct::Type>(keys.data(), keys.size()); |
| |
| CHIP_ERROR err = tester.WriteAttribute(GroupKeyManagement::Attributes::GroupKeyMap::Id, listToWrite, listWritingPattern) |
| .GetUnderlyingError(); |
| |
| ASSERT_EQ(err, CHIP_ERROR_DUPLICATE_KEY_ID); |
| |
| // Explicitly check that the write stops at the first duplicate entry, |
| // but the valid entries processed *before* the error (i.e., keys[0]) are persisted. |
| std::vector<GroupKeyManagement::Structs::GroupKeyMapStruct::Type> expectedKeys; |
| expectedKeys.push_back(keys[0]); |
| |
| VerifyGroupKeysMatch(kTestFabricIndex, expectedKeys); |
| } |
| } |
| |
| TEST_F(TestGroupKeyManagementCluster, TestWriteGroupKeyMapAttribute) |
| { |
| for (ListWritingPattern listWritingPattern : { ListWritingPattern::ReplaceAll, ListWritingPattern::ClearAllThenAppendItems }) |
| { |
| auto keys = TestHelpers::CreateGroupKeyMapList(2, kTestFabricIndex); |
| |
| PrepopulateGroupKeyMap(keys, listWritingPattern); |
| VerifyGroupKeysMatch(kTestFabricIndex, keys); |
| } |
| } |
| |
| const chip::EndpointId kTestEndpoint1 = 10; |
| const chip::EndpointId kTestEndpoint2 = 11; |
| constexpr uint64_t kStartTimeOffset = 100; |
| |
| // spec states - group should be deleted if the endpoint member list's last member is removed |
| TEST_F(TestGroupKeyManagementCluster, TestGroupEndpointLifecycle) |
| { |
| CHIP_ERROR err; |
| |
| auto doesGroupExist = [&](GroupId gid) { |
| auto * iter = mRealProvider.IterateGroupInfo(kTestFabricIndex); |
| if (iter == nullptr) |
| { |
| return false; |
| } |
| chip::Credentials::GroupDataProvider::GroupInfo info; |
| bool found = false; |
| while (iter->Next(info)) |
| { |
| if (info.group_id == gid) |
| { |
| found = true; |
| break; |
| } |
| } |
| iter->Release(); |
| return found; |
| }; |
| |
| auto getEndpointCountForGroup = [&](GroupId gid) { |
| auto * iter = mRealProvider.IterateEndpoints(kTestFabricIndex, gid); |
| if (iter == nullptr) |
| { |
| return size_t(0); |
| } |
| size_t count = iter->Count(); |
| iter->Release(); |
| return count; |
| }; |
| |
| ASSERT_FALSE(doesGroupExist(kTestGroupId)); |
| ASSERT_EQ(getEndpointCountForGroup(kTestGroupId), 0u); |
| |
| err = mRealProvider.AddEndpoint(kTestFabricIndex, kTestGroupId, kTestEndpoint1); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| |
| ASSERT_TRUE(doesGroupExist(kTestGroupId)); |
| ASSERT_EQ(getEndpointCountForGroup(kTestGroupId), 1u); |
| |
| err = mRealProvider.AddEndpoint(kTestFabricIndex, kTestGroupId, kTestEndpoint2); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| |
| ASSERT_TRUE(doesGroupExist(kTestGroupId)); |
| ASSERT_EQ(getEndpointCountForGroup(kTestGroupId), 2u); |
| |
| err = mRealProvider.RemoveEndpoint(kTestFabricIndex, kTestGroupId, kTestEndpoint1); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| |
| ASSERT_TRUE(doesGroupExist(kTestGroupId)); |
| ASSERT_EQ(getEndpointCountForGroup(kTestGroupId), 1u); |
| |
| err = mRealProvider.RemoveEndpoint(kTestFabricIndex, kTestGroupId, kTestEndpoint2); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| |
| ASSERT_FALSE(doesGroupExist(kTestGroupId)); |
| ASSERT_EQ(getEndpointCountForGroup(kTestGroupId), 0u); |
| } |
| |
| const uint64_t kStartTime = 123456789; |
| |
| static const chip::Credentials::GroupDataProvider::EpochKey kKey = { |
| .start_time = 0, .key = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 } |
| }; |
| |
| static const chip::Credentials::GroupDataProvider::EpochKey kNewKey = { |
| .start_time = 0, .key = { 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 } |
| }; |
| |
| // writing a valid key set |
| TEST_F(TestGroupKeyManagementCluster, TestKeySetWriteCommand) |
| { |
| |
| GroupKeyManagement::Commands::KeySetWrite::Type requestData; |
| |
| requestData.groupKeySet = { |
| .groupKeySetID = kTestKeySetId, |
| .groupKeySecurityPolicy = GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst, |
| .epochKey0 = chip::ByteSpan(kKey.key, sizeof(kKey.key)), |
| .epochStartTime0 = kStartTime, |
| .epochKey1 = DataModel::NullNullable, |
| .epochStartTime1 = DataModel::NullNullable, |
| .epochKey2 = DataModel::NullNullable, |
| .epochStartTime2 = DataModel::NullNullable, |
| }; |
| |
| auto result = tester.Invoke(GroupKeyManagement::Commands::KeySetWrite::Id, requestData); |
| EXPECT_TRUE(result.IsSuccess()); |
| Credentials::GroupDataProvider::KeySet storedKeySet; |
| CHIP_ERROR err = mRealProvider.GetKeySet(kTestFabricIndex, kTestKeySetId, storedKeySet); |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| ASSERT_EQ(storedKeySet.keyset_id, kTestKeySetId); |
| ASSERT_EQ(storedKeySet.policy, GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst); |
| ASSERT_EQ(storedKeySet.num_keys_used, 1); |
| ASSERT_EQ(storedKeySet.epoch_keys[0].start_time, kStartTime); |
| } |
| |
| // Spec states that writing a KeySet with an existing KeySetID on the same fabric should overwrite the existing one |
| TEST_F(TestGroupKeyManagementCluster, TestKeySetWriteSameId) |
| { |
| GroupKeyManagement::Commands::KeySetWrite::Type requestData1; |
| requestData1.groupKeySet = { |
| .groupKeySetID = kTestKeySetId, |
| .groupKeySecurityPolicy = GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst, |
| .epochKey0 = chip::ByteSpan(kKey.key, sizeof(kKey.key)), |
| .epochStartTime0 = kStartTime, |
| .epochKey1 = DataModel::NullNullable, |
| .epochStartTime1 = DataModel::NullNullable, |
| .epochKey2 = DataModel::NullNullable, |
| .epochStartTime2 = DataModel::NullNullable, |
| }; |
| auto result1 = tester.Invoke(GroupKeyManagement::Commands::KeySetWrite::Id, requestData1); |
| |
| EXPECT_TRUE(result1.IsSuccess()); |
| chip::Credentials::GroupDataProvider::KeySet storedKeySet; |
| CHIP_ERROR err = mRealProvider.GetKeySet(kTestFabricIndex, kTestKeySetId, storedKeySet); |
| |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| ASSERT_EQ(storedKeySet.policy, GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst); |
| |
| GroupKeyManagement::Commands::KeySetWrite::Type requestData2; |
| requestData2.groupKeySet = { |
| .groupKeySetID = kTestKeySetId, |
| .groupKeySecurityPolicy = GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst, |
| .epochKey0 = chip::ByteSpan(kNewKey.key, sizeof(kNewKey.key)), |
| .epochStartTime0 = kStartTime + kStartTimeOffset, |
| .epochKey1 = DataModel::NullNullable, |
| .epochStartTime1 = DataModel::NullNullable, |
| .epochKey2 = DataModel::NullNullable, |
| .epochStartTime2 = DataModel::NullNullable, |
| }; |
| |
| auto result2 = tester.Invoke(GroupKeyManagement::Commands::KeySetWrite::Id, requestData2); |
| |
| EXPECT_TRUE(result2.IsSuccess()); |
| |
| err = mRealProvider.GetKeySet(kTestFabricIndex, kTestKeySetId, storedKeySet); |
| |
| ASSERT_EQ(err, CHIP_NO_ERROR); |
| ASSERT_EQ(storedKeySet.policy, GroupKeyManagement::GroupKeySecurityPolicyEnum::kTrustFirst); |
| ASSERT_EQ(storedKeySet.num_keys_used, 1); |
| ASSERT_EQ(storedKeySet.epoch_keys[0].start_time, kStartTime + kStartTimeOffset); |
| } |
| |
| class MockGroupDataProvider : public Credentials::GroupDataProviderImpl |
| { |
| public: |
| MockGroupDataProvider() : GroupDataProviderImpl() {} |
| ~MockGroupDataProvider() override = default; |
| |
| // Always returns nullptr to simulate iterator pool exhaustion. |
| GroupKeyIterator * IterateGroupKeys(FabricIndex fabric_index) override { return nullptr; } |
| }; |
| |
| // When IterateGroupKeys returns nullptr, WriteGroupKeyMap (AppendItem path) must detect |
| // this and return CHIP_ERROR_NO_MEMORY rather than dereferencing a null pointer. |
| TEST_F(TestGroupKeyManagementCluster, TestWriteGroupKeyMapAttributeIteratorExhausted) |
| { |
| MockGroupDataProvider mockGroupDataProvider; |
| |
| auto * storage = &mTestContext.StorageDelegate(); |
| mockGroupDataProvider.SetStorageDelegate(storage); |
| mockGroupDataProvider.SetSessionKeystore(&mMockKeystore); |
| ASSERT_EQ(mockGroupDataProvider.Init(), CHIP_NO_ERROR); |
| Credentials::SetGroupDataProvider(&mockGroupDataProvider); |
| |
| // Sanity check: Ensure the mock is working as intended and IterateGroupKeys returns nullptr. |
| ASSERT_EQ(mockGroupDataProvider.IterateGroupKeys(kTestFabricIndex), nullptr); |
| |
| // An AppendItem write triggers the IterateGroupKeys call inside WriteGroupKeyMap. Thus, we use ClearAllThenAppendItems and we |
| // do not test ReplaceAll since it does not call IterateGroupKeys. |
| // With the mock returning nullptr (to simulate ObjectPool exhaustion), the code must return CHIP_ERROR_NO_MEMORY instead of |
| // crashing with a null-pointer dereference. |
| auto keys = TestHelpers::CreateGroupKeyMapList(1, kTestFabricIndex); |
| auto listToWrite = app::DataModel::List<const GroupKeyManagement::Structs::GroupKeyMapStruct::Type>(keys.data(), keys.size()); |
| |
| GroupKeyManagementCluster mockCluster{ { fabricHelper.GetFabricTable(), mockGroupDataProvider } }; |
| ClusterTester tester2{ mockCluster }; |
| |
| CHIP_ERROR err = tester2 |
| .WriteAttribute(GroupKeyManagement::Attributes::GroupKeyMap::Id, listToWrite, |
| ListWritingPattern::ClearAllThenAppendItems) |
| .GetUnderlyingError(); |
| |
| EXPECT_EQ(err, CHIP_ERROR_NO_MEMORY); |
| } |
| |
| // Provider that drives the GroupTable read into the endpoint-encoding failure path and makes a |
| // leaked EndpointIterator observable on any pool backend (the host build uses the heap pool, which |
| // never exhausts, so the leak cannot be detected by watching IterateEndpoints return nullptr). |
| class LeakDetectingGroupDataProvider : public Credentials::GroupDataProviderImpl |
| { |
| public: |
| int liveEndpointIterators = 0; |
| |
| // The GroupTable read loops over groups; without at least one group it never builds a |
| // GroupTableCodec and the endpoint-encoding path under test is never reached. Yield exactly one. |
| GroupInfoIterator * IterateGroupInfo(FabricIndex fabric_index) override { return new SingleGroupInfoIterator(); } |
| |
| // Return the counting/flooding iterator instead of the real pool-backed one. This is the hook |
| // that both forces the failure (flood) and observes the leak (counter). |
| EndpointIterator * IterateEndpoints(FabricIndex fabric_index, std::optional<GroupId> group_id = std::nullopt) override |
| { |
| return new FloodEndpointIterator(*this); |
| } |
| |
| private: |
| // Minimal GroupInfoIterator that yields a single group then stops. |
| class SingleGroupInfoIterator : public GroupInfoIterator |
| { |
| public: |
| size_t Count() override { return 1; } |
| bool Next(GroupInfo & item) override |
| { |
| if (mDone) |
| { |
| return false; |
| } |
| mDone = true; |
| item.group_id = 0x0001; |
| item.SetName("G"_span); |
| return true; |
| } |
| void Release() override { delete this; } |
| |
| private: |
| bool mDone = false; |
| }; |
| |
| // EndpointIterator that (a) emits far more endpoints than fit in the encode buffer, so |
| // writer.Put() fails partway through the endpoints array, and (b) bumps the provider's live |
| // counter on construction / down on Release so a skipped Release is observable. |
| class FloodEndpointIterator : public EndpointIterator |
| { |
| public: |
| explicit FloodEndpointIterator(LeakDetectingGroupDataProvider & provider) : mProvider(provider) |
| { |
| ++mProvider.liveEndpointIterators; |
| } |
| size_t Count() override { return kFloodCount; } |
| bool Next(GroupEndpoint & item) override |
| { |
| if (mEmitted >= kFloodCount) |
| { |
| return false; |
| } |
| item.group_id = 0x0001; |
| item.endpoint_id = static_cast<EndpointId>(mEmitted++); |
| return true; |
| } |
| void Release() override |
| { |
| --mProvider.liveEndpointIterators; |
| delete this; |
| } |
| |
| private: |
| // Far exceeds the encoder buffer so writer.Put() is guaranteed to fail inside the endpoints array. |
| static constexpr size_t kFloodCount = 1000; |
| LeakDetectingGroupDataProvider & mProvider; |
| size_t mEmitted = 0; |
| }; |
| }; |
| |
| TEST_F(TestGroupKeyManagementCluster, TestGroupTableReadReleasesEndpointIteratorOnEncodeFailure) |
| { |
| LeakDetectingGroupDataProvider leakProvider; |
| |
| auto * storage = &mTestContext.StorageDelegate(); |
| leakProvider.SetStorageDelegate(storage); |
| leakProvider.SetSessionKeystore(&mMockKeystore); |
| ASSERT_EQ(leakProvider.Init(), CHIP_NO_ERROR); |
| Credentials::SetGroupDataProvider(&leakProvider); |
| |
| GroupKeyManagementCluster leakCluster{ { fabricHelper.GetFabricTable(), leakProvider } }; |
| ASSERT_EQ(leakCluster.Startup(mTestContext.Get()), CHIP_NO_ERROR); |
| |
| ClusterTester leakTester{ leakCluster }; |
| leakTester.SetFabricIndex(kTestFabricIndex); |
| |
| uint32_t sink = 0; |
| (void) leakTester.ReadAttribute(GroupKeyManagement::Attributes::GroupTable::Id, sink); |
| |
| // The endpoint iterator was released despite the encode failing. |
| // Fails (==1) without the fix, passes (==0) with it. |
| EXPECT_EQ(leakProvider.liveEndpointIterators, 0); |
| |
| leakCluster.Shutdown(ClusterShutdownType::kClusterShutdown); |
| } |
| |
| } // namespace |