blob: f6e6c0ad422913580e44076d6f516df1c7616a6a [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@import LocalAuthentication;
@import XCTest;
@import local_auth_ios;
#import <OCMock/OCMock.h>
// Set a long timeout to avoid flake due to slow CI.
static const NSTimeInterval kTimeout = 30.0;
/**
* A context factory that returns preset contexts.
*/
@interface StubAuthContextFactory : NSObject <FLAAuthContextFactory>
@property(copy, nonatomic) NSMutableArray *contexts;
- (instancetype)initWithContexts:(NSArray *)contexts;
@end
@implementation StubAuthContextFactory
- (instancetype)initWithContexts:(NSArray *)contexts {
self = [super init];
if (self) {
_contexts = [contexts mutableCopy];
}
return self;
}
- (LAContext *)createAuthContext {
NSAssert(self.contexts.count > 0, @"Insufficient test contexts provided");
LAContext *context = [self.contexts firstObject];
[self.contexts removeObjectAtIndex:0];
return context;
}
@end
#pragma mark -
@interface FLTLocalAuthPluginTests : XCTestCase
@end
@implementation FLTLocalAuthPluginTests
- (void)setUp {
self.continueAfterFailure = NO;
}
- (void)testSuccessfullAuthWithBiometrics {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(YES, nil);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(YES),
@"localizedReason" : reason,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testSuccessfullAuthWithoutBiometrics {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(YES, nil);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testFailedAuthWithBiometrics {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(YES),
@"localizedReason" : reason,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[FlutterError class]]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testFailedWithUnknownErrorCode {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[FlutterError class]]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testSystemCancelledWithoutStickyAuth {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorSystemCancel userInfo:nil]);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
@"stickyAuth" : @(NO)
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertFalse([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testFailedAuthWithoutBiometrics {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[FlutterError class]]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testLocalizedFallbackTitle {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
NSString *localizedFallbackTitle = @"a title";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(YES, nil);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call =
[FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
@"localizedFallbackTitle" : localizedFallbackTitle,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
OCMVerify([mockAuthContext setLocalizedFallbackTitle:localizedFallbackTitle]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testSkippedLocalizedFallbackTitle {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
NSString *reason = @"a reason";
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
// a background thread.
void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) {
void (^reply)(BOOL, NSError *);
[invocation getArgument:&reply atIndex:4];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
reply(YES, nil);
});
};
OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:reason reply:[OCMArg any]])
.andDo(backgroundThreadReplyCaller);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"authenticate"
arguments:@{
@"biometricOnly" : @(NO),
@"localizedReason" : reason,
}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testDeviceSupportsBiometrics_withEnrolledHardware {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertTrue([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testDeviceSupportsBiometrics_withNoBiometricHardware {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"deviceSupportsBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSNumber class]]);
XCTAssertFalse([result boolValue]);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testGetEnrolledBiometrics_withFaceID {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"face");
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testGetEnrolledBiometrics_withTouchID {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 1);
XCTAssertEqualObjects(result[0], @"fingerprint");
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
- (void)testGetEnrolledBiometrics_withoutEnrolledHardware {
id mockAuthContext = OCMClassMock([LAContext class]);
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
initWithContextFactory:[[StubAuthContextFactory alloc]
initWithContexts:@[ mockAuthContext ]]];
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
// Write error
NSError *__autoreleasing *authError;
[invocation getArgument:&authError atIndex:3];
*authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil];
// Write return value
BOOL returnValue = NO;
NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)];
[invocation setReturnValue:&nsReturnValue];
};
OCMStub([mockAuthContext canEvaluatePolicy:policy
error:(NSError * __autoreleasing *)[OCMArg anyPointer]])
.andDo(canEvaluatePolicyHandler);
FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getEnrolledBiometrics"
arguments:@{}];
XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"];
[plugin handleMethodCall:call
result:^(id _Nullable result) {
XCTAssertTrue([NSThread isMainThread]);
XCTAssertTrue([result isKindOfClass:[NSArray class]]);
XCTAssertEqual([result count], 0);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:kTimeout handler:nil];
}
@end