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