From 8f2810b49be0c6aaab7439450d8e3286d65aeaa5 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Tue, 11 Feb 2025 15:05:48 -0500 Subject: [PATCH] Add a test to exercise MTRCommandWithRequiredResponse encode/decode. --- .../CHIP/MTRCommandWithRequiredResponse.mm | 30 +++- .../Framework/CHIPTests/MTRDeviceTests.m | 139 +++++++++++++++++- 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRCommandWithRequiredResponse.mm b/src/darwin/Framework/CHIP/MTRCommandWithRequiredResponse.mm index 0a8e9eda8a420d..81a695aca7497e 100644 --- a/src/darwin/Framework/CHIP/MTRCommandWithRequiredResponse.mm +++ b/src/darwin/Framework/CHIP/MTRCommandWithRequiredResponse.mm @@ -14,9 +14,11 @@ * limitations under the License. */ +#import + #import "MTRDeviceDataValidation.h" #import "MTRLogging_Internal.h" -#import +#import "MTRUtilities.h" @implementation MTRCommandWithRequiredResponse - (instancetype)initWithPath:(MTRCommandPath *)path @@ -66,7 +68,14 @@ - (nullable instancetype)initWithCoder:(NSCoder *)decoder return nil; } - _commandFields = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[ [NSDictionary class], [NSString class], [NSNumber class], [NSArray class], [NSData class] ]] forKey:sFieldsKey]; + // The classes of things that can appear in a data-value dictionary. + static NSSet * const sDataValueClasses = [NSSet setWithArray:@[ NSDictionary.class, NSArray.class, NSData.class, NSString.class, NSNumber.class ]]; + + // Unfortunately, decodeDictionaryWithKeysOfClasses:objectsOfClasses:forKey: + // does not work when the objects stored in the dictionary can include + // collections, so we have to use decodeObjectOfClasses: and then manually + // validate we got a dictionary. + _commandFields = [decoder decodeObjectOfClasses:sDataValueClasses forKey:sFieldsKey]; if (_commandFields) { if (![_commandFields isKindOfClass:NSDictionary.class]) { MTR_LOG_ERROR("MTRCommandWithRequiredResponse decoded %@ for commandFields, not NSDictionary.", _commandFields); @@ -79,7 +88,7 @@ - (nullable instancetype)initWithCoder:(NSCoder *)decoder } } - _requiredResponse = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[ [NSDictionary class], [NSString class], [NSNumber class], [NSArray class], [NSData class] ]] forKey:sExpectedResultKey]; + _requiredResponse = [decoder decodeObjectOfClasses:sDataValueClasses forKey:sExpectedResultKey]; if (_requiredResponse) { if (![_requiredResponse isKindOfClass:NSDictionary.class]) { MTR_LOG_ERROR("MTRCommandWithRequiredResponse decoded %@ for requiredResponse, not NSDictionary.", _requiredResponse); @@ -116,4 +125,19 @@ - (void)encodeWithCoder:(NSCoder *)coder } } +- (BOOL)_isEqualToOther:(MTRCommandWithRequiredResponse *)other +{ + return MTREqualObjects(_path, other.path) + && MTREqualObjects(_commandFields, other.commandFields) + && MTREqualObjects(_requiredResponse, other.requiredResponse); +} + +- (BOOL)isEqual:(id)object +{ + if ([object class] != [self class]) { + return NO; + } + return [self _isEqualToOther:object]; +} + @end diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 7184e434a4eff9..71117b43176b96 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -3350,7 +3350,7 @@ - (void)test031_MTRDeviceAttributeCacheLocalTestStorage XCTAssertTrue(storedAttributeCountDifferenceFromMTRDeviceReport > 300); } -- (void)doEncodeDecodeRoundTrip:(id)encodable +- (NSData *)_encodeEncodable:(id)encodable { // We know all our encodables are in fact NSObject. NSObject * obj = (NSObject *) encodable; @@ -3358,6 +3358,15 @@ - (void)doEncodeDecodeRoundTrip:(id)encodable NSError * encodeError; NSData * encodedData = [NSKeyedArchiver archivedDataWithRootObject:encodable requiringSecureCoding:YES error:&encodeError]; XCTAssertNil(encodeError, @"Failed to encode %@", NSStringFromClass(obj.class)); + return encodedData; +} + +- (void)doEncodeDecodeRoundTrip:(id)encodable +{ + NSData * encodedData = [self _encodeEncodable:encodable]; + + // We know all our encodables are in fact NSObject. + NSObject * obj = (NSObject *) encodable; NSError * decodeError; id decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:obj.class] fromData:encodedData error:&decodeError]; @@ -3367,6 +3376,19 @@ - (void)doEncodeDecodeRoundTrip:(id)encodable XCTAssertEqualObjects(obj, decodedValue, @"Decoding for %@ did not round-trip correctly", NSStringFromClass([obj class])); } +- (void)_ensureDecodeFails:(id)encodable +{ + NSData * encodedData = [self _encodeEncodable:encodable]; + + // We know all our encodables are in fact NSObject. + NSObject * obj = (NSObject *) encodable; + + NSError * decodeError; + id decodedValue = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:obj.class] fromData:encodedData error:&decodeError]; + XCTAssertNil(decodedValue); + XCTAssertNotNil(decodeError); +} + - (void)test032_MTRPathClassesEncoding { // Test attribute path encode / decode @@ -6023,6 +6045,121 @@ - (void)test045_MTRDeviceInvokeGroups [self waitForExpectations:@[ updateFabricLabelExpectingWrongValueExpectation ] timeout:(2 * kTimeoutInSeconds)]; } +- (void)test046_MTRCommandWithRequiredResponseEncoding +{ + // Basic test with no command fields or required response. + __auto_type * onPath = [MTRCommandPath commandPathWithEndpointID:@(1) + clusterID:@(MTRClusterIDTypeOnOffID) + commandID:@(MTRCommandIDTypeClusterOnOffCommandOnID)]; + __auto_type * onCommand = [[MTRCommandWithRequiredResponse alloc] initWithPath:onPath commandFields:nil requiredResponse:nil]; + [self doEncodeDecodeRoundTrip:onCommand]; + + // Test with both command fields and an interesting required response. + // + // NSSecureCoding tracks object identity, so we need to create new objects + // for every instance of a thing we decode/encode with a given coder to make + // sure all codepaths are exercised. Use a block that returns a new + // dictionary each time to handle this. + __auto_type structureWithAllTypes = ^{ + return @{ + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(0), + MTRDataKey : @ { + MTRTypeKey : MTRSignedIntegerValueType, + MTRValueKey : @(5), + }, + }, + @{ + MTRContextTagKey : @(1), + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(5), + }, + }, + @{ + MTRContextTagKey : @(2), + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }, + @{ + MTRContextTagKey : @(3), + MTRDataKey : @ { + MTRTypeKey : MTRUTF8StringValueType, + MTRValueKey : @("abc"), + }, + }, + @{ + MTRContextTagKey : @(4), + MTRDataKey : @ { + MTRTypeKey : MTROctetStringValueType, + MTRValueKey : [[NSData alloc] initWithBase64EncodedString:@"APJj" options:0], + }, + }, + @{ + MTRContextTagKey : @(5), + MTRDataKey : @ { + MTRTypeKey : MTRFloatValueType, + MTRValueKey : @(1.0), + }, + }, + @{ + MTRContextTagKey : @(6), + MTRDataKey : @ { + MTRTypeKey : MTRDoubleValueType, + MTRValueKey : @(5.0), + }, + }, + @{ + MTRContextTagKey : @(7), + MTRDataKey : @ { + MTRTypeKey : MTRNullValueType, + }, + }, + @{ + MTRContextTagKey : @(8), + MTRDataKey : @ { + MTRTypeKey : MTRArrayValueType, + MTRValueKey : @[ + @{ + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(9), + }, + }, + ], + } + }, + ], + }; + }; + + // Invalid commandFields (not a dictionary) + onCommand.commandFields = (id) @[]; + [self _ensureDecodeFails:onCommand]; + + // Invalid required response (not a dictionary) + onCommand.commandFields = nil; + onCommand.requiredResponse = (id) @[]; + [self _ensureDecodeFails:onCommand]; + + // Invalid required response (key is not NSNumber) + onCommand.requiredResponse = @{ + @("abc") : structureWithAllTypes(), + }; + [self _ensureDecodeFails:onCommand]; + + onCommand.commandFields = structureWithAllTypes(); + onCommand.requiredResponse = @{ + @(1) : structureWithAllTypes(), + @(13) : structureWithAllTypes(), + }; + [self doEncodeDecodeRoundTrip:onCommand]; +} + @end @interface MTRDeviceEncoderTests : XCTestCase