MultiDeviceSimulatorTests.m   [plain text]

//
// MultiDeviceSimulatorTests.m
// MultiDeviceSimulatorTests
//
//

#import <XCTest/XCTest.h>
#import <Foundation/Foundation.h>
#import <Foundation/NSXPCConnection_Private.h>
#import <Security/Security.h>

#import "DeviceSimulatorProtocol.h"
#import "MultiDeviceNetworking.h"
#import <objc/runtime.h>

@interface MDDevice : NSObject<DeviceSimulatorProtocol>
@property NSXPCConnection *connection;
@property NSString *name;
- (instancetype)initWithConnection:(NSXPCConnection *)connection;
@end

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wprotocol"
@implementation MDDevice

- (instancetype)initWithConnection:(NSXPCConnection *)connection
{
self = [super init];
if (self) {
self.connection = connection;
}
return self;
}

/* Oh, ObjC, you are my friend */
- (void)forwardInvocation:(NSInvocation *)invocation
{
struct objc_method_description desc = protocol_getMethodDescription(@protocol(DeviceSimulatorProtocol), [invocation selector], true, true);
if (desc.name == NULL) {
[super forwardInvocation:invocation];
} else {
__block bool dooooooEeeeetExclamationPoint = true;
NSLog(@"forwarding to [%@]: %s", self.name, sel_getName(desc.name));
id object = [self.connection synchronousRemoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
NSLog(@"peer failed with: %@", error);
dooooooEeeeetExclamationPoint = false;
//abort();
}];
if(dooooooEeeeetExclamationPoint) {
[invocation invokeWithTarget:object];
}
}
}
@end
#pragma clang diagnostic pop

@interface MultiDeviceSimulatorTests : XCTestCase <NSXPCListenerDelegate>
@property NSMutableDictionary<NSString *,MDDevice *> *connections;
@property MultiDeviceNetworking *network;
@property MDDevice *masterDevice;
@property NSMutableArray <MDDevice *> *minionDevices;
@end

static NSString *testInstanceUUID;

@implementation MultiDeviceSimulatorTests

+ (void)setUp
{
testInstanceUUID = [[NSUUID UUID] UUIDString];
}

- (void)setUp
{
signal(SIGPIPE, SIG_IGN);
self.connections = [NSMutableDictionary dictionary];
self.network = [[MultiDeviceNetworking alloc] init];

self.minionDevices = [NSMutableArray array];
}

- (void)tearDown
{
__block uint64_t totalUserUsec = 0, totalSysUsec = 0, totalDiskUsage = 0;
NSMutableDictionary *result = [NSMutableDictionary dictionary];

for (NSString *name in self.connections) {
MDDevice *device = self.connections[name];
NSLog(@"device: %@", name);
[device diagnosticsCPUUsage:^(bool success, uint64_t user_usec, uint64_t sys_usec, NSError *error) {
NSLog(@"cpu %@: %d: u:%llu s:%llu", device.name, success, (unsigned long long)user_usec, (unsigned long long)sys_usec);
totalUserUsec += user_usec;
totalSysUsec += sys_usec;
result[[NSString stringWithFormat:@"cpu-%@", name]] = @{ @"user_usec" : @(user_usec), @"system_usec" : @(sys_usec)};
}];
[device diagnosticsDiskUsage:^(bool success, uint64_t usage, NSError *error) {
NSLog(@"disk %@: %d: %llu", device.name, success, (unsigned long long)usage);
totalDiskUsage += usage;
result[[NSString stringWithFormat:@"disk-%@", name]] = @{ @"usage" : @(usage) };
}];
}

for(MDDevice *dev in self.minionDevices) {
[dev sosLeaveCircle:^(bool success, NSError *error) {
;
}];
}
[self.masterDevice sosLeaveCircle:^(bool success, NSError *error) {
;
}];

self.minionDevices = NULL;
self.masterDevice = NULL;

result[@"cpu-total"] = @{ @"user_usec" : @(totalUserUsec), @"system_usec" : @(totalSysUsec)};
result[@"disk-total"] = @{ @"disk" : @(totalDiskUsage) };

NSLog(@"Total cpu: u:%llu s:%llu", (unsigned long long)totalUserUsec, (unsigned long long)totalSysUsec);
NSLog(@"Total disk: %llu", (unsigned long long)totalDiskUsage);

/* XXX check for leaks in all devices */
for (NSString *name in self.connections) {
MDDevice *device = self.connections[name];
[device.connection invalidate];
}
self.connections = NULL;
[self.network dumpKVSState];
[self.network dumpCounters];
[self.network disconnectAll];
[self.network clearTestExpectations];
self.network = NULL;

NSData * jsonData = [NSJSONSerialization dataWithJSONObject:result options:0 error:NULL];

[jsonData writeToFile:[NSString stringWithFormat:@"/tmp/test-result-%@", [self name]] atomically:NO];

}

- (void)setupMasterDevice
{
self.masterDevice = [self device:@"ipad" model:@"iPad" version:@"15E143a"];

[self.masterDevice setupSOSCircle:@"user" password:@"foo" complete:^void(bool success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
}];

[self.masterDevice sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCInCircle, @"expected to be in circle: %@", error);
}];
}

//MARK: - Device logic

- (MDDevice *)device:(NSString *)name model:(NSString *)model version:(NSString *)version {
MDDevice *device = self.connections[name];
if (device != NULL) {
return NULL;
}

NSXPCConnection *conn = [[NSXPCConnection alloc] initWithServiceName:@"com.apple.Security.DeviceSimulator"];
conn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(DeviceSimulatorProtocol)];
[conn _setUUID:[NSUUID UUID]]; // select a random instance
[conn resume];

device = [[MDDevice alloc] initWithConnection:conn];
device.name = name;

self.connections[name] = device;

[device setDevice:name
version:version
model:model
testInstance:testInstanceUUID
network:[self.network endpoint]
complete:^(BOOL success) {
if (!success) {
abort();
}
}];
return device;
}

- (bool)addDeviceToCircle: (MDDevice *) dev {
__block bool added = false;
__block NSString *devPeerID = NULL;
__block NSError *localErr = nil;

[dev setupSOSCircle:@"user" password:@"foo" complete:^void(bool success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
added = success;
}];

if(added) {
[dev sosRequestToJoin:^(bool success, NSString *peerID, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
XCTAssertNotEqual(peerID, NULL, "Expected to find peerID for peer2");
devPeerID = peerID;
added &= success;
}];
}

if(added) {
__block bool done = false;
for(int tries=0; tries < 5; tries++) {
sleep(2);

[self.masterDevice sosApprovePeer:devPeerID complete:^(BOOL success, NSError *error) {
localErr = [error copy];
if(success) {
localErr = nil;
done = true;
}
}];
if(done) break;
}
added &= done;
}
XCTAssert(added, "Expect success (for approve of %@): %@", devPeerID, localErr);
return added;
}

- (void)addKeychainItems:(unsigned long)items toDevice:(MDDevice *)device
{
NSDictionary *addItem = @{
(__bridge id)kSecClass :(__bridge id)kSecClassInternetPassword,
(__bridge id)kSecValueData : [@"foo" dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccessGroup: @"com.apple.cfnetwork",
(__bridge id)kSecAttrSyncViewHint: @"PCS-MasterKey",
(__bridge id)kSecAttrDescription: @"delete me if found",
(__bridge id)kSecAttrServer: @"server",
(__bridge id)kSecAttrAccount: @"account",
(__bridge id)kSecAttrSynchronizable: @YES,
(__bridge id)kSecAttrPath: @"/path",
(__bridge id)kSecAttrIsInvisible: @YES,
};

[device secItemAdd:addItem complete:^void(OSStatus status, NSDictionary *result) {
NSLog(@"Result string was dev1: %d %@", (int)status, result);
XCTAssertEqual(status, 0, "Expect success");
}];

}

- (void)runSigninWithAdditionalDevices:(unsigned)additionalDeviceCount keychainItems:(unsigned long)items {

for (unsigned n = 0; n < additionalDeviceCount; n++) {
MDDevice *dev = [self device:[NSString stringWithFormat:@"mac-%u", n] model:@"Mac Pro" version:@"17E121"];
if(dev) {
[self addDeviceToCircle: dev];
[self.minionDevices addObject:dev];
}
}

if (items) {
[self addKeychainItems:items toDevice:self.masterDevice];
}
}

- (void)testPref3Devices {
[self setupMasterDevice];

[self measureBlock:^{
[self runSigninWithAdditionalDevices:2 keychainItems:0];
}];
}

#if 0 /* disabled because of 10min time limit in bats (for now) */

- (void)testPref3Devices1 {
[self setupMasterDevice];
[self runSigninWithAdditionalDevices:2 keychainItems:1];
sleep(60);
}

- (void)testPref3Devices10 {
[self setupMasterDevice];
[self runSigninWithAdditionalDevices:2 keychainItems:10];
sleep(60);
}

- (void)testPref3Devices100 {
[self setupMasterDevice];
[self runSigninWithAdditionalDevices:2 keychainItems:100];
sleep(60);
}

- (void)testPref3Devices1000 {
[self setupMasterDevice];
[self runSigninWithAdditionalDevices:2 keychainItems:1000];
sleep(60);
sleep(1);
}

- (void)testPref6Devices {
[self setupMasterDevice];

[self measureBlock:^{
[self runSigninWithAdditionalDevices:5 keychainItems:0];
}];
}

- (void) testDevices6Retired {
[self setupMasterDevice];

[self measureBlock:^{
[self runSigninWithAdditionalDevices:5 keychainItems:0];
}];

}
#endif

- (void)test2Device {

NSLog(@"create devices");
MDDevice *dev1 = [self device:@"ipad" model:@"iPad" version:@"15E143a"];
MDDevice *dev2 = [self device:@"mac" model:@"Mac Pro" version:@"17E121"];

/*
* using PCS-MasterKey for direct syncing during inital sync
*/

NSDictionary *addItem = @{
(__bridge id)kSecClass :(__bridge id)kSecClassInternetPassword,
(__bridge id)kSecValueData : [@"foo" dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccessGroup: @"com.apple.cfnetwork",
(__bridge id)kSecAttrSyncViewHint: @"PCS-MasterKey",
(__bridge id)kSecAttrDescription: @"delete me if found",
(__bridge id)kSecAttrServer: @"server",
(__bridge id)kSecAttrAccount: @"account",
(__bridge id)kSecAttrSynchronizable: @YES,
(__bridge id)kSecAttrPath: @"/path",
(__bridge id)kSecAttrIsInvisible: @YES,
};

NSDictionary *findItem = @{
(__bridge id)kSecClass :(__bridge id)kSecClassInternetPassword,
(__bridge id)kSecAttrAccessGroup: @"com.apple.cfnetwork",
(__bridge id)kSecAttrServer: @"server",
(__bridge id)kSecAttrAccount: @"account",
(__bridge id)kSecAttrSynchronizable: @YES,
};

[dev1 secItemAdd:addItem complete:^void(OSStatus status, NSDictionary *result) {
NSLog(@"Result string was dev1: %d %@", (int)status, result);
XCTAssertEqual(status, 0, "Expect success");
}];
[dev1 secItemCopyMatching:findItem complete:^(OSStatus status, NSArray<NSDictionary *> *result) {
NSLog(@"Result string was dev1: %d %@", (int)status, result);
XCTAssertEqual(status, 0, "Expect success");
}];

/*
* Setup and validate device 1
*/

XCTestExpectation *expection = [self expectationWithDescription:@"expect to create circle"];
[dev1 setupSOSCircle:@"user" password:@"foo" complete:^void(bool success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
[expection fulfill];
}];
[self waitForExpectationsWithTimeout:5.0 handler:nil];

[dev1 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCInCircle, @"expected to be in circle: %@", error);
}];
[dev2 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCError, @"expected to be in error: %@", error);
}];

/*
* Setup and validate device 2
*/

expection = [self expectationWithDescription:@"expect to create circle"];
[dev2 setupSOSCircle:@"user" password:@"foo" complete:^void(bool success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
[expection fulfill];
}];
[self waitForExpectationsWithTimeout:5.0 handler:nil];

[dev1 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCInCircle, @"expected to be in circle: %@", error);
}];
[dev2 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCNotInCircle, @"expected to be NOT in circle: %@", error);
}];

NSLog(@"Update all views (dev1)");
expection = [self expectationWithDescription:@"expect circle update"];
expection.assertForOverFulfill = false;
[self.network setTestExpectation:expection forKey:@"oak"];

expection.assertForOverFulfill = false;
[dev1 sosEnableAllViews:^(BOOL success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
}];

[self waitForExpectationsWithTimeout:5.0 handler:nil];
[self.network clearTestExpectations];

__block NSString *peerID1 = NULL;
[dev1 sosPeerID:^(NSString *peerID) {
XCTAssertNotEqual(peerID, NULL, @"expected to find a peerID for peer1");
peerID1 = peerID;
}];

[dev2 sosPeerID:^(NSString *peerID) {
XCTAssertEqual(peerID, NULL, @"expected to NOT find peerID for peer2");
}];

[dev1 sosPeerID:^(NSString *peerID) {
XCTAssertNotEqual(peerID, NULL, @"expected to find a peerID for peer1");
XCTAssertEqualObjects(peerID1, peerID, "dev1 changed ?");
}];

/*
* Validate second device can request to join
*/

expection = [self expectationWithDescription:@"expect circle update"];
expection.assertForOverFulfill = false;
[self.network setTestExpectation:expection forKey:@"oak"];

__block NSString *peerID2 = NULL;
[dev2 sosRequestToJoin:^(bool success, NSString *peerID, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
XCTAssertNotEqual(peerID, NULL, @"expected to find peerID for peer2");
peerID2 = peerID;
}];
[self waitForExpectationsWithTimeout:5.0 handler:nil];
[self.network clearTestExpectations];

/*
* Check that device 2 can't self join
*/

expection = [self expectationWithDescription:@"expect device 2 can't self join"];
[dev2 sosApprovePeer:peerID2 complete:^(BOOL success, NSError *error) {
XCTAssert(!success, "Expect failure: %@", error);
[expection fulfill];
}];
[self waitForExpectationsWithTimeout:5.0 handler:nil];

[dev2 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCRequestPending, @"expected to be pending request: %@", error);
}];

[dev1 sosPeerID:^(NSString *peerID) {
XCTAssertNotEqual(peerID, NULL, @"expected to find a peerID for peer1");
XCTAssertEqualObjects(peerID1, peerID, "dev1 changed ?");
}];

/*
* Approve device 2 and enable all views
*/

NSLog(@"approve");
expection = [self expectationWithDescription:@"expect circle update"];
expection.assertForOverFulfill = false;
[self.network setTestExpectation:expection forKey:@"oak"];

[dev1 sosApprovePeer:peerID2 complete:^(BOOL success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
}];
[self waitForExpectationsWithTimeout:60.0 handler:nil];
[self.network clearTestExpectations];

/*
* Validate device 2 made it into circle and have a peerID
*/

[dev1 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCInCircle, @"expected to be in circle: %@", error);
}];
[dev2 sosCircleStatus:^void(SOSCCStatus status, NSError *error) {
XCTAssertEqual(status, kSOSCCInCircle, @"expected to be in circle: %@", error);
}];

[dev2 sosPeerID:^(NSString *peerID) {
XCTAssertNotEqual(peerID, NULL, @"expected to find a peerID for peer2");
peerID2 = peerID;
//XCTAssertEqualObject(peerID1, peerID2, "expect peerID to be different");
}];

/*
* Enable view for syncing
*/

NSString *netID1toID2 = [NSString stringWithFormat:@"ak|%@:%@", peerID1, peerID2];
NSString *netID2toID1 = [NSString stringWithFormat:@"ak|%@:%@", peerID2, peerID1];

expection = [self expectationWithDescription:@"expect traffic from 1->2"];
expection.assertForOverFulfill = false;
[self.network setTestExpectation:expection forKey:netID1toID2];

expection = [self expectationWithDescription:@"expect traffic from 2->1"];
expection.assertForOverFulfill = false;
[self.network setTestExpectation:expection forKey:netID2toID1];

expection = [self expectationWithDescription:@"expect circle update"];
expection.assertForOverFulfill = false;
[self.network setTestExpectation:expection forKey:@"oak"];

/*
* Perform initial sync
*/

NSLog(@"initial sync");
expection = [self expectationWithDescription:@"perform initial sync"];
[dev2 sosWaitForInitialSync:^(bool success, NSError *error) {
XCTAssert(success, "Expect success for syncing: %@", error);
[expection fulfill];
}];

/*
*
*/

NSLog(@"Update all views (dev2)");
[dev2 sosEnableAllViews:^(BOOL success, NSError *error) {
XCTAssert(success, "Expect success: %@", error);
}];

[self waitForExpectationsWithTimeout:60.0 handler:nil];
[self.network clearTestExpectations];

/*
* check syncing did its thing
*/
NSLog(@"SecItemCopyMatching");
[dev1 secItemCopyMatching:findItem complete:^(OSStatus status, NSArray<NSDictionary *> *result) {
NSLog(@"Result string was dev1: %d %@", (int)status, result);
XCTAssertEqual(status, 0, "Expect success");
}];

[dev2 secItemCopyMatching:findItem complete:^(OSStatus status, NSArray<NSDictionary *> *result) {
NSLog(@"Result string was dev2: %d %@", (int)status, result);
XCTAssertEqual(status, 0, "Expect success");
}];

NSLog(@"done");
}

@end