blob: 8b5b0f7c9dcf9767023fd6428bffd3aa3a69cf09 [file] [log] [blame]
/*
*
* Copyright (c) 2022 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.
*/
// module headers
#import <Matter/Matter.h>
#import "MTRErrorTestUtils.h"
#import "MTRTestKeys.h"
#import "MTRTestResetCommissioneeHelper.h"
#import "MTRTestStorage.h"
// system dependencies
#import <XCTest/XCTest.h>
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
static NSString * kOnboardingPayload1 = @"MT:-24J0SO527K10648G00"; // Discriminator: 1111
static NSString * kOnboardingPayload2 = @"MT:-24J0AFN00L10648G00"; // Discriminator: 1112
static const uint16_t kLocalPort = 5541;
static const uint16_t kTestVendorId = 0xFFF1u;
static const uint16_t kOTAProviderEndpointId = 0;
static MTRDevice * sConnectedDevice1;
static MTRDevice * sConnectedDevice2;
// Singleton controller we use.
static MTRDeviceController * sController = nil;
// Keys we can use to restart the controller.
static MTRTestKeys * sTestKeys = nil;
@interface MTROTAProviderTestControllerDelegate : NSObject <MTRDeviceControllerDelegate>
@property (nonatomic, readonly) XCTestExpectation * expectation;
@property (nonatomic, readonly) NSNumber * commissioneeNodeID;
@end
@implementation MTROTAProviderTestControllerDelegate
- (id)initWithExpectation:(XCTestExpectation *)expectation commissioneeNodeID:(NSNumber *)nodeID
{
self = [super init];
if (self) {
_expectation = expectation;
_commissioneeNodeID = nodeID;
}
return self;
}
- (void)controller:(MTRDeviceController *)controller commissioningSessionEstablishmentDone:(NSError * _Nullable)error
{
XCTAssertEqual(error.code, 0);
NSError * commissionError = nil;
[sController commissionNodeWithID:self.commissioneeNodeID
commissioningParams:[[MTRCommissioningParameters alloc] init]
error:&commissionError];
XCTAssertNil(commissionError);
// Keep waiting for onCommissioningComplete
}
- (void)controller:(MTRDeviceController *)controller commissioningComplete:(NSError *)error
{
XCTAssertEqual(error.code, 0);
[_expectation fulfill];
_expectation = nil;
}
@end
typedef void (^QueryImageCompletion)(
MTROTASoftwareUpdateProviderClusterQueryImageResponseParams * _Nullable data, NSError * _Nullable error);
typedef void (^ApplyUpdateRequestCompletion)(
MTROTASoftwareUpdateProviderClusterApplyUpdateResponseParams * _Nullable data, NSError * _Nullable error);
typedef void (^BlockQueryCompletion)(NSData * _Nullable data, BOOL isEOF);
typedef void (^QueryImageHandler)(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion);
typedef void (^ApplyUpdateRequestHandler)(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams * params, ApplyUpdateRequestCompletion completion);
typedef void (^NotifyUpdateAppliedHandler)(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams * params, MTRStatusCompletion completion);
typedef void (^BDXTransferBeginHandler)(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator,
NSNumber * offset, MTRStatusCompletion completion);
typedef void (^BDXQueryHandler)(NSNumber * nodeID, MTRDeviceController * controller, NSNumber * blockSize, NSNumber * blockIndex,
NSNumber * bytesToSkip, BlockQueryCompletion completion);
typedef void (^BDXTransferEndHandler)(NSNumber * nodeID, MTRDeviceController * controller, NSError * _Nullable error);
@interface MTROTAProviderDelegateImpl : NSObject <MTROTAProviderDelegate>
@property (nonatomic, nullable) QueryImageHandler queryImageHandler;
@property (nonatomic, nullable) ApplyUpdateRequestHandler applyUpdateRequestHandler;
@property (nonatomic, nullable) NotifyUpdateAppliedHandler notifyUpdateAppliedHandler;
@property (nonatomic, nullable) BDXTransferBeginHandler transferBeginHandler;
@property (nonatomic, nullable) BDXQueryHandler blockQueryHandler;
@property (nonatomic, nullable) BDXTransferEndHandler transferEndHandler;
@end
@implementation MTROTAProviderDelegateImpl
- (void)handleQueryImageForNodeID:(NSNumber *)nodeID
controller:(MTRDeviceController *)controller
params:(MTROTASoftwareUpdateProviderClusterQueryImageParams *)params
completion:(QueryImageCompletion)completion
{
XCTAssertEqual(controller, sController);
if (self.queryImageHandler) {
self.queryImageHandler(nodeID, controller, params, completion);
} else {
XCTFail(@"Unexpected attempt to query for an image");
[self respondNotAvailableWithCompletion:completion];
}
}
- (void)handleApplyUpdateRequestForNodeID:(NSNumber *)nodeID
controller:(MTRDeviceController *)controller
params:(MTROTASoftwareUpdateProviderClusterApplyUpdateRequestParams *)params
completion:(ApplyUpdateRequestCompletion)completion
{
XCTAssertEqual(controller, sController);
if (self.applyUpdateRequestHandler) {
self.applyUpdateRequestHandler(nodeID, controller, params, completion);
} else {
XCTFail(@"Unexpected attempt to apply an update");
[self respondWithErrorToApplyUpdateRequestWithCompletion:completion];
}
}
- (void)handleNotifyUpdateAppliedForNodeID:(NSNumber *)nodeID
controller:(MTRDeviceController *)controller
params:(MTROTASoftwareUpdateProviderClusterNotifyUpdateAppliedParams *)params
completion:(MTRStatusCompletion)completion
{
XCTAssertEqual(controller, sController);
if (self.notifyUpdateAppliedHandler) {
self.notifyUpdateAppliedHandler(nodeID, controller, params, completion);
} else {
XCTFail(@"Unexpected update application");
[self respondErrorWithCompletion:completion];
}
}
- (void)handleBDXTransferSessionBeginForNodeID:(NSNumber *)nodeID
controller:(MTRDeviceController *)controller
fileDesignator:(NSString *)fileDesignator
offset:(NSNumber *)offset
completion:(MTRStatusCompletion)completion
{
XCTAssertEqual(controller, sController);
if (self.transferBeginHandler) {
self.transferBeginHandler(nodeID, controller, fileDesignator, offset, completion);
} else {
XCTFail(@"Unexpected attempt to begin BDX transfer");
[self respondErrorWithCompletion:completion];
}
}
- (void)handleBDXQueryForNodeID:(NSNumber *)nodeID
controller:(MTRDeviceController *)controller
blockSize:(NSNumber *)blockSize
blockIndex:(NSNumber *)blockIndex
bytesToSkip:(NSNumber *)bytesToSkip
completion:(BlockQueryCompletion)completion
{
XCTAssertEqual(controller, sController);
if (self.blockQueryHandler) {
self.blockQueryHandler(nodeID, controller, blockSize, blockIndex, bytesToSkip, completion);
} else {
XCTFail(@"Unexpected attempt to get BDX block");
completion(nil, YES);
}
}
- (void)handleBDXTransferSessionEndForNodeID:(NSNumber *)nodeID
controller:(MTRDeviceController *)controller
error:(NSError * _Nullable)error
{
if (self.transferEndHandler) {
self.transferEndHandler(nodeID, controller, error);
} else {
XCTFail(@"Unexpected end of BDX transfer");
}
}
- (void)respondNotAvailableWithCompletion:(QueryImageCompletion)completion
{
__auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init];
responseParams.status = @(MTROTASoftwareUpdateProviderOTAQueryStatusNotAvailable);
completion(responseParams, nil);
}
- (void)respondBusyWithDelay:(NSNumber *)delay completion:(QueryImageCompletion)completion
{
__auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init];
responseParams.status = @(MTROTASoftwareUpdateProviderOTAQueryStatusBusy);
responseParams.delayedActionTime = delay;
completion(responseParams, nil);
}
- (void)respondAvailableWithDelay:(NSNumber *)delay
uri:(NSString *)uri
updateToken:(NSData *)updateToken
completion:(QueryImageCompletion)completion
{
__auto_type * responseParams = [[MTROTASoftwareUpdateProviderClusterQueryImageResponseParams alloc] init];
responseParams.status = @(MTROTASoftwareUpdateProviderOTAQueryStatusUpdateAvailable);
responseParams.delayedActionTime = delay;
responseParams.imageURI = uri;
// TODO: Figure out whether we need better
// SoftwareVersion/SoftwareVersionString/UpdateToken bits.
responseParams.softwareVersion = @(18);
responseParams.softwareVersionString = @"18";
responseParams.updateToken = updateToken;
completion(responseParams, nil);
}
- (void)respondWithErrorToApplyUpdateRequestWithCompletion:(ApplyUpdateRequestCompletion)completion
{
[self respondErrorWithCompletion:^(NSError * _Nullable error) {
completion(nil, error);
}];
}
- (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];
}
- (void)respondErrorWithCode:(MTRErrorCode)code completion:(MTRStatusCompletion)completion
{
[self respondError:[NSError errorWithDomain:MTRErrorDomain code:code userInfo:nil] completion:completion];
}
- (void)respondError:(NSError *)error completion:(MTRStatusCompletion)completion
{
completion(error);
}
- (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;
@interface MTROTAProviderTests : XCTestCase
@end
static BOOL sStackInitRan = NO;
static BOOL sNeedsStackShutdown = YES;
@implementation MTROTAProviderTests
+ (void)tearDown
{
// Global teardown, runs once
if (sNeedsStackShutdown) {
// We don't need to worry about ResetCommissionee. If we get here,
// we're running only one of our test methods (using
// -only-testing:MatterTests/MTROTAProviderTests/testMethodName), since
// we did not run test999_TearDown.
[self shutdownStack];
}
}
- (void)setUp
{
// Per-test setup, runs before each test.
[super setUp];
[self setContinueAfterFailure:NO];
if (sStackInitRan == NO) {
[self initStack];
}
XCTAssertNil(sOTAProviderDelegate.queryImageHandler);
XCTAssertNil(sOTAProviderDelegate.applyUpdateRequestHandler);
XCTAssertNil(sOTAProviderDelegate.notifyUpdateAppliedHandler);
XCTAssertNil(sOTAProviderDelegate.transferBeginHandler);
XCTAssertNil(sOTAProviderDelegate.blockQueryHandler);
XCTAssertNil(sOTAProviderDelegate.transferEndHandler);
}
- (void)tearDown
{
// Per-test teardown, runs after each test.
[super tearDown];
// Reset all handlers so tests don't interfere with each other.
sOTAProviderDelegate.queryImageHandler = nil;
sOTAProviderDelegate.applyUpdateRequestHandler = nil;
sOTAProviderDelegate.notifyUpdateAppliedHandler = nil;
sOTAProviderDelegate.transferBeginHandler = nil;
sOTAProviderDelegate.blockQueryHandler = nil;
sOTAProviderDelegate.transferEndHandler = nil;
}
- (MTRDevice *)commissionDeviceWithPayload:(NSString *)payloadString nodeID:(NSNumber *)nodeID
{
XCTestExpectation * expectation =
[self expectationWithDescription:[NSString stringWithFormat:@"Commissioning Complete for %@", nodeID]];
__auto_type * deviceControllerDelegate = [[MTROTAProviderTestControllerDelegate alloc] initWithExpectation:expectation
commissioneeNodeID:nodeID];
dispatch_queue_t callbackQueue = dispatch_queue_create("com.chip.device_controller_delegate", DISPATCH_QUEUE_SERIAL);
[sController setDeviceControllerDelegate:deviceControllerDelegate queue:callbackQueue];
NSError * error;
__auto_type * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:payloadString error:&error];
XCTAssertNotNil(payload);
XCTAssertNil(error);
[sController setupCommissioningSessionWithPayload:payload newNodeID:nodeID error:&error];
XCTAssertNil(error);
[self waitForExpectations:@[ expectation ] timeout:kPairingTimeoutInSeconds];
return [MTRDevice deviceWithNodeID:nodeID controller:sController];
}
- (void)initStack
{
sStackInitRan = YES;
__auto_type * factory = [MTRDeviceControllerFactory sharedInstance];
XCTAssertNotNil(factory);
__auto_type * storage = [[MTRTestStorage alloc] init];
sOTAProviderDelegate = [[MTROTAProviderDelegateImpl alloc] init];
__auto_type * factoryParams = [[MTRDeviceControllerFactoryParams alloc] initWithStorage:storage];
factoryParams.port = @(kLocalPort);
factoryParams.otaProviderDelegate = sOTAProviderDelegate;
factoryParams.shouldStartServer = YES;
BOOL ok = [factory startControllerFactory:factoryParams error:nil];
XCTAssertTrue(ok);
__auto_type * testKeys = [[MTRTestKeys alloc] init];
XCTAssertNotNil(testKeys);
sTestKeys = testKeys;
// Needs to match what startControllerOnExistingFabric calls elsewhere in
// this file do.
__auto_type * params = [[MTRDeviceControllerStartupParams alloc] initWithIPK:testKeys.ipk fabricID:@(1) nocSigner:testKeys];
params.vendorID = @(kTestVendorId);
MTRDeviceController * controller = [factory createControllerOnNewFabric:params error:nil];
XCTAssertNotNil(controller);
sController = controller;
sConnectedDevice1 = [self commissionDeviceWithPayload:kOnboardingPayload1 nodeID:@(kDeviceId1)];
sConnectedDevice2 = [self commissionDeviceWithPayload:kOnboardingPayload2 nodeID:@(kDeviceId2)];
}
+ (void)shutdownStack
{
sNeedsStackShutdown = NO;
MTRDeviceController * controller = sController;
XCTAssertNotNil(controller);
[controller shutdown];
XCTAssertFalse([controller isRunning]);
[[MTRDeviceControllerFactory sharedInstance] stopControllerFactory];
}
- (void)test000_SetUp
{
// Nothing to do here; our setUp method handled this already. This test
// just exists to make the setup not look like it's happening inside other
// tests.
}
- (XCTestExpectation *)announceProviderToDevice:(MTRDevice *)device
{
dispatch_queue_t queue = dispatch_get_main_queue();
XCTestExpectation * responseExpectation =
[self expectationWithDescription:[NSString stringWithFormat:@"AnnounceOTAProvider to %@ succeeded", device]];
__auto_type * params = [[MTROTASoftwareUpdateRequestorClusterAnnounceOTAProviderParams alloc] init];
params.providerNodeID = [sController controllerNodeID];
params.vendorID = @(kTestVendorId);
params.announcementReason = @(MTROTASoftwareUpdateRequestorOTAAnnouncementReasonSimpleAnnouncement);
params.endpoint = @(kOTAProviderEndpointId);
__auto_type * cluster = [[MTRClusterOTASoftwareUpdateRequestor alloc] initWithDevice:device endpointID:@(0) queue:queue];
[cluster announceOTAProviderWithParams:params
expectedValues:nil
expectedValueInterval:nil
completion:^(NSError * _Nullable error) {
XCTAssertNil(error);
[responseExpectation fulfill];
}];
return responseExpectation;
}
- (void)test001_ReceiveOTAQuery
{
// Test that if we advertise ourselves as a provider we end up getting a
// QueryImage callbacks that we can respond to.
__auto_type * device = sConnectedDevice1;
XCTestExpectation * queryExpectation = [self expectationWithDescription:@"handleQueryImageForNodeID called"];
sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
XCTAssertEqualObjects(nodeID, @(kDeviceId1));
XCTAssertEqual(controller, sController);
[sOTAProviderDelegate respondNotAvailableWithCompletion:completion];
[queryExpectation fulfill];
};
// Advertise ourselves as an OTA provider.
XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];
[self waitForExpectations:@[ queryExpectation, announceResponseExpectation ] timeout:kTimeoutInSeconds];
sOTAProviderDelegate.queryImageHandler = nil;
}
- (void)test002_ReceiveTwoQueriesExplicitBusy
{
// Test that if we advertise ourselves as a provider and respond BUSY to
// QueryImage callback, then we get a second QueryImage callback later on
// that we can then respond to however we wish.
__auto_type * device = sConnectedDevice1;
XCTestExpectation * queryExpectation1 = [self expectationWithDescription:@"handleQueryImageForNodeID called first time"];
XCTestExpectation * queryExpectation2 = [self expectationWithDescription:@"handleQueryImageForNodeID called second time"];
const uint16_t busyDelay = 1; // 1 second
__block QueryImageHandler handleSecondQuery;
sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
sOTAProviderDelegate.queryImageHandler = handleSecondQuery;
XCTAssertEqualObjects(nodeID, @(kDeviceId1));
XCTAssertEqual(controller, sController);
[sOTAProviderDelegate respondBusyWithDelay:@(busyDelay) completion:completion];
[queryExpectation1 fulfill];
};
handleSecondQuery = ^(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
XCTAssertEqualObjects(nodeID, @(kDeviceId1));
XCTAssertEqual(controller, sController);
[sOTAProviderDelegate respondNotAvailableWithCompletion:completion];
[queryExpectation2 fulfill];
};
// Advertise ourselves as an OTA provider.
XCTestExpectation * announceResponseExpectation = [self announceProviderToDevice:device];
// Make sure we get our queries in order. Give it a bit more time, because
// there will be a delay between the two queries.
[self waitForExpectations:@[ queryExpectation1, queryExpectation2 ] timeout:(kTimeoutInSeconds + busyDelay) enforceOrder:YES];
[self waitForExpectations:@[ announceResponseExpectation ] timeout:kTimeoutInSeconds];
}
- (void)test003_ReceiveSecondQueryWhileHandlingBDX
{
// In this test we do the following:
//
// 1) Advertise ourselves to device1.
// 2) When device1 queries for an image, claim to have one.
// 3) When device1 tries to start a bdx transfer, stall it and advertise to device2.
// 4) When device2 queries for an image, claim to have one. Since we are
// in the middle of doing BDX with device1, this actually responds with Busy.
// 5) Error out of the device1 transfer.
// 6) Wait for device2 to query us again.
__auto_type * device1 = sConnectedDevice1;
__auto_type * device2 = sConnectedDevice2;
__block XCTestExpectation * announceResponseExpectation2;
XCTestExpectation * queryExpectation1 = [self expectationWithDescription:@"handleQueryImageForNodeID called first time"];
XCTestExpectation * queryExpectation2 = [self expectationWithDescription:@"handleQueryImageForNodeID called second time"];
XCTestExpectation * queryExpectation3 = [self expectationWithDescription:@"handleQueryImageForNodeID called third time"];
const uint16_t busyDelay = 1; // 1 second
NSString * fakeImageURI = @"No such image, really";
__block QueryImageHandler handleThirdQuery;
sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
XCTAssertEqualObjects(nodeID, @(kDeviceId1));
XCTAssertEqual(controller, sController);
[sOTAProviderDelegate respondAvailableWithDelay:@(0)
uri:fakeImageURI
updateToken:[sOTAProviderDelegate generateUpdateToken]
completion:completion];
[queryExpectation1 fulfill];
};
sOTAProviderDelegate.transferBeginHandler = ^(NSNumber * nodeID, MTRDeviceController * controller, NSString * fileDesignator,
NSNumber * offset, MTRStatusCompletion outerCompletion) {
XCTAssertEqualObjects(nodeID, @(kDeviceId1));
XCTAssertEqual(controller, sController);
// Don't actually respond until the second requestor has queried us for
// an image. We need to reset queryImageHandler here, so we can close
// over outerCompletion.
sOTAProviderDelegate.queryImageHandler = ^(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion innerCompletion) {
sOTAProviderDelegate.queryImageHandler = handleThirdQuery;
sOTAProviderDelegate.transferBeginHandler = nil;
XCTAssertEqualObjects(nodeID, @(kDeviceId2));
XCTAssertEqual(controller, sController);
// 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
updateToken:[sOTAProviderDelegate generateUpdateToken]
completion:innerCompletion];
[sOTAProviderDelegate respondErrorWithCompletion:outerCompletion];
[queryExpectation2 fulfill];
};
announceResponseExpectation2 = [self announceProviderToDevice:device2];
};
handleThirdQuery = ^(NSNumber * nodeID, MTRDeviceController * controller,
MTROTASoftwareUpdateProviderClusterQueryImageParams * params, QueryImageCompletion completion) {
XCTAssertEqualObjects(nodeID, @(kDeviceId2));
XCTAssertEqual(controller, sController);
[sOTAProviderDelegate respondNotAvailableWithCompletion:completion];
[queryExpectation3 fulfill];
};
// Advertise ourselves as an OTA provider.
XCTestExpectation * announceResponseExpectation1 = [self announceProviderToDevice:device1];
// Make sure we get our queries in order. Give it a bit more time, because
// there will be a delay between the two queries.
[self waitForExpectations:@[ queryExpectation1, queryExpectation2, queryExpectation3 ]
timeout:(kTimeoutInSeconds + busyDelay * 3)
enforceOrder:YES];
[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];
ResetCommissionee(device, dispatch_get_main_queue(), self, kTimeoutInSeconds);
device = [MTRBaseDevice deviceWithNodeID:@(kDeviceId2) controller:sController];
ResetCommissionee(device, dispatch_get_main_queue(), self, kTimeoutInSeconds);
[[self class] shutdownStack];
}
@end