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