updates
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import LocalAuthentication
|
||||
|
||||
final class MockProtocol: URLProtocol, @unchecked Sendable {
|
||||
static var handler: ((URLRequest) throws -> (Int, String))!
|
||||
@@ -152,6 +154,50 @@ final class MockProtocol: URLProtocol, @unchecked Sendable {
|
||||
disconnected.disconnect()
|
||||
await disconnected.restoreSessionIfNeeded()
|
||||
check(disconnected.error == nil && !disconnected.connected, "Explicit disconnect suppresses automatic login for this launch")
|
||||
let protection = AppModel()
|
||||
protection.locked = true; protection.biometric = true
|
||||
var authCalls = 0
|
||||
protection.authenticate = { _ in authCalls += 1; return true }
|
||||
await protection.updateScene(.active)
|
||||
check(!protection.locked && authCalls == 1, "Face ID starts automatically on opening the locked app")
|
||||
await protection.updateScene(.inactive)
|
||||
await protection.updateScene(.active)
|
||||
check(authCalls == 1, "Authentication inactive/active transitions do not prompt again")
|
||||
await protection.updateScene(.background)
|
||||
check(protection.locked, "Background locks protected app")
|
||||
protection.authenticate = { _ in authCalls += 1; throw LAError(.userCancel) }
|
||||
await protection.updateScene(.active)
|
||||
check(protection.locked && protection.error == nil, "Cancelled Face ID stays locked without an alert")
|
||||
await protection.updateScene(.inactive)
|
||||
await protection.updateScene(.active)
|
||||
check(authCalls == 2, "Cancelled authentication does not automatically loop")
|
||||
protection.authenticate = { _ in authCalls += 1; return true }
|
||||
await protection.unlock()
|
||||
check(!protection.locked && authCalls == 3, "Manual unlock remains available after cancellation")
|
||||
await protection.updateScene(.background)
|
||||
await protection.updateScene(.active)
|
||||
check(!protection.locked && authCalls == 4, "Returning from background automatically authenticates again")
|
||||
await protection.updateScene(.background)
|
||||
protection.authenticate = { _ in
|
||||
authCalls += 1
|
||||
await protection.unlock()
|
||||
return true
|
||||
}
|
||||
await protection.updateScene(.active)
|
||||
check(authCalls == 5 && !protection.authenticating, "Overlapping unlock attempts share one authentication prompt")
|
||||
await protection.updateScene(.background)
|
||||
protection.authenticate = { _ in
|
||||
await protection.updateScene(.background)
|
||||
return true
|
||||
}
|
||||
await protection.updateScene(.active)
|
||||
check(protection.locked, "An old authentication result cannot unlock after backgrounding")
|
||||
protection.authenticate = { _ in
|
||||
await protection.updateScene(.inactive)
|
||||
return true
|
||||
}
|
||||
await protection.updateScene(.active)
|
||||
check(!protection.locked, "Face ID success while system prompt is inactive unlocks the app")
|
||||
another.close(); transport.close(); print("\(checks) contract checks passed.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import copy
|
||||
import datetime
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
spec = importlib.util.spec_from_file_location('verify_profile', pathlib.Path(__file__).resolve().parents[1] / 'scripts/verify_profile.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
class ProfileTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = {'CFBundleIdentifier': 'net.absdev.Tarnax'}
|
||||
self.profile = {
|
||||
'ApplicationIdentifierPrefix': ['TESTTEAM'],
|
||||
'Entitlements': {'application-identifier': 'TESTTEAM.net.absdev.Tarnax'},
|
||||
'ExpirationDate': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1),
|
||||
'ProvisionedDevices': ['test-phone'],
|
||||
}
|
||||
|
||||
def test_exact(self):
|
||||
module.validate(self.app, self.profile, 'test-phone', True)
|
||||
|
||||
def test_wildcard(self):
|
||||
for identifier in ['TESTTEAM.*', 'TESTTEAM.net.absdev.*']:
|
||||
with self.subTest(identifier=identifier):
|
||||
self.profile['Entitlements']['application-identifier'] = identifier
|
||||
module.validate(self.app, self.profile, 'test-phone')
|
||||
|
||||
def test_rejections(self):
|
||||
for mutation, options in [
|
||||
({'Entitlements': {'application-identifier': 'OTHERTEAM.*'}}, {}),
|
||||
({'Entitlements': {'application-identifier': 'TESTTEAM.net.other.*'}}, {}),
|
||||
({'ProvisionedDevices': ['other-phone']}, {'device': 'test-phone'}),
|
||||
({'ExpirationDate': datetime.datetime(2000, 1, 1)}, {}),
|
||||
({'ProvisionedDevices': []}, {}),
|
||||
({'Entitlements': {'application-identifier': 'TESTTEAM.*', 'get-task-allow': True}}, {'distribution': True}),
|
||||
]:
|
||||
with self.subTest(mutation=mutation):
|
||||
profile = copy.deepcopy(self.profile)
|
||||
profile.update(mutation)
|
||||
with self.assertRaises(ValueError):
|
||||
module.validate(self.app, profile, **options)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
BUILD_DIR="$(mktemp -d)"
|
||||
source scripts/common.sh
|
||||
BUILD_DIR="$(mktemp -d "$TARNAX_SCRATCH_ROOT/contracts.XXXXXX")"
|
||||
trap 'rm -rf "$BUILD_DIR"' EXIT
|
||||
xcrun swiftc -parse-as-library -module-cache-path "$BUILD_DIR/cache" Tarnax/Core.swift Tests/ContractTests.swift -o "$BUILD_DIR/ContractTests"
|
||||
"$BUILD_DIR/ContractTests"
|
||||
|
||||
Reference in New Issue
Block a user