From 4923b715d195eadaa2afa08ab32da96214ee8dae Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Fri, 14 Jul 2023 14:45:05 -0400 Subject: [PATCH] Add Darwin API for starting multiple controllers on the same fabric. Fixes https://github.com/project-chip/connectedhomeip/issues/27394 --- .../Framework/CHIP/MTRDeviceController.mm | 1 + .../CHIP/MTRDeviceControllerFactory.h | 16 ++ .../CHIP/MTRDeviceControllerFactory.mm | 86 ++++++----- .../CHIP/MTRDeviceControllerStartupParams.mm | 31 ++-- ...TRDeviceControllerStartupParams_Internal.h | 27 ++-- .../Framework/CHIPTests/MTRControllerTests.m | 114 +++++++++++++- .../Framework/CHIPTests/MTRDeviceTests.m | 144 ++++++++++++++++++ 7 files changed, 353 insertions(+), 66 deletions(-) diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm index 8b34769b09ecea..bc1d2e6718db0a 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceController.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm @@ -374,6 +374,7 @@ - (BOOL)startup:(MTRDeviceControllerStartupParamsInternal *)startupParams // bring-up. commissionerParams.removeFromFabricTableOnShutdown = false; commissionerParams.deviceAttestationVerifier = _factory.deviceAttestationVerifier; + commissionerParams.permitMultiControllerFabrics = startupParams.isAdditionalController; auto & factory = chip::Controller::DeviceControllerFactory::GetInstance(); diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h index 0423aeae585c70..c2edb3d3583c98 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.h @@ -149,6 +149,22 @@ API_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)) - (MTRDeviceController * _Nullable)createControllerOnExistingFabric:(MTRDeviceControllerStartupParams *)startupParams error:(NSError * __autoreleasing *)error; +/** + * Create an additional MTRDeviceController on an existing fabric. Returns nil on failure. + * + * The fabric is identified by the root public key and fabric id in + * the startupParams. + * + * This method will fail if there is no such fabric. + * + * This method will fail if the MTRDeviceControllerStartupParams don't follow + * the rules for creating a controller on a new fabric. In particular, the + * vendor ID must not be nil. + */ +- (MTRDeviceController * _Nullable)createAdditionalControllerOnExistingFabric:(MTRDeviceControllerStartupParams *)startupParams + error:(NSError * __autoreleasing *)error + MTR_NEWLY_AVAILABLE; + /** * Create a MTRDeviceController on a new fabric. Returns nil on failure. * diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm index d7fb8e7b119cf0..a75741b2f5a274 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm @@ -619,8 +619,21 @@ - (MTRDeviceController * _Nullable)_startDeviceController:(MTRDeviceControllerSt return controller; } +- (MTRDeviceController * _Nullable)createAdditionalControllerOnExistingFabric:(MTRDeviceControllerStartupParams *)startupParams + error:(NSError * __autoreleasing *)error +{ + return [self _createControllerOnExistingFabric:startupParams isAdditional:YES error:error]; +} + - (MTRDeviceController * _Nullable)createControllerOnExistingFabric:(MTRDeviceControllerStartupParams *)startupParams error:(NSError * __autoreleasing *)error +{ + return [self _createControllerOnExistingFabric:startupParams isAdditional:NO error:error]; +} + +- (MTRDeviceController * _Nullable)_createControllerOnExistingFabric:(MTRDeviceControllerStartupParams *)startupParams + isAdditional:(BOOL)isAdditional + error:(NSError * __autoreleasing *)error { [self _assertCurrentQueueIsNotMatterQueue]; @@ -643,28 +656,37 @@ - (MTRDeviceController * _Nullable)createControllerOnExistingFabric:(MTRDeviceCo auto * controllersCopy = [self getRunningControllers]; - for (MTRDeviceController * existing in controllersCopy) { - BOOL isRunning = YES; // assume the worst - if ([existing isRunningOnFabric:fabricTable fabricIndex:fabric->GetFabricIndex() isRunning:&isRunning] - != CHIP_NO_ERROR) { - MTR_LOG_ERROR("Can't tell what fabric a controller is running on. Not safe to start."); - fabricError = CHIP_ERROR_INTERNAL; - return nil; + MTRDeviceControllerStartupParamsInternal * params; + if (!isAdditional) { + for (MTRDeviceController * existing in controllersCopy) { + BOOL isRunning = YES; // assume the worst + if ([existing isRunningOnFabric:fabricTable fabricIndex:fabric->GetFabricIndex() isRunning:&isRunning] + != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Can't tell what fabric a controller is running on. Not safe to start."); + fabricError = CHIP_ERROR_INTERNAL; + return nil; + } + + if (isRunning) { + MTR_LOG_ERROR("Can't start on existing fabric: another controller is running on it"); + fabricError = CHIP_ERROR_INCORRECT_STATE; + return nil; + } } - if (isRunning) { - MTR_LOG_ERROR("Can't start on existing fabric: another controller is running on it"); - fabricError = CHIP_ERROR_INCORRECT_STATE; - return nil; - } + params = + [[MTRDeviceControllerStartupParamsInternal alloc] _initForExistingFabricEntry:fabricTable + fabricIndex:fabric->GetFabricIndex() + keystore:self->_keystore + advertiseOperational:self.advertiseOperational + params:startupParams]; + } else { + params = [[MTRDeviceControllerStartupParamsInternal alloc] _initForNewFabricEntry:fabricTable + keystore:self->_keystore + advertiseOperational:self.advertiseOperational + params:startupParams + isAdditionalController:YES]; } - - auto * params = - [[MTRDeviceControllerStartupParamsInternal alloc] initForExistingFabric:fabricTable - fabricIndex:fabric->GetFabricIndex() - keystore:self->_keystore - advertiseOperational:self.advertiseOperational - params:startupParams]; if (params == nil) { fabricError = CHIP_ERROR_NO_MEMORY; } @@ -679,22 +701,6 @@ - (MTRDeviceController * _Nullable)createControllerOnNewFabric:(MTRDeviceControl { [self _assertCurrentQueueIsNotMatterQueue]; - if (startupParams.vendorID == nil) { - MTR_LOG_ERROR("Must provide vendor id when starting controller on new fabric"); - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]; - } - return nil; - } - - if (startupParams.intermediateCertificate != nil && startupParams.rootCertificate == nil) { - MTR_LOG_ERROR("Must provide a root certificate when using an intermediate certificate"); - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]; - } - return nil; - } - return [self _startDeviceController:startupParams fabricChecker:^MTRDeviceControllerStartupParamsInternal *(FabricTable * fabricTable, CHIP_ERROR & fabricError) { @@ -712,10 +718,12 @@ - (MTRDeviceController * _Nullable)createControllerOnNewFabric:(MTRDeviceControl return nil; } - auto * params = [[MTRDeviceControllerStartupParamsInternal alloc] initForNewFabric:fabricTable - keystore:self->_keystore - advertiseOperational:self.advertiseOperational - params:startupParams]; + auto * params = + [[MTRDeviceControllerStartupParamsInternal alloc] _initForNewFabricEntry:fabricTable + keystore:self->_keystore + advertiseOperational:self.advertiseOperational + params:startupParams + isAdditionalController:NO]; if (params == nil) { fabricError = CHIP_ERROR_NO_MEMORY; } diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm index 771995bf01bc66..ade519fc78fa63 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams.mm @@ -211,15 +211,26 @@ - (instancetype)initWithParams:(MTRDeviceControllerStartupParams *)params return self; } -- (instancetype)initForNewFabric:(chip::FabricTable *)fabricTable - keystore:(chip::Crypto::OperationalKeystore *)keystore - advertiseOperational:(BOOL)advertiseOperational - params:(MTRDeviceControllerStartupParams *)params +- (instancetype)_initForNewFabricEntry:(chip::FabricTable *)fabricTable + keystore:(chip::Crypto::OperationalKeystore *)keystore + advertiseOperational:(BOOL)advertiseOperational + params:(MTRDeviceControllerStartupParams *)params + isAdditionalController:(BOOL)isAdditionalController { if (!(self = [self initWithParams:params])) { return nil; } + if (self.vendorID == nil) { + MTR_LOG_ERROR("Must provide vendor id when starting controller on new fabric"); + return nil; + } + + if (self.intermediateCertificate != nil && self.rootCertificate == nil) { + MTR_LOG_ERROR("Must provide a root certificate when using an intermediate certificate"); + return nil; + } + if (self.nocSigner == nil && self.operationalCertificate == nil) { MTR_LOG_ERROR("No way to get an operational certificate: nocSigner and operationalCertificate are both nil"); return nil; @@ -248,15 +259,16 @@ - (instancetype)initForNewFabric:(chip::FabricTable *)fabricTable _fabricTable = fabricTable; _keystore = keystore; _advertiseOperational = advertiseOperational; + _isAdditionalController = isAdditionalController; return self; } -- (instancetype)initForExistingFabric:(FabricTable *)fabricTable - fabricIndex:(FabricIndex)fabricIndex - keystore:(chip::Crypto::OperationalKeystore *)keystore - advertiseOperational:(BOOL)advertiseOperational - params:(MTRDeviceControllerStartupParams *)params +- (instancetype)_initForExistingFabricEntry:(FabricTable *)fabricTable + fabricIndex:(FabricIndex)fabricIndex + keystore:(chip::Crypto::OperationalKeystore *)keystore + advertiseOperational:(BOOL)advertiseOperational + params:(MTRDeviceControllerStartupParams *)params { if (!(self = [self initWithParams:params])) { return nil; @@ -384,6 +396,7 @@ - (instancetype)initForExistingFabric:(FabricTable *)fabricTable _fabricIndex.Emplace(fabricIndex); _keystore = keystore; _advertiseOperational = advertiseOperational; + _isAdditionalController = NO; return self; } diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h index 24acc7a2dbb3a8..5802c200f82be2 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDeviceControllerStartupParams_Internal.h @@ -58,6 +58,8 @@ MTR_HIDDEN @property (nonatomic, assign, readonly) BOOL advertiseOperational; +@property (nonatomic, assign, readonly) BOOL isAdditionalController; + /** * Helper method that checks that our keypairs match our certificates. * Specifically: @@ -73,21 +75,24 @@ MTR_HIDDEN - (BOOL)keypairsMatchCertificates; /** - * Initialize for controller bringup on a new fabric. + * Initialize for controller bringup on a new fabric entry (which might be a new + * fabric or an additional controller on an existing fabric). */ -- (instancetype)initForNewFabric:(chip::FabricTable *)fabricTable - keystore:(chip::Crypto::OperationalKeystore *)keystore - advertiseOperational:(BOOL)advertiseOperational - params:(MTRDeviceControllerStartupParams *)params; +- (instancetype)_initForNewFabricEntry:(chip::FabricTable *)fabricTable + keystore:(chip::Crypto::OperationalKeystore *)keystore + advertiseOperational:(BOOL)advertiseOperational + params:(MTRDeviceControllerStartupParams *)params + isAdditionalController:(BOOL)isAdditionalController; /** - * Initialize for controller bringup on an existing fabric. + * Initialize for controller bringup on an existing fabric entry, identified by + * the provided fabricIndex. */ -- (instancetype)initForExistingFabric:(chip::FabricTable *)fabricTable - fabricIndex:(chip::FabricIndex)fabricIndex - keystore:(chip::Crypto::OperationalKeystore *)keystore - advertiseOperational:(BOOL)advertiseOperational - params:(MTRDeviceControllerStartupParams *)params; +- (instancetype)_initForExistingFabricEntry:(chip::FabricTable *)fabricTable + fabricIndex:(chip::FabricIndex)fabricIndex + keystore:(chip::Crypto::OperationalKeystore *)keystore + advertiseOperational:(BOOL)advertiseOperational + params:(MTRDeviceControllerStartupParams *)params; /** * Should use initForExistingFabric or initForNewFabric to initialize diff --git a/src/darwin/Framework/CHIPTests/MTRControllerTests.m b/src/darwin/Framework/CHIPTests/MTRControllerTests.m index e7a18bcb13804f..4d31e27ac67536 100644 --- a/src/darwin/Framework/CHIPTests/MTRControllerTests.m +++ b/src/darwin/Framework/CHIPTests/MTRControllerTests.m @@ -23,6 +23,7 @@ // system dependencies #import +#import "MTRFabricInfoChecker.h" #import "MTRTestKeys.h" #import "MTRTestOTAProvider.h" #import "MTRTestStorage.h" @@ -1524,8 +1525,6 @@ - (void)testControllerCATs [controller shutdown]; XCTAssertFalse([controller isRunning]); - fprintf(stderr, "DOING TOO LONG TEST\n"); - // // Trying to bring up the same fabric with too-long CATs should fail, if we // are taking the provided CATs into account. @@ -1536,8 +1535,6 @@ - (void)testControllerCATs controller = [factory createControllerOnExistingFabric:params error:nil]; XCTAssertNil(controller); - fprintf(stderr, "DOING INVALID TEST\n"); - // // Trying to bring up the same fabric with invalid CATs should fail, if we // are taking the provided CATs into account. @@ -1545,12 +1542,115 @@ - (void)testControllerCATs params.nodeID = @(17); params.operationalKeypair = operationalKeys; params.caseAuthenticatedTags = invalidCATs; - fprintf(stderr, "BRINGING UP CONTROLLER\n"); controller = [factory createControllerOnExistingFabric:params error:nil]; - fprintf(stderr, "CONTROLLER SHOULD BE NIL\n"); XCTAssertNil(controller); - fprintf(stderr, "STOPPING FACTORY\n"); + [factory stopControllerFactory]; + XCTAssertFalse([factory isRunning]); +} + +- (void)testAdditionalController +{ + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * storage = [[MTRTestStorage alloc] init]; + __auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage]; + XCTAssertTrue([factory startControllerFactory:factoryParams error:nil]); + XCTAssertTrue([factory isRunning]); + + __auto_type * rootKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(rootKeys); + + __auto_type * params1 = [[MTRDeviceControllerStartupParams alloc] initWithIPK:rootKeys.ipk fabricID:@(1) nocSigner:rootKeys]; + XCTAssertNotNil(params1); + + params1.vendorID = @(kTestVendorId); + + NSError * error; + + // Start a main controller with node id 0x10001. + NSNumber * nodeID1 = @(0x10001); + params1.nodeID = nodeID1; + MTRDeviceController * controller1 = [factory createControllerOnNewFabric:params1 error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller1); + XCTAssertTrue([controller1 isRunning]); + XCTAssertEqualObjects(controller1.controllerNodeID, nodeID1); + + // Start an additional controller with node id 0x10002. + NSNumber * nodeID2 = @(0x10002); + params1.nodeID = nodeID2; + MTRDeviceController * controller2 = [factory createAdditionalControllerOnExistingFabric:params1 error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller2); + XCTAssertTrue([controller2 isRunning]); + XCTAssertEqualObjects(controller2.controllerNodeID, nodeID2); + + // Start an additional controller with node id 0x10003, using explicit + // certificates. + NSNumber * nodeID3 = @(0x10003); + __auto_type * root = [MTRCertificates createRootCertificate:rootKeys issuerID:nil fabricID:nil error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(root); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + __auto_type * operational = [MTRCertificates createOperationalCertificate:rootKeys + signingCertificate:root + operationalPublicKey:operationalKeys.publicKey + fabricID:@(1) + nodeID:nodeID3 + caseAuthenticatedTags:nil + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(operational); + + __auto_type * params2 = [[MTRDeviceControllerStartupParams alloc] initWithIPK:rootKeys.ipk + operationalKeypair:operationalKeys + operationalCertificate:operational + intermediateCertificate:nil + rootCertificate:root]; + params2.vendorID = @(kTestVendorId); + + MTRDeviceController * controller3 = [factory createAdditionalControllerOnExistingFabric:params2 error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(controller3); + XCTAssertTrue([controller3 isRunning]); + XCTAssertEqualObjects(controller3.controllerNodeID, nodeID3); + + __auto_type fabrics = factory.knownFabrics; + CheckFabricInfo(fabrics, [NSMutableSet setWithArray:@[ + @{ + @"rootPublicKey" : [rootKeys publicKeyData], + @"vendorID" : @(kTestVendorId), + @"fabricID" : @(1), + @"nodeID" : nodeID1, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(1) + }, + @{ + @"rootPublicKey" : [rootKeys publicKeyData], + @"vendorID" : @(kTestVendorId), + @"fabricID" : @(1), + @"nodeID" : nodeID2, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(2) + }, + @{ + @"rootPublicKey" : [rootKeys publicKeyData], + @"vendorID" : @(kTestVendorId), + @"fabricID" : @(1), + @"nodeID" : nodeID3, + @"label" : @"", + @"hasIntermediateCertificate" : @(NO), + @"fabricIndex" : @(3) + } + ]]); + [factory stopControllerFactory]; XCTAssertFalse([factory isRunning]); } diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 0d9b693296bc2d..fbcc6df34aa921 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -2382,6 +2382,150 @@ - (void)test026_LocationAttribute [self waitForExpectations:@[ expectation ] timeout:kTimeoutInSeconds]; } +- (void)test027_AdditionalController +{ + // Bring up a second controller with a different node id. + __auto_type * factory = [MTRDeviceControllerFactory sharedInstance]; + XCTAssertNotNil(factory); + + __auto_type * fabrics = factory.knownFabrics; + XCTAssertNotNil(fabrics); + XCTAssertEqual(fabrics.count, 1); + + __auto_type * root = fabrics[0].rootCertificate; + XCTAssertNotNil(root); + + __auto_type * operationalKeys = [[MTRTestKeys alloc] init]; + XCTAssertNotNil(operationalKeys); + + NSError * error; + NSNumber * newNodeID = @(0x123123); + __auto_type * operational = [MTRCertificates createOperationalCertificate:sTestKeys + signingCertificate:root + operationalPublicKey:operationalKeys.publicKey + fabricID:@(1) + nodeID:newNodeID + caseAuthenticatedTags:nil + error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(operational); + + __auto_type * params = [[MTRDeviceControllerStartupParams alloc] initWithIPK:sTestKeys.ipk + operationalKeypair:operationalKeys + operationalCertificate:operational + intermediateCertificate:nil + rootCertificate:root]; + XCTAssertNotNil(params); + + params.vendorID = @(kTestVendorId); + + MTRDeviceController * newController = [factory createAdditionalControllerOnExistingFabric:params error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(newController); + XCTAssertTrue([newController isRunning]); + + XCTAssertEqualObjects([newController controllerNodeID], newNodeID); + XCTAssertNotEqualObjects([sController controllerNodeID], newNodeID); + + __auto_type * device1 = GetConnectedDevice(); + __auto_type * device2 = [MTRBaseDevice deviceWithNodeID:@(kDeviceId) controller:newController]; + + dispatch_queue_t queue = dispatch_get_main_queue(); + __auto_type * onOff1 = [[MTRBaseClusterOnOff alloc] initWithDevice:device1 endpointID:@(1) queue:queue]; + __auto_type * onOff2 = [[MTRBaseClusterOnOff alloc] initWithDevice:device2 endpointID:@(1) queue:queue]; + + // Check that device1 can read the On/Off attribute + XCTestExpectation * canReadExpectation1 = [self expectationWithDescription:@"Initial commissioner can read on/off"]; + [onOff1 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(err); + XCTAssertEqualObjects(value, @(0)); + [canReadExpectation1 fulfill]; + }]; + + [self waitForExpectations:@[ canReadExpectation1 ] timeout:kTimeoutInSeconds]; + + // Check that device2 cannot read the On/Off attribute due to missing ACLs. + XCTestExpectation * cantReadExpectation1 = [self expectationWithDescription:@"New node can't read on/off yet"]; + [onOff2 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(value); + XCTAssertNotNil(err); + XCTAssertEqual([MTRErrorTestUtils errorToZCLErrorCode:err], MTRInteractionErrorCodeUnsupportedAccess); + [cantReadExpectation1 fulfill]; + }]; + + [self waitForExpectations:@[ cantReadExpectation1 ] timeout:kTimeoutInSeconds]; + + // Now change ACLs so that device2 can read. + __auto_type * admin1 = [[MTRAccessControlClusterAccessControlEntryStruct alloc] init]; + admin1.privilege = @(MTRAccessControlEntryPrivilegeAdminister); + admin1.authMode = @(MTRAccessControlEntryAuthModeCASE); + admin1.subjects = @[ sController.controllerNodeID ]; + + __auto_type * admin2 = [[MTRAccessControlClusterAccessControlEntryStruct alloc] init]; + admin2.privilege = @(MTRAccessControlEntryPrivilegeAdminister); + admin2.authMode = @(MTRAccessControlEntryAuthModeCASE); + admin2.subjects = @[ newController.controllerNodeID ]; + + __auto_type * acl1 = [[MTRBaseClusterAccessControl alloc] initWithDevice:device1 endpointID:@(0) queue:queue]; + + XCTestExpectation * let2ReadExpectation = [self expectationWithDescription:@"ACLs changed so new node can read"]; + [acl1 writeAttributeACLWithValue:@[ admin1, admin2 ] + completion:^(NSError * _Nullable err) { + XCTAssertNil(err); + [let2ReadExpectation fulfill]; + }]; + + [self waitForExpectations:@[ let2ReadExpectation ] timeout:kTimeoutInSeconds]; + + // Check that device2 can read the On/Off attribute + XCTestExpectation * canReadExpectation2 = [self expectationWithDescription:@"New node can read on/off"]; + [onOff2 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(err); + XCTAssertEqualObjects(value, @(0)); + [canReadExpectation2 fulfill]; + }]; + + [self waitForExpectations:@[ canReadExpectation2 ] timeout:kTimeoutInSeconds]; + + // Check that device1 can still read the On/Off attribute + XCTestExpectation * canReadExpectation3 = [self expectationWithDescription:@"Initial commissioner can still read on/off"]; + [onOff1 readAttributeOnOffWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable err) { + XCTAssertNil(err); + XCTAssertEqualObjects(value, @(0)); + [canReadExpectation3 fulfill]; + }]; + + [self waitForExpectations:@[ canReadExpectation3 ] timeout:kTimeoutInSeconds]; + + // Check that the two devices are running on the same fabric. + __auto_type * opCreds1 = [[MTRBaseClusterOperationalCredentials alloc] initWithDevice:device1 endpoint:0 queue:queue]; + __auto_type * opCreds2 = [[MTRBaseClusterOperationalCredentials alloc] initWithDevice:device2 endpoint:0 queue:queue]; + + __block NSNumber * fabricIndex; + XCTestExpectation * readFabricIndexExpectation1 = + [self expectationWithDescription:@"Fabric index read by initial commissioner"]; + [opCreds1 readAttributeCurrentFabricIndexWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable readError) { + XCTAssertNil(readError); + XCTAssertNotNil(value); + fabricIndex = value; + [readFabricIndexExpectation1 fulfill]; + }]; + + [self waitForExpectations:@[ readFabricIndexExpectation1 ] timeout:kTimeoutInSeconds]; + + XCTestExpectation * readFabricIndexExpectation2 = [self expectationWithDescription:@"Fabric index read by new node"]; + [opCreds2 readAttributeCurrentFabricIndexWithCompletion:^(NSNumber * _Nullable value, NSError * _Nullable readError) { + XCTAssertNil(readError); + XCTAssertNotNil(value); + XCTAssertEqualObjects(value, fabricIndex); + [readFabricIndexExpectation2 fulfill]; + }]; + + [self waitForExpectations:@[ readFabricIndexExpectation2 ] timeout:kTimeoutInSeconds]; + + [newController shutdown]; +} + - (void)test900_SubscribeAllAttributes { MTRBaseDevice * device = GetConnectedDevice();