Обычное обновление iOS-приложения может внезапно привести к тому, что токен входа окажется «не найден». Новая версия при этом не аварийно завершается, а API продолжает работать. Реальная причина обычно скрывается в изменении service, account, группы доступа или атрибутов защиты Keychain. Чтобы надёжно выявлять такие проблемы на облачном Mac, тестирование должно начинаться со сценария «старая версия записывает, новая читает», а не ограничиваться чистой установкой.
Сначала определите контракт миграции
Рассматривайте запись Keychain как структуру данных, для которой требуется обратная совместимость. Зафиксируйте как минимум пять параметров: класс записи, service, account, access group и kSecAttrAccessible. Если в новой версии нужно изменить имена, не заменяйте условия поиска сразу. Сначала прочитайте запись по старым условиям, сохраните её в новой структуре и только после подтверждения успешной записи удалите старую.
Рекомендуется назначать каждой миграции целочисленную версию, например keychainSchemaVersion = 2, и сохранять отметку о завершении в обычных настройках приложения. Эта отметка нужна только для предотвращения повторной миграции и не доказывает, что данные действительно присутствуют в Keychain. При запуске приложение всё равно должно проверять целевую запись, чтобы корректно обрабатывать ситуацию, когда отметка существует, а данные уже удалены.
Критерий успешной миграции — не успешный возврат функции, а возможность прочитать старые данные в новой версии, безопасно выполнить миграцию повторно и сохранить исходную запись для восстановления в случае ошибки.
Сначала составьте минимальную матрицу приёмочного тестирования:
| Сценарий | Исходное состояние | Ожидаемый результат |
|---|---|---|
| Чистая установка | Старой записи нет | Создана запись нового формата |
| Обновление поверх старой версии | Старая запись не повреждена | После миграции запись доступна для чтения |
| Повторный запуск | Миграция уже завершена | Повторная запись не выполняется |
| Старая запись повреждена | Неверный формат данных | Исходное состояние сохраняется, возвращается понятная ошибка |
| Изменилась группа доступа | Новая версия не имеет доступа к старой группе | Выпуск блокируется, конфигурация подписи проверяется |
Создайте тестовые фикстуры старой и новой версий
Сохраните два устанавливаемых файла .app для симулятора: старая версия заполняет Keychain тестовыми данными, а новая выполняет миграцию и проверку. Между установкой двух сборок не запускайте erase и не удаляйте приложение, иначе исходные условия теста изменятся. Добавьте в тестовые сборки аргументы запуска, которые действуют только в среде автоматизации, например -UITestSeedLegacyKeychain и -UITestVerifyKeychainV2. Рабочая сборка не должна реагировать на эти аргументы.
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
Результат заполнения должен поддаваться проверке. После записи данных старая версия может вывести через тестовый интерфейс файл состояния без конфиденциальных значений. В нём следует указывать только версию записи, результат операции и случайный идентификатор тестового случая. После завершения проверки новая версия также записывает результат, а CI сравнивает только состояния, не выводя содержимое токена.
Не допускайте взаимного влияния параллельных тестов
Не запускайте несколько наборов тестов Keychain параллельно на одном симуляторе. Создавайте отдельный симулятор для каждой задачи либо выполняйте тестовые наборы последовательно. Добавляйте к service и account случайный суффикс тестового случая, чтобы данные, оставшиеся после предыдущего аварийного прерывания, не привели к ложному успешному результату следующего теста.
Сохраняйте диагностические результаты в коде
Не преобразуйте все ошибки в nil. Сохраняйте OSStatus, чтобы различать отсутствие записи, несоответствие прав доступа и неверные параметры. Функция поиска может возвращать явно определённый тип результата:
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)
}
Во время миграции сначала добавьте запись нового формата, затем прочитайте её обратно и сравните дайджест содержимого, после чего удалите старую запись. Не удаляйте данные до записи новых: если сохранение завершится ошибкой из-за прав доступа или параметров, восстановить пользовательские данные будет невозможно. В журналах фиксируйте только код состояния, версию миграции и идентификатор тестового случая, но не исходные учётные данные.
Проверьте подпись и группы доступа
Если код не менялся, но запрос внезапно начал возвращать errSecItemNotFound, сравните entitlements старого и нового артефактов. Изменение префикса группы доступа, идентификатора приложения или Keychain access groups может сделать исходную запись невидимой для новой версии.
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
Сохраняйте файл различий как вложение CI, но не копируйте материалы подписи в журнал сборки. Если группу доступа действительно необходимо изменить, предусмотрите переходную версию, которая имеет доступ одновременно к старой и новой группам и выполняет перенос. Не исходите из предположения, что новая версия сможет напрямую прочитать любую старую группу.
Симулятор подходит для проверки алгоритма миграции и ветвей обработки ошибок, но не воспроизводит все особенности подписи и контроля доступа. Перед выпуском выполните тот же процесс установки старой версии, заполнения данных и обновления поверх неё на контролируемом физическом устройстве. Результаты этого теста следует регистрировать отдельно от результатов симулятора.
Очистка, сбор данных при сбоях и блокировка выпуска
После успешного теста удалите тестовые записи через внутренний тестовый интерфейс приложения, точно указав service и account. Не используйте слишком широкие условия удаления, чтобы случайно не очистить другие данные на том же тестовом устройстве. При неудачном тесте сначала сохраните симулятор, соберите entitlements старой и новой версий, OSStatus, сведения об этапах миграции и журналы приложения, а затем решайте, можно ли его удалить.
Условия допуска к выпуску должны проверять три требования: новая версия читает данные старой; две последовательные миграции дают одинаковый результат; некорректные входные данные не приводят к удалению старой записи. Нарушение любого из этих требований должно блокировать передачу артефакта на следующие этапы распространения.
Наконец, закрепите входные данные сценария миграции: идентификатор старой сборки, идентификатор новой сборки, версию системы симулятора и идентификатор тестового случая. Тогда даже после повторной инициализации облачного Mac тот же путь обновления можно будет восстановить из архивных артефактов. Самая сложная часть проблем с Keychain — не запись данных, а отсутствие старого состояния. Если старую фикстуру, различия подписи и коды ошибок можно воспроизвести, миграция превращается из риска для рабочей среды в обычный регрессионный тест.
Часто задаваемые вопросы
Почему недостаточно проверить Keychain после чистой установки?
Чистая установка подтверждает только запись данных текущей версией. Она не показывает, сможет ли обновление прочитать service, account, группу доступа и атрибуты защиты, созданные старой сборкой.
Что проверять первым при ошибке миграции Keychain?
Сохраните OSStatus от SecItemCopyMatching, затем сравните entitlements, группы доступа, service, account и kSecAttrAccessible у обеих сборок. Не удаляйте старую запись до завершения диагностики.
Достаточно ли тестирования в симуляторе?
Нет. Симулятор подходит для регулярной проверки запросов и логики миграции, но подпись, управление доступом и отдельные защитные механизмы нужно дополнительно проверять на контролируемом устройстве.
Настройте выделенный облачный Mac для конвейера разработки
MangoVM предлагает физические узлы Apple Silicon в двух конфигурациях: M4 и M4 Pro. Выберите срок аренды, регион и дополнительные параметры хранилища, чтобы проверить полную информацию о заказе.