Files
buzz/mobile/ios/RunnerTests/BrowserAuthBridgeTests.swift
Wes 0d19ca69d3 fix(mobile): wait for app activation before browser auth (#8647)
## Summary

Opening an invite from Safari can deliver the login request while iOS
still considers Buzz inactive. `ASWebAuthenticationSession.start()` then
returns false, showing a connection error until the user retries. Wait
for app activation before starting the retained session, with a
ten-second timeout and cancellation cleanup.

Extract the existing browser-auth bridge from the size-limited
AppDelegate and add one native journey test through its Flutter method
channel. URL validation, callback fencing, and normal active-app login
retain their existing behavior. Temporary diagnostic logging and
generated dependency changes are excluded.

### Related issue

No duplicate found in the open handoff/authentication search. Related
invite-consent work: #8601; this fixes the subsequent iOS authentication
presentation race.

### Testing

- Focused `RunnerTests/BrowserAuthBridgeTests` passed on an iPhone 18
Pro / iOS 27 simulator. Covers inactive handoff, cancellation before
activation, retry, duplicate activation notifications, and immediate
active-app start.
- A local build reproduced the original inactive-app `start=false`
failure; the fixed build opened authentication on the first
background-to-app attempt. The same fixed build subsequently completed
an existing-account staging flow.
- Mutation check passed in isolated source, Git metadata, and build
outputs: clean control passed; bypassing the production activation wait
failed the inactive-handoff assertion; restored source passed again,
with its SHA-256 verified against the committed file.
- Self-review found no blocking issue. CI and final review remain
pending. Mandatory pre-commit and pre-push hooks passed, including
mobile analysis and the hook-run Flutter suite. No full local suite was
manually invoked.
- Human acceptance: from Safari, tap **Accept invite in Buzz** and
confirm sign-in opens on the first attempt without **Try again**. Final
acceptance of this PR remains pending.

The separately reported callback failure may have involved production
rather than staging. That cause is unconfirmed and is outside this
change.

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
2026-10-29 09:54:21 -06:00

66 lines
2.9 KiB
Swift

import Flutter
import UIKit
import XCTest
@testable import Buzz
final class BrowserAuthBridgeTests: XCTestCase {
@MainActor
func testInviteHandoffWaitsForActivationAndCancelledAttemptCannotReopen() {
let messenger = BrowserAuthTestMessenger()
var active = false
var starts = 0
let bridge = BuzzBrowserAuthBridge(messenger: messenger, isActive: { active },
startSession: { _ in starts += 1; return true })
defer { withExtendedLifetime(bridge) {} }
let arguments = ["url": "https://app.builderlabstaging.xyz/api/goose/v1/auth/login"]
var results: [String] = []
messenger.invoke("start", arguments: arguments) {
results.append(($0 as? FlutterError)?.code ?? "success")
}
XCTAssertEqual(starts, 0, "An incoming invite must not present auth while inactive")
XCTAssertTrue(results.isEmpty)
messenger.invoke("cancel", arguments: [:]) { _ in }
XCTAssertEqual(results, ["cancelled"])
active = true
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
XCTAssertEqual(starts, 0, "Cancelled handoffs must not reopen sign-in")
active = false
messenger.invoke("start", arguments: arguments) {
results.append(($0 as? FlutterError)?.code ?? "success")
}
XCTAssertEqual(starts, 0)
active = true
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
XCTAssertEqual(starts, 1, "Activation must present the original request without a retry")
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
XCTAssertEqual(starts, 1)
XCTAssertEqual(results, ["cancelled"])
messenger.invoke("cancel", arguments: [:]) { _ in }
XCTAssertEqual(results, ["cancelled", "cancelled"])
messenger.invoke("start", arguments: arguments) { _ in }
XCTAssertEqual(starts, 2, "Ordinary active-app login should start immediately")
messenger.invoke("cancel", arguments: [:]) { _ in }
}
}
private final class BrowserAuthTestMessenger: NSObject, FlutterBinaryMessenger {
private var handler: FlutterBinaryMessageHandler?
func send(onChannel channel: String, message: Data?) {}
func send(onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply?) {
callback?(nil)
}
func setMessageHandlerOnChannel(_ channel: String,
binaryMessageHandler handler: FlutterBinaryMessageHandler?) -> FlutterBinaryMessengerConnection {
self.handler = handler
return 1
}
func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) { handler = nil }
func invoke(_ method: String, arguments: [String: String], reply: @escaping (Any?) -> Void) {
let codec = FlutterStandardMethodCodec.sharedInstance()
handler?(codec.encode(FlutterMethodCall(methodName: method, arguments: arguments))) {
reply($0.flatMap { codec.decodeEnvelope($0) })
}
}
}