mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Harden PTT audio and the 1.7.1 release (#1423)
Centralize PTT and voice audio-session ownership, harden courier/bridge/outbox delivery and recovery, correct location and delivery-state races, add privacy/release metadata, and ship reproducible universal Arti slices with Release CI coverage. Validated by the full iOS suite, repeated audio/fragment/performance regressions, BitFoundation tests, strict lint and dead-code analysis, universal iOS Release builds, and iOS/macOS archives.
This commit is contained in:
@@ -41,12 +41,17 @@ private final class StubLocationManaging: LocationStateManaging {
|
||||
weak var delegate: CLLocationManagerDelegate?
|
||||
var desiredAccuracy: CLLocationAccuracy = 0
|
||||
var distanceFilter: CLLocationDistance = 0
|
||||
var authorizationStatus: CLAuthorizationStatus = .denied
|
||||
var authorizationStatus: CLAuthorizationStatus
|
||||
private(set) var stopUpdatingLocationCallCount = 0
|
||||
|
||||
init(authorizationStatus: CLAuthorizationStatus = .denied) {
|
||||
self.authorizationStatus = authorizationStatus
|
||||
}
|
||||
|
||||
func requestWhenInUseAuthorization() {}
|
||||
func requestLocation() {}
|
||||
func startUpdatingLocation() {}
|
||||
func stopUpdatingLocation() {}
|
||||
func stopUpdatingLocation() { stopUpdatingLocationCallCount += 1 }
|
||||
}
|
||||
|
||||
private final class StubLocationGeocoder: LocationStateGeocoding {
|
||||
@@ -62,7 +67,11 @@ private final class StubLocationGeocoder: LocationStateGeocoding {
|
||||
// MARK: - Helpers
|
||||
|
||||
@MainActor
|
||||
private func makeLocationManager(storage: UserDefaults? = nil) -> LocationStateManager {
|
||||
private func makeLocationManager(
|
||||
storage: UserDefaults? = nil,
|
||||
authorizationStatus: CLAuthorizationStatus = .denied,
|
||||
shouldInitializeCoreLocation: Bool = false
|
||||
) -> LocationStateManager {
|
||||
let suiteName = "GeoChannelCoordinatorContextTests-\(UUID().uuidString)"
|
||||
let defaults = storage ?? UserDefaults(suiteName: suiteName)!
|
||||
if storage == nil {
|
||||
@@ -70,9 +79,9 @@ private func makeLocationManager(storage: UserDefaults? = nil) -> LocationStateM
|
||||
}
|
||||
return LocationStateManager(
|
||||
storage: defaults,
|
||||
locationManager: StubLocationManaging(),
|
||||
locationManager: StubLocationManaging(authorizationStatus: authorizationStatus),
|
||||
geocoder: StubLocationGeocoder(),
|
||||
shouldInitializeCoreLocation: false
|
||||
shouldInitializeCoreLocation: shouldInitializeCoreLocation
|
||||
)
|
||||
}
|
||||
|
||||
@@ -172,14 +181,21 @@ struct GeoChannelCoordinatorContextTests {
|
||||
@Test @MainActor
|
||||
func buildingCellJoinsSamplingOnlyAfterNotesReveal() async {
|
||||
TorManager.shared.setAppForeground(true)
|
||||
let locationManager = makeLocationManager()
|
||||
let locationManager = makeLocationManager(
|
||||
authorizationStatus: .authorizedAlways,
|
||||
shouldInitializeCoreLocation: true
|
||||
)
|
||||
#expect(await waitUntil { locationManager.permissionState == .authorized })
|
||||
let context = MockGeoChannelContext()
|
||||
let revealed = CurrentValueSubject<Bool, Never>(false)
|
||||
let notesEnabled = CurrentValueSubject<Bool, Never>(true)
|
||||
let coordinator = GeoChannelCoordinator(
|
||||
locationManager: locationManager,
|
||||
bookmarksStore: locationManager,
|
||||
torManager: TorManager.shared,
|
||||
notesRevealed: revealed.eraseToAnyPublisher(),
|
||||
locationNotesEnabled: true,
|
||||
locationNotesSettings: notesEnabled.eraseToAnyPublisher(),
|
||||
context: context
|
||||
)
|
||||
defer { withExtendedLifetime(coordinator) {} }
|
||||
@@ -205,13 +221,66 @@ struct GeoChannelCoordinatorContextTests {
|
||||
revealed.send(true)
|
||||
#expect(await waitUntil { context.beginSamplingCalls.last?.contains(building) == true })
|
||||
#expect(context.beginSamplingCalls.last?.count == GeohashChannelLevel.allCases.count)
|
||||
|
||||
// The app-info kill switch must narrow the already-live sampling set
|
||||
// immediately, without waiting for another location update.
|
||||
notesEnabled.send(false)
|
||||
#expect(await waitUntil {
|
||||
context.beginSamplingCalls.last?.contains(building) == false &&
|
||||
context.beginSamplingCalls.last?.count == GeohashChannelLevel.allCases.count - 1
|
||||
})
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func permissionRevocationEndsCachedRegionalSampling_butBookmarksRemainEligible() async {
|
||||
TorManager.shared.setAppForeground(true)
|
||||
let locationManager = makeLocationManager(
|
||||
authorizationStatus: .authorizedAlways,
|
||||
shouldInitializeCoreLocation: true
|
||||
)
|
||||
#expect(await waitUntil { locationManager.permissionState == .authorized })
|
||||
let context = MockGeoChannelContext()
|
||||
let revealed = CurrentValueSubject<Bool, Never>(true)
|
||||
let coordinator = GeoChannelCoordinator(
|
||||
locationManager: locationManager,
|
||||
bookmarksStore: locationManager,
|
||||
torManager: TorManager.shared,
|
||||
notesRevealed: revealed.eraseToAnyPublisher(),
|
||||
locationNotesEnabled: true,
|
||||
locationNotesSettings: Empty().eraseToAnyPublisher(),
|
||||
context: context
|
||||
)
|
||||
defer { withExtendedLifetime(coordinator) {} }
|
||||
|
||||
locationManager.locationManager(
|
||||
CLLocationManager(),
|
||||
didUpdateLocations: [CLLocation(latitude: 21.2850, longitude: -157.8357)]
|
||||
)
|
||||
#expect(await waitUntil {
|
||||
context.beginSamplingCalls.last?.count == GeohashChannelLevel.allCases.count
|
||||
})
|
||||
let cachedChannels = locationManager.availableChannels
|
||||
let endCountBeforeRevocation = context.endSamplingCount
|
||||
|
||||
locationManager.locationManager(CLLocationManager(), didChangeAuthorization: .denied)
|
||||
|
||||
#expect(await waitUntil {
|
||||
locationManager.permissionState == .denied &&
|
||||
context.endSamplingCount > endCountBeforeRevocation
|
||||
})
|
||||
#expect(locationManager.availableChannels == cachedChannels)
|
||||
|
||||
// A bookmark is an explicit remote scope and does not derive from
|
||||
// the now-revoked device location.
|
||||
locationManager.addBookmark("u4pru")
|
||||
#expect(await waitUntil { context.beginSamplingCalls.last == ["u4pru"] })
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func releasedContext_isHeldWeaklyAndSafelyIgnored() async {
|
||||
let locationManager = makeLocationManager()
|
||||
var context: MockGeoChannelContext? = MockGeoChannelContext()
|
||||
weak var weakContext = context
|
||||
let weakContext = { [weak context] in context }
|
||||
let coordinator = GeoChannelCoordinator(
|
||||
locationManager: locationManager,
|
||||
bookmarksStore: locationManager,
|
||||
@@ -223,7 +292,7 @@ struct GeoChannelCoordinatorContextTests {
|
||||
|
||||
// The coordinator must not keep the owner alive (it is owned by it).
|
||||
context = nil
|
||||
#expect(weakContext == nil)
|
||||
#expect(weakContext() == nil)
|
||||
|
||||
// Events after the owner is gone are safely dropped.
|
||||
locationManager.select(.location(GeohashChannel(level: .city, geohash: "u4pru")))
|
||||
|
||||
Reference in New Issue
Block a user