OTLockStateNetworkingTests.m   [plain text]


/*
* Copyright (c) 2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this
* file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_LICENSE_HEADER_END@
*/

#if OCTAGON

#import <Foundation/Foundation.h>

#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

#import "OTTestsBase.h"

static NSString* const testContextID = @"Foo";
static NSString* const testDSID = @"123456789";

static NSString* OTCKRecordBottledPeerType = @"OTBottledPeer";
/* Octagon Trust BottledPeerSchema  */
static NSString* OTCKRecordEscrowRecordID = @"escrowRecordID";
static NSString* OTCKRecordRecordID = @"bottledPeerRecordID";
static NSString* OTCKRecordSPID = @"spID";
static NSString* OTCKRecordEscrowSigningSPKI = @"escrowSigningSPKI";
static NSString* OTCKRecordPeerSigningSPKI = @"peerSigningSPKI";
static NSString* OTCKRecordEscrowSigningPubKey = @"escrowSigningPubKey";
static NSString* OTCKRecordPeerSigningPubKey = @"peerSigningPubKey";
static NSString* OTCKRecordSignatureFromEscrow = @"signatureUsingEscrow";
static NSString* OTCKRecordSignatureFromPeerKey = @"signatureUsingPeerKey";
static NSString* OTCKRecordBottle = @"bottle";

static NSString* OTCKRecordPeerID = @"peerID";

@interface OTLockStateNetworkingTests : OTTestsBase
@property (nonatomic, strong) OTBottledPeerRecord* fakeBottledPeerRecord;
@end

@implementation OTLockStateNetworkingTests

- (void)setUp {
    [super setUp];
    
    self.continueAfterFailure = NO;
    NSError* error = nil;
    
    OTBottledPeer *bp = [[OTBottledPeer alloc]initWithPeerID:self.egoPeerID spID:self.sosPeerID peerSigningKey:self.peerSigningKey peerEncryptionKey:self.peerEncryptionKey escrowKeys:self.escrowKeys error:&error];

    XCTAssertNotNil(bp, @"plaintext should not be nil");
    XCTAssertNil(error, @"error should be nil");
    XCTAssertNotNil(self.escrowKeys.signingKey, @"signing public key should not be nil");
    XCTAssertNotNil(self.escrowKeys.encryptionKey, @"encryption public key should not be nil");

    OTBottledPeerSigned *bpSigned = [[OTBottledPeerSigned alloc]initWithBottledPeer:bp escrowedSigningKey:self.escrowKeys.signingKey peerSigningKey:self.peerSigningKey error:&error];

    self.fakeBottledPeerRecord = [bpSigned asRecord:self.sosPeerID];

    [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.

}

- (void)tearDown {

    [super tearDown];
}

//Bottle Check tests

-(void) testGrabbingBottleLocallyCheckPerfectConditions
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];
    [self startCKKSSubsystem];

    __block NSData* localEntropy = nil;
    __block NSString* localBottleID = nil;

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer fired"];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       localEntropy = entropy;
                                       localBottleID = bottleID;
                                       XCTAssertNotNil(entropy, "entropy should not be nil");
                                       XCTAssertNotNil(bottleID, "bottle id should not be nil");
                                       XCTAssertNotNil(signingPublicKey, "signing pub key should not be nil");
                                       XCTAssertNil(error, "error should be nil");
                                   }];
    [self waitForExpectationsWithTimeout:1.0 handler:nil];

    self.spiBlockExpectation = [self expectationWithDescription:@"launch bottled peer fired"];

    NSMutableDictionary* recordDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:[[NSNumber alloc] initWithInt:1], OTCKRecordBottledPeerType, nil];

    [self expectAddedCKModifyRecords:recordDictionary holdFetch:NO];

    [self.otControl launchBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertNil(error, "error should be nil");
    }];

    [self waitForExpectationsWithTimeout:1.0 handler:nil];

    NSError* localError = nil;
    XCTAssertTrue([self.context doesThisDeviceHaveABottle:&localError] == BOTTLE, @"should have a bottle");
    XCTAssertNil(localError, "error should be nil");
}

-(void) testGrabbingBottleFromCloudKitCheckPerfectConditions
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    CKRecord* newRecord = [[CKRecord alloc]initWithRecordType:OTCKRecordBottledPeerType];
    newRecord[OTCKRecordPeerID] = self.fakeBottledPeerRecord.peerID;
    newRecord[OTCKRecordSPID] = @"spID";
    newRecord[OTCKRecordEscrowSigningSPKI] = self.fakeBottledPeerRecord.escrowedSigningSPKI;
    newRecord[OTCKRecordPeerSigningSPKI] = self.fakeBottledPeerRecord.peerSigningSPKI;
    newRecord[OTCKRecordEscrowRecordID] = self.fakeBottledPeerRecord.escrowRecordID;
    newRecord[OTCKRecordBottle] = self.fakeBottledPeerRecord.bottle;
    newRecord[OTCKRecordSignatureFromEscrow] = self.fakeBottledPeerRecord.signatureUsingEscrowKey;
    newRecord[OTCKRecordSignatureFromPeerKey] = self.fakeBottledPeerRecord.signatureUsingPeerKey;

    [self.otFakeZone addToZone:newRecord];

    [self startCKKSSubsystem];

    NSError* localError = nil;
    XCTAssertTrue([self.context doesThisDeviceHaveABottle:&localError] == BOTTLE, @"should have a bottle");
    XCTAssertNil(localError, "error should be nil");
}

-(void) testBottleCheckWhenLocked
{
    NSError* error = nil;
    self.aksLockState = true;
    [self.lockStateTracker recheck];
    [self setUpRampRecordsInCloudKitWithFeatureOff];

    XCTAssertTrue([self.context doesThisDeviceHaveABottle:&error] == UNCLEAR, @"bottle check should return unclear");

    XCTAssertNotNil(error, "error should not be nil");
    XCTAssertTrue(error.code == -25308, @"error should be interaction not allowed");
}

-(void) testBottleCheckWithNoNetwork
{
    NSError* error = nil;
    self.accountStatus = CKAccountStatusAvailable;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.reachabilityFlags = 0;
    [self.reachabilityTracker recheck];
    XCTAssertTrue([self.context doesThisDeviceHaveABottle:&error] == UNCLEAR, @"bottle check should return unclear");
    XCTAssertTrue(error.code == OTErrorNoNetwork, @"should have returned no network error");
}

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

    self.accountStatus = CKAccountStatusNoAccount;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    XCTAssertTrue([self.context doesThisDeviceHaveABottle:&error] == UNCLEAR, @"bottle check should return unclear");
    XCTAssertTrue(error.code == OTErrorNotSignedIn, @"should have returned not signed in error");
}


//Preflight tests
-(void)testPreflightNotSignedIn
{
    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer fired"];

    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusNoAccount;

    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "entropy should not be nil");
                                       XCTAssertNil(bottleID, "bottle id should not be nil");
                                       XCTAssertNil(signingPublicKey, "signing pub key should not be nil");
                                       XCTAssertTrue(error.code == OTErrorNotSignedIn, @"should have returned not signed in error");
                                   }];

    [self waitForExpectationsWithTimeout:1.0 handler:nil];

}

-(void) testPreflightWithNoNetwork
{
    self.accountStatus = CKAccountStatusAvailable;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.reachabilityFlags = 0;
    [self.reachabilityTracker recheck];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "entropy should not be nil");
                                       XCTAssertNil(bottleID, "bottle id should not be nil");
                                       XCTAssertNil(signingPublicKey, "signing pub key should not be nil");
                                       XCTAssertTrue(error.code == OTErrorNoNetwork, @"should have returned OTErrorNoNetwork in error");
                                   }];

}

-(void) testPreflightWhenLocked
{
    self.aksLockState = true;
    [self.lockStateTracker recheck];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "entropy should not be nil");
                                       XCTAssertNil(bottleID, "bottle id should not be nil");
                                       XCTAssertNil(signingPublicKey, "signing pub key should not be nil");
                                       XCTAssertTrue(error.code == errSecInteractionNotAllowed, @"should have returned errSecInteractionNotAllowed in error");
                                   }];
}

//Launch Bottle tests
-(void)testLaunchNotSignedIn
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusNoAccount;

    [self startCKKSSubsystem];

    [self.enroll.accountTracker notifyCKAccountStatusChangeAndWaitForSignal];
    [self.context.accountTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer fired"];

    [self.otControl preflightBottledPeer:OTDefaultContext
                                    dsid:@"dsid"
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "shouldn't return any entropy");
                                       XCTAssertNil(bottleID, "shouldn't return a bottle ID");
                                       XCTAssertNil(signingPublicKey, "shouldn't return a signingPublicKey");
                                       XCTAssertTrue(error.code == OTErrorNotSignedIn, "should return a OTErrorNotSignedIn error");
                                   }];
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];

    self.spiBlockExpectation = [self expectationWithDescription:@"launch SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    NSString* localBottleID = @"random bottle id";
    [self.otControl launchBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertTrue(error.code == OTErrorNotSignedIn, "should return a OTErrorNotSignedIn error");
    }];

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

-(void) testLaunchWithNoNetwork
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusAvailable;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.reachabilityFlags = 0;
    [self.reachabilityTracker recheck];

    [self startCKKSSubsystem];

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer fired"];

    [self.otControl preflightBottledPeer:OTDefaultContext
                                    dsid:@"dsid"
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "shouldn't return any entropy");
                                       XCTAssertNil(bottleID, "shouldn't return a bottle ID");
                                       XCTAssertNil(signingPublicKey, "shouldn't return a signingPublicKey");
                                       XCTAssertTrue(error.code == OTErrorNoNetwork, "should return a OTErrorNoNetwork error");
                                   }];
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];


    self.spiBlockExpectation = [self expectationWithDescription:@"launch SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    NSString* localBottleID = @"random bottle id";
    [self.otControl launchBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertTrue(error.code == OTErrorNoNetwork, "should return a OTErrorNoNetwork error");
    }];

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

-(void) testLaunchWhenLocked
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.aksLockState = true;
    [self.lockStateTracker recheck];

    [self startCKKSSubsystem];

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer fired"];

    [self.otControl preflightBottledPeer:OTDefaultContext
                                    dsid:@"dsid"
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "shouldn't return any entropy");
                                       XCTAssertNil(bottleID, "shouldn't return a bottle ID");
                                       XCTAssertNil(signingPublicKey, "shouldn't return a signingPublicKey");
                                       XCTAssertTrue(error.code == errSecInteractionNotAllowed, "should return a errSecInteractionNotAllowed error");
                                   }];
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];


    self.spiBlockExpectation = [self expectationWithDescription:@"launch SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    NSString* localBottleID = @"random bottle id";
    [self.otControl launchBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertTrue(error.code == errSecInteractionNotAllowed, "should return a errSecInteractionNotAllowed error");
    }];

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

//Scrub tests
-(void)testScrubNotSignedIn
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusNoAccount;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "entropy should be nil");
                                       XCTAssertNil(bottleID, "bottle id should be nil");
                                       XCTAssertNil(signingPublicKey, "signing pub key should be nil");
                                       XCTAssertTrue(error.code == OTErrorNotSignedIn, "should return a OTErrorNotSignedIn error");
                                   }];

    [self waitForExpectationsWithTimeout:1.0 handler:nil];

    __block NSString* localBottleID = @"random bottle id";
    self.spiBlockExpectation = [self expectationWithDescription:@"scrub bottled peer SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl scrubBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertTrue(error.code == OTErrorNotSignedIn, "should return a OTErrorNotSignedIn error");
    }];

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];

}

-(void) testScrubWithNoNetwork
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusAvailable;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.reachabilityFlags = 0;
    [self.reachabilityTracker recheck];

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "entropy should be nil");
                                       XCTAssertNil(bottleID, "bottle id should be nil");
                                       XCTAssertNil(signingPublicKey, "signing pub key should be nil");
                                       XCTAssertTrue(error.code == OTErrorNoNetwork, "should return a OTErrorNoNetwork error");
                                   }];

    [self waitForExpectationsWithTimeout:1.0 handler:nil];

    __block NSString* localBottleID = @"random bottle id";
    self.spiBlockExpectation = [self expectationWithDescription:@"scrub bottled peer SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl scrubBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertTrue(error.code == OTErrorNoNetwork, "should return a OTErrorNoNetwork error");
    }];

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

-(void) testScrubWhenLocked
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.aksLockState = true;
    [self.lockStateTracker recheck];

    self.spiBlockExpectation = [self expectationWithDescription:@"preflight bottled peer SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl preflightBottledPeer:testContextID
                                    dsid:testDSID
                                   reply:^(NSData * _Nullable entropy, NSString * _Nullable bottleID, NSData * _Nullable signingPublicKey, NSError * _Nullable error) {
                                       [self.spiBlockExpectation fulfill];
                                       XCTAssertNil(entropy, "entropy should be nil");
                                       XCTAssertNil(bottleID, "bottle id should be nil");
                                       XCTAssertNil(signingPublicKey, "signing pub key should be nil");
                                       XCTAssertTrue(error.code == errSecInteractionNotAllowed, "should return a errSecInteractionNotAllowed error");
                                   }];

    [self waitForExpectationsWithTimeout:1.0 handler:nil];

    __block NSString* localBottleID = @"random bottle id";
    self.spiBlockExpectation = [self expectationWithDescription:@"scrub bottled peer SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl scrubBottledPeer:testContextID bottleID:localBottleID reply:^(NSError * _Nullable error) {
        [self.spiBlockExpectation fulfill];
        XCTAssertTrue(error.code == errSecInteractionNotAllowed, "should return a errSecInteractionNotAllowed error");
    }];

    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

//Restore tests
-(void)testRestoreNotSignedIn
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusNoAccount;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.spiBlockExpectation = [self expectationWithDescription:@"restore SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl restore:testContextID
                       dsid:testDSID
                     secret:self.secret
             escrowRecordID:self.sosPeerID
                      reply:^(NSData* signingKeyData, NSData* encryptionKeyData, NSError* _Nullable error) {
                          [self.spiBlockExpectation fulfill];
                          XCTAssertNil(signingKeyData, "Signing key data should be nil");
                          XCTAssertNil(encryptionKeyData, "encryption key data should be nil");
                          XCTAssertTrue(error.code == OTErrorNotSignedIn, "should return a OTErrorNotSignedIn error");
                      }];
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];

}

-(void) testRestoreWithNoNetwork
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.accountStatus = CKAccountStatusAvailable;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.reachabilityFlags = 0;
    [self.reachabilityTracker recheck];

    self.spiBlockExpectation = [self expectationWithDescription:@"restore SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl restore:testContextID
                       dsid:testDSID
                     secret:self.secret
             escrowRecordID:self.sosPeerID
                      reply:^(NSData* signingKeyData, NSData* encryptionKeyData, NSError* _Nullable error) {
                          [self.spiBlockExpectation fulfill];
                          XCTAssertNil(signingKeyData, "Signing key data should be nil");
                          XCTAssertNil(encryptionKeyData, "encryption key data should be nil");
                          XCTAssertTrue(error.code == OTErrorNoNetwork, "should return a OTErrorNoNetwork error");
                      }];
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

-(void) testRestoreWhenLocked
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    self.aksLockState = true;
    [self.lockStateTracker recheck];

    self.spiBlockExpectation = [self expectationWithDescription:@"restore SPI fired"];
    self.expectation = [self expectationWithDescription:@"ramp scheduler fired"];

    [self.otControl restore:testContextID
                       dsid:testDSID
                     secret:self.secret
             escrowRecordID:self.sosPeerID
                      reply:^(NSData* signingKeyData, NSData* encryptionKeyData, NSError* _Nullable error) {
                          [self.spiBlockExpectation fulfill];
                          XCTAssertNil(signingKeyData, "Signing key data should be nil");
                          XCTAssertNil(encryptionKeyData, "encryption key data should be nil");
                          XCTAssertTrue(error.code == errSecInteractionNotAllowed, "should return a errSecInteractionNotAllowed error");
                      }];
    [self waitForCKModifications];
    OCMVerifyAllWithDelay(self.mockDatabase, 8);
    [self waitForExpectationsWithTimeout:1.0 handler:nil];
}

//Generic Ramp tests
-(void)testEnrollRampNotSignedIn
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    NSError* error = nil;
    NSInteger retryAfter = 0;

    self.accountStatus = CKAccountStatusNoAccount;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    [self.enroll checkRampState:&retryAfter qos:NSQualityOfServiceUserInitiated error:&error];

    XCTAssertTrue(error.code == OTErrorNotSignedIn, "should return a OTErrorNotSignedIn error");

}

-(void) testEnrollRampWithNoNetwork
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    NSError* error = nil;
    NSInteger retryAfter = 0;

    self.accountStatus = CKAccountStatusAvailable;
    [self startCKKSSubsystem];

    [self.accountStateTracker notifyCKAccountStatusChangeAndWaitForSignal];

    self.reachabilityFlags = 0;
    [self.reachabilityTracker recheck];

    [self.enroll checkRampState:&retryAfter qos:NSQualityOfServiceUserInitiated error:&error];

    XCTAssertTrue(error.code == OTErrorNoNetwork, "should return a OTErrorNoNetwork error");
}

-(void) testEnrollRampWhenLocked
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    NSError* error = nil;
    NSInteger retryAfter = 0;

    self.aksLockState = true;
    [self.lockStateTracker recheck];

    [self.enroll checkRampState:&retryAfter qos:NSQualityOfServiceUserInitiated error:&error];

    XCTAssertTrue(error.code == errSecInteractionNotAllowed, "should return a errSecInteractionNotAllowed error");
}

-(void) testTimeBetweenCFUAttempts
{
    [self setUpRampRecordsInCloudKitWithFeatureOn];

    NSError* error = nil;

    [self.manager scheduledCloudKitRampCheck:&error];
    XCTAssertNotNil(self.manager.lastPostedCoreFollowUp, "core followup should have been posted");
    NSDate* firstTime = self.manager.lastPostedCoreFollowUp;

    sleep(2);

    [self.manager scheduledCloudKitRampCheck:&error];
    XCTAssertNotNil(self.manager.lastPostedCoreFollowUp, "core followup should have been posted");
    NSDate* secondTime = self.manager.lastPostedCoreFollowUp;

    XCTAssertTrue([secondTime timeIntervalSinceDate:firstTime] >= 2, "time difference should be slightly more than 2 seconds");
}

@end
#endif