Testing iOS Cookie Isolation and Logout Boundaries on a Cloud Mac
If account B receives account A’s data during an account-switching regression test on the same cloud Mac, don’t rush to reset the simulator. Inspect the cookie stores, request headers, and tasks still in flight separately. The old session may be coming from shared storage, a header set manually by the networking wrapper, or even a response that arrives after logout.
Start with an offline check using the networking framework built into macOS, then apply the same assertions in the iOS simulator. Keep the checks in the repository so they can be rerun after every checkout on the cloud Mac, without depending on state left behind by a previous graphical session.
Define Session Ownership First
Create a separate session object for each signed-in account, and let it own its configuration, cookie store, and collection of tasks. Don’t make mutable configuration a global singleton, and don’t expect changes to the original configuration to affect an existing URLSession: the session copies its configuration when it is created.
Document at least three invariants: account A’s cookies must not enter account B’s requests; logout must delete only data owned by the current account; and responses from before logout must not restore the signed-in state. If anonymous requests need no cookies at all, disable automatic cookie handling and check that the wrapper does not manually set a Cookie header.
ephemeralmeans session data is not persisted to disk. It does not mean there are no cookies in memory, nor does it automatically isolate two application accounts.
Verify Store Independence with a Script
Save the following as cookie-check.swift. It uses only fictitious session values and the local localhost domain. It sends no network requests and does not access the default shared store.
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")
Run it on a machine where command-line tool setup is already complete:
set -eu
xcrun swift cookie-check.swift
A successful run produces no output; a failed assertion exits with a nonzero status. Clearing all of a here assumes that it belongs exclusively to account A. If the application still uses shared storage, you must first filter cookies by account ownership rather than copy this delete-all approach.
The script verifies store independence and the scope of deletion. It does not prove that real requests are wired up correctly. Passing this check alone is not a networking-layer acceptance pass.
Test Real Requests and Responses
Reuse the actual network client in the application’s test target, creating separate sessions for accounts A and B. Have a controllable test server return Set-Cookie headers with the same cookie name but different values. Then call an echo endpoint and assert that the cookies received by the server belong to the corresponding account.
Requests must pass through the application’s networking wrapper; don’t bypass interceptors just for the test. Pay particular attention to whether default headers, retry handlers, or redirect handling read from shared storage again. When testing automatic cookie handling, don’t fill in request headers manually either: doing so can hide incorrect wiring underneath.
Cover Cookie Attribute Boundaries
Add cases for Path, Secure, and expiration: a cookie scoped to a narrow path must not appear on unrelated paths, and an expired cookie must no longer be sent. Validate secure cookies in a controlled HTTPS environment. A local HTTP fixture cannot verify secure-connection semantics.
Cookies with the same name can coexist when their domains or paths differ. Assertions should compare sets of names, domains, paths, and values, rather than assume the store will always contain exactly one session cookie. Avoid making rigid assumptions about ordering in the request header string, too; parse it and compare its meaning instead.
Handle Late Responses During Logout
First move the account into a “logging out” state and reject new task creation. Then cancel or drain existing tasks. After that, delete the account’s cookies, invalidate the old session, and create a new object for the next login.
Deletion alone leaves a race condition: an old request may receive a Set-Cookie header later and write back to the old store. Cancellation is not a synchronization barrier either; callbacks may already be queued. Assign a session generation to each login and check it before processing responses. An old generation must not update the current user state.
In the test, delay account A’s response, trigger logout, sign in as account B, and only then release A’s response. Finally, check B’s UI state, application-level identity, and the cookies sent with the next request. Checking only that the store is empty at the instant of logout will not catch this class of bug.
Run the Regression Gate in Two Layers
For every merge, run the offline storage script first, then run the request-echo and late-response tests on a fixed simulator. Each test should create its own configuration and fixtures, stop its server and cancel its tasks when finished, and avoid sharing mutable sessions so parallel runs cannot contaminate one another.
When recording failures, retain request IDs, session generations, and cookie names and attributes, but not real cookie values. If values must be compared, use fixed markers from the test fixtures. Do not copy production login state onto the cloud Mac.
Finally, deliberately make the two accounts share a store and confirm that the tests fail. Then deliberately remove the generation check and confirm that the late response is caught. Only when the gate reliably detects both regressions can you say it tests account boundaries, rather than merely a few assertions that happen to pass.
Frequently asked questions
Does an ephemeral configuration guarantee cookie isolation?
No. Ephemeral sessions can retain cookies in memory. Create separate configurations, verify that their stores are distinct, and check that your networking layer does not inject a shared store or a manual Cookie header.
Is calling reset enough when a user logs out?
No. Block new requests, cancel or drain existing tasks, clear the account's cookies, and invalidate the old session. reset also affects caches and credentials, so it is not a substitute for an explicit logout sequence.
Give your next build a dedicated Mac.
Choose a cloud Mac that isn’t a virtual machine, and keep your existing Xcode and CI workflows. Select a machine based on your workload; check the console for real-time availability.