From 96a9fbd6d0362f6e4ca4c034d59d35d5ea8ce6e1 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Mon, 1 May 2023 15:30:17 -0400 Subject: [PATCH] Add methods for doing schema-checks on response-value dictionaries. This adds a way to initialize an MTRAttributeReport or MTREventReport from a response-value dictionary, if we have known schema for the attribute/event path. Fixes https://github.com/project-chip/connectedhomeip/issues/26305 --- src/darwin/Framework/CHIP/MTRBaseDevice.h | 103 +- src/darwin/Framework/CHIP/MTRBaseDevice.mm | 305 +++++- .../Framework/CHIP/MTRBaseDevice_Internal.h | 4 +- src/darwin/Framework/CHIP/MTRError.h | 10 + src/darwin/Framework/CHIP/MTRError_Internal.h | 1 + .../CHIPTests/MTRDataValueParserTests.m | 916 ++++++++++++++++++ .../Framework/CHIPTests/MTRDeviceTests.m | 149 ++- .../Matter.xcodeproj/project.pbxproj | 4 + 8 files changed, 1462 insertions(+), 30 deletions(-) create mode 100644 src/darwin/Framework/CHIPTests/MTRDataValueParserTests.m diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.h b/src/darwin/Framework/CHIP/MTRBaseDevice.h index 0d4fbdd1f498f2..776d0446afed1d 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.h @@ -584,12 +584,55 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) @interface MTRAttributeReport : NSObject @property (nonatomic, readonly, copy) MTRAttributePath * path; -// value is nullable because nullable attributes can have nil as value. + +/** + * value will be nil in the following cases: + * + * * There was an error. In this case, "error" will not be nil. + * * The attribute is nullable and the value of the attribute is null. + * + * If value is not nil, the actual type of value will depend on the + * schema-defined (typically defiend in the Matter specification) type of the + * attribute as follows: + * + * * list: NSArray of whatever type the list entries are. + * * struct: The corresponding structure interface defined by Matter.framework + * * octet string: NSData + * * string: NSString + * * discrete/analog types: NSNumber + * + * Derived types are represented as the base type, except for "string". + */ @property (nonatomic, readonly, copy, nullable) id value; -// If this specific path resulted in an error, the error (in the -// MTRInteractionErrorDomain or MTRErrorDomain) that corresponds to this -// path. + +/** + * If this specific path resulted in an error, the error (in the + * MTRInteractionErrorDomain or MTRErrorDomain) that corresponds to this + * path. + */ @property (nonatomic, readonly, copy, nullable) NSError * error; + +/** + * Initialize an MTRAttributeReport with a response-value dictionary of the sort + * that MTRDeviceResponseHandler would receive. + * + * Will return nil and hand out an error if the response-value dictionary is not + * an attribute response. + * + * Will set the value property to nil and the error property to non-nil, even if + * the schema for the value is not known, if the response-value is an error, not + * data. + * + * Will return nil and hand out an error if the response-value is data in the + * following cases: + * + * * The response is for a cluster/attribute combination for which the schema is + * unknown and hence the type of the data is not known. + * * The data does not match the known schema. + */ +- (nullable instancetype)initWithResponseValue:(NSDictionary *)responseValue + error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE; + @end typedef NS_ENUM(NSUInteger, MTREventTimeType) { @@ -605,22 +648,62 @@ typedef NS_ENUM(NSUInteger, MTREventPriority) { @interface MTREventReport : NSObject @property (nonatomic, readonly, copy) MTREventPath * path; + +/** + * eventNumber will not have a useful value if error" is not nil. + */ @property (nonatomic, readonly, copy) NSNumber * eventNumber; // EventNumber type (uint64_t) + +/** + * priority will not have a useful value if "error" is not nil. + */ @property (nonatomic, readonly, copy) NSNumber * priority; // PriorityLevel type (MTREventPriority) -// Either systemUpTime or timestampDate will be valid depending on eventTimeType +/** + * Either systemUpTime or timestampDate will be valid depending on + * eventTimeType, if "error" is nil. If "error" is not nil, none of + * eventTimeType, systemUpTime, timestampDate should be expected to have useful + * values. + */ @property (nonatomic, readonly) MTREventTimeType eventTimeType API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); @property (nonatomic, readonly) NSTimeInterval systemUpTime API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); @property (nonatomic, readonly, copy, nullable) NSDate * timestampDate API_AVAILABLE(ios(16.5), macos(13.4), watchos(9.5), tvos(16.5)); -// An instance of one of the event payload interfaces. -@property (nonatomic, readonly, copy) id value; +/** + * An instance of one of the event payload interfaces, or nil if error is not + * nil (in which case there is no payload available). + */ +@property (nonatomic, readonly, copy, nullable) id value; -// If this specific path resulted in an error, the error (in the -// MTRInteractionErrorDomain or MTRErrorDomain) that corresponds to this -// path. +/** + * If this specific path resulted in an error, the error (in the + * MTRInteractionErrorDomain or MTRErrorDomain) that corresponds to this + * path. + */ @property (nonatomic, readonly, copy, nullable) NSError * error; + +/** + * Initialize an MTREventReport with a response-value dictionary of the sort + * that MTRDeviceResponseHandler would receive. + * + * Will return nil and hand out an error if the response-value dictionary is not + * an event response. + * + * Will set the value property to nil and the error property to non-nil, even if + * the schema for the value is not known, if the response-value is an error, not + * data. + * + * Will return nil and hand out an error if the response-value is data in the + * following cases: + * + * * The response is for a cluster/event combination for which the schema is + * unknown and hence the type of the data is not known. + * * The data does not match the known schema. + */ +- (nullable instancetype)initWithResponseValue:(NSDictionary *)responseValue + error:(NSError * __autoreleasing *)error MTR_NEWLY_AVAILABLE; + @end @interface MTRBaseDevice (Deprecated) diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index f3251dbbc4db8f..6b8b7dde746799 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -25,12 +25,14 @@ #import "MTRDevice_Internal.h" #import "MTRError_Internal.h" #import "MTREventTLVValueDecoder_Internal.h" +#import "MTRFramework.h" #import "MTRLogging_Internal.h" #import "MTRSetupPayload_Internal.h" #include "app/ConcreteAttributePath.h" #include "app/ConcreteCommandPath.h" #include "app/ConcreteEventPath.h" +#include "app/StatusResponse.h" #include "lib/core/CHIPError.h" #include "lib/core/DataModelTypes.h" @@ -2161,6 +2163,12 @@ - (id)copyWithZone:(NSZone *)zone { return [MTRAttributePath attributePathWithEndpointID:self.endpoint clusterID:self.cluster attributeID:_attribute]; } + +- (ConcreteAttributePath)_asConcretePath +{ + return ConcreteAttributePath([self.endpoint unsignedShortValue], static_cast([self.cluster unsignedLongValue]), + static_cast([self.attribute unsignedLongValue])); +} @end @implementation MTRAttributePath (Deprecated) @@ -2200,6 +2208,12 @@ - (id)copyWithZone:(NSZone *)zone { return [MTREventPath eventPathWithEndpointID:self.endpoint clusterID:self.cluster eventID:_event]; } + +- (ConcreteEventPath)_asConcretePath +{ + return ConcreteEventPath([self.endpoint unsignedShortValue], static_cast([self.cluster unsignedLongValue]), + static_cast([self.event unsignedLongValue])); +} @end @implementation MTREventPath (Deprecated) @@ -2239,7 +2253,82 @@ + (instancetype)commandPathWithEndpointId:(NSNumber *)endpointId clusterId:(NSNu } @end +static void LogStringAndReturnError(NSString * errorStr, MTRErrorCode errorCode, NSError * __autoreleasing * error) +{ + MTR_LOG_ERROR("%s", errorStr.UTF8String); + if (!error) { + return; + } + + NSDictionary * userInfo = @ { NSLocalizedFailureReasonErrorKey : NSLocalizedString(errorStr, nil) }; + *error = [NSError errorWithDomain:MTRErrorDomain code:errorCode userInfo:userInfo]; +} + +static void LogStringAndReturnError(NSString * errorStr, CHIP_ERROR errorCode, NSError * __autoreleasing * error) +{ + MTR_LOG_ERROR("%s", errorStr.UTF8String); + if (!error) { + return; + } + + *error = [MTRError errorForCHIPErrorCode:errorCode]; +} + +static bool CheckMemberOfType(NSDictionary * responseValue, NSString * memberName, Class expectedClass, + NSString * errorMessage, NSError * __autoreleasing * error) +{ + id _Nullable value = responseValue[memberName]; + if (value == nil) { + LogStringAndReturnError([NSString stringWithFormat:@"%s is null when not expected to be", memberName.UTF8String], + MTRErrorCodeInvalidArgument, error); + return false; + } + + if (![value isKindOfClass:expectedClass]) { + LogStringAndReturnError(errorMessage, MTRErrorCodeInvalidArgument, error); + return false; + } + + return true; +} + +// Allocates a buffer, encodes the data-value as TLV, and points the TLV::Reader to +// the data. Returns false if any of that fails, in which case error gets set. +static bool EncodeDataValueToTLV(Platform::ScopedMemoryBuffer & buffer, size_t bufferSize, NSDictionary * data, + TLV::TLVReader & reader, NSError * __autoreleasing * error) +{ + if (!buffer.Calloc(bufferSize)) { + LogStringAndReturnError(@"Unable to allocate encoding buffer.", CHIP_ERROR_NO_MEMORY, error); + return false; + } + + TLV::TLVWriter writer; + writer.Init(buffer.Get(), bufferSize); + + CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, writer, TLV::AnonymousTag()); + if (errorCode != CHIP_NO_ERROR) { + LogStringAndReturnError(@"Unable to encode data-value to TLV.", errorCode, error); + return false; + } + + reader.Init(buffer.Get(), writer.GetLengthWritten()); + + errorCode = reader.Next(TLV::AnonymousTag()); + if (errorCode != CHIP_NO_ERROR) { + LogStringAndReturnError(@"data-value TLV encoding did not create a TLV element.", errorCode, error); + return false; + } + + return true; +} + @implementation MTRAttributeReport ++ (void)initialize +{ + // One of our init methods ends up doing Platform::MemoryAlloc. + MTRFrameworkInit(); +} + - (instancetype)initWithPath:(const ConcreteDataAttributePath &)path value:(id _Nullable)value error:(NSError * _Nullable)error { if (self = [super init]) { @@ -2249,6 +2338,83 @@ - (instancetype)initWithPath:(const ConcreteDataAttributePath &)path value:(id _ } return self; } + +- (nullable instancetype)initWithResponseValue:(NSDictionary *)responseValue + error:(NSError * __autoreleasing *)error +{ + if (!(self = [super init])) { + return nil; + } + + // In theory, the types of all the things in the dictionary will be correct + // if our consumer passes in an actual response-value dictionary, but + // double-check just to be sure + if (!CheckMemberOfType(responseValue, MTRAttributePathKey, [MTRAttributePath class], + @"response-value attribute path is not an MTRAttributePath.", error)) { + return nil; + } + MTRAttributePath * path = responseValue[MTRAttributePathKey]; + + id _Nullable value = responseValue[MTRErrorKey]; + if (value != nil) { + if (!CheckMemberOfType(responseValue, MTRErrorKey, [NSError class], @"response-value error is not an NSError.", error)) { + return nil; + } + + _path = path; + _value = nil; + _error = value; + return self; + } + + if (!CheckMemberOfType( + responseValue, MTRDataKey, [NSDictionary class], @"response-value data is not a data-value dictionary.", error)) { + return nil; + } + NSDictionary * data = responseValue[MTRDataKey]; + + // Encode the data to TLV and then decode from that, to reuse existing code. + // We don't know exactly how much data we can have here; if our value is a list it + // might well be larger than the amount of data that would fit in a single packet. + // + // We could start with some small buffer size and try growing it until it's + // big enough to encode into, but in practice we'd probably have to cap that + // at some max size anyway. So just start with something that is likely to + // work for any conceivable attribute, like 20KiB. + + constexpr size_t bufferSize = 20 * 1042; + Platform::ScopedMemoryBuffer buffer; + TLV::TLVReader reader; + if (!EncodeDataValueToTLV(buffer, bufferSize, data, reader, error)) { + return nil; + } + + auto attributePath = [path _asConcretePath]; + + CHIP_ERROR errorCode = CHIP_ERROR_INTERNAL; + id decodedValue = MTRDecodeAttributeValue(attributePath, reader, &errorCode); + if (errorCode == CHIP_NO_ERROR) { + _path = path; + _value = decodedValue; + _error = nil; + return self; + } + + if (errorCode == CHIP_ERROR_IM_MALFORMED_ATTRIBUTE_PATH_IB) { + LogStringAndReturnError(@"No known schema for decoding attribute value.", MTRErrorCodeUnknownSchema, error); + return nil; + } + + // Treat all other errors as schema errors. + LogStringAndReturnError(@"Attribute decoding failed schema check.", MTRErrorCodeSchemaMismatch, error); + return nil; +} + +- (id)copyWithZone:(NSZone *)zone +{ + return [[MTRAttributeReport alloc] initWithPath:[self.path _asConcretePath] value:self.value error:self.error]; +} + @end @interface MTREventReport () { @@ -2257,12 +2423,17 @@ @interface MTREventReport () { @end @implementation MTREventReport ++ (void)initialize +{ + // One of our init methods ends up doing Platform::MemoryAlloc. + MTRFrameworkInit(); +} + - (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path eventNumber:(NSNumber *)eventNumber priority:(PriorityLevel)priority timestamp:(const Timestamp &)timestamp - value:(id _Nullable)value - error:(NSError * _Nullable)error + value:(id)value { if (self = [super init]) { _path = [[MTREventPath alloc] initWithPath:path]; @@ -2282,10 +2453,125 @@ - (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path return nil; } _value = value; + _error = nil; + } + return self; +} + +- (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path error:(NSError *)error +{ + if (self = [super init]) { + _path = [[MTREventPath alloc] initWithPath:path]; + // Use some sort of initialized values for our members, even though + // those values are meaningless in this case. + _eventNumber = @(0); + _priority = @(MTREventPriorityDebug); + _eventTimeType = MTREventTimeTypeSystemUpTime; + _systemUpTime = 0; + _timestampDate = nil; + _value = nil; _error = error; } return self; } + +- (nullable instancetype)initWithResponseValue:(NSDictionary *)responseValue + error:(NSError * __autoreleasing *)error +{ + if (!(self = [super init])) { + return nil; + } + + // In theory, the types of all the things in the dictionary will be correct + // if our consumer passes in an actual response-value dictionary, but + // double-check just to be sure + if (!CheckMemberOfType( + responseValue, MTREventPathKey, [MTREventPath class], @"response-value event path is not an MTREventPath.", error)) { + return nil; + } + MTREventPath * path = responseValue[MTREventPathKey]; + + id _Nullable value = responseValue[MTRErrorKey]; + if (value != nil) { + if (!CheckMemberOfType(responseValue, MTRErrorKey, [NSError class], @"response-value error is not an NSError.", error)) { + return nil; + } + + return [self initWithPath:[path _asConcretePath] error:value]; + } + + if (!CheckMemberOfType( + responseValue, MTRDataKey, [NSDictionary class], @"response-value data is not a data-value dictionary.", error)) { + return nil; + } + NSDictionary * data = responseValue[MTRDataKey]; + + // Encode the data to TLV and then decode from that, to reuse existing code. + // For an event, we know it always fits in a single Matter message. + constexpr size_t bufferSize = kMaxSecureSduLengthBytes; + Platform::ScopedMemoryBuffer buffer; + TLV::TLVReader reader; + if (!EncodeDataValueToTLV(buffer, bufferSize, data, reader, error)) { + return nil; + } + auto eventPath = [path _asConcretePath]; + + CHIP_ERROR errorCode = CHIP_ERROR_INTERNAL; + id decodedValue = MTRDecodeEventPayload(eventPath, reader, &errorCode); + if (errorCode == CHIP_NO_ERROR) { + // Validate our other members. + if (!CheckMemberOfType( + responseValue, MTREventNumberKey, [NSNumber class], @"response-value event number is not an NSNumber", error)) { + return nil; + } + _eventNumber = responseValue[MTREventNumberKey]; + + if (!CheckMemberOfType( + responseValue, MTREventPriorityKey, [NSNumber class], @"response-value event priority is not an NSNumber", error)) { + return nil; + } + _priority = responseValue[MTREventPriorityKey]; + + if (!CheckMemberOfType(responseValue, MTREventTimeTypeKey, [NSNumber class], + @"response-value event time type is not an NSNumber", error)) { + return nil; + } + NSNumber * wrappedTimeType = responseValue[MTREventTimeTypeKey]; + if (wrappedTimeType.unsignedIntegerValue == MTREventTimeTypeSystemUpTime) { + if (!CheckMemberOfType(responseValue, MTREventSystemUpTimeKey, [NSNumber class], + @"response-value event system uptime time is not an NSNumber", error)) { + return nil; + } + NSNumber * wrappedSystemTime = responseValue[MTREventSystemUpTimeKey]; + _systemUpTime = wrappedSystemTime.doubleValue; + } else if (wrappedTimeType.unsignedIntegerValue == MTREventTimeTypeTimestampDate) { + if (!CheckMemberOfType(responseValue, MTREventTimestampDateKey, [NSDate class], + @"response-value event timestampe is not an NSDate", error)) { + return nil; + } + _timestampDate = responseValue[MTREventTimestampDateKey]; + } else { + LogStringAndReturnError([NSString stringWithFormat:@"Invalid event time type: %lu", wrappedTimeType.unsignedLongValue], + MTRErrorCodeInvalidArgument, error); + return nil; + } + _eventTimeType = static_cast(wrappedTimeType.unsignedIntegerValue); + + _path = path; + _value = decodedValue; + _error = nil; + return self; + } + + if (errorCode == CHIP_ERROR_IM_MALFORMED_EVENT_PATH_IB) { + LogStringAndReturnError(@"No known schema for decoding event payload.", MTRErrorCodeUnknownSchema, error); + return nil; + } + + // Treat all other errors as schema errors. + LogStringAndReturnError(@"Event payload decoding failed schema check.", MTRErrorCodeSchemaMismatch, error); + return nil; +} @end @implementation MTREventReport (Deprecated) @@ -2324,12 +2610,15 @@ - (NSNumber *)timestamp return; } - [mEventReports addObject:[[MTREventReport alloc] initWithPath:aEventHeader.mPath - eventNumber:@(aEventHeader.mEventNumber) - priority:aEventHeader.mPriorityLevel - timestamp:aEventHeader.mTimestamp - value:value - error:error]]; + if (error != nil) { + [mEventReports addObject:[[MTREventReport alloc] initWithPath:aEventHeader.mPath error:error]]; + } else { + [mEventReports addObject:[[MTREventReport alloc] initWithPath:aEventHeader.mPath + eventNumber:@(aEventHeader.mEventNumber) + priority:aEventHeader.mPriorityLevel + timestamp:aEventHeader.mTimestamp + value:value]]; + } } void SubscriptionCallback::OnAttributeData( diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h index d6be73f96375c2..c0c709fce7e7dc 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h @@ -108,12 +108,12 @@ static inline MTRTransportType MTRMakeTransportType(chip::Transport::Type type) @end @interface MTREventReport () +- (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path error:(NSError *)error; - (instancetype)initWithPath:(const chip::app::ConcreteEventPath &)path eventNumber:(NSNumber *)eventNumber priority:(chip::app::PriorityLevel)priority timestamp:(const chip::app::Timestamp &)timestamp - value:(id _Nullable)value - error:(NSError * _Nullable)error; + value:(id)value; @end @interface MTRAttributeRequestPath () diff --git a/src/darwin/Framework/CHIP/MTRError.h b/src/darwin/Framework/CHIP/MTRError.h index ffed8c8be3b9df..c7d367f78a8fa8 100644 --- a/src/darwin/Framework/CHIP/MTRError.h +++ b/src/darwin/Framework/CHIP/MTRError.h @@ -59,6 +59,16 @@ typedef NS_ERROR_ENUM(MTRErrorDomain, MTRErrorCode){ * into a fabric when it's already part of that fabric. */ MTRErrorCodeFabricExists = 11, + /** + * MTRErrorCodeUnknownSchema means the schema for the given cluster/attribute, + * cluster/event, or cluster/command combination is not known. + */ + MTRErrorCodeUnknownSchema MTR_NEWLY_AVAILABLE = 12, + /** + * MTRErrorCodeSchemaMismatch means that provided data did not match the + * expected schema. + */ + MTRErrorCodeSchemaMismatch MTR_NEWLY_AVAILABLE = 13, }; // clang-format on diff --git a/src/darwin/Framework/CHIP/MTRError_Internal.h b/src/darwin/Framework/CHIP/MTRError_Internal.h index 6eaba254b36398..d41046ecbf5bd7 100644 --- a/src/darwin/Framework/CHIP/MTRError_Internal.h +++ b/src/darwin/Framework/CHIP/MTRError_Internal.h @@ -16,6 +16,7 @@ */ #import +#import #include #include diff --git a/src/darwin/Framework/CHIPTests/MTRDataValueParserTests.m b/src/darwin/Framework/CHIPTests/MTRDataValueParserTests.m new file mode 100644 index 00000000000000..a12b20319fda9a --- /dev/null +++ b/src/darwin/Framework/CHIPTests/MTRDataValueParserTests.m @@ -0,0 +1,916 @@ +/* + * + * Copyright (c) 2023 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. + */ + +#import + +#import + +@interface MTRDataValueParserTests : XCTestCase +@end + +@implementation MTRDataValueParserTests + +- (void)setUp +{ + // Per-test setup, runs before each test. + [super setUp]; + [self setContinueAfterFailure:NO]; +} + +- (void)test001_UnsignedIntAttribute +{ + // Pressure Measurement, Tolerance + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0403) attributeID:@(3)], + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @(5)); + XCTAssertNil(report.error); +} + +- (void)test002_SignedIntAttribute +{ + // Pressure Measurement, MeasuredValue + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0403) attributeID:@(2)], + MTRDataKey : @ { + MTRTypeKey : MTRSignedIntegerValueType, + MTRValueKey : @(7), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @(7)); + XCTAssertNil(report.error); +} + +- (void)test003_BooleanAttribute +{ + // On/Off, OnOff + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0006) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @(YES)); + XCTAssertNil(report.error); +} + +- (void)test004_StringAttribute +{ + // Basic Information, SerialNumber + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0028) attributeID:@(0xf)], + MTRDataKey : @ { + MTRTypeKey : MTRUTF8StringValueType, + MTRValueKey : @"hello", + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @"hello"); + XCTAssertNil(report.error); +} + +- (void)test005_OctetStringAttribute +{ + // Thread Network Diagnostics, ChannelPage0Mask + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(5) clusterID:@(0x0035) attributeID:@(0x3c)], + MTRDataKey : @ { + MTRTypeKey : MTROctetStringValueType, + MTRValueKey : [@"binary" dataUsingEncoding:NSUTF8StringEncoding], + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, [@"binary" dataUsingEncoding:NSUTF8StringEncoding]); + XCTAssertNil(report.error); +} + +- (void)test006_NullableOctetStringAttribute +{ + // Thread Network Diagnostics, ChannelPage0Mask + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(5) clusterID:@(0x0035) attributeID:@(0x3c)], + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertNil(report.value); + XCTAssertNil(report.error); +} + +- (void)test007_FloatAttribute +{ + // Media Playback, PlaybackSpeed + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0x0506) attributeID:@(4)], + MTRDataKey : @ { + MTRTypeKey : MTRFloatValueType, + MTRValueKey : @(1.5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @(1.5)); + XCTAssertNil(report.error); +} + +- (void)test008_DoubleAttribute +{ + // Unit Testing, float_double + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0xFFF1FC05) attributeID:@(0x18)], + MTRDataKey : @ { + MTRTypeKey : MTRDoubleValueType, + MTRValueKey : @(1.5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @(1.5)); + XCTAssertNil(report.error); +} + +- (void)test009_NullableDoubleAttribute +{ + // Unit Testing, nullable_float_double + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0xFFF1FC05) attributeID:@(0x4018)], + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertNil(report.value); + XCTAssertNil(report.error); +} + +- (void)test0010_StructAttribute +{ + // Basic Information, CapabilityMinima + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0028) attributeID:@(0x0013)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(0), // CaseSessionsPerFabric + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(4), + }, + }, + @{ + MTRContextTagKey : @(1), // SubscriptionsPerFabric + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(3), + }, + }, + ], + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[MTRBasicInformationClusterCapabilityMinimaStruct class]]); + + MTRBasicInformationClusterCapabilityMinimaStruct * data = report.value; + XCTAssertEqualObjects(data.caseSessionsPerFabric, @(4)); + XCTAssertEqualObjects(data.subscriptionsPerFabric, @(3)); + + XCTAssertNil(report.error); +} + +- (void)test0011_StructAttributeOtherOrder +{ + // Basic Information, CapabilityMinima + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0028) attributeID:@(0x0013)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // SubscriptionsPerFabric + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(3), + }, + }, + @{ + MTRContextTagKey : @(0), // CaseSessionsPerFabric + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(4), + }, + }, + ] + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[MTRBasicInformationClusterCapabilityMinimaStruct class]]); + + MTRBasicInformationClusterCapabilityMinimaStruct * data = report.value; + XCTAssertEqualObjects(data.caseSessionsPerFabric, @(4)); + XCTAssertEqualObjects(data.subscriptionsPerFabric, @(3)); + + XCTAssertNil(report.error); +} + +- (void)test0012_ListAttribute +{ + // Descriptor, DeviceTypeList + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x001d) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRArrayValueType, + MTRValueKey : @[ + @{ + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(0), // DeviceType + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(100), + }, + }, + @{ + MTRContextTagKey : @(1), // Revision + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(17), + }, + }, + ], + }, + }, + @{ + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // Revision + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(19), + }, + }, + @{ + MTRContextTagKey : @(0), // DeviceType + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(200), + }, + }, + ], + }, + }, + ], + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[NSArray class]]); + + NSArray * array = report.value; + XCTAssertTrue([array[0] isKindOfClass:[MTRDescriptorClusterDeviceTypeStruct class]]); + MTRDescriptorClusterDeviceTypeStruct * deviceType = array[0]; + XCTAssertEqualObjects(deviceType.deviceType, @(100)); + XCTAssertEqualObjects(deviceType.revision, @(17)); + + XCTAssertTrue([array[1] isKindOfClass:[MTRDescriptorClusterDeviceTypeStruct class]]); + deviceType = array[1]; + XCTAssertEqualObjects(deviceType.deviceType, @(200)); + XCTAssertEqualObjects(deviceType.revision, @(19)); + + XCTAssertNil(report.error); +} + +- (void)test0013_UnsignedIntAttributeSignMismatch +{ + // Pressure Measurement, Tolerance + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0403) attributeID:@(3)], + MTRDataKey : @ { + MTRTypeKey : MTRSignedIntegerValueType, + MTRValueKey : @(5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0014_SignedIntAttributeSignMismatch +{ + // Pressure Measurement, MeasuredValue + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0403) attributeID:@(2)], + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(7), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0015_UnknownAttribute +{ + // On/Off, nonexistent attribute + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(7) attributeID:@(0x1000)], + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(7), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeUnknownSchema); +} + +- (void)test0016_UnknownCluster +{ + // Unknown cluster. + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0xFFF1FFF1) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(7), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeUnknownSchema); +} + +- (void)test0017_StringVsOctetStringMismatch +{ + // Basic Information, SerialNumber + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0028) attributeID:@(0xf)], + MTRDataKey : @ { + MTRTypeKey : MTROctetStringValueType, + MTRValueKey : [@"binary" dataUsingEncoding:NSUTF8StringEncoding], + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0018_OctetStringVsStringMismatch +{ + // Thread Network Diagnostics, ChannelPage0Mask + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(5) clusterID:@(0x0035) attributeID:@(0x3c)], + MTRDataKey : @ { + MTRTypeKey : MTRUTF8StringValueType, + MTRValueKey : @"hello", + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0019_DoubleVsFloatMismatch +{ + // Unit Testing, float_double + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0xFFF1FC05) attributeID:@(0x18)], + MTRDataKey : @ { + MTRTypeKey : MTRFloatValueType, + MTRValueKey : @(1.5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + // If float was encoded, decoding as double is allowed. + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertEqualObjects(report.value, @(1.5)); + + XCTAssertNil(report.error); +} + +- (void)test0020_FloatVsDoubleMismatch +{ + // Media Playback, PlaybackSpeed + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0x0506) attributeID:@(4)], + MTRDataKey : @ { + MTRTypeKey : MTRDoubleValueType, + MTRValueKey : @(1.5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test021_StringVsNullMismatch +{ + // Basic Information, SerialNumber + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0028) attributeID:@(0xf)], + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test022_OctetStringVsNullMismatch +{ + // Unit Testing, octet_string + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0xFFF1FC05) attributeID:@(0x19)], + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test023_DoubleVsNullMismatch +{ + // Unit Testing, float_double + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(17) clusterID:@(0xFFF1FC05) attributeID:@(0x18)], + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0024_StructFieldIntegerTypeMismatch +{ + // Descriptor, DeviceTypeList + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x001d) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRArrayValueType, + MTRValueKey : @[ + @{ + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(0), // DeviceType + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(100), + }, + }, + @{ + MTRContextTagKey : @(1), // Revision + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(17), + }, + }, + ], + }, + }, + @{ + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // Revision + MTRDataKey : @ { + MTRTypeKey : MTRSignedIntegerValueType, // Wrong type here. + MTRValueKey : @(19), + }, + }, + @{ + MTRContextTagKey : @(0), // DeviceType + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(200), + }, + }, + ], + }, + }, + ], + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0025_EventPayloadWithSystemUptime +{ + // Access Control, AccessControlExtensionChanged + NSDictionary * input = @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(0x001F) eventID:@(1)], + MTREventNumberKey : @(180), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeSystemUpTime), + MTREventSystemUpTimeKey : @(27.5), + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // AdminNodeID + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(177), + }, + }, + @{ + MTRContextTagKey : @(2), // AdminPasscodeID + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }, + @{ + MTRContextTagKey : @(3), // ChangeType + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(MTRAccessControlChangeTypeAdded), + }, + }, + @{ + MTRContextTagKey : @(4), // LatestValue + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // Data + MTRDataKey : @ { + MTRTypeKey : MTROctetStringValueType, + MTRValueKey : [@"extension" dataUsingEncoding:NSUTF8StringEncoding], + }, + }, + ], + }, + }, + ], + }, + }; + + NSError * error; + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertNotNil(report.path); + XCTAssertEqualObjects(report.path.endpoint, @(0)); + XCTAssertEqualObjects(report.path.cluster, @(0x001F)); + XCTAssertEqualObjects(report.path.event, @(1)); + + XCTAssertEqualObjects(report.eventNumber, @(180)); + XCTAssertEqualObjects(report.priority, @(MTREventPriorityInfo)); + XCTAssertEqual(report.eventTimeType, MTREventTimeTypeSystemUpTime); + XCTAssertEqual(report.systemUpTime, 27.5); + + XCTAssertNotNil(report.value); + + XCTAssertTrue([report.value isKindOfClass:[MTRAccessControlClusterAccessControlExtensionChangedEvent class]]); + + MTRAccessControlClusterAccessControlExtensionChangedEvent * payload = report.value; + XCTAssertEqualObjects(payload.adminNodeID, @(177)); + XCTAssertNil(payload.adminPasscodeID); + XCTAssertEqualObjects(payload.changeType, @(MTRAccessControlChangeTypeAdded)); + XCTAssertNotNil(payload.latestValue); + + XCTAssertTrue([payload.latestValue isKindOfClass:[MTRAccessControlClusterAccessControlExtensionStruct class]]); + XCTAssertEqualObjects(payload.latestValue.data, [@"extension" dataUsingEncoding:NSUTF8StringEncoding]); + + XCTAssertNil(report.error); +} + +- (void)test0026_EventReportWithTimestampDate +{ + // Basic Information, Shutdown + NSDictionary * input = @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(0x0028) eventID:@(1)], + MTREventNumberKey : @(190), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : [NSDate date], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], + }, + }; + + NSError * error; + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertNotNil(report.path); + XCTAssertEqualObjects(report.path.endpoint, @(0)); + XCTAssertEqualObjects(report.path.cluster, @(0x0028)); + XCTAssertEqualObjects(report.path.event, @(1)); + + XCTAssertEqualObjects(report.eventNumber, @(190)); + XCTAssertEqualObjects(report.priority, @(MTREventPriorityInfo)); + XCTAssertEqual(report.eventTimeType, MTREventTimeTypeTimestampDate); + XCTAssertEqualObjects(report.timestampDate, input[MTREventTimestampDateKey]); + + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[MTRBasicInformationClusterShutDownEvent class]]); +} + +- (void)test0027_AttributeWithDataAndError +{ + // Pressure Measurement, Tolerance + NSDictionary * input = @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(0x0403) attributeID:@(3)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeTimeout userInfo:nil], + // Include data too, which should be ignored. + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(5), + }, + }; + + NSError * error; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertEqualObjects(input[MTRAttributePathKey], report.path); + XCTAssertNil(report.value); + XCTAssertEqualObjects(report.error, input[MTRErrorKey]); +} + +- (void)test0028_EventReportWithDataAndError +{ + // Basic Information, Shutdown + NSDictionary * input = @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(0x0028) eventID:@(1)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeTimeout userInfo:nil], + + // All the other keys should be ignored + MTREventNumberKey : @(190), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : [NSDate date], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], + }, + }; + + NSError * error; + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNotNil(report); + XCTAssertNil(error); + + XCTAssertNotNil(report.path); + XCTAssertEqualObjects(report.path.endpoint, @(0)); + XCTAssertEqualObjects(report.path.cluster, @(0x0028)); + XCTAssertEqualObjects(report.path.event, @(1)); + + XCTAssertEqualObjects(report.eventNumber, @(0)); + XCTAssertEqualObjects(report.priority, @(0)); + XCTAssertNil(report.value); + XCTAssertEqualObjects(report.error, input[MTRErrorKey]); +} + +- (void)test0029_EventPayloadFailingSchemaCheck +{ + // Access Control, AccessControlExtensionChanged + NSDictionary * input = @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(0x001F) eventID:@(1)], + MTREventNumberKey : @(180), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeSystemUpTime), + MTREventSystemUpTimeKey : @(27.5), + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // AdminNodeID + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(177), + }, + }, + @{ + MTRContextTagKey : @(2), // AdminPasscodeID + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }, + @{ + MTRContextTagKey : @(3), // ChangeType + MTRDataKey : @ { + MTRTypeKey : MTRSignedIntegerValueType, // Should be unsigned + MTRValueKey : @(MTRAccessControlChangeTypeAdded), + }, + }, + @{ + MTRContextTagKey : @(4), // LatestValue + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), // Data + MTRDataKey : @ { + MTRTypeKey : MTROctetStringValueType, + MTRValueKey : [@"extension" dataUsingEncoding:NSUTF8StringEncoding], + }, + }, + ], + }, + }, + ], + }, + }; + + NSError * error; + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeSchemaMismatch); +} + +- (void)test0030_EventReportWithUnknownCluster +{ + NSDictionary * input = @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(0xFF1FF1) eventID:@(0)], + MTREventNumberKey : @(190), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : [NSDate date], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], + }, + }; + + NSError * error; + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeUnknownSchema); +} + +- (void)test0031_EventReportWithUnknownEvent +{ + NSDictionary * input = @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(0x0028) eventID:@(1000)], + MTREventNumberKey : @(190), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : [NSDate date], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], + }, + }; + + NSError * error; + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:input error:&error]; + XCTAssertNil(report); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, MTRErrorCodeUnknownSchema); +} + +@end diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 2a444a85ce0e81..2c3d949ba79ecc 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -293,6 +293,19 @@ - (void)test001_ReadAttribute XCTAssertNil(result[@"error"]); XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]); XCTAssertTrue([result[@"data"][@"type"] isEqualToString:@"Array"]); + + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:result + error:nil]; + XCTAssertNotNil(report); + XCTAssertEqualObjects(report.path.cluster, @(29)); + XCTAssertEqualObjects(report.path.attribute, @(0)); + XCTAssertNil(report.error); + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[NSArray class]]); + + for (id entry in report.value) { + XCTAssertTrue([entry isKindOfClass:[MTRDescriptorClusterDeviceTypeStruct class]]); + } } XCTAssertTrue([resultArray count] > 0); } @@ -465,16 +478,25 @@ - (void)test005_Subscribe XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0); XCTAssertTrue([values isKindOfClass:[NSArray class]]); NSDictionary * result = values[0]; + + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:result error:nil]; + XCTAssertNotNil(report); + XCTAssertEqualObjects(report.path.endpoint, @(1)); + XCTAssertEqualObjects(report.path.cluster, @(6)); + XCTAssertEqualObjects(report.path.attribute, @(0)); + XCTAssertNil(report.error); + XCTAssertNotNil(report.value); + XCTAssertEqualObjects(report.value, @(YES)); + MTRAttributePath * path = result[@"attributePath"]; XCTAssertEqual([path.endpoint unsignedIntegerValue], 1); XCTAssertEqual([path.cluster unsignedIntegerValue], 6); XCTAssertEqual([path.attribute unsignedIntegerValue], 0); XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]); XCTAssertTrue([result[@"data"][@"type"] isEqualToString:@"Boolean"]); - if ([result[@"data"][@"value"] boolValue] == YES) { - [reportExpectation fulfill]; - globalReportHandler = nil; - } + XCTAssertTrue([result[@"data"][@"value"] boolValue]); + [reportExpectation fulfill]; + globalReportHandler = nil; }; // Send commands to trigger attribute change @@ -517,16 +539,25 @@ - (void)test005_Subscribe XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0); XCTAssertTrue([values isKindOfClass:[NSArray class]]); NSDictionary * result = values[0]; + + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:result error:nil]; + XCTAssertNotNil(report); + XCTAssertEqualObjects(report.path.endpoint, @(1)); + XCTAssertEqualObjects(report.path.cluster, @(6)); + XCTAssertEqualObjects(report.path.attribute, @(0)); + XCTAssertNil(report.error); + XCTAssertNotNil(report.value); + XCTAssertEqualObjects(report.value, @(NO)); + MTRAttributePath * path = result[@"attributePath"]; XCTAssertEqual([path.endpoint unsignedIntegerValue], 1); XCTAssertEqual([path.cluster unsignedIntegerValue], 6); XCTAssertEqual([path.attribute unsignedIntegerValue], 0); XCTAssertTrue([result[@"data"] isKindOfClass:[NSDictionary class]]); XCTAssertTrue([result[@"data"][@"type"] isEqualToString:@"Boolean"]); - if ([result[@"data"][@"value"] boolValue] == NO) { - [reportExpectation fulfill]; - globalReportHandler = nil; - } + XCTAssertFalse([result[@"data"][@"value"] boolValue]); + [reportExpectation fulfill]; + globalReportHandler = nil; }; // Send command to trigger attribute change @@ -592,6 +623,16 @@ - (void)test006_ReadAttributeFailure XCTAssertEqual([resultArray count], 1); NSDictionary * result = resultArray[0]; + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:result error:nil]; + XCTAssertNotNil(report); + XCTAssertEqualObjects(report.path.endpoint, @(0)); + XCTAssertEqualObjects(report.path.cluster, @(10000)); + XCTAssertEqualObjects(report.path.attribute, @(0)); + XCTAssertNotNil(report.error); + XCTAssertNil(report.value); + XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:report.error], + EMBER_ZCL_STATUS_UNSUPPORTED_CLUSTER); + MTRAttributePath * path = result[@"attributePath"]; XCTAssertEqual(path.endpoint.unsignedIntegerValue, 0); XCTAssertEqual(path.cluster.unsignedIntegerValue, 10000); @@ -925,6 +966,16 @@ - (void)test011_ReadCachedAttribute NSLog(@"Read attribute cache value: %@, error %@", values, error); XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0); XCTAssertEqual([values count], 1); + + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:values[0] error:nil]; + XCTAssertNotNil(report); + XCTAssertEqualObjects(report.path.endpoint, @(1)); + XCTAssertEqualObjects(report.path.cluster, @(6)); + XCTAssertEqualObjects(report.path.attribute, @(0)); + XCTAssertNil(report.error); + XCTAssertNotNil(report.value); + XCTAssertEqualObjects(report.value, @(NO)); + MTRAttributePath * path = values[0][@"attributePath"]; XCTAssertEqual([path.endpoint unsignedShortValue], 1); XCTAssertEqual([path.cluster unsignedLongValue], 6); @@ -950,6 +1001,14 @@ - (void)test011_ReadCachedAttribute XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:error], 0); XCTAssertTrue([values count] > 0); for (NSDictionary * value in values) { + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:value error:nil]; + XCTAssertNotNil(report); + XCTAssertEqualObjects(report.path.cluster, @(6)); + XCTAssertEqualObjects(report.path.attribute, @(0)); + XCTAssertNil(report.error); + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[NSNumber class]]); + MTRAttributePath * path = value[@"attributePath"]; XCTAssertEqual([path.cluster unsignedLongValue], 6); XCTAssertEqual([path.attribute unsignedLongValue], 0); @@ -1635,8 +1694,7 @@ - (void)test020_ReadMultipleAttributes [MTRAttributeRequestPath requestPathWithEndpointID:nil clusterID:@40 attributeID:@7] ]; - NSArray * eventPaths = - [NSArray arrayWithObjects:[MTREventRequestPath requestPathWithEndpointID:nil clusterID:@40 eventID:@0], nil]; + NSArray * eventPaths = @[ [MTREventRequestPath requestPathWithEndpointID:nil clusterID:@40 eventID:@0] ]; [device readAttributePaths:attributePaths eventPaths:eventPaths @@ -1655,6 +1713,21 @@ - (void)test020_ReadMultipleAttributes for (NSDictionary * result in resultArray) { if ([result objectForKey:@"eventPath"]) { ++eventResultCount; + + __auto_type * report = [[MTREventReport alloc] initWithResponseValue:result error:nil]; + XCTAssertNotNil(report); + XCTAssertNotNil(report.path); + XCTAssertEqualObjects(report.path.endpoint, @(0)); + XCTAssertEqualObjects(report.path.cluster, @(40)); + XCTAssertEqualObjects(report.path.event, @(0)); + XCTAssertNotNil(report.eventNumber); + XCTAssertEqualObjects(report.priority, @(MTREventPriorityCritical)); + XCTAssertEqual(report.eventTimeType, MTREventTimeTypeTimestampDate); + XCTAssertNotNil(report.timestampDate); + XCTAssertNotNil(report.value); + XCTAssertTrue([report.value isKindOfClass:[MTRBasicInformationClusterStartUpEvent class]]); + XCTAssertNil(report.error); + MTREventPath * path = result[@"eventPath"]; XCTAssertEqualObjects(path.endpoint, @0); XCTAssertEqualObjects(path.cluster, @40); @@ -1682,6 +1755,62 @@ - (void)test020_ReadMultipleAttributes } } else if ([result objectForKey:@"attributePath"]) { ++attributeResultCount; + + __auto_type * report = [[MTRAttributeReport alloc] initWithResponseValue:result error:nil]; + XCTAssertNotNil(report); + XCTAssertNil(report.error); + XCTAssertNotNil(report.value); + switch ([report.path.attribute unsignedLongValue]) { + case 0: + XCTAssertEqualObjects(report.path.cluster, @29); + XCTAssertTrue([report.value isKindOfClass:[NSArray class]]); + for (id entry in report.value) { + XCTAssertTrue([entry isKindOfClass:[MTRDescriptorClusterDeviceTypeStruct class]]); + } + break; + case 1: + XCTAssertEqualObjects(report.path.cluster, @29); + XCTAssertTrue([report.value isKindOfClass:[NSArray class]]); + for (id entry in report.value) { + XCTAssertTrue([entry isKindOfClass:[NSNumber class]]); + } + break; + case 2: + XCTAssertEqualObjects(report.path.cluster, @29); + XCTAssertTrue([report.value isKindOfClass:[NSArray class]]); + for (id entry in report.value) { + XCTAssertTrue([entry isKindOfClass:[NSNumber class]]); + } + break; + case 3: + XCTAssertEqualObjects(report.path.cluster, @29); + XCTAssertTrue([report.value isKindOfClass:[NSArray class]]); + for (id entry in report.value) { + XCTAssertTrue([entry isKindOfClass:[NSNumber class]]); + } + break; + case 4: + XCTAssertEqualObjects(report.path.cluster, @40); + XCTAssertEqualObjects(report.path.endpoint, @0); + XCTAssertTrue([report.value isKindOfClass:[NSNumber class]]); + break; + case 5: + XCTAssertEqualObjects(report.path.cluster, @40); + XCTAssertEqualObjects(report.path.endpoint, @0); + XCTAssertTrue([report.value isKindOfClass:[NSString class]]); + break; + case 6: + XCTAssertEqualObjects(report.path.cluster, @40); + XCTAssertEqualObjects(report.path.endpoint, @0); + XCTAssertTrue([report.value isKindOfClass:[NSString class]]); + break; + case 7: + XCTAssertEqualObjects(report.path.cluster, @40); + XCTAssertEqualObjects(report.path.endpoint, @0); + XCTAssertTrue([report.value isKindOfClass:[NSNumber class]]); + break; + } + MTRAttributePath * path = result[@"attributePath"]; if ([path.attribute unsignedIntegerValue] < 4) { XCTAssertEqualObjects(path.cluster, @29); diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index 600aad44180541..2561d02229b67b 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 517BF3F0282B62B800A8B7DB /* MTRCertificates.h in Headers */ = {isa = PBXBuildFile; fileRef = 517BF3EE282B62B800A8B7DB /* MTRCertificates.h */; settings = {ATTRIBUTES = (Public, ); }; }; 517BF3F1282B62B800A8B7DB /* MTRCertificates.mm in Sources */ = {isa = PBXBuildFile; fileRef = 517BF3EF282B62B800A8B7DB /* MTRCertificates.mm */; }; 517BF3F3282B62CB00A8B7DB /* MTRCertificateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 517BF3F2282B62CB00A8B7DB /* MTRCertificateTests.m */; }; + 51A2F1322A00402A00F03298 /* MTRDataValueParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51A2F1312A00402A00F03298 /* MTRDataValueParserTests.m */; }; 51B22C1E2740CB0A008D5055 /* MTRStructsObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 51B22C1D2740CB0A008D5055 /* MTRStructsObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51B22C222740CB1D008D5055 /* MTRCommandPayloadsObjc.h in Headers */ = {isa = PBXBuildFile; fileRef = 51B22C212740CB1D008D5055 /* MTRCommandPayloadsObjc.h */; settings = {ATTRIBUTES = (Public, ); }; }; 51B22C262740CB32008D5055 /* MTRStructsObjc.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51B22C252740CB32008D5055 /* MTRStructsObjc.mm */; }; @@ -440,6 +441,7 @@ 517BF3EE282B62B800A8B7DB /* MTRCertificates.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRCertificates.h; sourceTree = ""; }; 517BF3EF282B62B800A8B7DB /* MTRCertificates.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRCertificates.mm; sourceTree = ""; }; 517BF3F2282B62CB00A8B7DB /* MTRCertificateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRCertificateTests.m; sourceTree = ""; }; + 51A2F1312A00402A00F03298 /* MTRDataValueParserTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRDataValueParserTests.m; sourceTree = ""; }; 51B22C1D2740CB0A008D5055 /* MTRStructsObjc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRStructsObjc.h; sourceTree = ""; }; 51B22C212740CB1D008D5055 /* MTRCommandPayloadsObjc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRCommandPayloadsObjc.h; sourceTree = ""; }; 51B22C252740CB32008D5055 /* MTRStructsObjc.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRStructsObjc.mm; sourceTree = ""; }; @@ -1057,6 +1059,7 @@ 5173A47829C0E82300F67F48 /* MTRFabricInfoTests.m */, 51742B4D29CB6B88009974FE /* MTRPairingTests.m */, 5142E39729D377F000A206F0 /* MTROTAProviderTests.m */, + 51A2F1312A00402A00F03298 /* MTRDataValueParserTests.m */, B202529D2459E34F00F97062 /* Info.plist */, ); path = CHIPTests; @@ -1466,6 +1469,7 @@ 517BF3F3282B62CB00A8B7DB /* MTRCertificateTests.m in Sources */, 5142E39829D377F000A206F0 /* MTROTAProviderTests.m in Sources */, 51E24E73274E0DAC007CCF6E /* MTRErrorTestUtils.mm in Sources */, + 51A2F1322A00402A00F03298 /* MTRDataValueParserTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };