diff --git a/.github/workflows/darwin.yaml b/.github/workflows/darwin.yaml index 04d23b3f3c5075..6c82a17f224bf9 100644 --- a/.github/workflows/darwin.yaml +++ b/.github/workflows/darwin.yaml @@ -143,8 +143,8 @@ jobs: # all-clusters-app and from other requestors. # # And a different port from the test harness too; the test harness uses port 5541. - ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5542 --discriminator 1111 --KVS /tmp/chip-ota-requestor-kvs1 > >(tee /tmp/darwin/framework-tests/ota-requestor-app-1.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-1.log >&2) & - ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5543 --discriminator 1112 --KVS /tmp/chip-ota-requestor-kvs2 > >(tee /tmp/darwin/framework-tests/ota-requestor-app-2.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-2.log >&2) & + ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5542 --discriminator 1111 --KVS /tmp/chip-ota-requestor-kvs1 --otaDownloadPath /tmp/chip-ota-requestor-downloaded-image1 --autoApplyImage > >(tee /tmp/darwin/framework-tests/ota-requestor-app-1.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-1.log >&2) & + ../../../out/debug/chip-ota-requestor-app --interface-id -1 --secured-device-port 5543 --discriminator 1112 --KVS /tmp/chip-ota-requestor-kvs2 --otaDownloadPath /tmp/chip-ota-requestor-downloaded-image2 --autoApplyImage > >(tee /tmp/darwin/framework-tests/ota-requestor-app-2.log) 2> >(tee /tmp/darwin/framework-tests/ota-requestor-app-err-2.log >&2) & xcodebuild test -target "Matter" -scheme "Matter Framework Tests" -sdk macosx OTHER_CFLAGS='${inherited} -Werror -Wconversion -Wno-incomplete-umbrella -Wno-unguarded-availability-new' > >(tee /tmp/darwin/framework-tests/darwin-tests.log) 2> >(tee /tmp/darwin/framework-tests/darwin-tests-err.log >&2) working-directory: src/darwin/Framework - name: Uploading log files diff --git a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m index 901bf7ef06862e..8b5b0f7c9dcf97 100644 --- a/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m +++ b/src/darwin/Framework/CHIPTests/MTROTAProviderTests.m @@ -28,6 +28,7 @@ static const uint16_t kPairingTimeoutInSeconds = 10; static const uint16_t kTimeoutInSeconds = 3; +static const uint16_t kTimeoutWithUpdateInSeconds = 10; static const uint64_t kDeviceId1 = 0x12341234; static const uint64_t kDeviceId2 = 0x12341235; // NOTE: These onboarding payloads are for the chip-ota-requestor-app, not chip-all-clusters-app @@ -123,6 +124,7 @@ - (void)handleQueryImageForNodeID:(NSNumber *)nodeID if (self.queryImageHandler) { self.queryImageHandler(nodeID, controller, params, completion); } else { + XCTFail(@"Unexpected attempt to query for an image"); [self respondNotAvailableWithCompletion:completion]; } } @@ -137,6 +139,7 @@ - (void)handleApplyUpdateRequestForNodeID:(NSNumber *)nodeID if (self.applyUpdateRequestHandler) { self.applyUpdateRequestHandler(nodeID, controller, params, completion); } else { + XCTFail(@"Unexpected attempt to apply an update"); [self respondWithErrorToApplyUpdateRequestWithCompletion:completion]; } } @@ -151,6 +154,7 @@ - (void)handleNotifyUpdateAppliedForNodeID:(NSNumber *)nodeID if (self.notifyUpdateAppliedHandler) { self.notifyUpdateAppliedHandler(nodeID, controller, params, completion); } else { + XCTFail(@"Unexpected update application"); [self respondErrorWithCompletion:completion]; } } @@ -166,6 +170,7 @@ - (void)handleBDXTransferSessionBeginForNodeID:(NSNumber *)nodeID if (self.transferBeginHandler) { self.transferBeginHandler(nodeID, controller, fileDesignator, offset, completion); } else { + XCTFail(@"Unexpected attempt to begin BDX transfer"); [self respondErrorWithCompletion:completion]; } } @@ -182,6 +187,7 @@ - (void)handleBDXQueryForNodeID:(NSNumber *)nodeID if (self.blockQueryHandler) { self.blockQueryHandler(nodeID, controller, blockSize, blockIndex, bytesToSkip, completion); } else { + XCTFail(@"Unexpected attempt to get BDX block"); completion(nil, YES); } } @@ -192,6 +198,8 @@ - (void)handleBDXTransferSessionEndForNodeID:(NSNumber *)nodeID { if (self.transferEndHandler) { self.transferEndHandler(nodeID, controller, error); + } else { + XCTFail(@"Unexpected end of BDX transfer"); } } @@ -210,7 +218,10 @@ - (void)respondBusyWithDelay:(NSNumber *)delay completion:(QueryImageCompletion) completion(responseParams, nil); } -- (void)respondAvailableWithDelay:(NSNumber *)delay uri:(NSString *)uri completion:(QueryImageCompletion)completion +- (void)respondAvailableWithDelay:(NSNumber *)delay + uri:(NSString *)uri + updateToken:(NSData *)updateToken + completion:(QueryImageCompletion)completion { __auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init]; responseParams.status = @(MTROTASoftwareUpdateProviderOTAQueryStatusUpdateAvailable); @@ -220,8 +231,7 @@ - (void)respondAvailableWithDelay:(NSNumber *)delay uri:(NSString *)uri completi // SoftwareVersion/SoftwareVersionString/UpdateToken bits. responseParams.softwareVersion = @(18); responseParams.softwareVersionString = @"18"; - const char updateToken[] = "12345678"; - responseParams.updateToken = [NSData dataWithBytes:updateToken length:sizeof(updateToken)]; + responseParams.updateToken = updateToken; completion(responseParams, nil); } @@ -232,6 +242,14 @@ - (void)respondWithErrorToApplyUpdateRequestWithCompletion:(ApplyUpdateRequestCo }]; } +- (void)respondWithDiscontinueToApplyUpdateRequestWithCompletion:(ApplyUpdateRequestCompletion)completion +{ + __auto_type * params = [[MTROTASoftwareUpdateProviderClusterApplyUpdateResponseParams alloc] init]; + params.action = @(MTROTASoftwareUpdateProviderOTAApplyUpdateActionDiscontinue); + params.delayedActionTime = @(0); + completion(params, nil); +} + - (void)respondErrorWithCompletion:(MTRStatusCompletion)completion { [self respondErrorWithCode:MTRErrorCodeGeneralError completion:completion]; @@ -252,6 +270,19 @@ - (void)respondSuccess:(MTRStatusCompletion)completion completion(nil); } +- (NSData *)generateUpdateToken +{ + const size_t dataSize = 16; + const size_t randomBytesAtOnce = sizeof(uint32_t); + XCTAssertEqual(dataSize % randomBytesAtOnce, 0); + NSMutableData * data = [NSMutableData dataWithCapacity:16]; + for (unsigned i = 0; i < dataSize / randomBytesAtOnce; ++i) { + uint32_t randomBytes = arc4random(); + [data appendBytes:&randomBytes length:randomBytesAtOnce]; + } + return [NSData dataWithData:data]; +} + @end static MTROTAProviderDelegateImpl * sOTAProviderDelegate; @@ -502,7 +533,10 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) { XCTAssertEqualObjects(nodeID, @(kDeviceId1)); XCTAssertEqual(controller, sController); - [sOTAProviderDelegate respondAvailableWithDelay:@(0) uri:fakeImageURI completion:completion]; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) + uri:fakeImageURI + updateToken:[sOTAProviderDelegate generateUpdateToken] + completion:completion]; [queryExpectation1 fulfill]; }; sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator, @@ -516,11 +550,7 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion innerCompletion) { sOTAProviderDelegate.queryImageHandler = handleThirdQuery; - sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, - NSString * fileDesignator, NSNumber * offset, MTRStatusCompletion completion) { - // Should be no more queries. - XCTFail(@"Unexpected attempt to begin BDX transfer"); - }; + sOTAProviderDelegate.transferBeginHandler = nil; XCTAssertEqualObjects(nodeID, @(kDeviceId2)); XCTAssertEqual(controller, sController); @@ -528,7 +558,10 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX // We respond UpdateAvailable, but since we are in the middle of // handling OTA for device1 we expect the requestor to get Busy and // try again. - [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay) uri:fakeImageURI completion:innerCompletion]; + [sOTAProviderDelegate respondAvailableWithDelay:@(busyDelay) + uri:fakeImageURI + updateToken:[sOTAProviderDelegate generateUpdateToken] + completion:innerCompletion]; [sOTAProviderDelegate respondErrorWithCompletion:outerCompletion]; [queryExpectation2 fulfill]; }; @@ -557,6 +590,177 @@ - (void)test003_ReceiveSecondQueryWhileHandlingBDX [self waitForExpectations:@[ announceResponseExpectation1, announceResponseExpectation2 ] timeout:kTimeoutInSeconds]; } +- (void)test004_DoBDXTransferDenyUpdateRequest +{ + // In this test we do the following: + // + // 1) Create an actual image we can send to the device, with a valid header + // but garbage data. + // 2) Advertise ourselves to device. + // 3) When device queries for an image, claim to have one. + // 4) When device tries to start a bdx transfer, respond with success. + // 5) Send the data as the BDX transfer proceeds. + // 6) When device invokes ApplyUpdateRequest, respond with Discontinue so + // that the update does not actually proceed. + __auto_type * device = sConnectedDevice1; + + XCTestExpectation * queryExpectation = [self expectationWithDescription:@"handleQueryImageForNodeID called"]; + XCTestExpectation * bdxBeginExpectation = [self expectationWithDescription:@"handleBDXTransferSessionBeginForNodeID called"]; + XCTestExpectation * bdxQueryExpectation = [self expectationWithDescription:@"handleBDXQueryForNodeID called"]; + XCTestExpectation * bdxEndExpectation = [self expectationWithDescription:@"handleBDXTransferSessionEndForNodeID called"]; + XCTestExpectation * applyUpdateRequestExpectation = + [self expectationWithDescription:@"handleApplyUpdateRequestForNodeID called"]; + + NSData * updateToken = [sOTAProviderDelegate generateUpdateToken]; + + // First, create an image. Make it at least 4096 bytes long, so we get + // multiple BDX blocks going. + const size_t rawImageSize = 4112; + NSData * rawImagePiece = [@"1234567890abcdef" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertEqual(rawImageSize % rawImagePiece.length, 0); + NSMutableData * fakeImage = [NSMutableData dataWithCapacity:rawImageSize]; + while (fakeImage.length < rawImageSize) { + [fakeImage appendData:rawImagePiece]; + } + NSString * rawImagePath = @"/tmp/test004-raw-image"; + NSString * imagePath = @"/tmp/test004-image"; + + [[NSFileManager defaultManager] createFileAtPath:rawImagePath contents:fakeImage attributes:nil]; + + // Find the right absolute path to our ota_image_tool.py script. PWD should + // point to our src/darwin/Framework, while the script is in + // src/app/ota_image_tool.py. + NSString * pwd = [[NSProcessInfo processInfo] environment][@"PWD"]; + NSString * imageToolPath = [NSString + pathWithComponents:@[ [pwd substringToIndex:(pwd.length - @"darwin/Framework".length)], @"app", @"ota_image_tool.py" ]]; + + NSTask * task = [[NSTask alloc] init]; + [task setLaunchPath:imageToolPath]; + [task setArguments:@[ + @"create", @"-v", @"0xFFF1", @"-p", @"0x8001", @"-vn", @"2", @"-vs", @"2.0", @"-da", @"sha256", rawImagePath, imagePath + ]]; + NSError * launchError = nil; + [task launchAndReturnError:&launchError]; + XCTAssertNil(launchError); + [task waitUntilExit]; + XCTAssertEqual([task terminationStatus], 0); + + __block NSFileHandle * readHandle; + __block uint64_t imageSize; + __block uint32_t lastBlockIndex = UINT32_MAX; + + // TODO: Maybe we should move more of this logic into sOTAProviderDelegate + // or some other helper, once we have multiple tests sending images? For + // example, we could have something where you can do one of two things: + // + // 1) register a "raw image" with it, and it generates the + // image-with header. + // 2) register a pre-generated image with it and it uses "ota_image_tool.py + // extract" to extract the raw image. + // + // Once that's done the helper could track the transfer state for a + // particular image, etc, with us just forwarding our notifications to it. + sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + + sOTAProviderDelegate.queryImageHandler = nil; + [sOTAProviderDelegate respondAvailableWithDelay:@(0) uri:imagePath updateToken:updateToken completion:completion]; + [queryExpectation fulfill]; + }; + sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator, + NSNumber * offset, MTRStatusCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(fileDesignator, imagePath); + XCTAssertEqualObjects(offset, @(0)); + + readHandle = [NSFileHandle fileHandleForReadingAtPath:fileDesignator]; + XCTAssertNotNil(readHandle); + + NSError * endSeekError; + XCTAssertTrue([readHandle seekToEndReturningOffset:&imageSize error:&endSeekError]); + XCTAssertNil(endSeekError); + + sOTAProviderDelegate.transferBeginHandler = nil; + [sOTAProviderDelegate respondSuccess:completion]; + [bdxBeginExpectation fulfill]; + }; + sOTAProviderDelegate.blockQueryHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize, + NSNumber * blockIndex, NSNumber * bytesToSkip, BlockQueryCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(blockSize, @(1024)); // Seems to always be 1024. + XCTAssertEqualObjects(blockIndex, @(lastBlockIndex + 1)); + XCTAssertEqualObjects(bytesToSkip, @(0)); // Don't expect to see skips here. + // Make sure we actually end up with multiple blocks. + XCTAssertTrue(blockSize.unsignedLongLongValue < rawImageSize); + + XCTAssertNotNil(readHandle); + uint64_t offset = blockSize.unsignedLongLongValue * blockIndex.unsignedLongLongValue; + NSError * seekError = nil; + [readHandle seekToOffset:offset error:&seekError]; + XCTAssertNil(seekError); + + NSError * readError = nil; + NSData * data = [readHandle readDataUpToLength:blockSize.unsignedLongValue error:&readError]; + XCTAssertNil(readError); + XCTAssertNotNil(data); + + BOOL isEOF = offset + blockSize.unsignedLongValue >= imageSize; + + ++lastBlockIndex; + + if (isEOF) { + sOTAProviderDelegate.blockQueryHandler = nil; + } + + completion(data, isEOF); + + if (isEOF) { + [bdxQueryExpectation fulfill]; + } + }; + sOTAProviderDelegate.transferEndHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertNil(error); + + sOTAProviderDelegate.transferEndHandler = nil; + [bdxEndExpectation fulfill]; + }; + sOTAProviderDelegate.applyUpdateRequestHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, + MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion) { + XCTAssertEqualObjects(nodeID, @(kDeviceId1)); + XCTAssertEqual(controller, sController); + XCTAssertEqualObjects(params.updateToken, updateToken); + XCTAssertEqualObjects(params.newVersion, @(18)); // TODO: Factor this out better! + + XCTAssertTrue([[NSFileManager defaultManager] contentsEqualAtPath:rawImagePath + andPath:@"/tmp/chip-ota-requestor-downloaded-image1"]); + + sOTAProviderDelegate.applyUpdateRequestHandler = nil; + [sOTAProviderDelegate respondWithDiscontinueToApplyUpdateRequestWithCompletion:completion]; + [applyUpdateRequestExpectation fulfill]; + }; + + // Advertise ourselves as an OTA provider. + XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device]; + + // Make sure we get our callbacks in order. Give it a bit more time, because + // we want to allow time for the BDX download. + [self waitForExpectations:@[ queryExpectation, bdxBeginExpectation, bdxQueryExpectation ] + timeout:(kTimeoutWithUpdateInSeconds) enforceOrder:YES]; + + // Nothing really defines the ordering of bdxEndExpectation and + // applyUpdateRequestExpectation with respect to each other, and nothing + // defines the ordering of announceResponseExpectation with respect to _any_ + // of the above expectations. + [self waitForExpectations:@[ bdxEndExpectation, applyUpdateRequestExpectation, announceResponseExpectation ] + timeout:kTimeoutInSeconds]; +} + - (void)test999_TearDown { __auto_type * device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId1) controller:sController];