Une mise à niveau iOS ordinaire peut suffire à rendre soudainement un jeton de connexion « introuvable ». La nouvelle version ne plante pas et les API fonctionnent normalement : le véritable changement se cache souvent dans le service, l’account, le groupe d’accès ou l’attribut de protection de Keychain. Pour détecter ce type de problème de manière fiable sur un Mac dans le cloud, les tests doivent commencer par le scénario « écriture avec l’ancienne version, lecture avec la nouvelle », et non se limiter à une installation vierge.
Définir d’abord le contrat de migration
Considérez chaque entrée Keychain comme une structure de données dont la compatibilité doit être préservée. Consignez au minimum cinq éléments : la classe de l’entrée, le service, l’account, l’access group et kSecAttrAccessible. Si la nouvelle version doit renommer un champ, ne remplacez pas directement les critères de recherche. Commencez par lire l’entrée avec les anciens critères, écrivez-la selon la nouvelle structure, vérifiez la réussite de l’opération, puis supprimez l’ancienne entrée.
Il est recommandé d’attribuer une version entière à chaque migration, par exemple keychainSchemaVersion = 2, et d’enregistrer le marqueur d’achèvement dans les préférences standard. Ce marqueur sert uniquement à éviter de répéter la migration ; il ne prouve pas que les données Keychain existent encore. Au démarrage, l’application doit toujours rechercher l’entrée cible afin de gérer le cas où « le marqueur existe, mais les données ont été supprimées ».
Une migration n’est pas réussie simplement parce qu’une fonction renvoie un succès. La nouvelle version doit pouvoir lire les anciennes données, une nouvelle exécution ne doit pas les altérer et, en cas d’échec, l’entrée d’origine doit rester disponible pour permettre une récupération.
Commencez par établir la matrice de validation minimale :
| Scénario | État initial | Résultat attendu |
|---|---|---|
| Installation vierge | Aucune ancienne entrée | Création de l’entrée au nouveau format |
| Mise à niveau par-dessus l’ancienne version | Ancienne entrée intacte | Lecture possible après la migration |
| Démarrages répétés | Migration déjà terminée | Aucune écriture en double |
| Ancienne entrée endommagée | Format de données incorrect | Conservation de l’état et retour d’une erreur explicite |
| Changement de groupe d’accès | La nouvelle version ne peut pas accéder à l’ancien groupe | Blocage de la publication et vérification de la configuration de signature |
Construire les jeux de test des anciennes et nouvelles versions
Conservez deux fichiers .app installables dans le simulateur : l’ancienne version sert à initialiser les données, tandis que la nouvelle effectue la migration et la validation. N’exécutez pas erase et ne désinstallez pas l’application entre les deux builds, car cela modifierait les conditions initiales du test. Ajoutez au build de test des arguments de lancement activés uniquement dans l’environnement d’automatisation, tels que -UITestSeedLegacyKeychain et -UITestVerifyKeychainV2. Le build de production ne doit pas réagir à ces arguments.
set -euo pipefail
UDID="${SIMULATOR_UDID:?missing simulator id}"
OLD_APP="${OLD_APP_PATH:?missing old app path}"
NEW_APP="${NEW_APP_PATH:?missing new app path}"
BUNDLE_ID="com.example.product"
xcrun simctl bootstatus "$UDID" -b
xcrun simctl install "$UDID" "$OLD_APP"
xcrun simctl launch --terminate-running-process \
"$UDID" "$BUNDLE_ID" -UITestSeedLegacyKeychain
xcrun simctl install "$UDID" "$NEW_APP"
xcrun simctl launch --terminate-running-process \
"$UDID" "$BUNDLE_ID" -UITestVerifyKeychainV2
L’initialisation des données doit elle-même être vérifiable. Après l’écriture par l’ancienne version, l’interface de test peut produire un fichier d’état ne contenant aucune valeur sensible. Ce fichier consigne uniquement la version de l’entrée, le résultat de l’écriture et un identifiant aléatoire de cas de test. Une fois la validation terminée, la nouvelle version écrit également un résultat. La CI compare uniquement les états et n’affiche jamais le contenu du jeton.
Éviter la contamination entre exécutions concurrentes
N’exécutez pas plusieurs groupes de tests Keychain en parallèle dans un même simulateur. Créez un simulateur distinct pour chaque tâche ou exécutez les suites de tests en série. Ajoutez un suffixe aléatoire propre au cas de test au service et à l’account, afin que les données laissées par une exécution précédente interrompue anormalement ne provoquent pas un faux résultat positif lors du test suivant.
Conserver des résultats exploitables pour le diagnostic
Ne convertissez pas tous les échecs en nil. En conservant l’OSStatus, vous pouvez distinguer une entrée introuvable, une incompatibilité d’autorisations et une erreur de paramètre. La fonction de recherche peut renvoyer un type de résultat explicite :
enum KeychainReadResult {
case found(Data)
case missing
case failed(OSStatus)
}
func readLegacyToken(service: String, account: String) -> KeychainReadResult {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: account,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecItemNotFound {
return .missing
}
guard status == errSecSuccess, let data = item as? Data else {
return .failed(status)
}
return .found(data)
}
Lors de la migration, commencez par ajouter l’entrée au nouveau format. Relisez-la ensuite, comparez l’empreinte de son contenu, puis supprimez l’ancienne entrée. Ne supprimez jamais avant d’écrire : si l’écriture échoue à cause d’un problème d’autorisation ou de paramètre, les données de l’utilisateur deviendront irrécupérables. Les journaux doivent contenir uniquement le code d’état, la version de migration et l’identifiant du cas de test, jamais les identifiants d’origine.
Vérifier la signature et les groupes d’accès
Si le code n’a pas changé mais que la recherche renvoie soudainement errSecItemNotFound, comparez les entitlements des anciens et nouveaux artefacts. Toute modification du préfixe du groupe d’accès, de l’identifiant de l’application ou des Keychain access groups peut rendre l’ancienne entrée invisible pour la nouvelle version.
codesign -d --entitlements :- "$OLD_APP_PATH" > old-entitlements.plist
codesign -d --entitlements :- "$NEW_APP_PATH" > new-entitlements.plist
diff -u old-entitlements.plist new-entitlements.plist
Conservez le fichier de différences comme pièce jointe de la CI, mais ne copiez pas les éléments de signature dans les journaux de build. Si le groupe d’accès doit réellement changer, prévoyez une version de transition autorisée à accéder à la fois à l’ancien et au nouveau groupe pour effectuer le transfert. Ne partez pas du principe que la nouvelle version peut lire directement n’importe quel ancien groupe.
Le simulateur convient pour valider l’algorithme de migration et les branches d’erreur, mais il ne reproduit pas tous les comportements liés à la signature et au contrôle d’accès. Avant la publication, exécutez également sur un appareil physique contrôlé le même processus d’installation de l’ancienne version, d’initialisation des données et de mise à niveau par-dessus celle-ci. Consignez ce résultat séparément des tests effectués dans le simulateur.
Nettoyage, collecte des preuves d’échec et critères de publication
À la fin d’un test réussi, supprimez les entrées de test via l’interface de test intégrée à l’application, en ciblant précisément le service et l’account. N’utilisez pas de requête de suppression trop large, au risque d’effacer d’autres données présentes sur le même appareil de test. En cas d’échec, conservez d’abord le simulateur, puis collectez les entitlements de l’ancienne et de la nouvelle version, l’OSStatus, les étapes de migration et les journaux de l’application avant de décider s’il faut le supprimer.
Le contrôle préalable à la publication doit vérifier trois points : les données de l’ancienne version sont lisibles par la nouvelle ; deux exécutions consécutives de la migration produisent le même résultat ; une entrée anormale n’entraîne pas la suppression de l’ancienne entrée. L’échec d’un seul de ces contrôles doit empêcher l’artefact de passer à l’étape de distribution suivante.
Enfin, conservez des entrées fixes pour le script de migration : l’identifiant du build de l’ancienne version, celui du build de la nouvelle version, la version système du simulateur et l’identifiant du cas de test. Ainsi, même si le Mac dans le cloud est réinitialisé, le même parcours de mise à niveau pourra être reproduit à partir des artefacts archivés. La principale difficulté des problèmes Keychain n’est pas l’écriture, mais l’absence de l’état antérieur. Dès lors que les jeux de test de l’ancienne version, les différences de signature et les codes d’erreur sont reproductibles, la migration cesse d’être un risque en production et devient un test de régression ordinaire.
Questions fréquentes
Pourquoi un test après installation propre ne suffit-il pas ?
Il prouve seulement que la version actuelle sait créer une entrée. Il ne vérifie pas qu’elle peut relire le service, le compte, le groupe d’accès et les attributs de protection écrits par l’ancienne version.
Que faut-il examiner en premier après un échec de migration Keychain ?
Conservez le code OSStatus de SecItemCopyMatching, puis comparez les entitlements, groupes d’accès, services, comptes et valeurs kSecAttrAccessible des deux versions avant toute suppression.
Le simulateur peut-il remplacer la validation sur appareil ?
Non. Il convient aux tests reproductibles des requêtes et de la migration, mais la signature, le contrôle d’accès et certains comportements de sécurité exigent aussi une validation sur un appareil maîtrisé.
Configurez un Mac cloud fixe pour vos pipelines de développement
MangoVM propose des nœuds physiques Apple Silicon en deux versions : M4 et M4 Pro. Choisissez la durée de location, la région et les options de stockage pour vérifier le récapitulatif complet de votre commande.