diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index ccefbb18497414..45113db6a18d25 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -633,10 +634,11 @@ static CHIP_ERROR MTREncodeTLVFromDataValueDictionary(id object, chip::TLV::TLVW } NSString * typeName = ((NSDictionary *) object)[MTRTypeKey]; id value = ((NSDictionary *) object)[MTRValueKey]; - if (!typeName) { + if (![typeName isKindOfClass:[NSString class]]) { MTR_LOG_ERROR("Error: Object to encode is corrupt"); return CHIP_ERROR_INVALID_ARGUMENT; } + if ([typeName isEqualToString:MTRSignedIntegerValueType]) { if (![value isKindOfClass:[NSNumber class]]) { MTR_LOG_ERROR("Error: Object to encode has corrupt signed integer type: %@", [value class]); @@ -1219,10 +1221,53 @@ - (void)writeAttributeWithEndpointID:(NSNumber *)endpointID auto onFailureCb = [failureCb, bridge]( const app::ConcreteAttributePath * attribPath, CHIP_ERROR aError) { failureCb(bridge, aError); }; - return chip::Controller::WriteAttribute(session, + // To handle list chunking properly, we have to convert lists into + // DataModel::List here, because that's special-cased in + // WriteClient. + if (![value isKindOfClass:NSDictionary.class]) { + MTR_LOG_ERROR("Error: Unsupported object to write as attribute value: %@", value); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + NSDictionary * dataValue = value; + NSString * typeName = dataValue[MTRTypeKey]; + if (![typeName isKindOfClass:NSString.class]) { + MTR_LOG_ERROR("Error: Object to encode is corrupt: %@", dataValue); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + if (![typeName isEqualToString:MTRArrayValueType]) { + return chip::Controller::WriteAttribute(session, + static_cast([endpointID unsignedShortValue]), + static_cast([clusterID unsignedLongValue]), + static_cast([attributeID unsignedLongValue]), MTRDataValueDictionaryDecodableType(value), + onSuccessCb, onFailureCb, (timeoutMs == nil) ? NullOptional : Optional([timeoutMs unsignedShortValue])); + } + + // Now we are dealing with a list. + NSArray *> * arrayValue = value[MTRValueKey]; + if (![arrayValue isKindOfClass:NSArray.class]) { + MTR_LOG_ERROR("Error: Object to encode claims to be a list but isn't: %@", arrayValue); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + std::vector encodableVector; + encodableVector.reserve(arrayValue.count); + + for (NSDictionary * arrayItem in arrayValue) { + if (![arrayItem isKindOfClass:NSDictionary.class]) { + MTR_LOG_ERROR("Error: Can't encode corrupt list: %@", arrayValue); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + encodableVector.push_back(MTRDataValueDictionaryDecodableType(arrayItem[MTRDataKey])); + } + + DataModel::List encodableList(encodableVector.data(), encodableVector.size()); + return chip::Controller::WriteAttribute>(session, static_cast([endpointID unsignedShortValue]), static_cast([clusterID unsignedLongValue]), - static_cast([attributeID unsignedLongValue]), MTRDataValueDictionaryDecodableType(value), + static_cast([attributeID unsignedLongValue]), encodableList, onSuccessCb, onFailureCb, (timeoutMs == nil) ? NullOptional : Optional([timeoutMs unsignedShortValue])); }); std::move(*bridge).DispatchAction(self); diff --git a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m index 2383341afa4d55..2874e6cfff509e 100644 --- a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m +++ b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m @@ -18,6 +18,7 @@ // module headers #import +#import "MTRDeviceTestDelegate.h" #import "MTRErrorTestUtils.h" #import "MTRTestKeys.h" #import "MTRTestResetCommissioneeHelper.h" @@ -1564,6 +1565,172 @@ - (void)test007_DoBDXTransferIncrementalOtaUpdate } #endif // ENABLE_REAL_OTA_UPDATE_TESTS +- (void)test008_TestWriteDefaultOTAProviders +{ + __auto_type * runner = [[MTROTARequestorAppRunner alloc] initWithPayload:kOnboardingPayload1 testcase:self]; + MTRDevice * device = [runner commissionWithNodeID:@(kDeviceId1)]; + + dispatch_queue_t queue = dispatch_get_main_queue(); + + __auto_type dataValue = ^(uint16_t endpoint) { + return @{ + MTRTypeKey : MTRArrayValueType, + MTRValueKey : @[ + @{ + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[ + @{ + MTRContextTagKey : @(1), + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(kDeviceId1), + }, + }, + @{ + MTRContextTagKey : @(2), + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(endpoint), + }, + }, + ], + }, + }, + ], + }; + }; + + { + // Test with MTRBaseDevice first. + MTRBaseDevice * baseDevice = [MTRBaseDevice deviceWithNodeID:device.nodeID + controller:device.deviceController]; + + __auto_type * cluster = [[MTRBaseClusterOTASoftwareUpdateRequestor alloc] initWithDevice:baseDevice + endpointID:@(0) + queue:queue]; + __auto_type * providerLocation = [[MTROTASoftwareUpdateRequestorClusterProviderLocation alloc] init]; + providerLocation.providerNodeID = @(kDeviceId1); + providerLocation.endpoint = @(0); + __auto_type * value = @[ providerLocation ]; + + __auto_type * writeBaseClusterExpectation = [self expectationWithDescription:@"Write succeeded via MTRBaseCluster"]; + [cluster writeAttributeDefaultOTAProvidersWithValue:value + completion:^(NSError * _Nullable error) { + XCTAssertNil(error); + [writeBaseClusterExpectation fulfill]; + }]; + [self waitForExpectations:@[ writeBaseClusterExpectation ] timeout:kTimeoutInSeconds]; + + __auto_type * writeBaseDeviceExpectation = [self expectationWithDescription:@"Write succeeded via MTRBaseDevice"]; + [baseDevice writeAttributeWithEndpointID:@(0) + clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID) + attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID) + value:dataValue(0) + timedWriteTimeout:nil + queue:queue + completion:^(NSArray *> * _Nullable values, NSError * _Nullable error) { + XCTAssertNil(error); + XCTAssertNotNil(values); + XCTAssertEqual(values.count, 1); + + for (NSDictionary * value in values) { + XCTAssertNil(value[MTRErrorKey]); + } + [writeBaseDeviceExpectation fulfill]; + }]; + [self waitForExpectations:@[ writeBaseDeviceExpectation ] timeout:kTimeoutInSeconds]; + } + + { + // Now test with MTRDevice + __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init]; + // Make sure we don't have expected value notifications confusing our + // attribute reports. + delegate.skipExpectedValuesForWrite = YES; + + XCTestExpectation * gotReportsExpectation = [self expectationWithDescription:@"Subscription established"]; + delegate.onReportEnd = ^() { + [gotReportsExpectation fulfill]; + }; + + [device setDelegate:delegate queue:queue]; + + [self waitForExpectations:@[ gotReportsExpectation ] timeout:60]; + + delegate.onReportEnd = nil; + + __auto_type * expectedAttributePath = [MTRAttributePath attributePathWithEndpointID:@(0) + clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID) + attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID)]; + + __block __auto_type * expectedValue = dataValue(1); + + __block __auto_type * writeExpectation = [self expectationWithDescription:@"Write succeeded via MTRCluster"]; + delegate.onAttributeDataReceived = ^(NSArray *> * data) { + XCTAssertNotNil(data); + XCTAssertEqual(data.count, 1); + NSDictionary * item = data[0]; + + XCTAssertNil(item[MTRErrorKey]); + + MTRAttributePath * path = item[MTRAttributePathKey]; + XCTAssertNotNil(path); + + XCTAssertEqualObjects(path, expectedAttributePath); + + NSDictionary * receivedValue = item[MTRDataKey]; + + // We can't use XCTAssertEqualObjects to compare receivedValue to + // expectedValue here, because receivedValue has a DataVersion + // that's missing from expectedValue, and the struct in it has an + // extra FabricIndex field. + XCTAssertEqualObjects(receivedValue[MTRTypeKey], MTRArrayValueType); + + NSArray * receivedArray = receivedValue[MTRValueKey]; + NSArray * expectedArray = expectedValue[MTRValueKey]; + + XCTAssertEqual(receivedArray.count, expectedArray.count); + + for (NSUInteger i = 0; i < receivedArray.count; ++i) { + NSDictionary * receivedItem = receivedArray[i][MTRDataKey]; + NSDictionary * expectedItem = expectedArray[i][MTRDataKey]; + + XCTAssertEqual(receivedItem[MTRTypeKey], MTRStructureValueType); + XCTAssertEqual(expectedItem[MTRTypeKey], MTRStructureValueType); + + NSArray * receivedFields = receivedItem[MTRValueKey]; + NSArray * expectedFields = expectedItem[MTRValueKey]; + + // Account for the extra FabricIndex. + XCTAssertEqual(receivedFields.count, expectedFields.count + 1); + for (NSUInteger j = 0; j < expectedFields.count; ++j) { + XCTAssertEqualObjects(receivedFields[j], expectedFields[j]); + } + } + + [writeExpectation fulfill]; + }; + + __auto_type * cluster = [[MTRClusterOTASoftwareUpdateRequestor alloc] initWithDevice:device + endpointID:@(0) + queue:queue]; + [cluster writeAttributeDefaultOTAProvidersWithValue:expectedValue + expectedValueInterval:@(0)]; + [self waitForExpectations:@[ writeExpectation ] timeout:kTimeoutInSeconds]; + + expectedValue = dataValue(2); + writeExpectation = [self expectationWithDescription:@"Write succeeded via MTRDevice"]; + [device writeAttributeWithEndpointID:@(0) + clusterID:@(MTRClusterIDTypeOTASoftwareUpdateRequestorID) + attributeID:@(MTRAttributeIDTypeClusterOTASoftwareUpdateRequestorAttributeDefaultOTAProvidersID) + value:expectedValue + expectedValueInterval:@(0) + timedWriteTimeout:nil]; + [self waitForExpectations:@[ writeExpectation ] timeout:kTimeoutInSeconds]; + } +} + - (void)test999_TearDown { [[self class] shutdownStack];