Line data Source code
1 : /*
2 : * Copyright (c) 2025 Project CHIP Authors
3 : *
4 : * Licensed under the Apache License, Version 2.0 (the "License");
5 : * you may not use this file except in compliance with the License.
6 : * You may obtain a copy of the License at
7 : *
8 : * http://www.apache.org/licenses/LICENSE-2.0
9 : *
10 : * Unless required by applicable law or agreed to in writing, software
11 : * distributed under the License is distributed on an "AS IS" BASIS,
12 : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 : * See the License for the specific language governing permissions and
14 : * limitations under the License.
15 : */
16 :
17 : #pragma once
18 : #include "FabricTestFixture.h"
19 : #include <app/AttributeValueDecoder.h>
20 : #include <app/AttributeValueEncoder.h>
21 : #include <app/CommandHandler.h>
22 : #include <app/ConcreteAttributePath.h>
23 : #include <app/ConcreteClusterPath.h>
24 : #include <app/ConcreteCommandPath.h>
25 : #include <app/ConcreteEventPath.h>
26 : #include <app/data-model-provider/ActionReturnStatus.h>
27 : #include <app/data-model-provider/MetadataTypes.h>
28 : #include <app/data-model-provider/tests/ReadTesting.h>
29 : #include <app/data-model-provider/tests/WriteTesting.h>
30 : #include <app/data-model/List.h>
31 : #include <app/data-model/NullObject.h>
32 : #include <app/server-cluster/ServerClusterInterface.h>
33 : #include <app/server-cluster/testing/MockCommandHandler.h>
34 : #include <app/server-cluster/testing/TestServerClusterContext.h>
35 : #include <clusters/shared/Attributes.h>
36 : #include <credentials/FabricTable.h>
37 : #include <credentials/PersistentStorageOpCertStore.h>
38 : #include <crypto/CHIPCryptoPAL.h>
39 : #include <lib/core/CHIPError.h>
40 : #include <lib/core/CHIPPersistentStorageDelegate.h>
41 : #include <lib/core/DataModelTypes.h>
42 : #include <lib/core/TLVReader.h>
43 : #include <lib/support/ReadOnlyBuffer.h>
44 : #include <lib/support/Span.h>
45 : #include <memory>
46 : #include <optional>
47 : #include <type_traits>
48 : #include <vector>
49 :
50 : namespace chip {
51 : namespace Testing {
52 : // Helper class for testing clusters.
53 : //
54 : // This class ensures that data read by attribute is referencing valid memory for all
55 : // read requests until the ClusterTester object goes out of scope. (for the case where the underlying read references a list or
56 : // string that points to TLV data).
57 : //
58 : // Read/Write of all attribute types should work, but make sure to use ::Type for encoding
59 : // and ::DecodableType for decoding structure types.
60 : //
61 : // Example of usage:
62 : //
63 : // ExampleCluster cluster(someEndpointId);
64 : //
65 : // // Possibly steps to setup the cluster
66 : //
67 : // ClusterTester tester(cluster);
68 : // app::Clusters::ExampleCluster::Attributes::FeatureMap::TypeInfo::DecodableType features;
69 : // ASSERT_EQ(tester.ReadAttribute(FeatureMap::Id, features), CHIP_NO_ERROR);
70 : //
71 : // app::Clusters::ExampleCluster::Attributes::ExampleListAttribute::TypeInfo::DecodableType list;
72 : // ASSERT_EQ(tester.ReadAttribute(LabelList::Id, list), CHIP_NO_ERROR);
73 : // auto it = list.begin();
74 : // while (it.Next())
75 : // {
76 : // ASSERT_GT(it.GetValue().label.size(), 0u);
77 : // }
78 : //
79 : class ClusterTester
80 : {
81 : public:
82 178 : ClusterTester(app::ServerClusterInterface & cluster) : mCluster(cluster), mFabricTestFixture(nullptr) {}
83 :
84 : // Constructor with FabricHelper
85 : ClusterTester(app::ServerClusterInterface & cluster, FabricTestFixture * fabricHelper) :
86 : mCluster(cluster), mFabricTestFixture(fabricHelper)
87 : {}
88 :
89 51 : app::ServerClusterContext & GetServerClusterContext() { return mTestServerClusterContext.Get(); }
90 :
91 : // Read attribute into `out` parameter.
92 : // The `out` parameter must be of the correct type for the attribute being read.
93 : // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::DecodableType` for the `out` parameter to be spec
94 : // compliant (see the comment of the class for usage example).
95 : // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster.
96 : // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path.
97 : // @returns `CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute)` if the attribute is not present in AttributeList.
98 : template <typename T>
99 249 : app::DataModel::ActionReturnStatus ReadAttribute(AttributeId attr_id, T & out)
100 : {
101 249 : VerifyOrReturnError(VerifyClusterPathsValid(), CHIP_ERROR_INCORRECT_STATE);
102 :
103 : // Verify that the attribute is present in AttributeList before attempting to read it.
104 : // This ensures tests match real-world behavior where the Interaction Model checks AttributeList first.
105 249 : auto checkStatus = VerifyAttributeInAttributeList(attr_id);
106 249 : VerifyOrReturnError(checkStatus.IsSuccess(), checkStatus);
107 :
108 246 : auto path = mCluster.GetPaths()[0];
109 :
110 : // Store the read operation in a vector<std::unique_ptr<...>> to ensure its lifetime
111 : // using std::unique_ptr because ReadOperation is non-copyable and non-movable
112 : // vector reallocation is not an issue since we store unique_ptrs
113 246 : std::unique_ptr<chip::Testing::ReadOperation> readOperation =
114 : std::make_unique<chip::Testing::ReadOperation>(path.mEndpointId, path.mClusterId, attr_id);
115 :
116 246 : mReadOperations.push_back(std::move(readOperation));
117 246 : chip::Testing::ReadOperation & readOperationRef = *mReadOperations.back().get();
118 :
119 246 : Access::SubjectDescriptor subjectDescriptor{ .fabricIndex = mHandler.GetAccessingFabricIndex() };
120 246 : readOperationRef.SetSubjectDescriptor(subjectDescriptor);
121 :
122 246 : std::unique_ptr<app::AttributeValueEncoder> encoder = readOperationRef.StartEncoding();
123 246 : app::DataModel::ActionReturnStatus status = mCluster.ReadAttribute(readOperationRef.GetRequest(), *encoder);
124 246 : VerifyOrReturnError(status.IsSuccess(), status);
125 243 : ReturnErrorOnFailure(readOperationRef.FinishEncoding());
126 :
127 243 : std::vector<chip::Testing::DecodedAttributeData> attributeData;
128 243 : ReturnErrorOnFailure(readOperationRef.GetEncodedIBs().Decode(attributeData));
129 243 : VerifyOrReturnError(attributeData.size() == 1u, CHIP_ERROR_INCORRECT_STATE);
130 :
131 243 : return app::DataModel::Decode(attributeData[0].dataReader, out);
132 246 : }
133 :
134 : // Write attribute from `value` parameter.
135 : // The `value` parameter must be of the correct type for the attribute being written.
136 : // Use `app::Clusters::<ClusterName>::Attributes::<AttributeName>::TypeInfo::Type` for the `value` parameter to be spec
137 : // compliant (see the comment of the class for usage example).
138 : // Will construct the attribute path using the first path returned by `GetPaths()` on the cluster.
139 : // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path.
140 : // @returns `CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute)` if the attribute is not present in AttributeList.
141 : template <typename T>
142 59 : app::DataModel::ActionReturnStatus WriteAttribute(AttributeId attr, const T & value)
143 : {
144 59 : const auto & paths = mCluster.GetPaths();
145 :
146 59 : VerifyOrReturnError(paths.size() == 1u, CHIP_ERROR_INCORRECT_STATE);
147 :
148 : // Verify that the attribute is present in AttributeList before attempting to write it.
149 : // This ensures tests match real-world behavior where the Interaction Model checks AttributeList first.
150 59 : auto checkStatus = VerifyAttributeInAttributeList(attr);
151 59 : VerifyOrReturnError(checkStatus.IsSuccess(), checkStatus);
152 :
153 59 : app::ConcreteAttributePath path(paths[0].mEndpointId, paths[0].mClusterId, attr);
154 59 : chip::Testing::WriteOperation writeOp(path);
155 :
156 : // Create a stable object on the stack
157 59 : Access::SubjectDescriptor subjectDescriptor{ .fabricIndex = mHandler.GetAccessingFabricIndex() };
158 59 : writeOp.SetSubjectDescriptor(subjectDescriptor);
159 :
160 : uint8_t buffer[1024];
161 59 : TLV::TLVWriter writer;
162 59 : writer.Init(buffer);
163 :
164 : // - DataModel::Encode(integral, enum, etc.) for simple types.
165 : // - DataModel::Encode(List<X>) for lists (which iterates and calls Encode on elements).
166 : // - DataModel::Encode(Struct) for non-fabric-scoped structs.
167 : // - Note: For attribute writes, DataModel::EncodeForWrite is usually preferred for fabric-scoped types,
168 : // but the generic DataModel::Encode often works as a top-level function.
169 : // If you use EncodeForWrite, you ensure fabric-scoped list items are handled correctly:
170 :
171 : if constexpr (app::DataModel::IsFabricScoped<T>::value)
172 : {
173 3 : ReturnErrorOnFailure(chip::app::DataModel::EncodeForWrite(writer, TLV::AnonymousTag(), value));
174 : }
175 : else
176 : {
177 56 : ReturnErrorOnFailure(chip::app::DataModel::Encode(writer, TLV::AnonymousTag(), value));
178 : }
179 :
180 59 : TLV::TLVReader reader;
181 59 : reader.Init(buffer, writer.GetLengthWritten());
182 59 : ReturnErrorOnFailure(reader.Next());
183 :
184 59 : app::AttributeValueDecoder decoder(reader, *writeOp.GetRequest().subjectDescriptor);
185 :
186 59 : return mCluster.WriteAttribute(writeOp.GetRequest(), decoder);
187 : }
188 :
189 : // Result structure for Invoke operations, containing both status and decoded response.
190 : template <typename ResponseType>
191 : struct InvokeResult
192 : {
193 : std::optional<app::DataModel::ActionReturnStatus> status;
194 : std::optional<ResponseType> response;
195 :
196 : // Returns true if the command was successful and response is available
197 148 : bool IsSuccess() const
198 : {
199 : if constexpr (std::is_same_v<ResponseType, app::DataModel::NullObjectType>)
200 45 : return status.has_value() && status->IsSuccess();
201 : else
202 103 : return status.has_value() && status->IsSuccess() && response.has_value();
203 : }
204 : };
205 :
206 : // Invoke a command and return the decoded result.
207 : // The `request` parameter must be of the correct type for the command being invoked.
208 : // Use `app::Clusters::<ClusterName>::Commands::<CommandName>::Type` for the `request` parameter to be spec compliant
209 : // Will construct the command path using the first path returned by `GetPaths()` on the cluster.
210 : // @returns `CHIP_ERROR_INCORRECT_STATE` if `GetPaths()` doesn't return a list with one path.
211 : template <typename RequestType, typename ResponseType = typename RequestType::ResponseType>
212 185 : [[nodiscard]] InvokeResult<ResponseType> Invoke(chip::CommandId commandId, const RequestType & request)
213 : {
214 185 : InvokeResult<ResponseType> result;
215 :
216 185 : const auto & paths = mCluster.GetPaths();
217 185 : VerifyOrReturnValue(paths.size() == 1u, result);
218 :
219 185 : mHandler.ClearResponses();
220 185 : mHandler.ClearStatuses();
221 :
222 185 : const app::DataModel::InvokeRequest invokeRequest = { .path = { paths[0].mEndpointId, paths[0].mClusterId, commandId } };
223 :
224 185 : TLV::TLVWriter writer;
225 185 : writer.Init(mTlvBuffer);
226 :
227 185 : TLV::TLVReader reader;
228 :
229 370 : VerifyOrReturnValue(request.Encode(writer, TLV::AnonymousTag()) == CHIP_NO_ERROR, result);
230 370 : VerifyOrReturnValue(writer.Finalize() == CHIP_NO_ERROR, result);
231 :
232 185 : reader.Init(mTlvBuffer, writer.GetLengthWritten());
233 370 : VerifyOrReturnValue(reader.Next(TLV::kTLVType_Structure, TLV::AnonymousTag()) == CHIP_NO_ERROR, result);
234 :
235 185 : result.status = mCluster.InvokeCommand(invokeRequest, reader, &mHandler);
236 :
237 : // If InvokeCommand returned nullopt, it means the command implementation handled the response.
238 : // We need to check the mock handler for a data response or a status response.
239 185 : if (!result.status.has_value())
240 : {
241 139 : if (mHandler.HasResponse())
242 : {
243 : // A data response was added, so the command is successful.
244 139 : result.status = app::DataModel::ActionReturnStatus(CHIP_NO_ERROR);
245 : }
246 0 : else if (mHandler.HasStatus())
247 : {
248 : // A status response was added. Use the last one.
249 0 : result.status = app::DataModel::ActionReturnStatus(mHandler.GetLastStatus().status);
250 : }
251 : else
252 : {
253 : // Neither response nor status was provided; this is unexpected.
254 : // This would happen either in error (as mentioned here) or if the command is supposed
255 : // to be handled asynchronously. ClusterTester does not support such asynchronous processing.
256 0 : result.status = app::DataModel::ActionReturnStatus(CHIP_ERROR_INCORRECT_STATE);
257 0 : ChipLogError(
258 : Test, "InvokeCommand returned nullopt, but neither HasResponse nor HasStatus is true. Setting error status.");
259 : }
260 : }
261 :
262 : // If command was successful and there's a response, decode it (skip for NullObjectType)
263 : if constexpr (!std::is_same_v<ResponseType, app::DataModel::NullObjectType>)
264 : {
265 139 : if (result.status.has_value() && result.status->IsSuccess() && mHandler.HasResponse())
266 : {
267 139 : ResponseType decodedResponse;
268 139 : CHIP_ERROR decodeError = mHandler.DecodeResponse(decodedResponse);
269 278 : if (decodeError == CHIP_NO_ERROR)
270 : {
271 139 : result.response = std::move(decodedResponse);
272 : }
273 : else
274 : {
275 : // Decode failed; reflect error in status and log
276 0 : result.status = app::DataModel::ActionReturnStatus(decodeError);
277 0 : ChipLogError(Test, "DecodeResponse failed: %s", decodeError.AsString());
278 : }
279 : }
280 : }
281 :
282 185 : return result;
283 : }
284 :
285 : // convenience method: most requests have a `GetCommandId` (and GetClusterId() as well).
286 : template <typename RequestType, typename ResponseType = typename RequestType::ResponseType>
287 11 : [[nodiscard]] InvokeResult<ResponseType> Invoke(const RequestType & request)
288 : {
289 11 : return Invoke(RequestType::GetCommandId(), request);
290 : }
291 :
292 : // Returns the next generated event from the event generator in the test server cluster context
293 1 : std::optional<LogOnlyEvents::EventInformation> GetNextGeneratedEvent()
294 : {
295 1 : return mTestServerClusterContext.EventsGenerator().GetNextEvent();
296 : }
297 :
298 8 : std::vector<app::AttributePathParams> & GetDirtyList() { return mTestServerClusterContext.ChangeListener().DirtyList(); }
299 :
300 65 : void SetFabricIndex(FabricIndex fabricIndex) { mHandler.SetFabricIndex(fabricIndex); }
301 :
302 : FabricTestFixture * GetFabricHelper() { return mFabricTestFixture; }
303 :
304 : private:
305 249 : bool VerifyClusterPathsValid()
306 : {
307 249 : auto paths = mCluster.GetPaths();
308 249 : if (paths.size() != 1)
309 : {
310 0 : ChipLogError(Test, "cluster.GetPaths() did not return exactly one path");
311 0 : return false;
312 : }
313 249 : return true;
314 : }
315 :
316 : // Verifies that an attribute is present in the cluster's AttributeList.
317 : // @returns CHIP_NO_ERROR if the attribute is found in AttributeList.
318 : // @returns CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute) if the attribute is not found in AttributeList.
319 : // @returns error status if Attributes cannot be retrieved.
320 308 : app::DataModel::ActionReturnStatus VerifyAttributeInAttributeList(AttributeId attr_id)
321 : {
322 308 : auto path = mCluster.GetPaths()[0];
323 :
324 : // Get the list of attributes from the cluster's metadata
325 308 : ReadOnlyBufferBuilder<app::DataModel::AttributeEntry> builder;
326 308 : CHIP_ERROR err = mCluster.Attributes(path, builder);
327 308 : ReturnErrorOnFailure(err);
328 :
329 308 : ReadOnlyBuffer<app::DataModel::AttributeEntry> attributeEntries = builder.TakeBuffer();
330 :
331 : // Check if the requested attribute ID is present in the attribute list
332 1076 : for (const auto & entry : attributeEntries)
333 : {
334 1073 : if (entry.attributeId == attr_id)
335 : {
336 305 : return CHIP_NO_ERROR;
337 : }
338 : }
339 :
340 : // Attribute not found in AttributeList
341 3 : return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute);
342 308 : }
343 :
344 : TestServerClusterContext mTestServerClusterContext{};
345 : app::ServerClusterInterface & mCluster;
346 :
347 : // Buffer size for TLV encoding/decoding of command payloads.
348 : // 256 bytes was chosen as a conservative upper bound for typical command payloads in tests.
349 : // All command payloads used in tests must fit within this buffer; tests with larger payloads will fail.
350 : // If protocol or test requirements change, this value may need to be increased.
351 : static constexpr size_t kTlvBufferSize = 256;
352 :
353 : chip::Testing::MockCommandHandler mHandler;
354 : uint8_t mTlvBuffer[kTlvBufferSize];
355 : std::vector<std::unique_ptr<ReadOperation>> mReadOperations;
356 :
357 : FabricTestFixture * mFabricTestFixture;
358 : };
359 :
360 : } // namespace Testing
361 : } // namespace chip
|