Если при регрессионном тестировании переключения аккаунтов на одном облачном Mac аккаунт Б после входа получает данные аккаунта А, не спешите сбрасывать симулятор. Проверьте по отдельности хранилище cookie, заголовки запросов и задачи, которые ещё выполняются: старая сессия может сохраняться из-за общего хранилища, заголовков, вручную заданных в сетевой обёртке, или даже ответа, пришедшего уже после выхода.
Сначала выполним проверку без сетевых запросов средствами штатного сетевого фреймворка macOS, а затем перенесём те же проверки в симулятор iOS. Сохраните проверочный код в репозитории: тогда его можно будет воспроизводимо запускать после каждого получения кода на облачном Mac, не полагаясь на состояние, оставшееся от предыдущего графического сеанса.
Сначала определите, кому принадлежит сессия
Создайте для каждого вошедшего в систему аккаунта отдельный объект сессии, которому принадлежат конфигурация, хранилище cookie и набор задач. Не используйте изменяемую конфигурацию как глобальный синглтон и не рассчитывайте, что изменение исходной конфигурации повлияет на уже созданную URLSession: при создании сессия копирует конфигурацию.
Зафиксируйте как минимум три требования: cookie аккаунта А не попадают в запросы аккаунта Б; при выходе удаляются только данные текущего аккаунта; старый ответ, пришедший после выхода, не может восстановить состояние авторизации. Если анонимным запросам cookie вообще не нужны, отключите их автоматическую обработку и убедитесь, что обёртка не добавляет заголовок Cookie вручную.
ephemeralозначает, что данные сессии не сохраняются на диск. Это не означает отсутствия cookie в памяти и тем более не гарантирует автоматической изоляции двух аккаунтов приложения.
Проверьте независимость хранилищ скриптом
Сохраните следующий код в файл cookie-check.swift. Он использует только вымышленные значения сессий и локальный домен localhost, не отправляет сетевых запросов и не обращается к общему хранилищу по умолчанию.
import Foundation
func require(_ condition: Bool, _ message: String) {
if !condition {
fatalError(message)
}
}
func makeCookie(_ value: String) -> HTTPCookie {
HTTPCookie(properties: [
.domain: "localhost",
.path: "/",
.name: "session",
.value: value
])!
}
let first = URLSessionConfiguration.ephemeral
let second = URLSessionConfiguration.ephemeral
guard let a = first.httpCookieStorage,
let b = second.httpCookieStorage else {
fatalError("Cookie storage unavailable")
}
require(a !== b, "Stores must be independent")
let target = URL(string: "http://localhost/account")!
a.setCookie(makeCookie("fixture-a"))
b.setCookie(makeCookie("fixture-b"))
func values(_ store: HTTPCookieStorage) -> [String] {
(store.cookies(for: target) ?? [])
.filter { $0.name == "session" }
.map(\.value)
.sorted()
}
require(values(a) == ["fixture-a"], "Account A mismatch")
require(values(b) == ["fixture-b"], "Account B mismatch")
for cookie in a.cookies ?? [] {
a.deleteCookie(cookie)
}
require(values(a).isEmpty, "Account A was not cleared")
require(values(b) == ["fixture-b"], "Account B was affected")
Выполните на машине, где уже завершена первоначальная настройка инструментов командной строки:
set -eu
xcrun swift cookie-check.swift
При успешном выполнении вывода не будет; при нарушении любого условия скрипт завершится с ненулевым кодом. Здесь хранилище a очищается целиком при условии, что оно принадлежит только аккаунту А. Если приложение по-прежнему использует общее хранилище, сначала нужно отобрать данные, принадлежащие нужному аккаунту, а не копировать удаление всех cookie.
Скрипт подтверждает независимость хранилищ и правильные границы удаления, но не доказывает, что реальные запросы используют нужное хранилище. Успех на этом уровне нельзя считать успешной приёмкой сетевого слоя.
Подключите реальные запросы и ответы
В тестовом таргете приложения используйте настоящий сетевой клиент и создайте отдельные сессии для аккаунтов А и Б. Настройте управляемый тестовый сервер так, чтобы он возвращал Set-Cookie с одинаковым именем, но разными значениями. Затем вызовите эндпоинт, возвращающий полученные данные запроса, и проверьте, что сервер получил cookie соответствующего аккаунта.
Запросы должны проходить через сетевую обёртку приложения: нельзя временно обходить перехватчики. Особое внимание уделите заголовкам по умолчанию, механизму повторных попыток и обработке перенаправлений — не обращаются ли они снова к общему хранилищу. Проверяя автоматическую работу с cookie, также не задавайте заголовок вручную: это может скрыть ошибку подключения хранилища на нижнем уровне.
Добавьте проверки границ действия атрибутов
Добавьте сценарии для Path, Secure и срока действия: cookie с узкой областью пути не должны попадать в запросы к посторонним путям; просроченные cookie не должны отправляться; cookie с атрибутом безопасности нужно проверять в контролируемой HTTPS-среде. Локальный HTTP-стенд не позволяет подтвердить корректность поведения для защищённых соединений.
Cookie с одинаковым именем могут одновременно существовать для разных доменов и путей. Проверки должны сравнивать множества сочетаний имени, домена, пути и значения, а не исходить из того, что в хранилище всегда есть только один session. Не делайте и лишних предположений о порядке элементов в строке заголовка: разбирайте её и сравнивайте смысловое содержимое.
При выходе обязательно учитывайте запоздавшие ответы
Сначала переведите аккаунт в состояние «выход выполняется» и запретите создание новых задач, затем отмените старые задачи или дождитесь их завершения. После этого удалите cookie данного аккаунта, сделайте старую сессию недействительной и создайте новый объект для следующего входа.
Одного удаления недостаточно из-за гонки: старый запрос может позже получить Set-Cookie и снова записать cookie в старое хранилище. Отмена тоже не служит синхронным барьером — обратный вызов уже мог попасть в очередь. Поэтому при каждом входе назначайте сессии номер поколения и проверяйте его перед обработкой ответа: ответ старого поколения не должен обновлять состояние текущего пользователя.
В тесте задержите ответ для аккаунта А, выполните выход и вход в аккаунт Б, а затем разрешите доставку ответа для А. В конце проверьте одновременно состояние интерфейса аккаунта Б, идентичность пользователя в логике приложения и cookie следующего запроса. Проверка только того, что хранилище пусто непосредственно в момент выхода, такую проблему не выявит.
Запускайте приёмочные проверки на двух уровнях
При каждом слиянии изменений сначала запускайте скрипт проверки хранилищ без сети, затем на фиксированном симуляторе — тесты с возвратом данных запроса и запоздавшими ответами. Каждый тест должен создавать собственную конфигурацию и тестовые фикстуры, а после завершения останавливать сервер и отменять задачи. Не используйте общие изменяемые сессии, чтобы параллельные запуски не влияли друг на друга.
В отчётах о сбоях сохраняйте идентификатор запроса, поколение сессии, имена и атрибуты cookie, но не реальные значения. Если нужно сравнивать значения, используйте фиксированные маркеры из тестовых фикстур; не переносите данные действующей авторизации из рабочей среды на облачный Mac.
В завершение намеренно подключите оба аккаунта к одному хранилищу и убедитесь, что тесты завершаются ошибкой. Затем намеренно уберите проверку поколения и убедитесь, что тест обнаруживает запоздавший ответ. Только стабильное выявление обоих типов регрессий подтверждает, что контрольная проверка действительно проверяет границы между аккаунтами, а не просто выполняет несколько случайно проходящих утверждений.
Часто задаваемые вопросы
Гарантирует ли ephemeral изоляцию cookie?
Нет. Временная сессия может хранить cookie в памяти. Создайте отдельные конфигурации, проверьте независимость хранилищ и убедитесь, что сетевой слой не подставляет общее хранилище или заголовок Cookie вручную.
Достаточно ли вызвать reset при выходе?
Нет. Сначала запретите новые запросы и завершите текущие задачи, затем удалите cookie аккаунта и аннулируйте старую сессию. reset также затрагивает кеш и учётные данные и не заменяет этот порядок.
Выделите Mac для следующей сборки.
Выберите облачный Mac на физической машине, не меняя привычные процессы Xcode и CI. Подберите конфигурацию под нагрузку проекта; актуальный статус доступности отображается в консоли.