OctagonTests+DeviceList.swift   [plain text]


#if OCTAGON

class OctagonDeviceListTests: OctagonTestsBase {
    func testSignInFailureBecauseUntrustedDevice() throws {
        // Check that we honor IdMS trusted device list that we got and reject device that
        // are not it

        self.startCKAccountStatusMock()

        // Must positively assert some device in list, so that the machine ID list isn't empty
        self.mockAuthKit.otherDevices.insert("some-machine-id")
        self.mockAuthKit.excludeDevices.insert(try! self.mockAuthKit.machineID())

        self.cuttlefishContext.startOctagonStateMachine()
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)

        let expectFail = self.expectation(description: "expect to fail")

        do {
            let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
            XCTAssertNil(clique, "Clique should be nil")
        } catch {
            expectFail.fulfill()
        }

        self.wait(for: [expectFail], timeout: 10)

        // Now, we should be in 'untrusted'
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
    }

    func testSignInWithIDMSBypass() throws {
        // Check that we can bypass IdMS trusted device list (needed for demo accounts)

        self.startCKAccountStatusMock()

        self.mockAuthKit.excludeDevices.formUnion(self.mockAuthKit.currentDeviceList())
        XCTAssertTrue(self.mockAuthKit.currentDeviceList().isEmpty, "should have zero devices")

        self.cuttlefishContext.startOctagonStateMachine()
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)

        do {
            let clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
            XCTAssertNotNil(clique, "Clique should not be nil")
        } catch {
            XCTFail("Shouldn't have errored making new friends: \(error)")
        }

        // Now, we should be in 'ready'
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
        self.assertConsidersSelfTrustedCachedAccountStatus(context: self.cuttlefishContext)

        // and all subCKKSes should enter ready...
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        self.assertSelfTLKSharesInCloudKit(context: self.cuttlefishContext)

        // And we haven't helpfully added anything to the MID list
        self.assertMIDList(context: self.cuttlefishContext, allowed: Set(), disallowed: Set(), unknown: Set())
    }

    func testRemovePeerWhenRemovedFromDeviceList() throws {
        self.startCKAccountStatusMock()

        XCTAssert(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should already have device 2 on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()

        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, tell peer1 about the change. It will trust the peer and upload some TLK shares
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        // Then peer2 drops off the device list. Peer 1 should distrust peer2.
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            XCTAssertTrue(!(newDynamicInfo?.includedPeerIDs.contains(peer2ID) ?? true), "peer1 should no longer trust peer2")
            updateTrustExpectation.fulfill()
            return nil
        }

        self.mockAuthKit.removeAndSendNotification(machineID: try! self.mockAuthKit2.machineID())

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .excludes, target: peer2ID)),
                     "peer 1 should distrust peer 2 after update")
    }

    func testRemovePeerWhenRemovedFromDeviceListViaIncompleteNotification() throws {
        self.startCKAccountStatusMock()

        XCTAssert(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should already have device 2 on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()

        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, tell peer1 about the change. It will trust the peer and upload some TLK shares
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        // Then peer2 drops off the device list. Peer 1 should distrust peer2.
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            XCTAssertTrue(!(newDynamicInfo?.includedPeerIDs.contains(peer2ID) ?? true), "peer1 should no longer trust peer2")
            updateTrustExpectation.fulfill()
            return nil
        }

        self.mockAuthKit.excludeDevices.insert(try! self.mockAuthKit2.machineID())
        XCTAssertFalse(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should not still have device 2 on the list")
        self.mockAuthKit.sendIncompleteNotification()

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .excludes, target: peer2ID)),
                      "peer 1 should distrust peer 2 after update")
    }

    func testTrustPeerWhenMissingFromDeviceList() throws {
        self.startCKAccountStatusMock()

        self.mockAuthKit.otherDevices.removeAll()
        XCTAssertEqual(self.mockAuthKit.currentDeviceList(), Set([self.mockAuthKit.currentMachineID]), "AuthKit should have exactly one device on the list")
        XCTAssertFalse(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should not already have device 2 on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()

        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, tell peer1 about the change. It will trust the peer, despite it missing from the list, and upload some TLK shares
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")
        self.assertMIDList(context: self.cuttlefishContext,
                           allowed: self.mockAuthKit.currentDeviceList(),
                           disallowed: Set(),
                           unknown: Set([self.mockAuthKit2.currentMachineID]))

        // On a follow-up update, peer1 should _not_ hit IDMS, even though there's an unknown peer ID in its DB
        let currentCount = self.mockAuthKit.fetchInvocations
        self.sendContainerChangeWaitForFetchForStates(context: self.cuttlefishContext, states: [OctagonStateReadyUpdated, OctagonStateReady])
        self.assertMIDList(context: self.cuttlefishContext,
                           allowed: self.mockAuthKit.currentDeviceList(),
                           disallowed: Set(),
                           unknown: Set([self.mockAuthKit2.currentMachineID]))
        XCTAssertEqual(currentCount, self.mockAuthKit.fetchInvocations, "Receving a push while having an unknown peer MID should not cause an AuthKit fetch")

        ////////

        // Then peer2 arrives on the device list. Peer 1 should update its dynamic info to no longer have a disposition for peer2.
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            // TODO: swift refuses to see the dispositions object on newDynamicInfo; ah well
            updateTrustExpectation.fulfill()
            return nil
        }

        self.mockAuthKit.addAndSendNotification(machineID: try! self.mockAuthKit2.machineID())

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")
    }

    func testRemoveSelfWhenRemovedFromOnlySelfList() throws {
        self.startCKAccountStatusMock()

        self.mockAuthKit.otherDevices.removeAll()
        XCTAssertEqual(self.mockAuthKit.currentDeviceList(), Set([self.mockAuthKit.currentMachineID]), "AuthKit should have exactly one device on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()
        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer1ID)),
                      "peer 1 should trust peer 1")

        // Then peer1 drops off the device list
        // It should remove trust in itself
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            XCTAssertEqual(newDynamicInfo!.includedPeerIDs.count, 0, "peer1 should no longer trust anyone")
            XCTAssertEqual(newDynamicInfo!.excludedPeerIDs, Set([peer1ID]), "peer1 should exclude itself")
            updateTrustExpectation.fulfill()
            return nil
        }

        self.mockAuthKit.removeAndSendNotification(machineID: try! self.mockAuthKit.machineID())

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTrust, within: 10 * NSEC_PER_SEC)
        self.assertConsidersSelfUntrusted(context: self.cuttlefishContext)
    }

    func testRemoveSelfWhenRemovedFromLargeDeviceList() throws {
        self.startCKAccountStatusMock()

        XCTAssert(self.mockAuthKit.currentDeviceList().count > 1, "AuthKit should have more than one device on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()
        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer1ID)),
                      "peer 1 should trust peer 1")

        // Then peer1 drops off the device list
        // It should remove trust in itself
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            XCTAssertEqual(newDynamicInfo!.includedPeerIDs.count, 0, "peer1 should no longer trust anyone")
            XCTAssertEqual(newDynamicInfo!.excludedPeerIDs, Set([peer1ID]), "peer1 should exclude itself")
            updateTrustExpectation.fulfill()
            return nil
        }

        self.mockAuthKit.removeAndSendNotification(machineID: try! self.mockAuthKit.machineID())

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTrust, within: 10 * NSEC_PER_SEC)
        self.assertConsidersSelfUntrusted(context: self.cuttlefishContext)
    }

    func testRemoveSelfWhenRemovedFromLargeDeviceListByIncompleteNotification() throws {
        self.startCKAccountStatusMock()

        XCTAssert(self.mockAuthKit.currentDeviceList().count > 1, "AuthKit should have more than one device on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()
        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer1ID)),
                      "peer 1 should trust peer 1")

        // Then peer1 drops off the device list
        // It should remove trust in itself
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            XCTAssertEqual(newDynamicInfo!.includedPeerIDs.count, 0, "peer1 should no longer trust anyone")
            XCTAssertEqual(newDynamicInfo!.excludedPeerIDs, Set([peer1ID]), "peer1 should exclude itself")
            updateTrustExpectation.fulfill()
            return nil
        }

        self.mockAuthKit.excludeDevices.insert(try! self.mockAuthKit.machineID())
        XCTAssertFalse(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit.currentMachineID), "AuthKit should not still have device 2 on the list")
        self.mockAuthKit.sendIncompleteNotification()

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTrust, within: 10 * NSEC_PER_SEC)

        self.assertConsidersSelfUntrusted(context: self.cuttlefishContext)
    }

    func testIgnoreRemoveFromWrongAltDSID() throws {
        self.startCKAccountStatusMock()

        XCTAssert(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should already have device 2 on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()

        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, tell peer1 about the change. It will trust the peer and upload some TLK shares
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        // We receive a 'remove' push for peer2's ID, but for the wrong DSID. The peer should do nothing useful.
        self.fakeCuttlefishServer.updateListener = { request in
            XCTFail("shouldn't have updated trust")
            return nil
        }

        self.mockAuthKit.sendRemoveNotification(machineID: try! self.mockAuthKit2.machineID(), altDSID: "completely-wrong")

        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        self.assertMIDList(context: self.cuttlefishContext,
                           allowed: self.mockAuthKit.currentDeviceList(),
                           disallowed: Set(),
                           unknown: Set())
    }

    func testIgnoreAddFromWrongAltDSID() throws {
        self.startCKAccountStatusMock()

        XCTAssert(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should already have device 2 on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()

        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, tell peer1 about the change. It will trust the peer and upload some TLK shares
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        // We receive a 'add' push for a new ID, but for the wrong DSID. The peer should do nothing useful.
        self.fakeCuttlefishServer.updateListener = { request in
            XCTFail("shouldn't have updated trust")
            return nil
        }

        let newMachineID = "newID"
        self.mockAuthKit.sendAddNotification(machineID: newMachineID, altDSID: "completely-wrong")

        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        // newMachineID should be on no lists
        self.assertMIDList(context: self.cuttlefishContext,
                           allowed: self.mockAuthKit.currentDeviceList(),
                           disallowed: Set(),
                           unknown: Set())
    }

    func testPeerJoiningWithUnknownMachineIDTriggersMachineIDFetchAfterGracePeriod() throws {
        self.startCKAccountStatusMock()

        // Peer 2 is not on Peer 1's machine ID list yet
        self.mockAuthKit.otherDevices.remove(self.mockAuthKit2.currentMachineID)

        _ = self.assertResetAndBecomeTrustedInDefaultContext()
        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        _ = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, add peer2 to the machineID list, but don't send peer1 a notification about the IDMS change
        self.mockAuthKit.otherDevices.insert(self.mockAuthKit2.currentMachineID)
        self.assertMIDList(context: self.cuttlefishContext, allowed: self.mockAuthKit.currentDeviceList().subtracting(Set([self.mockAuthKit2.currentMachineID])))

        let condition = CKKSCondition()
        self.mockAuthKit.fetchCondition = condition

        // But peer1 does get the cuttlefish push
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        // At this time, peer1 should trust peer2, but it should _not_ have fetched the AuthKit list,
        // because peer2 is still within the 48 hour grace period. peer1 is hoping for a push to arrive.

        XCTAssertNotEqual(condition.wait(2 * NSEC_PER_SEC), 0, "Octagon should not fetch the authkit machine ID list")
        let peer2MIDSet = Set([self.mockAuthKit2.currentMachineID])
        self.assertMIDList(context: self.cuttlefishContext,
                           allowed: self.mockAuthKit.currentDeviceList().subtracting(peer2MIDSet),
                           unknown: peer2MIDSet)

        // Now, let's pretend that three days pass, and do this again... Octagon should now fetch the MID list and become happy.
        let container = try self.tphClient.getContainer(withContainer: self.cuttlefishContext.containerName, context: self.cuttlefishContext.contextID)
        container.moc.performAndWait {
            var foundPeer2 = false
            for machinemo in container.containerMO.machines as? Set<MachineMO> ?? Set() {
                if machinemo.machineID == self.mockAuthKit2.currentMachineID {
                    foundPeer2 = true
                    //
                    machinemo.modified = Date(timeIntervalSinceNow: -60 * 60 * TimeInterval(72))
                    XCTAssertEqual(machinemo.status, Int64(TPMachineIDStatus.unknown.rawValue), "peer2's MID entry should be 'unknown'")
                }
            }

            XCTAssertTrue(foundPeer2, "Should have found an entry for peer2")
            try! container.moc.save()
        }

        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        XCTAssertEqual(condition.wait(10 * NSEC_PER_SEC), 0, "Octagon should fetch the authkit machine ID list")

        self.assertMIDList(context: self.cuttlefishContext, allowed: self.mockAuthKit.currentDeviceList())
    }

    func testTrustPeerWheMissingFromDeviceListAndLocked() throws {
        self.startCKAccountStatusMock()

        self.mockAuthKit.otherDevices.removeAll()
        XCTAssertEqual(self.mockAuthKit.currentDeviceList(), Set([self.mockAuthKit.currentMachineID]), "AuthKit should have exactly one device on the list")
        XCTAssertFalse(self.mockAuthKit.currentDeviceList().contains(self.mockAuthKit2.currentMachineID), "AuthKit should not already have device 2 on the list")

        let peer1ID = self.assertResetAndBecomeTrustedInDefaultContext()

        let joiningContext = self.makeInitiatorContext(contextID: "joiner", authKitAdapter: self.mockAuthKit2)
        let peer2ID = self.assertJoinViaEscrowRecovery(joiningContext: joiningContext, sponsor: self.cuttlefishContext)

        // Now, tell peer1 about the change. It will trust the peer, despite it missing from the list, and upload some TLK shares
        self.assertAllCKKSViewsUpload(tlkShares: 1)
        self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
        self.verifyDatabaseMocks()

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                     "peer 1 should trust peer 2 after update")

        // Then peer2 arrives on the device list. Peer 1 should update its dynamic info to no longer have a disposition for peer2.
        let updateTrustExpectation = self.expectation(description: "updateTrust")
        self.fakeCuttlefishServer.updateListener = { request in
            XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
            let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
                                                   sig: request.dynamicInfoAndSig.sig)
            XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")

            // TODO: swift refuses to see the dispositions object on newDynamicInfo; ah well
            updateTrustExpectation.fulfill()
            return nil
        }

        // Now, peer should lock and receive an Octagon push
        self.aksLockState = true
        self.lockStateTracker.recheck()

        // Now, peer2 should receive an Octagon push, try to realize it is preapproved, and get stuck
        self.sendContainerChange(context: self.cuttlefishContext)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)

        self.mockAuthKit.addAndSendNotification(machineID: try! self.mockAuthKit2.machineID())

        sleep(1)

        XCTAssertTrue(self.cuttlefishContext.stateMachine.possiblePendingFlags().contains("recd_push"), "Should have recd_push pending flag")

        // Now, peer should unlock and receive an Octagon push
        self.aksLockState = false
        self.lockStateTracker.recheck()

        sleep(1)
        XCTAssertEqual(self.cuttlefishContext.stateMachine.possiblePendingFlags(), [], "Should have 0 pending flags")
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)

        self.wait(for: [updateTrustExpectation], timeout: 10)
        self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
        self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)

        XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: peer1ID, opinion: .trusts, target: peer2ID)),
                      "peer 1 should trust peer 2 after update")
    }
}

#endif