| /* |
| * 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 <pw_unit_test/framework.h> |
| |
| #include <access/Privilege.h> |
| #include <app/ConcreteClusterPath.h> |
| #include <app/data-model-provider/MetadataTypes.h> |
| #include <app/server-cluster/DefaultServerCluster.h> |
| #include <app/server-cluster/ServerClusterExtension.h> |
| #include <app/server-cluster/testing/TestServerClusterContext.h> |
| #include <clusters/shared/GlobalIds.h> |
| #include <lib/core/CHIPError.h> |
| #include <lib/core/StringBuilderAdapters.h> |
| #include <lib/support/CodeUtils.h> |
| #include <lib/support/ReadOnlyBuffer.h> |
| #include <protocols/interaction_model/Constants.h> |
| |
| #include <app/server-cluster/testing/AttributeTesting.h> |
| #include <app/server-cluster/testing/ClusterTester.h> |
| #include <app/server-cluster/testing/ValidateGlobalAttributes.h> |
| |
| #include <string> |
| |
| namespace { |
| |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::DataModel; |
| using namespace chip::Testing; |
| using namespace chip::Protocols::InteractionModel; |
| |
| constexpr uint32_t kMockRevision = 123; |
| constexpr uint32_t kMockFeatureMap = 0x112233; |
| |
| /// a basic mock cluster, just supporting one path |
| class MockServerCluster : public DefaultServerCluster |
| { |
| public: |
| MockServerCluster(const ConcreteClusterPath & path) : DefaultServerCluster(path) {} |
| |
| DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) override |
| { |
| switch (request.path.mAttributeId) |
| { |
| case chip::app::Clusters::Globals::Attributes::ClusterRevision::Id: |
| return encoder.Encode(kMockRevision); |
| case chip::app::Clusters::Globals::Attributes::FeatureMap::Id: |
| return encoder.Encode(kMockFeatureMap); |
| default: |
| return Status::UnsupportedAttribute; |
| } |
| } |
| }; |
| |
| /// A dual path server cluster (just mocks GetPaths, nothing else) |
| class MockDualPathServerCluster : public DefaultServerCluster |
| { |
| public: |
| MockDualPathServerCluster(const ConcreteClusterPath & p1, const ConcreteClusterPath & p2) : |
| DefaultServerCluster(p1), mPaths{ p1, p2 } |
| {} |
| |
| // mock GetPaths, to have something |
| Span<const ConcreteClusterPath> GetPaths() const override { return Span(mPaths); } |
| |
| DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) override |
| { |
| switch (request.path.mAttributeId) |
| { |
| case chip::app::Clusters::Globals::Attributes::ClusterRevision::Id: |
| return encoder.Encode(kMockRevision); |
| case chip::app::Clusters::Globals::Attributes::FeatureMap::Id: |
| return encoder.Encode(kMockFeatureMap); |
| default: |
| return Status::UnsupportedAttribute; |
| } |
| } |
| |
| private: |
| const ConcreteClusterPath mPaths[2] = {}; |
| }; |
| |
| constexpr AttributeId kTestAttribute1 = 0xFFF10000; |
| constexpr AttributeId kTestAttribute2 = 0xFFF10001; |
| |
| constexpr DataModel::AttributeEntry kExtraAttributeMetadata[] = { |
| { kTestAttribute1, {} /* qualities */, Access::Privilege::kView /* readPriv */, Access::Privilege::kOperate /* writePriv */ }, |
| { kTestAttribute2, {} /* qualities */, Access::Privilege::kView /* readPriv */, std::nullopt /* writePriv */ }, |
| }; |
| |
| class TestableServerClusterExtension : public ServerClusterExtension |
| { |
| public: |
| TestableServerClusterExtension(const ConcreteClusterPath & path, ServerClusterInterface & underlying) : |
| ServerClusterExtension(path, underlying) |
| {} |
| |
| DataModel::ActionReturnStatus ReadAttribute(const DataModel::ReadAttributeRequest & request, |
| AttributeValueEncoder & encoder) override |
| { |
| if (mClusterPath == request.path) |
| { |
| switch (request.path.mAttributeId) |
| { |
| case kTestAttribute1: |
| return encoder.Encode<CharSpan>({ mStringAttribute.data(), mStringAttribute.size() }); |
| case kTestAttribute2: |
| return encoder.Encode<uint32_t>(1234); |
| } |
| } |
| return mUnderlying.ReadAttribute(request, encoder); |
| } |
| |
| CHIP_ERROR Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder) override |
| { |
| if (path == mClusterPath) |
| { |
| ReturnErrorOnFailure(builder.ReferenceExisting(kExtraAttributeMetadata)); |
| } |
| |
| // Delegate to underlying for other paths |
| return mUnderlying.Attributes(path, builder); |
| } |
| |
| DataModel::ActionReturnStatus WriteAttribute(const DataModel::WriteAttributeRequest & request, |
| AttributeValueDecoder & decoder) override |
| { |
| if (mClusterPath == request.path) |
| { |
| // we are guaranteed to only be called for writable attributes |
| if (request.path.mAttributeId == kTestAttribute1) |
| { |
| CharSpan value; |
| ReturnErrorOnFailure(decoder.Decode(value)); |
| mStringAttribute = std::string(value.data(), value.size()); |
| NotifyAttributeChanged(kTestAttribute1); |
| return Status::Success; |
| } |
| } |
| |
| return mUnderlying.WriteAttribute(request, decoder); |
| } |
| |
| void TestNotifyAttributeChanged(AttributeId id) { NotifyAttributeChanged(id); } |
| |
| private: |
| std::string mStringAttribute = "Sample String"; |
| }; |
| |
| struct TestServerClusterExtension : public ::testing::Test |
| { |
| static void SetUpTestSuite() { ASSERT_EQ(chip::Platform::MemoryInit(), CHIP_NO_ERROR); } |
| static void TearDownTestSuite() { chip::Platform::MemoryShutdown(); } |
| }; |
| |
| TEST_F(TestServerClusterExtension, TestExtensionPath) |
| { |
| const ConcreteClusterPath mockPath = { 1, 2 }; |
| MockServerCluster underlying(mockPath); |
| const ConcreteClusterPath extensionPath = { 1, 2 }; |
| |
| ServerClusterExtension extension(extensionPath, underlying); |
| |
| // The extension should return the paths of its underlying interface. |
| ASSERT_EQ(extension.GetPaths().size(), 1u); |
| ASSERT_EQ(extension.GetPaths()[0], mockPath); |
| } |
| |
| TEST_F(TestServerClusterExtension, TestGetDataVersion) |
| { |
| const ConcreteClusterPath mockPath = { 2, 3 }; |
| MockServerCluster underlying(mockPath); |
| |
| TestableServerClusterExtension extension(mockPath, underlying); |
| |
| // Initially, version is the same as underlying (since mVersionDelta is 0). |
| ASSERT_EQ(extension.GetDataVersion(mockPath), underlying.GetDataVersion(mockPath)); |
| |
| // Without a context, there is no need to increment (and cannot mark dirty). |
| extension.TestNotifyAttributeChanged(4); |
| ASSERT_EQ(extension.GetDataVersion(mockPath), |
| underlying.GetDataVersion(mockPath)); // Should still be the same as no context is set |
| |
| // Set a context and then notify change. This time mVersionDelta should increment. |
| TestServerClusterContext context; |
| ASSERT_EQ(extension.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| DataVersion oldVersion = extension.GetDataVersion(mockPath); |
| extension.TestNotifyAttributeChanged(5); |
| ASSERT_EQ(extension.GetDataVersion(mockPath), oldVersion + 1); |
| } |
| |
| TEST_F(TestServerClusterExtension, TestNotifyAttributeChangedWithContext) |
| { |
| const ConcreteClusterPath mockPath = { 1, 2 }; |
| const ConcreteClusterPath mockPath2 = { 1, 3 }; |
| MockDualPathServerCluster underlying(mockPath, mockPath2); |
| TestableServerClusterExtension extension(mockPath, underlying); |
| |
| // Verify that NotifyAttributeChanged does NOT mark dirty when no context is set. |
| extension.TestNotifyAttributeChanged(123); |
| // No context set, so no dirty list updates. Default TestServerClusterContext has an empty list. |
| // We cannot easily check mVersionDelta directly as it's protected in ServerClusterExtension. |
| // The effect of mVersionDelta is visible through GetDataVersion. |
| |
| // Create a ServerClusterContext and verify that attribute change notifications are processed. |
| TestServerClusterContext context; |
| ASSERT_EQ(extension.Startup(context.Get()), CHIP_NO_ERROR); |
| |
| // Clear any previous dirty marks from calls before Startup. |
| context.ChangeListener().DirtyList().clear(); |
| |
| DataVersion oldVersion = extension.GetDataVersion(mockPath); |
| extension.TestNotifyAttributeChanged(234); |
| ASSERT_EQ(extension.GetDataVersion(mockPath), oldVersion + 1); |
| ASSERT_EQ(extension.GetDataVersion(mockPath2), oldVersion); |
| |
| ASSERT_EQ(context.ChangeListener().DirtyList().size(), 1u); |
| ASSERT_EQ(context.ChangeListener().DirtyList()[0], AttributePathParams(mockPath.mEndpointId, mockPath.mClusterId, 234)); |
| } |
| |
| TEST_F(TestServerClusterExtension, TestExtensionAttributes) |
| { |
| const ConcreteClusterPath mockPath = { 1, 2 }; |
| MockServerCluster underlying(mockPath); |
| TestableServerClusterExtension extension(mockPath, underlying); |
| |
| chip::Testing::ClusterTester tester(extension); |
| |
| // Verify attribute listing includes global and extended attributes |
| ASSERT_TRUE(chip::Testing::IsAttributesListEqualTo(extension, { kExtraAttributeMetadata[0], kExtraAttributeMetadata[1] })); |
| |
| // Test reading kTestAttribute1 (string) |
| CharSpan readString; |
| ASSERT_EQ(tester.ReadAttribute(kTestAttribute1, readString), Status::Success); |
| ASSERT_TRUE(readString.data_equal("Sample String"_span)); |
| |
| // Test reading kTestAttribute2 (uint32_t) |
| uint32_t readUint{}; |
| ASSERT_EQ(tester.ReadAttribute(kTestAttribute2, readUint), Status::Success); |
| ASSERT_EQ(readUint, 1234u); |
| |
| // Test writing kTestAttribute1 (string) |
| CharSpan newString = "New Test String"_span; |
| ASSERT_EQ(tester.WriteAttribute(kTestAttribute1, newString), Status::Success); |
| |
| // Read back to verify write |
| CharSpan verifiedString; |
| ASSERT_EQ(tester.ReadAttribute(kTestAttribute1, verifiedString), Status::Success); |
| ASSERT_TRUE(verifiedString.data_equal(newString)); |
| } |
| |
| } // namespace |