--- /dev/null
+//
+// 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