Watch: share with the complication through a Keychain group, not an App Group
Automatic signing with an App Store Connect API key registers bundle IDs and profiles but cannot create or assign App Groups, so the archive failed. A keychain-access-groups entry needs no portal setup. The demo flag and last summary move into the same group; tests cover the round trip and the migration of a pre-complication token. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
67992dbf43
commit
27469dbc77
11 files changed
+132
-49
No files matched your search
@@ -2,6 +2,7 @@
|
||||
// the screens rely on.
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
import Testing
|
||||
@testable import WristCode
|
||||
|
||||
@@ -260,6 +261,67 @@ private let feedItemJSON = """
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads and writes the real shared Keychain group, so the entitlement and Info.plist group agree.
|
||||
@Suite(.serialized) struct SharedStateTests {
|
||||
@Test func accessGroupHasTeamPrefix() {
|
||||
#expect(DeviceStore.accessGroup.hasSuffix(".org.gbrown.wristcode.shared"))
|
||||
#expect(!DeviceStore.accessGroup.hasPrefix("$"))
|
||||
}
|
||||
|
||||
@Test func roundTrip() {
|
||||
let demo = SharedState.demo
|
||||
let summary = SharedState.lastSummary
|
||||
defer {
|
||||
SharedState.demo = demo
|
||||
SharedState.lastSummary = summary
|
||||
}
|
||||
|
||||
let saved = Summary(needsYou: 2, working: 1, next: nil, hostsOffline: 0)
|
||||
SharedState.lastSummary = saved
|
||||
#expect(SharedState.lastSummary == saved)
|
||||
SharedState.lastSummary = nil
|
||||
#expect(SharedState.lastSummary == nil)
|
||||
|
||||
SharedState.demo = true
|
||||
#expect(SharedState.demo)
|
||||
SharedState.demo = false
|
||||
#expect(!SharedState.demo)
|
||||
}
|
||||
|
||||
/// A token saved by a build before the complication sits in the app's own group. Launch moves it.
|
||||
@Test func migratesLegacyItems() throws {
|
||||
let before = DeviceStore.load()
|
||||
defer {
|
||||
if let before { DeviceStore.save(before) } else { DeviceStore.clearToken() }
|
||||
}
|
||||
let team = try #require(DeviceStore.accessGroup.split(separator: ".").first)
|
||||
let appGroup = "\(team).org.gbrown.wristcode.watchkitapp"
|
||||
func item(_ account: String) -> [String: Any] {
|
||||
[kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: "org.gbrown.wristcode",
|
||||
kSecAttrAccount as String: account]
|
||||
}
|
||||
for (account, value) in [("deviceToken", "wc_legacy"), ("gatewayURL", "https://legacy.example")] {
|
||||
SecItemDelete(item(account) as CFDictionary)
|
||||
var legacy = item(account)
|
||||
legacy[kSecAttrAccessGroup as String] = appGroup
|
||||
legacy[kSecValueData as String] = Data(value.utf8)
|
||||
#expect(SecItemAdd(legacy as CFDictionary, nil) == errSecSuccess)
|
||||
}
|
||||
#expect(DeviceStore.sharedData("deviceToken") == nil)
|
||||
// Before the move the app still finds the old items.
|
||||
#expect(DeviceStore.load()?.token == "wc_legacy")
|
||||
|
||||
DeviceStore.migrateToSharedGroup()
|
||||
|
||||
#expect(DeviceStore.sharedData("deviceToken") == Data("wc_legacy".utf8))
|
||||
#expect(DeviceStore.load() == Credentials(gatewayURL: URL(string: "https://legacy.example")!, token: "wc_legacy"))
|
||||
var old = item("deviceToken")
|
||||
old[kSecAttrAccessGroup as String] = appGroup
|
||||
#expect(SecItemCopyMatching(old as CFDictionary, nil) == errSecItemNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct DemoGatewayTests {
|
||||
@Test func allowMovesThreadToWorking() async throws {
|
||||
let gateway = DemoGateway()
|
||||
|
||||
Reference in new issue
Block a user