SecEscrowRequestTests.m   [plain text]

#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import <CoreCDP/CDPError.h>
#import <CoreCDP/CDPStateController.h>
#import <CloudServices/CloudServices.h>

#import "keychain/categories/NSError+UsefulConstructors.h"
#import "keychain/escrowrequest/Framework/SecEscrowRequest.h"
#import "keychain/escrowrequest/EscrowRequestServer.h"

#import "keychain/escrowrequest/EscrowRequestServerHelpers.h"
#import "keychain/escrowrequest/operations/EscrowRequestInformCloudServicesOperation.h"
#import "keychain/escrowrequest/operations/EscrowRequestPerformEscrowEnrollOperation.h"
#import "keychain/escrowrequest/EscrowRequestServerHelpers.h"

#import "keychain/escrowrequest/tests/MockSynchronousEscrowServer.h"

#include "keychain/ckks/CKKS.h"
#include "keychain/ckks/CKKSLockStateTracker.h"
#include "keychain/securityd/SecItemServer.h"
#include "keychain/securityd/spi.h"
#include "utilities/SecFileLocations.h"

#include "tests/secdmockaks/mockaks.h"

@interface SecEscrowRequestTests : XCTestCase
@property SecEscrowRequest* escrowRequest;
@property EscrowRequestServer* escrowServer;
@property CKKSLockStateTracker* lockStateTracker;

@property id escrowRequestServerClassMock;
@property id escrowRequestInformCloudServicesOperationMock;
@property id escrowRequestPerformEscrowEnrollOperationMock;
@end

@implementation SecEscrowRequestTests

+ (void)setUp {
securityd_init_local_spi();
EscrowRequestServerSetEnabled(true);
}

- (void)setUp {
NSString* testName = [self.name componentsSeparatedByString:@" "][1];
testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
secnotice("secescrowtest", "Beginning test %@", testName);

[SecMockAKS unlockAllClasses];

// Make a new fake keychain
NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", testName, arc4random()];
[[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", tmp_dir] withIntermediateDirectories:YES attributes:nil error:NULL];

SecCKKSDisable();
SecCKKSTestDisableSOS();

// Mock out the SBD layer
self.escrowRequestServerClassMock = OCMClassMock([EscrowRequestServer class]);
self.escrowRequestInformCloudServicesOperationMock = OCMClassMock([EscrowRequestInformCloudServicesOperation class]);
self.escrowRequestPerformEscrowEnrollOperationMock = OCMClassMock([EscrowRequestPerformEscrowEnrollOperation class]);

self.lockStateTracker = [[CKKSLockStateTracker alloc] init];
self.escrowServer = [[EscrowRequestServer alloc] initWithLockStateTracker:self.lockStateTracker];

id mockConnection = OCMPartialMock([[NSXPCConnection alloc] init]);
OCMStub([mockConnection remoteObjectProxyWithErrorHandler:[OCMArg any]]).andCall(self, @selector(escrowServer));
OCMStub([mockConnection synchronousRemoteObjectProxyWithErrorHandler:[OCMArg any]]).andCall(self, @selector(synchronousEscrowServer));
self.escrowRequest = [[SecEscrowRequest alloc] initWithConnection:mockConnection];

SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
SecKeychainDbReset(NULL);

// Actually load the database.
kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
}

- (NSData* _Nullable)mockTriggerCloudServicesPasscodeRequest:(NSString*)uuid error:(NSError**)error
{
return [@"certdata" dataUsingEncoding:NSUTF8StringEncoding];
}

- (NSData* _Nullable)mockTriggerCloudServicesPasscodeRequestAndLockClassA:(NSString*)uuid error:(NSError**)error
{
[SecMockAKS lockClassA];
[self.lockStateTracker recheck];
return [@"certdata" dataUsingEncoding:NSUTF8StringEncoding];
}

- (NSData* _Nullable)mockFailTriggerCloudServicesPasscodeRequest:(NSString*)uuid error:(NSError**)error
{
if(error) {
*error = [NSError errorWithDomain:@"NSURLErrorDomain"
code:-1009
description:@"The internet connection appears to be offline (mock)"];
}
return nil;
}

- (void)mockcdpUploadPrerecord:(SecEscrowPendingRecord*)recordToSend
secretType:(CDPDeviceSecretType)secretType
reply:(void (^)(bool didUpdate, NSError* _Nullable error))reply
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
reply(YES, nil);
});
}

- (void)mockFailcdpUploadPrerecord:(SecEscrowPendingRecord*)recordToSend
secretType:(CDPDeviceSecretType)secretType
reply:(void (^)(bool didUpdate, NSError* _Nullable error))reply
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
reply(NO, [NSError errorWithDomain:@"EscrowServiceErrorDomain" code:-6570 description:@"mock CLUBH error"]);
});
}

- (void)mockcdpFailBadPeerUploadPrerecord:(SecEscrowPendingRecord*)recordToSend
secretType:(CDPDeviceSecretType)secretType
reply:(void (^)(bool didUpdate, NSError* _Nullable error))reply
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
reply(NO, [NSError errorWithDomain:kSecureBackupErrorDomain code:kSecureBackupInternalError description:@"SOS peer ID mismatch"]);
});
}

- (void)mockcdpFailNoSOSPeerUploadPrerecord:(SecEscrowPendingRecord*)recordToSend
secretType:(CDPDeviceSecretType)secretType
reply:(void (^)(bool didUpdate, NSError* _Nullable error))reply
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
reply(NO, [NSError errorWithDomain:kSecureBackupErrorDomain code:kSecureBackupNotInSyncCircleError description:@"SOS peer not present"]);
});
}

- (void)mockcdpFailNoCDPPeerUploadPrerecord:(SecEscrowPendingRecord*)recordToSend
secretType:(CDPDeviceSecretType)secretType
reply:(void (^)(bool didUpdate, NSError* _Nullable error))reply
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^ {
reply(NO, [NSError errorWithDomain:CDPStateErrorDomain code:CDPStateErrorNoPeerIdFound description:@"SOS peer not present"]);
});
}

- (id<EscrowRequestXPCProtocol>)synchronousEscrowServer
{
return [[MockSynchronousEscrowServer alloc] initWithServer:self.escrowServer];
}

- (void)tearDown {
[self.escrowServer.controller.stateMachine haltOperation];
}

- (void)allCloudServicesCallsSucceed {
OCMStub([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));
}

- (void)testStateMachineEntersNothingToDo
{
[self.escrowServer.controller.stateMachine startOperation];
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.stateConditions[EscrowRequestStateNothingToDo] wait:10*NSEC_PER_SEC], "State machine enters NothingToDo");
}

- (void)testTriggerUpdate {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

XCTAssertNil([self.escrowRequest fetchRequestWaitingOnPasscode:&error], @"Should be no pending updates");
XCTAssertNil(error, @"Should be no error fetching pending updates");

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should churn through");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");
}

- (void)testTriggerUpdateWhileLocked {
[self allCloudServicesCallsSucceed];

[SecMockAKS lockClassA];

NSError* error = nil;

XCTAssertNil([self.escrowRequest fetchRequestWaitingOnPasscode:&error], @"Should be no pending updates");
XCTAssertNil(error, @"Should be no error fetching pending updates");

XCTAssertFalse([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should not be able to trigger an update when locked");
XCTAssertNotNil(error, @"Should be an error triggering an escrow update (while locked)");
error = nil;

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should churn through");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNil(pendingUUID, "Should be no pending request UUID after a failed trigger");
}

- (void)testMultipleTriggers {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 1, @"Should be one in-flight status");
}

- (void)testFetchUpdateWhileLocked {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

XCTAssertNil([self.escrowRequest fetchRequestWaitingOnPasscode:&error], @"Should be no pending updates");
XCTAssertNil(error, @"Should be no error fetching pending updates");

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should churn through");
error = nil;

[SecMockAKS lockClassA];

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNotNil(error, @"Should be an error fetching a pending UUID while the device is locked");
XCTAssertNil(pendingUUID, "Should be no pending request UUID while device is locked");
}

- (void)testReaskCloudServicesWhenLocked {
// In this test, CloudServices is able to succeed (and cache a certificate), but we can't write down that the operation succeeded.
NSError* error = nil;

// No rate limiting for us!
self.escrowServer.controller.forceIgnoreCloudServicesRateLimiting = true;

// Pause the state machine for a bit...
[self.escrowServer.controller.stateMachine startOperation];
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause");
[self.escrowServer.controller.stateMachine haltOperation];

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

// First, let's ensure that we properly handle the case where the device is locked when we go to ask CloudServices about things

[SecMockAKS lockClassA];
[self.lockStateTracker recheck];
[self.escrowServer.controller.stateMachine startOperation];
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.stateConditions[EscrowRequestStateWaitForUnlock] wait:10*NSEC_PER_SEC], "State machine enters waitforunlock");

// Now, we should call CloudServices, but the device locks between CS success and us writing it down
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequestAndLockClassA:error:));
[SecMockAKS unlockAllClasses];
[self.lockStateTracker recheck];

OCMVerifyAllWithDelay(self.escrowRequestInformCloudServicesOperationMock, 10);
// and we should be back in wait for unlock:
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.stateConditions[EscrowRequestStateWaitForUnlock] wait:10*NSEC_PER_SEC], "State machine enters waitforunlock");

// Then, unlock one last time, and let everything succeed
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));
[SecMockAKS unlockAllClasses];
[self.lockStateTracker recheck];

OCMVerifyAllWithDelay(self.escrowRequestInformCloudServicesOperationMock, 10);
}

- (void)testRateLimitSBDTriggerWhenFailing {

// Trigger an escrow update, which should call CloudServices and fail
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockFailTriggerCloudServicesPasscodeRequest:error:));

NSError* error = nil;
XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

// And, the state machine should pause
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

// But, there should be no pending requests (as we need to retry CloudServices)
NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNil(pendingUUID, "Should be no pending request UUID after a failed trigger");

// But, the controller will recover at some point (for now, disable rate limiting)
[self allCloudServicesCallsSucceed];
self.escrowServer.controller.forceIgnoreCloudServicesRateLimiting = true;
[self.escrowServer.controller.stateMachine pokeStateMachine];

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after the state machine retries certificate caching");
}

- (void)testRateLimitSBDUploadWhenFailing {

// Trigger an escrow update, which should call CloudServices
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

NSError* error = nil;
XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAllWithDelay(self.escrowRequestInformCloudServicesOperationMock, 5);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockFailcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Wait for the upload to fail...
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

//////////////////////////
// Trigger another update, to check coalescing
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test2" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");
OCMVerifyAllWithDelay(self.escrowRequestInformCloudServicesOperationMock, 5);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

NSString* pendingUUIDTheSecond = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNotNil(pendingUUIDTheSecond, @"Should have a request waiting on passcode");
XCTAssertEqualObjects(pendingUUID, pendingUUIDTheSecond, @"In-flight request should have been restarted");
//////////////

[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockFailcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUIDTheSecond
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 1, @"Should be one in-flight status");

// But if escrow plays nice again, kick state machine, and make sure we don't touch escrow proxy
[[[[self.escrowRequestPerformEscrowEnrollOperationMock stub] andCall:@selector(mockcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

[self.escrowServer.controller.stateMachine startOperation];

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

/* item should still be in flight */
statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 1, @"Should be one in-flight status");
}

- (void)testPrerecordCaching {
NSError* error = nil;

// Trigger an escrow update, which should call CloudServices
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

// Now, there should be a pending request

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

// Caching a prerecord will attempt an upload via the state machine. But, since we're testing prerecord fetching, we don't want that to happen.
// So, let the upload fail.
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockFailcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Wait for the upload to fail...
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

NSString* nowPendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNil(nowPendingUUID, "Should be no pending request UUID after a prerecord cache");

NSData* d2 = [self.escrowRequest fetchPrerecord:pendingUUID
error:&error];
XCTAssertNotNil(d2, @"Should be able to retrieve a cached prerecord");
XCTAssertNil(error, @"Should be no error fetching a cached prerecord");

XCTAssertEqualObjects(d,d2, @"Cached prerecord should be equal to original");
}

- (void)testPrerecordCachingWhileLocked {
NSError* error = nil;

// Trigger an escrow update, which should call CloudServices
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");

// Now, there should be a pending request
NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

[SecMockAKS lockClassA];
// Caching a prerecord will fail, and should not invoke CDP.
// So, let the upload fail.
[[[self.escrowRequestPerformEscrowEnrollOperationMock reject] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertFalse([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be not able to cache a prerecord while the device is locked");
XCTAssertNotNil(error, @"Should be an error caching a prerecord while the device is locked");

// Ensure we don't invoke CDP
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should rest");
OCMVerifyAll(self.escrowRequestPerformEscrowEnrollOperationMock);

// And now, when we unlock, there's still a pending request
error = nil;
[SecMockAKS unlockAllClasses];
pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");
}

- (void)testEscrowUploadViaStateMachine {
// This test should call CloudServices, which will succeed. It should then tell CoreCDP to upload the record.
[self allCloudServicesCallsSucceed];
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

[self.escrowServer.controller.stateMachine startOperation];
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.stateConditions[EscrowRequestStateNothingToDo] wait:10*NSEC_PER_SEC], "State machine enters NothingToDo when started");

NSError* error = nil;
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));
XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");
OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Don't call the "do it" RPC, but it should happen anyway
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);
}

- (void)testEscrowUploadViaRPCAfterFailure {
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockFailcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSError* error = nil;

OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Now, the state machine should try and fail the upload
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

// And pause for a while
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

// But if escrow plays nice again, a 'try again' RPC should succeed
[[[[self.escrowRequestPerformEscrowEnrollOperationMock stub] andCall:@selector(mockcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

uint64_t records = [self.escrowRequest storePrerecordsInEscrow:&error];
XCTAssertNil(error, @"Should be no error storing prerecords in escrow");
XCTAssertEqual(records, 1, @"Should have stored one record in escrow");

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 1, @"Should be one in-flight status");
for(id key in statuses.keyEnumerator) {
XCTAssertEqualObjects(statuses[key], @"complete", "Record should be in 'complete' state");
}
}

- (void)testEscrowUploadViaStateMachineAfterFailureDueToLockState {
// This test should call CloudServices, which will succeed. It should then tell CDP to upload the record, which will fail due to lock state.
[self allCloudServicesCallsSucceed];
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

[self.escrowServer.controller.stateMachine startOperation];
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.stateConditions[EscrowRequestStateNothingToDo] wait:10*NSEC_PER_SEC], "State machine enters NothingToDo when started");

NSError* error = nil;
OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));
XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");
OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

// We need to lock directly after the prerecord is cached, but before the CDP attempt is attempted
[self.escrowServer.controller.stateMachine haltOperation];

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

[SecMockAKS lockClassA];
[self.escrowServer.controller.stateMachine startOperation];

// The state machine should notice the unlock, and try again
XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.stateConditions[EscrowRequestStateWaitForUnlock] wait:10*NSEC_PER_SEC], "State machine enters waitforunlock");

// The upload should be tried after an unlock
[SecMockAKS unlockAllClasses];
[self.lockStateTracker recheck];

OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);
}

- (void)testEscrowUploadBadPeerFailure {
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpFailBadPeerUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSError* error = nil;

OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Now, the state machine should try and fail the upload
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 0, @"Should be zero in-flight status");
}

- (void)testEscrowUploadNoSOSPeerFailure {
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpFailNoSOSPeerUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSError* error = nil;

OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Now, the state machine should try and fail the upload
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 0, @"Should be zero in-flight status");
}

- (void)testEscrowUploadNoCDPSOSPeerFailure {
[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpFailNoCDPPeerUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

NSError* error = nil;

OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Now, the state machine should try and fail the upload
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 0, @"Should be zero in-flight status");
}

- (void)testPendingPreRecordsCheck {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 1, @"Should be one in-flight status");

BOOL result = [self.escrowRequest pendingEscrowUpload:&error];
XCTAssertNil(error, @"Should be no error checking for pending uploads");
XCTAssertEqual(result, true, @"pendingEscrowUpload should return true");

}

- (void)testClearedPreRecordsCheck {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpFailNoCDPPeerUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Now, the state machine should try and fail the upload
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

NSDictionary<NSString*, NSString*>* statuses = [self.escrowRequest fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 0, @"Should be zero in-flight status");

BOOL result = [self.escrowRequest pendingEscrowUpload:&error];
XCTAssertNil(error, @"Should be no error checking for pending uploads");
XCTAssertEqual(result, false, @"pendingEscrowUpload should return true");
}

- (void)testServerPendingPreRecordsCheck {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

XCTAssertTrue([self.escrowServer triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertTrue([self.escrowServer triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

XCTAssertTrue([self.escrowServer triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

NSDictionary<NSString*, NSString*>* statuses = [self.escrowServer fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 1, @"Should be one in-flight status");

BOOL result = [self.escrowServer pendingEscrowUpload:&error];
XCTAssertNil(error, @"Should be no error checking for pending uploads");
XCTAssertEqual(result, true, @"pendingEscrowUpload should return true");

}

- (void)testServerClearedPreRecordsCheck {
[self allCloudServicesCallsSucceed];

NSError* error = nil;

[[[[self.escrowRequestPerformEscrowEnrollOperationMock expect] andCall:@selector(mockcdpFailNoCDPPeerUploadPrerecord:secretType:reply:)
onObject:self] ignoringNonObjectArgs]
cdpUploadPrerecord:[OCMArg any] secretType:0 reply:[OCMArg any]];

OCMExpect([self.escrowRequestInformCloudServicesOperationMock triggerCloudServicesPasscodeRequest:[OCMArg any] error:[OCMArg anyObjectRef]]).andCall(self, @selector(mockTriggerCloudServicesPasscodeRequest:error:));

XCTAssertTrue([self.escrowRequest triggerEscrowUpdate:@"test" error:&error], @"Should be able to trigger an update");
XCTAssertNil(error, @"Should be no error triggering an escrow update");

OCMVerifyAll(self.escrowRequestServerClassMock);

XCTAssertEqual(0, [self.escrowServer.controller.stateMachine.paused wait:10*NSEC_PER_SEC], @"State machine should pause within some time");

NSString* pendingUUID = [self.escrowRequest fetchRequestWaitingOnPasscode:&error];
XCTAssertNil(error, @"Should be no error fetching a pending UUID");
XCTAssertNotNil(pendingUUID, "Should be a pending request UUID after a trigger");

NSData* d = [[NSData alloc]initWithBase64EncodedString:@"YXNkZgo=" options:0];
XCTAssertTrue([self.escrowRequest cachePrerecord:pendingUUID
serializedPrerecord:d
error:&error], @"Should be able to cache a prerecord");
XCTAssertNil(error, @"Should be no error caching a prerecord");

// Now, the state machine should try and fail the upload
OCMVerifyAllWithDelay(self.escrowRequestPerformEscrowEnrollOperationMock, 10);

NSDictionary<NSString*, NSString*>* statuses = [self.escrowServer fetchStatuses:&error];
XCTAssertNotNil(statuses, @"Should be able to fetch statuses");
XCTAssertNil(error, @"Should be no error fetching statuses");

XCTAssertEqual(statuses.count, 0, @"Should be zero in-flight status");

BOOL result = [self.escrowServer pendingEscrowUpload:&error];
XCTAssertNil(error, @"Should be no error checking for pending uploads");
XCTAssertEqual(result, false, @"pendingEscrowUpload should return true");
}

@end