mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 09:05:19 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdfd4345cf | ||
|
|
32e1c99441 | ||
|
|
399cdf95ab |
@@ -0,0 +1,66 @@
|
|||||||
|
//
|
||||||
|
// BLEOriginTTLPolicy.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Chooses the TTL a locally originated public broadcast leaves with.
|
||||||
|
///
|
||||||
|
/// Split out from `BLEService` so the choice is testable without a radio, and so
|
||||||
|
/// the reasoning lives in one place: see
|
||||||
|
/// `TransportConfig.broadcastOriginTTLRange` for why originating at a fixed
|
||||||
|
/// maximum identifies the author to any direct listener.
|
||||||
|
enum BLEOriginTTLPolicy {
|
||||||
|
/// Uniform draw from the configured range.
|
||||||
|
///
|
||||||
|
/// The randomizer is injectable so tests can pin the value; production uses
|
||||||
|
/// the system generator. Note that TTL is excluded from the packet signature
|
||||||
|
/// (`toBinaryDataForSigning` zeroes it so relays can decrement without
|
||||||
|
/// invalidating), so varying it per message is signature-safe and needs no
|
||||||
|
/// cross-platform agreement — a peer running any version simply sees a
|
||||||
|
/// smaller starting TTL and relays it normally.
|
||||||
|
static func originTTL(
|
||||||
|
range: ClosedRange<UInt8> = TransportConfig.broadcastOriginTTLRange,
|
||||||
|
randomTTL: (ClosedRange<UInt8>) -> UInt8 = { UInt8.random(in: $0) }
|
||||||
|
) -> UInt8 {
|
||||||
|
// A degenerate or inverted range must not trap; fall back to the
|
||||||
|
// documented default rather than crashing a send path.
|
||||||
|
guard range.lowerBound <= range.upperBound, range.lowerBound >= 1 else {
|
||||||
|
return TransportConfig.messageTTLDefault
|
||||||
|
}
|
||||||
|
return randomTTL(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gap after which the next voice frame counts as a new talk burst.
|
||||||
|
static let voiceBurstGap: TimeInterval = 1.0
|
||||||
|
|
||||||
|
/// TTL for a live-voice frame: one draw per talk burst, not per frame.
|
||||||
|
///
|
||||||
|
/// This distinction is the whole value. Voice leaves at roughly 15 frames a
|
||||||
|
/// second, so drawing per frame would hand an observer the maximum of the
|
||||||
|
/// range within a fraction of a second — averaging over a burst would
|
||||||
|
/// defeat the randomisation completely and cost reach for nothing. One draw
|
||||||
|
/// per burst makes a burst a single sample, the same as a text message.
|
||||||
|
///
|
||||||
|
/// Returns the TTL to use and the burst TTL to remember. A burst ends when
|
||||||
|
/// `voiceBurstGap` passes with no frame.
|
||||||
|
static func voiceBurstTTL(
|
||||||
|
now: Date,
|
||||||
|
lastFrameAt: Date?,
|
||||||
|
currentBurstTTL: UInt8?,
|
||||||
|
burstGap: TimeInterval = voiceBurstGap,
|
||||||
|
range: ClosedRange<UInt8> = TransportConfig.broadcastOriginTTLRange,
|
||||||
|
randomTTL: (ClosedRange<UInt8>) -> UInt8 = { UInt8.random(in: $0) }
|
||||||
|
) -> UInt8 {
|
||||||
|
if let currentBurstTTL,
|
||||||
|
let lastFrameAt,
|
||||||
|
now.timeIntervalSince(lastFrameAt) < burstGap {
|
||||||
|
return currentBurstTTL
|
||||||
|
}
|
||||||
|
return originTTL(range: range, randomTTL: randomTTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -227,6 +227,12 @@ final class BLEService: NSObject {
|
|||||||
private let bleMaxMTU = 512
|
private let bleMaxMTU = 512
|
||||||
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
private let maxMessageLength = InputValidator.Limits.maxMessageLength
|
||||||
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
private let messageTTL: UInt8 = TransportConfig.messageTTLDefault
|
||||||
|
/// Live-voice burst TTL state: one draw per talk burst rather than per
|
||||||
|
/// frame, so a ~15 fps stream is a single sample to an observer instead of
|
||||||
|
/// handing over the range maximum immediately. Both are touched only on
|
||||||
|
/// `messageQueue`.
|
||||||
|
private var currentVoiceBurstTTL: UInt8?
|
||||||
|
private var lastVoiceBurstFrameAt: Date?
|
||||||
// Flood/battery controls
|
// Flood/battery controls
|
||||||
private let maxInFlightAssemblies = TransportConfig.bleMaxInFlightAssemblies // cap concurrent fragment assemblies
|
private let maxInFlightAssemblies = TransportConfig.bleMaxInFlightAssemblies // cap concurrent fragment assemblies
|
||||||
private let highDegreeThreshold = TransportConfig.bleHighDegreeThreshold // for adaptive TTL/probabilistic relays
|
private let highDegreeThreshold = TransportConfig.bleHighDegreeThreshold // for adaptive TTL/probabilistic relays
|
||||||
@@ -869,7 +875,9 @@ final class BLEService: NSObject {
|
|||||||
timestamp: sendTimestampMs,
|
timestamp: sendTimestampMs,
|
||||||
payload: Data(content.utf8),
|
payload: Data(content.utf8),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
// Not the fixed maximum: that would mark every message this device
|
||||||
|
// wrote as originating here, to any direct listener.
|
||||||
|
ttl: BLEOriginTTLPolicy.originTTL()
|
||||||
)
|
)
|
||||||
guard let signedPacket = noiseService.signPacket(basePacket) else {
|
guard let signedPacket = noiseService.signPacket(basePacket) else {
|
||||||
SecureLogger.error("❌ Failed to sign public message", category: .security)
|
SecureLogger.error("❌ Failed to sign public message", category: .security)
|
||||||
@@ -980,7 +988,7 @@ final class BLEService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: Data(),
|
payload: Data(),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: messageTTL
|
ttl: BLEOriginTTLPolicy.originTTL()
|
||||||
)
|
)
|
||||||
|
|
||||||
if let signed = noiseService.signPacket(leavePacket) {
|
if let signed = noiseService.signPacket(leavePacket) {
|
||||||
@@ -1617,7 +1625,7 @@ final class BLEService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: self.messageTTL,
|
ttl: BLEOriginTTLPolicy.originTTL(),
|
||||||
version: 2
|
version: 2
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2892,7 +2900,12 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
let (connectedPeerIDs, advertisedCapabilities, advertisedBridgeCell): ([Data], PeerCapabilities, String?) = collectionsQueue.sync {
|
let (connectedPeerIDs, advertisedCapabilities, advertisedBridgeCell): ([Data], PeerCapabilities, String?) = collectionsQueue.sync {
|
||||||
(
|
(
|
||||||
peerRegistry.connectedRoutingData,
|
// Publishing the neighbour list hands the local adjacency graph
|
||||||
|
// to a single passive receiver; see
|
||||||
|
// TransportConfig.announceIncludesDirectNeighbors.
|
||||||
|
TransportConfig.announceIncludesDirectNeighbors
|
||||||
|
? peerRegistry.connectedRoutingData
|
||||||
|
: [],
|
||||||
PeerCapabilities.localSupported.union(runtimeCapabilities),
|
PeerCapabilities.localSupported.union(runtimeCapabilities),
|
||||||
runtimeCapabilities.contains(.bridge) ? localBridgeGeohash : nil
|
runtimeCapabilities.contains(.bridge) ? localBridgeGeohash : nil
|
||||||
)
|
)
|
||||||
@@ -2971,7 +2984,7 @@ final class BLEService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: envelope,
|
payload: envelope,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: self.messageTTL
|
ttl: BLEOriginTTLPolicy.originTTL()
|
||||||
)
|
)
|
||||||
// Pre-mark our own broadcast as processed to avoid handling a
|
// Pre-mark our own broadcast as processed to avoid handling a
|
||||||
// relayed self copy.
|
// relayed self copy.
|
||||||
@@ -3027,6 +3040,16 @@ final class BLEService: NSObject {
|
|||||||
guard !burstContent.isEmpty else { return }
|
guard !burstContent.isEmpty else { return }
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
// One TTL draw per talk burst. Drawing per frame would leak the
|
||||||
|
// maximum within a fraction of a second at ~15 frames/sec.
|
||||||
|
let now = Date()
|
||||||
|
let burstTTL = BLEOriginTTLPolicy.voiceBurstTTL(
|
||||||
|
now: now,
|
||||||
|
lastFrameAt: self.lastVoiceBurstFrameAt,
|
||||||
|
currentBurstTTL: self.currentVoiceBurstTTL
|
||||||
|
)
|
||||||
|
self.currentVoiceBurstTTL = burstTTL
|
||||||
|
self.lastVoiceBurstFrameAt = now
|
||||||
let packet = BitchatPacket(
|
let packet = BitchatPacket(
|
||||||
type: MessageType.voiceFrame.rawValue,
|
type: MessageType.voiceFrame.rawValue,
|
||||||
senderID: self.myPeerIDData,
|
senderID: self.myPeerIDData,
|
||||||
@@ -3034,7 +3057,7 @@ final class BLEService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: burstContent,
|
payload: burstContent,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: self.messageTTL
|
ttl: burstTTL
|
||||||
)
|
)
|
||||||
guard let signedPacket = self.noiseService.signPacket(packet) else {
|
guard let signedPacket = self.noiseService.signPacket(packet) else {
|
||||||
SecureLogger.error("❌ Failed to sign voice frame", category: .security)
|
SecureLogger.error("❌ Failed to sign voice frame", category: .security)
|
||||||
@@ -7609,7 +7632,7 @@ extension BLEService {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: payload,
|
payload: payload,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: self.messageTTL
|
ttl: BLEOriginTTLPolicy.originTTL()
|
||||||
)
|
)
|
||||||
guard let signedPacket = self.noiseService.signPacket(basePacket) else {
|
guard let signedPacket = self.noiseService.signPacket(basePacket) else {
|
||||||
SecureLogger.error("❌ Failed to sign board packet", category: .security)
|
SecureLogger.error("❌ Failed to sign board packet", category: .security)
|
||||||
|
|||||||
@@ -6,6 +6,73 @@ enum TransportConfig {
|
|||||||
// BLE / Protocol
|
// BLE / Protocol
|
||||||
static let bleDefaultFragmentSize: Int = 469 // ~512 MTU minus protocol overhead
|
static let bleDefaultFragmentSize: Int = 469 // ~512 MTU minus protocol overhead
|
||||||
static let messageTTLDefault: UInt8 = 7 // Default TTL for mesh flooding
|
static let messageTTLDefault: UInt8 = 7 // Default TTL for mesh flooding
|
||||||
|
|
||||||
|
/// TTL range a public broadcast is originated with.
|
||||||
|
///
|
||||||
|
/// Originating every message at the maximum makes `ttl == messageTTLDefault`
|
||||||
|
/// a reliable "this device wrote it" marker for any direct listener, which
|
||||||
|
/// tells a passive observer who *said* a thing rather than merely who is
|
||||||
|
/// present. Drawing from a range makes the lower values ambiguous between an
|
||||||
|
/// origin and a relay: in a dense graph relays clamp broadcasts to 5, so an
|
||||||
|
/// origin emitting 5 is indistinguishable from relayed traffic, and in a
|
||||||
|
/// sparse chain a 6 could be an origin or one hop from a 7.
|
||||||
|
///
|
||||||
|
/// **What this does not do, stated plainly.** Every relay branch in
|
||||||
|
/// `RelayController` emits `ttlLimit - 1`, so TTL is strictly decreasing and
|
||||||
|
/// the top of whatever range is chosen can only ever come from an origin.
|
||||||
|
/// With three values that is one message in three, and the probability that
|
||||||
|
/// a sender has revealed itself after `k` messages is `1 - (2/3)^k` — about
|
||||||
|
/// 87% by the fifth. So this meaningfully protects an occasional sender and
|
||||||
|
/// barely protects a chatty one.
|
||||||
|
///
|
||||||
|
/// Removing the marker entirely is not possible from the origin side: it
|
||||||
|
/// needs relays to sometimes *not* decrement, which trades directly against
|
||||||
|
/// TTL's job as the loop bound. Widening the range trades against reach.
|
||||||
|
/// Both belong in a follow-up with the mesh behaviour in scope; what this
|
||||||
|
/// constant buys is that a single observed packet is no longer conclusive.
|
||||||
|
///
|
||||||
|
/// The cost is reach: a message originated at 5 crosses two fewer hops than
|
||||||
|
/// one at 7. The upper bound stays at the default so the common case is
|
||||||
|
/// unchanged, and the floor is deliberately not lower than the dense-graph
|
||||||
|
/// relay clamp — going below it would cost reach without buying ambiguity
|
||||||
|
/// that clamp does not already provide.
|
||||||
|
///
|
||||||
|
/// Applied to public broadcasts that carry content this device authored:
|
||||||
|
/// public messages, group messages, broadcast files, board posts, live
|
||||||
|
/// voice (one draw per talk burst, see `BLEOriginTTLPolicy`), and leave.
|
||||||
|
/// Deliberately excluded:
|
||||||
|
///
|
||||||
|
/// - **Announces.** Link binding treats `ttl == messageTTLDefault` on an
|
||||||
|
/// announce as "direct link", and an announce's sender ID already
|
||||||
|
/// identifies the device — nothing to hide, something to break.
|
||||||
|
/// - **Directed traffic** (DMs, handshakes, courier envelopes, directed
|
||||||
|
/// files). Fewer hops means fewer deliveries, and the trade needs its own
|
||||||
|
/// look rather than riding along here.
|
||||||
|
/// - **Prekey bundles and gateway carriers.** A bundle already contains its
|
||||||
|
/// owner's key, and carriers are re-broadcasts rather than authorship.
|
||||||
|
static let broadcastOriginTTLRange: ClosedRange<UInt8> = 5...7
|
||||||
|
|
||||||
|
/// Whether signed announces advertise this device's direct neighbours.
|
||||||
|
///
|
||||||
|
/// The neighbour TLV carries up to ten 8-byte peer IDs, so a *single*
|
||||||
|
/// passive receiver can reconstruct the local adjacency graph — who is
|
||||||
|
/// standing next to whom — without needing several receivers or RSSI
|
||||||
|
/// trilateration. For a crowd, that is the most sensitive thing the radio
|
||||||
|
/// layer discloses, and unlike the identity keys it is not needed for the
|
||||||
|
/// protocol to work.
|
||||||
|
///
|
||||||
|
/// Turning it off costs source routing. `MeshTopologyTracker` builds its
|
||||||
|
/// adjacency map from these lists, and `computeRoute` needs that map, so
|
||||||
|
/// with every device silent there are no routes to compute and directed
|
||||||
|
/// traffic falls back to flooding — which is the documented fallback and is
|
||||||
|
/// already what happens whenever a route fails. Expect more airtime for
|
||||||
|
/// directed sends in dense meshes, and no correctness change.
|
||||||
|
///
|
||||||
|
/// Kept as a constant rather than a user setting because it is a protocol
|
||||||
|
/// trade-off, not a preference: flipping it back is a one-line change, and
|
||||||
|
/// receiving peers' lists is unaffected either way, so a mixed network
|
||||||
|
/// behaves sensibly during any transition.
|
||||||
|
static let announceIncludesDirectNeighbors = false
|
||||||
static let bleMaxInFlightAssemblies: Int = 128 // Cap concurrent fragment assemblies
|
static let bleMaxInFlightAssemblies: Int = 128 // Cap concurrent fragment assemblies
|
||||||
static let bleHighDegreeThreshold: Int = 6 // For adaptive TTL/probabilistic relays
|
static let bleHighDegreeThreshold: Int = 6 // For adaptive TTL/probabilistic relays
|
||||||
static let bleMaxConcurrentTransfers: Int = 2 // Limit simultaneous large media sends
|
static let bleMaxConcurrentTransfers: Int = 2 // Limit simultaneous large media sends
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ struct BLEServiceCoreTests {
|
|||||||
ble._test_handlePacket(packet, fromPeerID: sender, signingPublicKey: signingKey)
|
ble._test_handlePacket(packet, fromPeerID: sender, signingPublicKey: signingKey)
|
||||||
let receivedDuplicate = await TestHelpers.waitUntil(
|
let receivedDuplicate = await TestHelpers.waitUntil(
|
||||||
{ delegate.publicMessagesSnapshot().count > 1 },
|
{ delegate.publicMessagesSnapshot().count > 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!receivedDuplicate)
|
#expect(!receivedDuplicate)
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ struct BLEServiceCoreTests {
|
|||||||
|
|
||||||
let unsignedRelayed = await TestHelpers.waitUntil(
|
let unsignedRelayed = await TestHelpers.waitUntil(
|
||||||
{ outbound.count(ofType: .leave) > 0 },
|
{ outbound.count(ofType: .leave) > 0 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!unsignedRelayed)
|
#expect(!unsignedRelayed)
|
||||||
#expect(ble.currentPeerSnapshots().contains { $0.peerID == alicePeerID })
|
#expect(ble.currentPeerSnapshots().contains { $0.peerID == alicePeerID })
|
||||||
@@ -133,7 +133,7 @@ struct BLEServiceCoreTests {
|
|||||||
|
|
||||||
let badSignatureRelayed = await TestHelpers.waitUntil(
|
let badSignatureRelayed = await TestHelpers.waitUntil(
|
||||||
{ outbound.count(ofType: .leave) > 0 },
|
{ outbound.count(ofType: .leave) > 0 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!badSignatureRelayed)
|
#expect(!badSignatureRelayed)
|
||||||
#expect(ble.currentPeerSnapshots().contains { $0.peerID == alicePeerID })
|
#expect(ble.currentPeerSnapshots().contains { $0.peerID == alicePeerID })
|
||||||
@@ -1209,7 +1209,7 @@ struct BLEServiceCoreTests {
|
|||||||
let didObservePanicClosure = await withCheckedContinuation { continuation in
|
let didObservePanicClosure = await withCheckedContinuation { continuation in
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
let didObserveClosure = panicIngressObserver.waitUntilClosed(
|
let didObserveClosure = panicIngressObserver.waitUntilClosed(
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
gate.release()
|
gate.release()
|
||||||
continuation.resume(returning: didObserveClosure)
|
continuation.resume(returning: didObserveClosure)
|
||||||
@@ -1340,7 +1340,7 @@ struct BLEServiceCoreTests {
|
|||||||
// rotated sender IDs never bought a sixth response.
|
// rotated sender IDs never bought a sixth response.
|
||||||
let exceededBudget = await TestHelpers.waitUntil(
|
let exceededBudget = await TestHelpers.waitUntil(
|
||||||
{ outbound.count(ofType: .pong) > budget },
|
{ outbound.count(ofType: .pong) > budget },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!exceededBudget)
|
#expect(!exceededBudget)
|
||||||
#expect(outbound.count(ofType: .pong) == budget)
|
#expect(outbound.count(ofType: .pong) == budget)
|
||||||
|
|||||||
@@ -43,14 +43,14 @@ struct ChatViewModelRefactoringTests {
|
|||||||
transport.simulateConnect(peerID, nickname: "alice")
|
transport.simulateConnect(peerID, nickname: "alice")
|
||||||
|
|
||||||
let didResolve = await TestHelpers.waitUntil({ viewModel.getPeerIDForNickname("alice") != nil },
|
let didResolve = await TestHelpers.waitUntil({ viewModel.getPeerIDForNickname("alice") != nil },
|
||||||
timeout: TestConstants.settleTimeout)
|
timeout: TestConstants.shortTimeout)
|
||||||
#expect(didResolve)
|
#expect(didResolve)
|
||||||
|
|
||||||
// Action: User types /msg command
|
// Action: User types /msg command
|
||||||
viewModel.sendMessage("/msg @alice Hello Private World")
|
viewModel.sendMessage("/msg @alice Hello Private World")
|
||||||
|
|
||||||
let didSend = await TestHelpers.waitUntil({ transport.sentPrivateMessages.count == 1 },
|
let didSend = await TestHelpers.waitUntil({ transport.sentPrivateMessages.count == 1 },
|
||||||
timeout: TestConstants.settleTimeout)
|
timeout: TestConstants.shortTimeout)
|
||||||
#expect(didSend)
|
#expect(didSend)
|
||||||
|
|
||||||
// Assert:
|
// Assert:
|
||||||
@@ -74,7 +74,7 @@ struct ChatViewModelRefactoringTests {
|
|||||||
transport.simulateConnect(peerID, nickname: "troll")
|
transport.simulateConnect(peerID, nickname: "troll")
|
||||||
|
|
||||||
let didResolve = await TestHelpers.waitUntil({ viewModel.getPeerIDForNickname("troll") != nil },
|
let didResolve = await TestHelpers.waitUntil({ viewModel.getPeerIDForNickname("troll") != nil },
|
||||||
timeout: TestConstants.settleTimeout)
|
timeout: TestConstants.shortTimeout)
|
||||||
#expect(didResolve)
|
#expect(didResolve)
|
||||||
|
|
||||||
// Action
|
// Action
|
||||||
@@ -83,7 +83,7 @@ struct ChatViewModelRefactoringTests {
|
|||||||
// Assert
|
// Assert
|
||||||
// Verify identity manager was called to block "fingerprint_123"
|
// Verify identity manager was called to block "fingerprint_123"
|
||||||
let didBlock = await TestHelpers.waitUntil({ identity.isBlocked(fingerprint: "fingerprint_123") },
|
let didBlock = await TestHelpers.waitUntil({ identity.isBlocked(fingerprint: "fingerprint_123") },
|
||||||
timeout: TestConstants.settleTimeout)
|
timeout: TestConstants.shortTimeout)
|
||||||
#expect(didBlock)
|
#expect(didBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ struct ChatViewModelRefactoringTests {
|
|||||||
// Wait for async processing with proper timeout
|
// Wait for async processing with proper timeout
|
||||||
let found = await TestHelpers.waitUntil(
|
let found = await TestHelpers.waitUntil(
|
||||||
{ viewModel.privateChats[senderID]?.first?.content == "Secret" },
|
{ viewModel.privateChats[senderID]?.first?.content == "Secret" },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -140,7 +140,7 @@ struct ChatViewModelRefactoringTests {
|
|||||||
{
|
{
|
||||||
viewModel.publicMessages(for: .mesh).contains(where: { $0.content == "Public Hi" })
|
viewModel.publicMessages(for: .mesh).contains(where: { $0.content == "Public Hi" })
|
||||||
},
|
},
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ struct ChatViewModelCommandTests {
|
|||||||
transport.simulateConnect(peerID, nickname: "Alice")
|
transport.simulateConnect(peerID, nickname: "Alice")
|
||||||
let resolved = await TestHelpers.waitUntil({
|
let resolved = await TestHelpers.waitUntil({
|
||||||
viewModel.getPeerIDForNickname("Alice") == peerID
|
viewModel.getPeerIDForNickname("Alice") == peerID
|
||||||
}, timeout: TestConstants.negativeWaitWindow)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
#expect(resolved)
|
#expect(resolved)
|
||||||
|
|
||||||
viewModel.handleCommand("/msg Alice")
|
viewModel.handleCommand("/msg Alice")
|
||||||
@@ -422,7 +422,7 @@ struct ChatViewModelServiceLifecycleTests {
|
|||||||
transport.sentReadReceipts.contains {
|
transport.sentReadReceipts.contains {
|
||||||
$0.peerID == peerID && $0.receipt.originalMessageID == "read-1"
|
$0.peerID == peerID && $0.receipt.originalMessageID == "read-1"
|
||||||
}
|
}
|
||||||
}, timeout: TestConstants.negativeWaitWindow)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
#expect(sentReadReceipt)
|
#expect(sentReadReceipt)
|
||||||
#expect(!viewModel.unreadPrivateMessages.contains(peerID))
|
#expect(!viewModel.unreadPrivateMessages.contains(peerID))
|
||||||
@@ -506,7 +506,7 @@ struct ChatViewModelReceivingTests {
|
|||||||
|
|
||||||
let found = await TestHelpers.waitUntil({
|
let found = await TestHelpers.waitUntil({
|
||||||
viewModel.publicMessages(for: .mesh).contains { $0.content == "Public hello from Bob" }
|
viewModel.publicMessages(for: .mesh).contains { $0.content == "Public hello from Bob" }
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
#expect(found)
|
#expect(found)
|
||||||
}
|
}
|
||||||
@@ -535,11 +535,11 @@ struct ChatViewModelNoisePayloadTests {
|
|||||||
|
|
||||||
let stored = await TestHelpers.waitUntil({
|
let stored = await TestHelpers.waitUntil({
|
||||||
viewModel.privateChats[peerID]?.contains(where: { $0.id == "pm-noise-1" && $0.content == "Secret hello" }) == true
|
viewModel.privateChats[peerID]?.contains(where: { $0.id == "pm-noise-1" && $0.content == "Secret hello" }) == true
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
let acked = await TestHelpers.waitUntil({
|
let acked = await TestHelpers.waitUntil({
|
||||||
transport.sentDeliveryAcks.contains { $0.messageID == "pm-noise-1" && $0.peerID == peerID }
|
transport.sentDeliveryAcks.contains { $0.messageID == "pm-noise-1" && $0.peerID == peerID }
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
#expect(stored)
|
#expect(stored)
|
||||||
#expect(acked)
|
#expect(acked)
|
||||||
@@ -579,7 +579,7 @@ struct ChatViewModelNoisePayloadTests {
|
|||||||
return name == "Bob"
|
return name == "Bob"
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
#expect(delivered)
|
#expect(delivered)
|
||||||
}
|
}
|
||||||
@@ -617,7 +617,7 @@ struct ChatViewModelNoisePayloadTests {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
let conversationStoreUpdated = await TestHelpers.waitUntil({
|
let conversationStoreUpdated = await TestHelpers.waitUntil({
|
||||||
let messages = viewModel.conversations.conversationsByID[.directPeer(peerID)]?.messages ?? []
|
let messages = viewModel.conversations.conversationsByID[.directPeer(peerID)]?.messages ?? []
|
||||||
@@ -626,7 +626,7 @@ struct ChatViewModelNoisePayloadTests {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
#expect(privateChatUpdated)
|
#expect(privateChatUpdated)
|
||||||
#expect(conversationStoreUpdated)
|
#expect(conversationStoreUpdated)
|
||||||
@@ -730,7 +730,7 @@ struct ChatViewModelVerificationTests {
|
|||||||
|
|
||||||
let bound = await TestHelpers.waitUntil({
|
let bound = await TestHelpers.waitUntil({
|
||||||
viewModel.unifiedPeerService.peers.contains { $0.peerID == peerID }
|
viewModel.unifiedPeerService.peers.contains { $0.peerID == peerID }
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
#expect(bound)
|
#expect(bound)
|
||||||
|
|
||||||
let qr = VerificationService.VerificationQR(
|
let qr = VerificationService.VerificationQR(
|
||||||
@@ -982,7 +982,7 @@ struct ChatViewModelPeerTests {
|
|||||||
|
|
||||||
let cleaned = await TestHelpers.waitUntil({
|
let cleaned = await TestHelpers.waitUntil({
|
||||||
!viewModel.unreadPrivateMessages.contains(stalePeer)
|
!viewModel.unreadPrivateMessages.contains(stalePeer)
|
||||||
}, timeout: TestConstants.settleTimeout)
|
}, timeout: TestConstants.defaultTimeout)
|
||||||
|
|
||||||
#expect(cleaned)
|
#expect(cleaned)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ struct CourierEndToEndTests {
|
|||||||
))
|
))
|
||||||
let deposited = await TestHelpers.waitUntil(
|
let deposited = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(deposited)
|
#expect(deposited)
|
||||||
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
@@ -151,7 +151,7 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
||||||
let carried = await TestHelpers.waitUntil(
|
let carried = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(carried)
|
#expect(carried)
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ struct CourierEndToEndTests {
|
|||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let announced = await TestHelpers.waitUntil(
|
let announced = await TestHelpers.waitUntil(
|
||||||
{ bobOut.first(ofType: .announce) != nil },
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(announced)
|
#expect(announced)
|
||||||
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
||||||
@@ -169,7 +169,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let handedOver = await TestHelpers.waitUntil(
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(handedOver)
|
#expect(handedOver)
|
||||||
// With CoreBluetooth disabled there is no physical link for the send
|
// With CoreBluetooth disabled there is no physical link for the send
|
||||||
@@ -183,7 +183,7 @@ struct CourierEndToEndTests {
|
|||||||
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
||||||
let received = await TestHelpers.waitUntil(
|
let received = await TestHelpers.waitUntil(
|
||||||
{ !bobDelegate.snapshot().isEmpty },
|
{ !bobDelegate.snapshot().isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(received)
|
#expect(received)
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ struct CourierEndToEndTests {
|
|||||||
))
|
))
|
||||||
let deposited = await TestHelpers.waitUntil(
|
let deposited = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(deposited)
|
#expect(deposited)
|
||||||
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
@@ -237,7 +237,7 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
||||||
let carried = await TestHelpers.waitUntil(
|
let carried = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(carried)
|
#expect(carried)
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ struct CourierEndToEndTests {
|
|||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let announced = await TestHelpers.waitUntil(
|
let announced = await TestHelpers.waitUntil(
|
||||||
{ bobOut.first(ofType: .announce) != nil },
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(announced)
|
#expect(announced)
|
||||||
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
let announcePacket = try #require(bobOut.first(ofType: .announce))
|
||||||
@@ -253,7 +253,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let handedOver = await TestHelpers.waitUntil(
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(handedOver)
|
#expect(handedOver)
|
||||||
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
|
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
|
||||||
@@ -265,7 +265,7 @@ struct CourierEndToEndTests {
|
|||||||
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
||||||
let delivered = await TestHelpers.waitUntil(
|
let delivered = await TestHelpers.waitUntil(
|
||||||
{ !bobDelegate.snapshot().isEmpty },
|
{ !bobDelegate.snapshot().isEmpty },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!delivered)
|
#expect(!delivered)
|
||||||
}
|
}
|
||||||
@@ -293,7 +293,7 @@ struct CourierEndToEndTests {
|
|||||||
))
|
))
|
||||||
let deposited = await TestHelpers.waitUntil(
|
let deposited = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(deposited)
|
#expect(deposited)
|
||||||
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
@@ -301,7 +301,7 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
||||||
let carried = await TestHelpers.waitUntil(
|
let carried = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(carried)
|
#expect(carried)
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let leakedOnUnverifiedAnnounce = await TestHelpers.waitUntil(
|
let leakedOnUnverifiedAnnounce = await TestHelpers.waitUntil(
|
||||||
{ carolOut.count(ofType: .courierEnvelope) > 0 },
|
{ carolOut.count(ofType: .courierEnvelope) > 0 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!leakedOnUnverifiedAnnounce)
|
#expect(!leakedOnUnverifiedAnnounce)
|
||||||
#expect(!carol.courierStore.isEmpty)
|
#expect(!carol.courierStore.isEmpty)
|
||||||
@@ -318,7 +318,7 @@ struct CourierEndToEndTests {
|
|||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let announced = await TestHelpers.waitUntil(
|
let announced = await TestHelpers.waitUntil(
|
||||||
{ bobOut.first(ofType: .announce) != nil },
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(announced)
|
#expect(announced)
|
||||||
let verifiedAnnounce = try #require(bobOut.first(ofType: .announce))
|
let verifiedAnnounce = try #require(bobOut.first(ofType: .announce))
|
||||||
@@ -326,7 +326,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let handedOver = await TestHelpers.waitUntil(
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
{ carolOut.count(ofType: .courierEnvelope) == 1 },
|
{ carolOut.count(ofType: .courierEnvelope) == 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(handedOver)
|
#expect(handedOver)
|
||||||
#expect(!carol.courierStore.isEmpty)
|
#expect(!carol.courierStore.isEmpty)
|
||||||
@@ -355,7 +355,7 @@ struct CourierEndToEndTests {
|
|||||||
))
|
))
|
||||||
let deposited = await TestHelpers.waitUntil(
|
let deposited = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(deposited)
|
#expect(deposited)
|
||||||
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
@@ -363,14 +363,14 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
||||||
let carried = await TestHelpers.waitUntil(
|
let carried = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(carried)
|
#expect(carried)
|
||||||
|
|
||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let announced = await TestHelpers.waitUntil(
|
let announced = await TestHelpers.waitUntil(
|
||||||
{ bobOut.first(ofType: .announce) != nil },
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(announced)
|
#expect(announced)
|
||||||
let directAnnounce = try #require(bobOut.first(ofType: .announce))
|
let directAnnounce = try #require(bobOut.first(ofType: .announce))
|
||||||
@@ -385,7 +385,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let remoteHandover = await TestHelpers.waitUntil(
|
let remoteHandover = await TestHelpers.waitUntil(
|
||||||
{ carolOut.count(ofType: .courierEnvelope) == 1 },
|
{ carolOut.count(ofType: .courierEnvelope) == 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(remoteHandover)
|
#expect(remoteHandover)
|
||||||
#expect(!carol.courierStore.isEmpty)
|
#expect(!carol.courierStore.isEmpty)
|
||||||
@@ -398,7 +398,7 @@ struct CourierEndToEndTests {
|
|||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let reannounced = await TestHelpers.waitUntil(
|
let reannounced = await TestHelpers.waitUntil(
|
||||||
{ bobOut.all(ofType: .announce).contains { $0.timestamp != directAnnounce.timestamp } },
|
{ bobOut.all(ofType: .announce).contains { $0.timestamp != directAnnounce.timestamp } },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(reannounced)
|
#expect(reannounced)
|
||||||
let freshAnnounce = try #require(
|
let freshAnnounce = try #require(
|
||||||
@@ -410,7 +410,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let refloodedInCooldown = await TestHelpers.waitUntil(
|
let refloodedInCooldown = await TestHelpers.waitUntil(
|
||||||
{ carolOut.count(ofType: .courierEnvelope) > 1 },
|
{ carolOut.count(ofType: .courierEnvelope) > 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!refloodedInCooldown)
|
#expect(!refloodedInCooldown)
|
||||||
#expect(!carol.courierStore.isEmpty)
|
#expect(!carol.courierStore.isEmpty)
|
||||||
@@ -424,7 +424,7 @@ struct CourierEndToEndTests {
|
|||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let announcedAgain = await TestHelpers.waitUntil(
|
let announcedAgain = await TestHelpers.waitUntil(
|
||||||
{ bobOut.all(ofType: .announce).contains { $0.timestamp != directAnnounce.timestamp && $0.timestamp != freshAnnounce.timestamp } },
|
{ bobOut.all(ofType: .announce).contains { $0.timestamp != directAnnounce.timestamp && $0.timestamp != freshAnnounce.timestamp } },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(announcedAgain)
|
#expect(announcedAgain)
|
||||||
let directAgain = try #require(
|
let directAgain = try #require(
|
||||||
@@ -434,7 +434,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let handedOverWithoutLinkProof = await TestHelpers.waitUntil(
|
let handedOverWithoutLinkProof = await TestHelpers.waitUntil(
|
||||||
{ carolOut.count(ofType: .courierEnvelope) > 1 },
|
{ carolOut.count(ofType: .courierEnvelope) > 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!handedOverWithoutLinkProof)
|
#expect(!handedOverWithoutLinkProof)
|
||||||
#expect(!carol.courierStore.isEmpty)
|
#expect(!carol.courierStore.isEmpty)
|
||||||
@@ -457,7 +457,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let queuedPacket = await TestHelpers.waitUntil(
|
let queuedPacket = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!queuedPacket)
|
#expect(!queuedPacket)
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,7 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(packet, fromPeerID: alicePeerID, signingPublicKey: alice.getSigningPublicKeyData())
|
carol._test_handlePacket(packet, fromPeerID: alicePeerID, signingPublicKey: alice.getSigningPublicKeyData())
|
||||||
let stored = await TestHelpers.waitUntil(
|
let stored = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!stored)
|
#expect(!stored)
|
||||||
}
|
}
|
||||||
@@ -532,7 +532,7 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(packet, fromPeerID: alicePeerID, signingPublicKey: alice.getSigningPublicKeyData())
|
carol._test_handlePacket(packet, fromPeerID: alicePeerID, signingPublicKey: alice.getSigningPublicKeyData())
|
||||||
let stored = await TestHelpers.waitUntil(
|
let stored = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!stored)
|
#expect(!stored)
|
||||||
}
|
}
|
||||||
@@ -575,7 +575,7 @@ struct CourierEndToEndTests {
|
|||||||
carol._test_handlePacket(packet, fromPeerID: mallory.myPeerID, preseedPeer: false)
|
carol._test_handlePacket(packet, fromPeerID: mallory.myPeerID, preseedPeer: false)
|
||||||
let stored = await TestHelpers.waitUntil(
|
let stored = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!stored)
|
#expect(!stored)
|
||||||
}
|
}
|
||||||
@@ -602,14 +602,14 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let delivered = await TestHelpers.waitUntil(
|
let delivered = await TestHelpers.waitUntil(
|
||||||
{ !bobDelegate.snapshot().isEmpty },
|
{ !bobDelegate.snapshot().isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(delivered)
|
#expect(delivered)
|
||||||
// Give a duplicate delivery a chance to surface, then confirm the
|
// Give a duplicate delivery a chance to surface, then confirm the
|
||||||
// second copy never reached the delegate.
|
// second copy never reached the delegate.
|
||||||
let duplicated = await TestHelpers.waitUntil(
|
let duplicated = await TestHelpers.waitUntil(
|
||||||
{ bobDelegate.snapshot().count > 1 },
|
{ bobDelegate.snapshot().count > 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!duplicated)
|
#expect(!duplicated)
|
||||||
#expect(bobDelegate.snapshot().count == 1)
|
#expect(bobDelegate.snapshot().count == 1)
|
||||||
@@ -629,7 +629,7 @@ struct CourierEndToEndTests {
|
|||||||
|
|
||||||
let initiated = await TestHelpers.waitUntil(
|
let initiated = await TestHelpers.waitUntil(
|
||||||
{ outbound.count(ofType: .noiseHandshake) > 0 },
|
{ outbound.count(ofType: .noiseHandshake) > 0 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!initiated)
|
#expect(!initiated)
|
||||||
|
|
||||||
@@ -639,7 +639,7 @@ struct CourierEndToEndTests {
|
|||||||
ble.sendDeliveryAck(for: "msg-2", to: present)
|
ble.sendDeliveryAck(for: "msg-2", to: present)
|
||||||
let initiatedForPresent = await TestHelpers.waitUntil(
|
let initiatedForPresent = await TestHelpers.waitUntil(
|
||||||
{ outbound.count(ofType: .noiseHandshake) > 0 },
|
{ outbound.count(ofType: .noiseHandshake) > 0 },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(initiatedForPresent)
|
#expect(initiatedForPresent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ struct PrekeyEndToEndTests {
|
|||||||
peer.sendBroadcastAnnounce()
|
peer.sendBroadcastAnnounce()
|
||||||
let published = await TestHelpers.waitUntil(
|
let published = await TestHelpers.waitUntil(
|
||||||
{ tap.first(ofType: .announce) != nil && tap.first(ofType: .prekeyBundle) != nil },
|
{ tap.first(ofType: .announce) != nil && tap.first(ofType: .prekeyBundle) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(published)
|
#expect(published)
|
||||||
return (
|
return (
|
||||||
@@ -124,7 +124,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let cached = await TestHelpers.waitUntil(
|
let cached = await TestHelpers.waitUntil(
|
||||||
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(cached)
|
#expect(cached)
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ struct PrekeyEndToEndTests {
|
|||||||
))
|
))
|
||||||
let deposited = await TestHelpers.waitUntil(
|
let deposited = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(deposited)
|
#expect(deposited)
|
||||||
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
@@ -149,7 +149,7 @@ struct PrekeyEndToEndTests {
|
|||||||
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
carol._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, signingPublicKey: alice.noiseSigningPublicKeyData())
|
||||||
let carried = await TestHelpers.waitUntil(
|
let carried = await TestHelpers.waitUntil(
|
||||||
{ !carol.courierStore.isEmpty },
|
{ !carol.courierStore.isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(carried)
|
#expect(carried)
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ struct PrekeyEndToEndTests {
|
|||||||
bob.sendBroadcastAnnounce()
|
bob.sendBroadcastAnnounce()
|
||||||
let reannounced = await TestHelpers.waitUntil(
|
let reannounced = await TestHelpers.waitUntil(
|
||||||
{ bobOut.first(ofType: .announce) != nil },
|
{ bobOut.first(ofType: .announce) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(reannounced)
|
#expect(reannounced)
|
||||||
let handoverTrigger = try #require(bobOut.first(ofType: .announce))
|
let handoverTrigger = try #require(bobOut.first(ofType: .announce))
|
||||||
@@ -166,7 +166,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let handedOver = await TestHelpers.waitUntil(
|
let handedOver = await TestHelpers.waitUntil(
|
||||||
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
{ carolOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(handedOver)
|
#expect(handedOver)
|
||||||
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
|
let handoverPacket = try #require(carolOut.first(ofType: .courierEnvelope))
|
||||||
@@ -178,7 +178,7 @@ struct PrekeyEndToEndTests {
|
|||||||
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
bob._test_handlePacket(handoverPacket, fromPeerID: carol.myPeerID)
|
||||||
let received = await TestHelpers.waitUntil(
|
let received = await TestHelpers.waitUntil(
|
||||||
{ !bobDelegate.snapshot().isEmpty },
|
{ !bobDelegate.snapshot().isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(received)
|
#expect(received)
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ struct PrekeyEndToEndTests {
|
|||||||
bob._test_handlePacket(redelivery, fromPeerID: carol.myPeerID)
|
bob._test_handlePacket(redelivery, fromPeerID: carol.myPeerID)
|
||||||
let redelivered = await TestHelpers.waitUntil(
|
let redelivered = await TestHelpers.waitUntil(
|
||||||
{ bobDelegate.snapshot().count == 2 },
|
{ bobDelegate.snapshot().count == 2 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!redelivered)
|
#expect(!redelivered)
|
||||||
#expect(bobDelegate.snapshot().count == 1)
|
#expect(bobDelegate.snapshot().count == 1)
|
||||||
@@ -235,7 +235,7 @@ struct PrekeyEndToEndTests {
|
|||||||
))
|
))
|
||||||
let deposited = await TestHelpers.waitUntil(
|
let deposited = await TestHelpers.waitUntil(
|
||||||
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
{ aliceOut.first(ofType: .courierEnvelope) != nil },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(deposited)
|
#expect(deposited)
|
||||||
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
let depositPacket = try #require(aliceOut.first(ofType: .courierEnvelope))
|
||||||
@@ -248,7 +248,7 @@ struct PrekeyEndToEndTests {
|
|||||||
bob._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, preseedPeer: false)
|
bob._test_handlePacket(depositPacket, fromPeerID: alice.myPeerID, preseedPeer: false)
|
||||||
let received = await TestHelpers.waitUntil(
|
let received = await TestHelpers.waitUntil(
|
||||||
{ !bobDelegate.snapshot().isEmpty },
|
{ !bobDelegate.snapshot().isEmpty },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(received)
|
#expect(received)
|
||||||
let delivered = try #require(bobDelegate.snapshot().first)
|
let delivered = try #require(bobDelegate.snapshot().first)
|
||||||
@@ -272,7 +272,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let cached = await TestHelpers.waitUntil(
|
let cached = await TestHelpers.waitUntil(
|
||||||
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!cached)
|
#expect(!cached)
|
||||||
}
|
}
|
||||||
@@ -310,7 +310,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let cached = await TestHelpers.waitUntil(
|
let cached = await TestHelpers.waitUntil(
|
||||||
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!cached)
|
#expect(!cached)
|
||||||
}
|
}
|
||||||
@@ -328,7 +328,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let cached = await TestHelpers.waitUntil(
|
let cached = await TestHelpers.waitUntil(
|
||||||
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.defaultTimeout
|
||||||
)
|
)
|
||||||
#expect(cached)
|
#expect(cached)
|
||||||
// The verified bundle now participates in Alice's sync rounds.
|
// The verified bundle now participates in Alice's sync rounds.
|
||||||
@@ -364,7 +364,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let cached = await TestHelpers.waitUntil(
|
let cached = await TestHelpers.waitUntil(
|
||||||
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!cached)
|
#expect(!cached)
|
||||||
#expect(!alice._test_hasGossipPrekeyBundle(for: bob.myPeerID))
|
#expect(!alice._test_hasGossipPrekeyBundle(for: bob.myPeerID))
|
||||||
@@ -396,7 +396,7 @@ struct PrekeyEndToEndTests {
|
|||||||
|
|
||||||
let cached = await TestHelpers.waitUntil(
|
let cached = await TestHelpers.waitUntil(
|
||||||
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
{ alice.prekeyBundleStore.hasUsableBundle(for: bob.noiseStaticPublicKeyData()) },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!cached)
|
#expect(!cached)
|
||||||
#expect(!alice._test_hasGossipPrekeyBundle(for: bob.myPeerID))
|
#expect(!alice._test_hasGossipPrekeyBundle(for: bob.myPeerID))
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct GossipSyncManagerTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
|
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
|
||||||
try await TestHelpers.waitFor({ delegate.lastPacket != nil }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.lastPacket != nil }, timeout: TestConstants.shortTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastPacket = try #require(delegate.lastPacket, "Expected sync packet to be sent")
|
let lastPacket = try #require(delegate.lastPacket, "Expected sync packet to be sent")
|
||||||
@@ -394,7 +394,7 @@ struct GossipSyncManagerTests {
|
|||||||
)
|
)
|
||||||
manager.handleRequestSync(from: peer, request: request)
|
manager.handleRequestSync(from: peer, request: request)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 2 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 2 }, timeout: TestConstants.shortTimeout)
|
||||||
// Barrier: flush the sync queue so a late third packet would be visible.
|
// Barrier: flush the sync queue so a late third packet would be visible.
|
||||||
manager._performMaintenanceSynchronously(now: Date())
|
manager._performMaintenanceSynchronously(now: Date())
|
||||||
let sentPackets = delegate.packets
|
let sentPackets = delegate.packets
|
||||||
@@ -477,7 +477,7 @@ struct GossipSyncManagerTests {
|
|||||||
manager.handleRequestSync(from: peer, request: request)
|
manager.handleRequestSync(from: peer, request: request)
|
||||||
manager.handleRequestSync(from: peer, request: request)
|
manager.handleRequestSync(from: peer, request: request)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count >= 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count >= 1 }, timeout: TestConstants.shortTimeout)
|
||||||
// Barrier: both requests have been processed once this returns.
|
// Barrier: both requests have been processed once this returns.
|
||||||
manager._performMaintenanceSynchronously(now: Date())
|
manager._performMaintenanceSynchronously(now: Date())
|
||||||
#expect(delegate.packets.count == 1)
|
#expect(delegate.packets.count == 1)
|
||||||
@@ -498,7 +498,7 @@ struct GossipSyncManagerTests {
|
|||||||
|
|
||||||
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
|
manager.scheduleInitialSyncToPeer(PeerID(str: "FFFFFFFFFFFFFFFF"), delaySeconds: 0.0)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
let packet = try #require(delegate.packets.first)
|
let packet = try #require(delegate.packets.first)
|
||||||
let request = try #require(RequestSyncPacket.decode(from: packet.payload))
|
let request = try #require(RequestSyncPacket.decode(from: packet.payload))
|
||||||
let types = try #require(request.types)
|
let types = try #require(request.types)
|
||||||
@@ -553,7 +553,7 @@ struct GossipSyncManagerTests {
|
|||||||
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .fragment)
|
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .fragment)
|
||||||
manager.handleRequestSync(from: peer, request: request)
|
manager.handleRequestSync(from: peer, request: request)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
let sentPackets = delegate.packets
|
let sentPackets = delegate.packets
|
||||||
#expect(sentPackets.count == 1)
|
#expect(sentPackets.count == 1)
|
||||||
#expect(sentPackets[0].type == MessageType.fragment.rawValue)
|
#expect(sentPackets[0].type == MessageType.fragment.rawValue)
|
||||||
@@ -615,7 +615,7 @@ struct GossipSyncManagerTests {
|
|||||||
)
|
)
|
||||||
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
// Barrier: flush the sync queue so a late second packet would be visible.
|
// Barrier: flush the sync queue so a late second packet would be visible.
|
||||||
manager._performMaintenanceSynchronously(now: Date())
|
manager._performMaintenanceSynchronously(now: Date())
|
||||||
let sentPackets = delegate.packets
|
let sentPackets = delegate.packets
|
||||||
@@ -641,7 +641,7 @@ struct GossipSyncManagerTests {
|
|||||||
let stalledID = try #require(Data(hexString: "0102030405060708"))
|
let stalledID = try #require(Data(hexString: "0102030405060708"))
|
||||||
manager.requestMissingFragments(fragmentIDs: [stalledID])
|
manager.requestMissingFragments(fragmentIDs: [stalledID])
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
let sent = try #require(delegate.packets.first)
|
let sent = try #require(delegate.packets.first)
|
||||||
#expect(sent.type == MessageType.requestSync.rawValue)
|
#expect(sent.type == MessageType.requestSync.rawValue)
|
||||||
#expect(sent.ttl == 0)
|
#expect(sent.ttl == 0)
|
||||||
@@ -697,7 +697,7 @@ struct GossipSyncManagerTests {
|
|||||||
// And a .prekeyBundle sync request is answered with the stored packet.
|
// And a .prekeyBundle sync request is answered with the stored packet.
|
||||||
let request = RequestSyncPacket(p: 7, m: 1, data: Data(), types: .prekeyBundle)
|
let request = RequestSyncPacket(p: 7, m: 1, data: Data(), types: .prekeyBundle)
|
||||||
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
let served = try #require(delegate.packets.first)
|
let served = try #require(delegate.packets.first)
|
||||||
#expect(served.type == MessageType.prekeyBundle.rawValue)
|
#expect(served.type == MessageType.prekeyBundle.rawValue)
|
||||||
#expect(served.isRSR)
|
#expect(served.isRSR)
|
||||||
@@ -774,7 +774,7 @@ struct GossipSyncManagerTests {
|
|||||||
)
|
)
|
||||||
let restored = await TestHelpers.waitUntil(
|
let restored = await TestHelpers.waitUntil(
|
||||||
{ second._messageCount(for: PeerID(hexData: senderID)) == 1 },
|
{ second._messageCount(for: PeerID(hexData: senderID)) == 1 },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(restored)
|
#expect(restored)
|
||||||
}
|
}
|
||||||
@@ -844,7 +844,7 @@ struct GossipSyncManagerTests {
|
|||||||
!FileManager.default.fileExists(atPath: fileURL.path)
|
!FileManager.default.fileExists(atPath: fileURL.path)
|
||||||
&& manager._messageCount(for: PeerID(hexData: senderID)) == 0
|
&& manager._messageCount(for: PeerID(hexData: senderID)) == 0
|
||||||
},
|
},
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(erased)
|
#expect(erased)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ final class NearbyNotesCounterTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping @MainActor () -> Bool
|
condition: @escaping @MainActor () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -723,9 +723,9 @@ struct NoiseCoverageTests {
|
|||||||
// A failed startup requirement must not strand a late thread in
|
// A failed startup requirement must not strand a late thread in
|
||||||
// the blocking test double after the test has returned.
|
// the blocking test double after the test has returned.
|
||||||
oldSession.resumeDecrypt()
|
oldSession.resumeDecrypt()
|
||||||
_ = decryptResult.wait(timeout: TestConstants.settleTimeout)
|
_ = decryptResult.wait(timeout: 5)
|
||||||
if let promotionResultForCleanup {
|
if let promotionResultForCleanup {
|
||||||
_ = promotionResultForCleanup.wait(timeout: TestConstants.settleTimeout)
|
_ = promotionResultForCleanup.wait(timeout: 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,19 +751,15 @@ struct NoiseCoverageTests {
|
|||||||
promotionThread.name = "NoiseCoverageTests.staleDecrypt.promote"
|
promotionThread.name = "NoiseCoverageTests.staleDecrypt.promote"
|
||||||
promotionThread.qualityOfService = .userInitiated
|
promotionThread.qualityOfService = .userInitiated
|
||||||
promotionThread.start()
|
promotionThread.start()
|
||||||
try #require(promotionStarted.wait(timeout: .now() + TestConstants.settleTimeout) == .success)
|
try #require(promotionStarted.wait(timeout: .now() + 5) == .success)
|
||||||
#expect(
|
#expect(
|
||||||
// test-timing-ok: a NEGATIVE wait — it asserts the promotion has
|
|
||||||
// NOT completed yet, so a long deadline would only make the suite
|
|
||||||
// slow while still passing. A starved runner can only make this
|
|
||||||
// more likely to hold, never less.
|
|
||||||
promotionResult.wait(timeout: 0.05) == nil,
|
promotionResult.wait(timeout: 0.05) == nil,
|
||||||
"Promotion must wait for the exact decrypting-session lease"
|
"Promotion must wait for the exact decrypting-session lease"
|
||||||
)
|
)
|
||||||
|
|
||||||
oldSession.resumeDecrypt()
|
oldSession.resumeDecrypt()
|
||||||
let decrypted = try #require(decryptResult.wait(timeout: TestConstants.settleTimeout)).get()
|
let decrypted = try #require(decryptResult.wait(timeout: 5)).get()
|
||||||
_ = try #require(promotionResult.wait(timeout: TestConstants.settleTimeout)).get()
|
_ = try #require(promotionResult.wait(timeout: 5)).get()
|
||||||
|
|
||||||
#expect(decrypted.plaintext == Data("old session".utf8))
|
#expect(decrypted.plaintext == Data("old session".utf8))
|
||||||
#expect(decrypted.sessionGeneration == oldGeneration)
|
#expect(decrypted.sessionGeneration == oldGeneration)
|
||||||
|
|||||||
@@ -580,16 +580,8 @@ final class GeoRelayDirectoryTests: XCTestCase {
|
|||||||
/// constrained CI runners (2-core, serialized testing) can starve the
|
/// constrained CI runners (2-core, serialized testing) can starve the
|
||||||
/// detached utility-priority fetch task for seconds before it runs, and
|
/// detached utility-priority fetch task for seconds before it runs, and
|
||||||
/// a successful wait returns as soon as the condition becomes true.
|
/// a successful wait returns as soon as the condition becomes true.
|
||||||
/// Default deliberately far larger than the work being awaited.
|
|
||||||
///
|
|
||||||
/// The directory performs its fetch in a `Task.detached(priority: .utility)`,
|
|
||||||
/// and utility priority competes with every other suite on a CI runner. At
|
|
||||||
/// ten seconds the retry-scheduling test timed out at exactly 10.06s with
|
|
||||||
/// the retry never scheduled — which reads like a missing retry rather than
|
|
||||||
/// a starved background task. Returning as soon as the condition holds means
|
|
||||||
/// a longer deadline only extends the genuine-failure case.
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 10.0,
|
||||||
condition: @escaping @MainActor () async -> Bool
|
condition: @escaping @MainActor () async -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ struct PTTBurstPlayerTests {
|
|||||||
_ condition: () -> Bool,
|
_ condition: () -> Bool,
|
||||||
sourceLocation: SourceLocation = #_sourceLocation
|
sourceLocation: SourceLocation = #_sourceLocation
|
||||||
) async {
|
) async {
|
||||||
let deadline = ContinuousClock.now.advanced(by: .seconds(TestConstants.settleTimeout))
|
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
|
||||||
while !condition(), ContinuousClock.now < deadline {
|
while !condition(), ContinuousClock.now < deadline {
|
||||||
await Task.yield()
|
await Task.yield()
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000)
|
try? await Task.sleep(nanoseconds: 1_000_000)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ final class FavoritesPersistenceServiceTests: XCTestCase {
|
|||||||
|
|
||||||
service.addFavorite(peerNoisePublicKey: peerKey, peerNostrPublicKey: "npub1alice", peerNickname: "Alice")
|
service.addFavorite(peerNoisePublicKey: peerKey, peerNostrPublicKey: "npub1alice", peerNickname: "Alice")
|
||||||
|
|
||||||
wait(for: [expectation], timeout: TestConstants.settleTimeout)
|
wait(for: [expectation], timeout: 1.0)
|
||||||
XCTAssertTrue(service.isFavorite(peerKey))
|
XCTAssertTrue(service.isFavorite(peerKey))
|
||||||
XCTAssertEqual(service.getFavoriteStatus(for: peerKey)?.peerNickname, "Alice")
|
XCTAssertEqual(service.getFavoriteStatus(for: peerKey)?.peerNickname, "Alice")
|
||||||
XCTAssertNotNil(keychain.load(key: storageKey, service: serviceKey))
|
XCTAssertNotNil(keychain.load(key: storageKey, service: serviceKey))
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ final class GeohashPresenceServiceTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping @MainActor () -> Bool
|
condition: @escaping @MainActor () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ final class LocationStateManagerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping @MainActor () -> Bool
|
condition: @escaping @MainActor () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ final class NetworkActivationServiceTests: XCTestCase {
|
|||||||
context.service.start()
|
context.service.start()
|
||||||
context.service.setUserTorEnabled(false)
|
context.service.setUserTorEnabled(false)
|
||||||
|
|
||||||
wait(for: [notified], timeout: TestConstants.negativeWaitWindow)
|
wait(for: [notified], timeout: 1.0)
|
||||||
context.notificationCenter.removeObserver(token)
|
context.notificationCenter.removeObserver(token)
|
||||||
|
|
||||||
XCTAssertFalse(context.service.userTorEnabled)
|
XCTAssertFalse(context.service.userTorEnabled)
|
||||||
@@ -243,7 +243,7 @@ final class NetworkActivationServiceTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping @MainActor () -> Bool
|
condition: @escaping @MainActor () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -69,45 +69,25 @@ final class NetworkReachabilityGateTests: XCTestCase {
|
|||||||
XCTAssertNil(d.pendingRemaining(at: t0.addingTimeInterval(2.5)))
|
XCTAssertNil(d.pendingRemaining(at: t0.addingTimeInterval(2.5)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wiring only: a duplicate mid-window still yields exactly one committed
|
func test_monitor_duplicateUpdatesDoNotPostponeOfflineCommit() async {
|
||||||
/// `false`, published through the monitor's debounce.
|
let monitor = NWPathReachabilityMonitor(debounceInterval: 1.0)
|
||||||
///
|
|
||||||
/// This deliberately makes no assertion about *when* the flush fires. It
|
|
||||||
/// used to bound elapsed wall-clock time at 1.4 s to prove the deadline was
|
|
||||||
/// not restarted, which flaked on loaded CI runners — one observed run took
|
|
||||||
/// 3.75 s, because `Task.sleep` and the `asyncAfter` flush are both real
|
|
||||||
/// time and neither is bounded above on a busy machine. No wall-clock bound
|
|
||||||
/// can distinguish "deadline preserved" from "runner is slow", so the timing
|
|
||||||
/// property is asserted where it is computable instead:
|
|
||||||
/// `test_debounce_duplicateObservationsPreservePendingDeadline` drives
|
|
||||||
/// `ReachabilityDebounce` with injected timestamps and checks
|
|
||||||
/// `pendingRemaining` directly.
|
|
||||||
///
|
|
||||||
/// The clock is injected here so the debounce arithmetic is deterministic
|
|
||||||
/// even though the flush itself is scheduled in real time.
|
|
||||||
func test_monitor_duplicateUpdatesCommitOnceThroughTheDebounce() async {
|
|
||||||
let clock = MutableDate(now: Date(timeIntervalSince1970: 1_784_000_000))
|
|
||||||
let monitor = NWPathReachabilityMonitor(
|
|
||||||
debounceInterval: 0.2,
|
|
||||||
now: { clock.now }
|
|
||||||
)
|
|
||||||
var received: [Bool] = []
|
var received: [Bool] = []
|
||||||
let cancellable = monitor.reachabilityPublisher.sink { received.append($0) }
|
let cancellable = monitor.reachabilityPublisher.sink { received.append($0) }
|
||||||
defer { cancellable.cancel() }
|
defer { cancellable.cancel() }
|
||||||
|
|
||||||
|
let start = Date()
|
||||||
monitor.ingest(reachable: false)
|
monitor.ingest(reachable: false)
|
||||||
// Duplicate unsatisfied update mid-window (e.g. an interface detail
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||||
// change while still offline).
|
// Duplicate unsatisfied update mid-window (e.g. interface detail change
|
||||||
clock.now = clock.now.addingTimeInterval(0.1)
|
// while still offline) must not restart the debounce window.
|
||||||
monitor.ingest(reachable: false)
|
monitor.ingest(reachable: false)
|
||||||
// Past the original deadline, so the scheduled flush commits.
|
|
||||||
clock.now = clock.now.addingTimeInterval(0.2)
|
|
||||||
|
|
||||||
// Generous: this is a liveness check, not a latency bound. A real
|
let committed = await waitUntil(timeout: 2.0) { !received.isEmpty }
|
||||||
// regression — never committing — still fails, just later.
|
|
||||||
let committed = await waitUntil(timeout: 10.0) { !received.isEmpty }
|
|
||||||
XCTAssertTrue(committed)
|
XCTAssertTrue(committed)
|
||||||
XCTAssertEqual(received, [false])
|
XCTAssertEqual(received, [false])
|
||||||
|
// The flush must fire at the original ~1.0s deadline, not ~1.5s
|
||||||
|
// (a full interval after the duplicate).
|
||||||
|
XCTAssertLessThan(Date().timeIntervalSince(start), 1.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Service gating
|
// MARK: - Service gating
|
||||||
@@ -199,7 +179,7 @@ final class NetworkReachabilityGateTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping @MainActor () -> Bool
|
condition: @escaping @MainActor () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
@@ -259,13 +239,3 @@ private final class GateMockProxyController: NetworkActivationProxyControlling {
|
|||||||
private(set) var proxyModes: [Bool] = []
|
private(set) var proxyModes: [Bool] = []
|
||||||
func setProxyMode(useTor: Bool) { proxyModes.append(useTor) }
|
func setProxyMode(useTor: Bool) { proxyModes.append(useTor) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controllable clock, so debounce arithmetic is deterministic even where the
|
|
||||||
/// flush itself is scheduled in real time.
|
|
||||||
private final class MutableDate: @unchecked Sendable {
|
|
||||||
var now: Date
|
|
||||||
|
|
||||||
init(now: Date) {
|
|
||||||
self.now = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -105,11 +105,11 @@ struct NoiseEncryptionServiceTests {
|
|||||||
|
|
||||||
try establishSessions(alice: alice, bob: bob)
|
try establishSessions(alice: alice, bob: bob)
|
||||||
|
|
||||||
let authenticated = await TestHelpers.waitUntil({ recorder.count >= 2 }, timeout: TestConstants.settleTimeout)
|
let authenticated = await TestHelpers.waitUntil({ recorder.count >= 2 }, timeout: 5.0)
|
||||||
#expect(authenticated)
|
#expect(authenticated)
|
||||||
let generationAuthenticated = await TestHelpers.waitUntil(
|
let generationAuthenticated = await TestHelpers.waitUntil(
|
||||||
{ recorder.generationCount >= 1 },
|
{ recorder.generationCount >= 1 },
|
||||||
timeout: TestConstants.settleTimeout
|
timeout: 5.0
|
||||||
)
|
)
|
||||||
#expect(generationAuthenticated)
|
#expect(generationAuthenticated)
|
||||||
#expect(alice.hasEstablishedSession(with: bobPeerID))
|
#expect(alice.hasEstablishedSession(with: bobPeerID))
|
||||||
@@ -166,7 +166,7 @@ struct NoiseEncryptionServiceTests {
|
|||||||
#expect(!receiver.hasSession(with: claimedAlicePeerID))
|
#expect(!receiver.hasSession(with: claimedAlicePeerID))
|
||||||
let emittedAuthentication = await TestHelpers.waitUntil(
|
let emittedAuthentication = await TestHelpers.waitUntil(
|
||||||
{ recorder.count > 0 },
|
{ recorder.count > 0 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!emittedAuthentication)
|
#expect(!emittedAuthentication)
|
||||||
}
|
}
|
||||||
@@ -216,7 +216,7 @@ struct NoiseEncryptionServiceTests {
|
|||||||
#expect(try receiver.decrypt(after, from: alicePeerID) == Data("after".utf8))
|
#expect(try receiver.decrypt(after, from: alicePeerID) == Data("after".utf8))
|
||||||
let emittedReplacementAuthentication = await TestHelpers.waitUntil(
|
let emittedReplacementAuthentication = await TestHelpers.waitUntil(
|
||||||
{ recorder.count > 1 },
|
{ recorder.count > 1 },
|
||||||
timeout: TestConstants.negativeWaitWindow
|
timeout: TestConstants.shortTimeout
|
||||||
)
|
)
|
||||||
#expect(!emittedReplacementAuthentication)
|
#expect(!emittedReplacementAuthentication)
|
||||||
}
|
}
|
||||||
@@ -652,12 +652,12 @@ struct NoiseEncryptionServiceTests {
|
|||||||
)
|
)
|
||||||
let retried = await TestHelpers.waitUntil(
|
let retried = await TestHelpers.waitUntil(
|
||||||
{ recorder.messages.count == 1 },
|
{ recorder.messages.count == 1 },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(retried)
|
#expect(retried)
|
||||||
let retryExpired = await TestHelpers.waitUntil(
|
let retryExpired = await TestHelpers.waitUntil(
|
||||||
{ !service.hasSession(with: peerID) },
|
{ !service.hasSession(with: peerID) },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(retryExpired)
|
#expect(retryExpired)
|
||||||
#expect(recorder.timeoutCount == 1)
|
#expect(recorder.timeoutCount == 1)
|
||||||
@@ -710,12 +710,7 @@ struct NoiseEncryptionServiceTests {
|
|||||||
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
let alice = NoiseEncryptionService(keychain: MockKeychain())
|
||||||
let bob = NoiseEncryptionService(
|
let bob = NoiseEncryptionService(
|
||||||
keychain: MockKeychain(),
|
keychain: MockKeychain(),
|
||||||
// Generous for the same reason as the quarantine-restore test
|
ordinaryResponderHandshakeTimeout: 0.06,
|
||||||
// (#1483): this timeout also arms during the `establishSessions`
|
|
||||||
// setup handshake below, where bob is the responder. At 0.06 a
|
|
||||||
// preempted runner could fire it mid-setup, tear down the half-open
|
|
||||||
// responder, and make message 3 be answered as a fresh initiation.
|
|
||||||
ordinaryResponderHandshakeTimeout: 1.0,
|
|
||||||
ordinaryReconnectRollbackCooldown: 0.3
|
ordinaryReconnectRollbackCooldown: 0.3
|
||||||
)
|
)
|
||||||
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
let mallory = NoiseEncryptionService(keychain: MockKeychain())
|
||||||
@@ -746,12 +741,12 @@ struct NoiseEncryptionServiceTests {
|
|||||||
|
|
||||||
let restored = await TestHelpers.waitUntil(
|
let restored = await TestHelpers.waitUntil(
|
||||||
{ bob.hasEstablishedSession(with: alicePeerID) },
|
{ bob.hasEstablishedSession(with: alicePeerID) },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(restored)
|
#expect(restored)
|
||||||
let callbackArrived = await TestHelpers.waitUntil(
|
let callbackArrived = await TestHelpers.waitUntil(
|
||||||
{ recovery.timeoutCount == 1 },
|
{ recovery.timeoutCount == 1 },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(callbackArrived)
|
#expect(callbackArrived)
|
||||||
|
|
||||||
@@ -785,13 +780,7 @@ struct NoiseEncryptionServiceTests {
|
|||||||
let bob = NoiseEncryptionService(
|
let bob = NoiseEncryptionService(
|
||||||
keychain: MockKeychain(),
|
keychain: MockKeychain(),
|
||||||
ordinaryHandshakeTimeout: 0.04,
|
ordinaryHandshakeTimeout: 0.04,
|
||||||
// Also arms during the `establishSessions` setup handshake below,
|
ordinaryResponderHandshakeTimeout: 0.04
|
||||||
// where bob is the responder. Observed failing on a loaded CI
|
|
||||||
// runner with exactly the signature #1483 documented: the setup's
|
|
||||||
// `#expect(finalMessage == nil)` saw a 96-byte message 2, because
|
|
||||||
// the half-open responder had already been torn down and message 3
|
|
||||||
// was answered as a fresh initiation.
|
|
||||||
ordinaryResponderHandshakeTimeout: 1.0
|
|
||||||
)
|
)
|
||||||
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
let alicePeerID = PeerID(publicKey: alice.getStaticPublicKeyData())
|
||||||
let bobPeerID = PeerID(publicKey: bob.getStaticPublicKeyData())
|
let bobPeerID = PeerID(publicKey: bob.getStaticPublicKeyData())
|
||||||
@@ -825,12 +814,12 @@ struct NoiseEncryptionServiceTests {
|
|||||||
// initiates one bounded convergence retry; drop that message 1 too.
|
// initiates one bounded convergence retry; drop that message 1 too.
|
||||||
let retryPrepared = await TestHelpers.waitUntil(
|
let retryPrepared = await TestHelpers.waitUntil(
|
||||||
{ recovery.messages.count == 1 },
|
{ recovery.messages.count == 1 },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(retryPrepared)
|
#expect(retryPrepared)
|
||||||
let retryExpired = await TestHelpers.waitUntil(
|
let retryExpired = await TestHelpers.waitUntil(
|
||||||
{ !bob.hasSession(with: alicePeerID) },
|
{ !bob.hasSession(with: alicePeerID) },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(retryExpired)
|
#expect(retryExpired)
|
||||||
#expect(recovery.timeoutCount == 1)
|
#expect(recovery.timeoutCount == 1)
|
||||||
@@ -1037,7 +1026,7 @@ struct NoiseEncryptionServiceTests {
|
|||||||
|
|
||||||
let requested = await TestHelpers.waitUntil(
|
let requested = await TestHelpers.waitUntil(
|
||||||
{ recovery.messages.count == 1 },
|
{ recovery.messages.count == 1 },
|
||||||
timeout: TestConstants.longTimeout
|
timeout: 1
|
||||||
)
|
)
|
||||||
#expect(requested)
|
#expect(requested)
|
||||||
let retryMessage1 = try #require(recovery.messages.first)
|
let retryMessage1 = try #require(recovery.messages.first)
|
||||||
|
|||||||
@@ -960,7 +960,7 @@ final class NostrRelayManagerTests: XCTestCase {
|
|||||||
try context.sessionFactory.latestConnection(for: relayURL)?.emitEventMessage(subscriptionID: "ordered", event: event)
|
try context.sessionFactory.latestConnection(for: relayURL)?.emitEventMessage(subscriptionID: "ordered", event: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
let allDelivered = await waitUntil(timeout: TestConstants.settleTimeout) {
|
let allDelivered = await waitUntil(timeout: 5.0) {
|
||||||
receivedIDs.count == events.count
|
receivedIDs.count == events.count
|
||||||
}
|
}
|
||||||
XCTAssertTrue(allDelivered)
|
XCTAssertTrue(allDelivered)
|
||||||
@@ -1006,7 +1006,7 @@ final class NostrRelayManagerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
try context.sessionFactory.latestConnection(for: quietRelayURL)?.emitEventMessage(subscriptionID: "quiet", event: quietEvent)
|
try context.sessionFactory.latestConnection(for: quietRelayURL)?.emitEventMessage(subscriptionID: "quiet", event: quietEvent)
|
||||||
|
|
||||||
let quietDelivered = await waitUntil(timeout: TestConstants.settleTimeout) { quietDeliveredAfterBusyCount >= 0 }
|
let quietDelivered = await waitUntil(timeout: 5.0) { quietDeliveredAfterBusyCount >= 0 }
|
||||||
XCTAssertTrue(quietDelivered, "relay B's event was never delivered")
|
XCTAssertTrue(quietDelivered, "relay B's event was never delivered")
|
||||||
|
|
||||||
// The signal: B did not have to wait for A's entire backlog. If the two
|
// The signal: B did not have to wait for A's entire backlog. If the two
|
||||||
@@ -1019,7 +1019,7 @@ final class NostrRelayManagerTests: XCTestCase {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Both relays still drain fully and in order.
|
// Both relays still drain fully and in order.
|
||||||
let allDelivered = await waitUntil(timeout: TestConstants.settleTimeout) {
|
let allDelivered = await waitUntil(timeout: 5.0) {
|
||||||
busyDeliveredCount == busyEvents.count
|
busyDeliveredCount == busyEvents.count
|
||||||
}
|
}
|
||||||
XCTAssertTrue(allDelivered)
|
XCTAssertTrue(allDelivered)
|
||||||
@@ -1907,7 +1907,7 @@ final class NostrRelayManagerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping @MainActor () -> Bool
|
condition: @escaping @MainActor () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ struct NostrTransportTests {
|
|||||||
|
|
||||||
transport.sendPrivateMessage("hello over nostr", to: shortPeerID, recipientNickname: "Carol", messageID: "pm-1")
|
transport.sendPrivateMessage("hello over nostr", to: shortPeerID, recipientNickname: "Carol", messageID: "pm-1")
|
||||||
|
|
||||||
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
|
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
|
||||||
#expect(didSend)
|
#expect(didSend)
|
||||||
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
|
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
|
||||||
let privateMessage = try decodePrivateMessage(from: result.payload)
|
let privateMessage = try decodePrivateMessage(from: result.payload)
|
||||||
@@ -209,7 +209,7 @@ struct NostrTransportTests {
|
|||||||
|
|
||||||
transport.sendFavoriteNotification(to: fullPeerID, isFavorite: true)
|
transport.sendFavoriteNotification(to: fullPeerID, isFavorite: true)
|
||||||
|
|
||||||
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
|
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
|
||||||
#expect(didSend)
|
#expect(didSend)
|
||||||
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
|
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
|
||||||
let privateMessage = try decodePrivateMessage(from: result.payload)
|
let privateMessage = try decodePrivateMessage(from: result.payload)
|
||||||
@@ -250,7 +250,7 @@ struct NostrTransportTests {
|
|||||||
|
|
||||||
transport.sendDeliveryAck(for: "ack-1", to: fullPeerID)
|
transport.sendDeliveryAck(for: "ack-1", to: fullPeerID)
|
||||||
|
|
||||||
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
|
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
|
||||||
#expect(didSend)
|
#expect(didSend)
|
||||||
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
|
let result = try decodeEmbeddedPayload(from: probe.sentEvents[0], recipient: recipient)
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ struct NostrTransportTests {
|
|||||||
messageID: "geo-1"
|
messageID: "geo-1"
|
||||||
)
|
)
|
||||||
|
|
||||||
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: TestConstants.settleTimeout)
|
let didSend = await TestHelpers.waitUntil({ probe.sentEvents.count == 1 }, timeout: 5.0)
|
||||||
#expect(didSend)
|
#expect(didSend)
|
||||||
let event = probe.sentEvents[0]
|
let event = probe.sentEvents[0]
|
||||||
let result = try decodeEmbeddedPayload(from: event, recipient: recipient)
|
let result = try decodeEmbeddedPayload(from: event, recipient: recipient)
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import BitFoundation
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
@testable import bitchat
|
||||||
|
|
||||||
|
/// Two radio-layer metadata leaks that need no cross-platform agreement to
|
||||||
|
/// close: the neighbour list in announces, and the fixed origin TTL.
|
||||||
|
struct RadioMetadataTests {
|
||||||
|
|
||||||
|
// MARK: - Origin TTL
|
||||||
|
|
||||||
|
@Test func originTTLStaysInsideTheConfiguredRange() {
|
||||||
|
let range = TransportConfig.broadcastOriginTTLRange
|
||||||
|
for _ in 0..<200 {
|
||||||
|
let ttl = BLEOriginTTLPolicy.originTTL()
|
||||||
|
#expect(range.contains(ttl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The point of the change: a message must not always leave at the maximum,
|
||||||
|
/// because a direct listener reads `ttl == max` as "this device wrote it".
|
||||||
|
@Test func originTTLDoesNotAlwaysUseTheMaximum() {
|
||||||
|
var seen = Set<UInt8>()
|
||||||
|
for _ in 0..<500 {
|
||||||
|
seen.insert(BLEOriginTTLPolicy.originTTL())
|
||||||
|
}
|
||||||
|
#expect(seen.count > 1)
|
||||||
|
#expect(seen.contains { $0 < TransportConfig.messageTTLDefault })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func originTTLUsesTheInjectedRandomizer() {
|
||||||
|
let ttl = BLEOriginTTLPolicy.originTTL(range: 5...7, randomTTL: { _ in 6 })
|
||||||
|
#expect(ttl == 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A send path must never trap on a bad range.
|
||||||
|
@Test func degenerateRangesFallBackToTheDefault() {
|
||||||
|
#expect(BLEOriginTTLPolicy.originTTL(range: 0...0) == TransportConfig.messageTTLDefault)
|
||||||
|
// Single-value range is legitimate and must be honoured.
|
||||||
|
#expect(BLEOriginTTLPolicy.originTTL(range: 4...4) == 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The floor must not drop below the dense-graph relay clamp: going lower
|
||||||
|
/// costs reach without buying ambiguity the clamp does not already provide.
|
||||||
|
@Test func rangeSitsBetweenTheDenseClampAndTheDefault() {
|
||||||
|
let range = TransportConfig.broadcastOriginTTLRange
|
||||||
|
#expect(range.upperBound == TransportConfig.messageTTLDefault)
|
||||||
|
#expect(range.lowerBound >= TransportConfig.bleFragmentRelayTtlCapDense)
|
||||||
|
#expect(range.lowerBound >= 2, "TTL 1 is dropped by RelayController")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Voice burst TTL
|
||||||
|
|
||||||
|
/// Per-frame drawing would be worse than useless: at ~15 frames a second an
|
||||||
|
/// observer collects the range maximum almost immediately, so the sender is
|
||||||
|
/// marked within a fraction of a second while every low draw still costs
|
||||||
|
/// reach. One draw per burst makes a burst a single sample.
|
||||||
|
@Test func voiceFramesInOneBurstShareOneTTL() {
|
||||||
|
let start = Date(timeIntervalSince1970: 1_784_000_000)
|
||||||
|
let first = BLEOriginTTLPolicy.voiceBurstTTL(
|
||||||
|
now: start, lastFrameAt: nil, currentBurstTTL: nil, randomTTL: { _ in 6 }
|
||||||
|
)
|
||||||
|
// Subsequent frames inside the burst must reuse it even though the
|
||||||
|
// randomizer would now return something else.
|
||||||
|
var last = start
|
||||||
|
var current = first
|
||||||
|
for step in 1...20 {
|
||||||
|
let now = start.addingTimeInterval(Double(step) * 0.066)
|
||||||
|
current = BLEOriginTTLPolicy.voiceBurstTTL(
|
||||||
|
now: now, lastFrameAt: last, currentBurstTTL: current, randomTTL: { _ in 7 }
|
||||||
|
)
|
||||||
|
last = now
|
||||||
|
#expect(current == first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func aNewBurstRedrawsTheTTL() {
|
||||||
|
let start = Date(timeIntervalSince1970: 1_784_000_000)
|
||||||
|
let first = BLEOriginTTLPolicy.voiceBurstTTL(
|
||||||
|
now: start, lastFrameAt: nil, currentBurstTTL: nil, randomTTL: { _ in 5 }
|
||||||
|
)
|
||||||
|
#expect(first == 5)
|
||||||
|
|
||||||
|
// A gap longer than the burst window means a new talk burst.
|
||||||
|
let later = start.addingTimeInterval(BLEOriginTTLPolicy.voiceBurstGap + 0.5)
|
||||||
|
let second = BLEOriginTTLPolicy.voiceBurstTTL(
|
||||||
|
now: later, lastFrameAt: start, currentBurstTTL: first, randomTTL: { _ in 7 }
|
||||||
|
)
|
||||||
|
#expect(second == 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func voiceBurstTTLStaysInRange() {
|
||||||
|
var last: Date?
|
||||||
|
var current: UInt8?
|
||||||
|
let start = Date(timeIntervalSince1970: 1_784_000_000)
|
||||||
|
for step in 0..<200 {
|
||||||
|
// Gaps long enough to force a fresh draw each time.
|
||||||
|
let now = start.addingTimeInterval(Double(step) * 5)
|
||||||
|
let ttl = BLEOriginTTLPolicy.voiceBurstTTL(
|
||||||
|
now: now, lastFrameAt: last, currentBurstTTL: current
|
||||||
|
)
|
||||||
|
#expect(TransportConfig.broadcastOriginTTLRange.contains(ttl))
|
||||||
|
last = now
|
||||||
|
current = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Wiring
|
||||||
|
|
||||||
|
/// The first version of this change defined the policy and used it in
|
||||||
|
/// exactly one place, leaving voice, files, group messages and board posts
|
||||||
|
/// originating at a fixed maximum — i.e. still perfectly marked, while the
|
||||||
|
/// docs claimed otherwise. A policy that exists but is not wired is worse
|
||||||
|
/// than none, because it reads as solved.
|
||||||
|
///
|
||||||
|
/// This asserts against the source rather than behaviour because the send
|
||||||
|
/// paths need a live radio; it is a cheap guard against the specific
|
||||||
|
/// regression of adding a broadcast origination site and forgetting it.
|
||||||
|
@Test func everyAuthoredBroadcastOriginatesWithADrawnTTL() throws {
|
||||||
|
let source = URL(fileURLWithPath: #filePath)
|
||||||
|
.deletingLastPathComponent() // Services
|
||||||
|
.deletingLastPathComponent() // bitchatTests
|
||||||
|
.deletingLastPathComponent() // repo root
|
||||||
|
.appendingPathComponent("bitchat/Services/BLE/BLEService.swift")
|
||||||
|
let lines = try String(contentsOf: source, encoding: .utf8)
|
||||||
|
.components(separatedBy: .newlines)
|
||||||
|
|
||||||
|
// Packet constructions that still pin the fixed maximum.
|
||||||
|
let fixed = lines.enumerated().filter {
|
||||||
|
$0.element.contains("ttl: messageTTL") || $0.element.contains("ttl: self.messageTTL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each remaining one must be a deliberate exclusion. Announces keep the
|
||||||
|
// fixed TTL for link binding; the rest are directed, diagnostics, or
|
||||||
|
// re-broadcasts. Identify by the packet type named just above.
|
||||||
|
let allowedTypes = [
|
||||||
|
"announce", // link binding depends on ttl == max
|
||||||
|
"ping", "pong", // diagnostics; payload records origin TTL for hops
|
||||||
|
"noiseEncrypted", "noiseHandshake", "courierEnvelope", // directed
|
||||||
|
"fileTransfer", // the directed variant; the broadcast one is drawn
|
||||||
|
"prekeyBundle", // payload already names its owner
|
||||||
|
"nostrCarrier" // re-broadcast, not authorship
|
||||||
|
]
|
||||||
|
|
||||||
|
var unexplained: [String] = []
|
||||||
|
for (index, line) in fixed {
|
||||||
|
let window = lines[max(0, index - 14)...index].joined(separator: "\n")
|
||||||
|
guard !allowedTypes.contains(where: { window.contains("MessageType.\($0)") }) else { continue }
|
||||||
|
unexplained.append("BLEService.swift:\(index + 1) — \(line.trimmingCharacters(in: .whitespaces))")
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(
|
||||||
|
unexplained.isEmpty,
|
||||||
|
"""
|
||||||
|
These broadcast origination sites still use the fixed maximum TTL, \
|
||||||
|
which marks this device as the author to any direct listener. Use \
|
||||||
|
BLEOriginTTLPolicy.originTTL(), or add the type to allowedTypes here \
|
||||||
|
with the reason.
|
||||||
|
|
||||||
|
\(unexplained.joined(separator: "\n"))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Neighbour list
|
||||||
|
|
||||||
|
@Test func neighborAdvertisingIsOffByDefault() {
|
||||||
|
#expect(!TransportConfig.announceIncludesDirectNeighbors)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mechanism that makes this backward compatible: an empty list omits
|
||||||
|
/// the TLV entirely rather than emitting a zero-length one, and the decoder
|
||||||
|
/// treats its absence as "no topology offered".
|
||||||
|
@Test func emptyNeighborListOmitsTheTLV() throws {
|
||||||
|
let announcement = AnnouncementPacket(
|
||||||
|
nickname: "alice",
|
||||||
|
noisePublicKey: Data(repeating: 0x11, count: 32),
|
||||||
|
signingPublicKey: Data(repeating: 0x22, count: 32),
|
||||||
|
directNeighbors: [],
|
||||||
|
capabilities: [.bridge]
|
||||||
|
)
|
||||||
|
let encoded = try #require(announcement.encode())
|
||||||
|
|
||||||
|
// TLV type 0x04 is the neighbour list; it must not appear at all.
|
||||||
|
var offset = encoded.startIndex
|
||||||
|
var types: [UInt8] = []
|
||||||
|
while offset < encoded.endIndex {
|
||||||
|
guard encoded.distance(from: offset, to: encoded.endIndex) >= 2 else { break }
|
||||||
|
let type = encoded[offset]
|
||||||
|
let length = Int(encoded[encoded.index(after: offset)])
|
||||||
|
types.append(type)
|
||||||
|
offset = encoded.index(offset, offsetBy: 2 + length)
|
||||||
|
}
|
||||||
|
#expect(!types.contains(0x04))
|
||||||
|
|
||||||
|
let decoded = try #require(AnnouncementPacket.decode(from: encoded))
|
||||||
|
#expect(decoded.directNeighbors == nil)
|
||||||
|
// Everything else still round-trips, so old peers lose nothing but the
|
||||||
|
// topology hint.
|
||||||
|
#expect(decoded.nickname == "alice")
|
||||||
|
#expect(decoded.capabilities == [.bridge])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receiving a neighbour list must keep working: peers on older builds still
|
||||||
|
/// send one, and a mixed mesh has to behave sensibly.
|
||||||
|
@Test func receivedNeighborListsAreStillParsed() throws {
|
||||||
|
let neighbors = [Data(repeating: 0xA1, count: 8), Data(repeating: 0xB2, count: 8)]
|
||||||
|
let announcement = AnnouncementPacket(
|
||||||
|
nickname: "bob",
|
||||||
|
noisePublicKey: Data(repeating: 0x11, count: 32),
|
||||||
|
signingPublicKey: Data(repeating: 0x22, count: 32),
|
||||||
|
directNeighbors: neighbors
|
||||||
|
)
|
||||||
|
let encoded = try #require(announcement.encode())
|
||||||
|
let decoded = try #require(AnnouncementPacket.decode(from: encoded))
|
||||||
|
#expect(decoded.directNeighbors == neighbors)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -562,7 +562,7 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping () -> Bool
|
condition: @escaping () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ struct SecureIdentityStateManagerVouchTests {
|
|||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping () -> Bool
|
condition: @escaping () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ struct GossipSyncBoardTests {
|
|||||||
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .board)
|
let request = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .board)
|
||||||
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: request)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
let sent = try #require(delegate.packets.first)
|
let sent = try #require(delegate.packets.first)
|
||||||
#expect(sent.type == MessageType.boardPost.rawValue)
|
#expect(sent.type == MessageType.boardPost.rawValue)
|
||||||
#expect(sent.isRSR)
|
#expect(sent.isRSR)
|
||||||
@@ -69,7 +69,7 @@ struct GossipSyncBoardTests {
|
|||||||
let boardRequest = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .board)
|
let boardRequest = RequestSyncPacket(p: 4, m: 1, data: Data(), types: .board)
|
||||||
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: boardRequest)
|
manager.handleRequestSync(from: PeerID(str: "FFFFFFFFFFFFFFFF"), request: boardRequest)
|
||||||
|
|
||||||
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.settleTimeout)
|
try await TestHelpers.waitFor({ delegate.packets.count == 1 }, timeout: TestConstants.shortTimeout)
|
||||||
#expect(delegate.packets.count == 1)
|
#expect(delegate.packets.count == 1)
|
||||||
#expect(delegate.packets.first?.type == MessageType.boardPost.rawValue)
|
#expect(delegate.packets.first?.type == MessageType.boardPost.rawValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ final class RequestSyncManagerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
timeout: TimeInterval = TestConstants.settleTimeout,
|
timeout: TimeInterval = 1.0,
|
||||||
condition: @escaping () -> Bool
|
condition: @escaping () -> Bool
|
||||||
) async -> Bool {
|
) async -> Bool {
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
let deadline = Date().addingTimeInterval(timeout)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Foundation
|
|||||||
|
|
||||||
struct TestConstants {
|
struct TestConstants {
|
||||||
static let defaultTimeout: TimeInterval = 5.0
|
static let defaultTimeout: TimeInterval = 5.0
|
||||||
|
static let shortTimeout: TimeInterval = 1.0
|
||||||
/// For positive waits on work that hops through `Task.detached` or
|
/// For positive waits on work that hops through `Task.detached` or
|
||||||
/// background queues: those contend with every parallel test worker for
|
/// background queues: those contend with every parallel test worker for
|
||||||
/// the global executor, so a loaded CI runner can exceed
|
/// the global executor, so a loaded CI runner can exceed
|
||||||
@@ -18,47 +19,6 @@ struct TestConstants {
|
|||||||
/// so passing runs never pay the longer timeout.
|
/// so passing runs never pay the longer timeout.
|
||||||
static let longTimeout: TimeInterval = 10.0
|
static let longTimeout: TimeInterval = 10.0
|
||||||
|
|
||||||
/// **Default deadline for any "wait until this async thing settles" helper.**
|
|
||||||
///
|
|
||||||
/// Four separate tests flaked on CI during July 2026 with the same root
|
|
||||||
/// cause, and it is worth stating the rule rather than re-learning it a
|
|
||||||
/// fifth time: *a wait deadline is not a latency budget.* It exists so a
|
|
||||||
/// genuine hang eventually fails the suite. Size it for the worst-case
|
|
||||||
/// scheduler, never for how long the operation "should" take.
|
|
||||||
///
|
|
||||||
/// A CI runner executes many suites at once. Work behind `@MainActor`,
|
|
||||||
/// `Task.detached(priority: .utility)`, or a `DispatchQueue.asyncAfter` can
|
|
||||||
/// be starved for seconds — one observed run took 3.75 s for a 1 s
|
|
||||||
/// operation. Deadlines sized to the operation (the old 1 s defaults) turn
|
|
||||||
/// that starvation into a red build that reads like a product bug.
|
|
||||||
///
|
|
||||||
/// This costs nothing when tests pass, because every helper returns as soon
|
|
||||||
/// as its condition holds. It only extends the genuine-failure case.
|
|
||||||
///
|
|
||||||
/// `TestTimingHygieneTests` enforces that wait helpers default to at least
|
|
||||||
/// `minimumSettleTimeout`.
|
|
||||||
static let settleTimeout: TimeInterval = 30.0
|
|
||||||
|
|
||||||
/// Floor enforced by `TestTimingHygieneTests`. Anything below this is a
|
|
||||||
/// latency assumption in disguise.
|
|
||||||
static let minimumSettleTimeout: TimeInterval = 10.0
|
|
||||||
|
|
||||||
/// For waits whose **expected outcome is `false`** — "prove this does not
|
|
||||||
/// happen".
|
|
||||||
///
|
|
||||||
/// The floor above is wrong for these, and inverted: a negative wait always
|
|
||||||
/// runs its deadline out, so `settleTimeout` would spend 30 s per case
|
|
||||||
/// proving nothing extra. Starvation cannot cause a false failure here
|
|
||||||
/// either — a starved runner only makes the thing *less* likely to happen,
|
|
||||||
/// so the assertion still holds. Short is correct, and naming it says the
|
|
||||||
/// polarity out loud instead of leaving a bare literal that reads like the
|
|
||||||
/// mistake this file exists to prevent.
|
|
||||||
///
|
|
||||||
/// `TestTimingHygieneTests` accepts this by name. Using it for a wait you
|
|
||||||
/// expect to succeed reintroduces exactly the flake class it sits next to.
|
|
||||||
static let negativeWaitWindow: TimeInterval = 1.0
|
|
||||||
|
|
||||||
|
|
||||||
static let testNickname1 = "Alice"
|
static let testNickname1 = "Alice"
|
||||||
static let testNickname2 = "Bob"
|
static let testNickname2 = "Bob"
|
||||||
static let testNickname3 = "Charlie"
|
static let testNickname3 = "Charlie"
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Testing
|
|
||||||
|
|
||||||
/// Guards the test suite against the flake class that produced four separate
|
|
||||||
/// red builds in July 2026: **treating a wait deadline as a latency budget.**
|
|
||||||
///
|
|
||||||
/// A CI runner executes many suites at once, so work behind `@MainActor`,
|
|
||||||
/// `Task.detached(priority: .utility)`, or `DispatchQueue.asyncAfter` can be
|
|
||||||
/// starved for seconds. One observed run took 3.75 s for a 1 s operation.
|
|
||||||
/// Deadlines sized to how long the operation "should" take turn that starvation
|
|
||||||
/// into a red build that reads like a product bug, and the debugging cost lands
|
|
||||||
/// on whoever opened an unrelated PR.
|
|
||||||
///
|
|
||||||
/// Two rules, both enforced below:
|
|
||||||
///
|
|
||||||
/// 1. A wait helper's default deadline must be at least
|
|
||||||
/// `TestConstants.minimumSettleTimeout`. Waits return as soon as their
|
|
||||||
/// condition holds, so a generous deadline is free in the passing case.
|
|
||||||
/// 2. No test asserts an *upper bound* on elapsed wall-clock time. Such an
|
|
||||||
/// assertion cannot distinguish the behaviour under test from a slow
|
|
||||||
/// machine, so it can only be flaky. Assert the property somewhere it is
|
|
||||||
/// computable — with an injected clock, on the pure logic — instead.
|
|
||||||
///
|
|
||||||
/// Both rules can be waived per line with `\(Self.waiver)` plus a reason, for
|
|
||||||
/// the rare case where the timing itself is genuinely the thing under test.
|
|
||||||
struct TestTimingHygieneTests {
|
|
||||||
/// Opt-out marker. Reviewers should expect a reason next to it.
|
|
||||||
static let waiver = "test-timing-ok:"
|
|
||||||
|
|
||||||
private static let testsRoot = URL(fileURLWithPath: #filePath)
|
|
||||||
.deletingLastPathComponent() // TestUtilities
|
|
||||||
.deletingLastPathComponent() // bitchatTests
|
|
||||||
|
|
||||||
private struct Line {
|
|
||||||
let file: String
|
|
||||||
let number: Int
|
|
||||||
let text: String
|
|
||||||
/// True when the waiver appears on this line or in the comment block
|
|
||||||
/// immediately above it, so a reason can be written at readable length
|
|
||||||
/// rather than crammed onto the end of the code line.
|
|
||||||
let waived: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func swiftLines() throws -> [Line] {
|
|
||||||
let enumerator = FileManager.default.enumerator(
|
|
||||||
at: testsRoot,
|
|
||||||
includingPropertiesForKeys: nil
|
|
||||||
)
|
|
||||||
var out: [Line] = []
|
|
||||||
while let url = enumerator?.nextObject() as? URL {
|
|
||||||
guard url.pathExtension == "swift" else { continue }
|
|
||||||
// This file necessarily contains the patterns it bans.
|
|
||||||
guard url.lastPathComponent != "TestTimingHygieneTests.swift" else { continue }
|
|
||||||
let name = url.lastPathComponent
|
|
||||||
let texts = try String(contentsOf: url, encoding: .utf8)
|
|
||||||
.components(separatedBy: .newlines)
|
|
||||||
for (index, text) in texts.enumerated() {
|
|
||||||
// Scan back over an unbroken run of comment lines.
|
|
||||||
var waived = text.contains(waiver)
|
|
||||||
var back = index - 1
|
|
||||||
while !waived, back >= 0 {
|
|
||||||
let above = texts[back].trimmingCharacters(in: .whitespaces)
|
|
||||||
guard above.hasPrefix("//") else { break }
|
|
||||||
waived = above.contains(waiver)
|
|
||||||
back -= 1
|
|
||||||
}
|
|
||||||
out.append(Line(file: name, number: index + 1, text: text, waived: waived))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func isWaived(_ line: Line) -> Bool {
|
|
||||||
line.waived
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rule 1: no wait helper may default to a deadline below the floor.
|
|
||||||
@Test func waitHelpersDoNotDefaultToShortDeadlines() throws {
|
|
||||||
let lines = try Self.swiftLines()
|
|
||||||
#expect(!lines.isEmpty, "hygiene scan found no test sources — check the path")
|
|
||||||
|
|
||||||
// Two shapes, both of which have flaked here:
|
|
||||||
// a declaration default — `timeout: TimeInterval = 2.5`
|
|
||||||
// a wait call site — `wait(for:…, timeout: 1.0)`, `waitUntil(timeout: 5.0)`
|
|
||||||
//
|
|
||||||
// Deliberately NOT matched: a bare `timeout:` label on something that is
|
|
||||||
// not a wait, such as the injected production handshake timeouts in the
|
|
||||||
// Noise tests. Those are the behaviour under test, and a short value is
|
|
||||||
// correct there.
|
|
||||||
let patterns = [
|
|
||||||
#"(?:timeout|deadline)\s*:\s*TimeInterval\s*=\s*([0-9]+(?:\.[0-9]+)?)"#,
|
|
||||||
#"(?:wait|waitUntil|waitFor|fulfillment)\s*\([^)]*\btimeout:\s*([0-9]+(?:\.[0-9]+)?)"#
|
|
||||||
].map { try? NSRegularExpression(pattern: $0) }.compactMap { $0 }
|
|
||||||
#expect(patterns.count == 2, "hygiene regexes failed to compile")
|
|
||||||
|
|
||||||
// Named constants hide the same mistake behind a symbol, and did: the
|
|
||||||
// fifth flake of the session was `timeout: TestConstants.shortTimeout`
|
|
||||||
// (1 s) on a positive wait, which a literals-only scan cannot see.
|
|
||||||
// `shortTimeout` itself is deleted (Periphery flagged it dead once its
|
|
||||||
// last wait site converted); the ban stays so it cannot come back.
|
|
||||||
// `negativeWaitWindow` is deliberately absent — short is correct there.
|
|
||||||
let bannedConstants = ["shortTimeout", "defaultTimeout"]
|
|
||||||
|
|
||||||
var offenders: [String] = []
|
|
||||||
for line in lines where !Self.isWaived(line) {
|
|
||||||
let range = NSRange(line.text.startIndex..., in: line.text)
|
|
||||||
var flagged = false
|
|
||||||
for pattern in patterns {
|
|
||||||
guard let match = pattern.firstMatch(in: line.text, range: range),
|
|
||||||
let valueRange = Range(match.range(at: 1), in: line.text),
|
|
||||||
let value = TimeInterval(line.text[valueRange]),
|
|
||||||
value < TestConstants.minimumSettleTimeout else { continue }
|
|
||||||
offenders.append("\(line.file):\(line.number) — \(value)s: \(line.text.trimmingCharacters(in: .whitespaces))")
|
|
||||||
flagged = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
guard !flagged else { continue }
|
|
||||||
for name in bannedConstants
|
|
||||||
where line.text.contains("timeout: TestConstants.\(name)") {
|
|
||||||
offenders.append("\(line.file):\(line.number) — TestConstants.\(name): \(line.text.trimmingCharacters(in: .whitespaces))")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#expect(
|
|
||||||
offenders.isEmpty,
|
|
||||||
"""
|
|
||||||
Wait deadlines below \(TestConstants.minimumSettleTimeout)s are latency \
|
|
||||||
assumptions and will flake on a loaded runner. Use \
|
|
||||||
TestConstants.settleTimeout, or add "\(Self.waiver) <reason>" if the \
|
|
||||||
timing really is what the test asserts.
|
|
||||||
|
|
||||||
\(offenders.joined(separator: "\n"))
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rule 2: no test bounds elapsed wall-clock time from above.
|
|
||||||
///
|
|
||||||
/// This is the assertion that started it all — `XCTAssertLessThan(
|
|
||||||
/// Date().timeIntervalSince(start), 1.4)` proving a debounce deadline was
|
|
||||||
/// not restarted. It cannot separate "behaved correctly" from "runner was
|
|
||||||
/// busy", so it only ever fails for the wrong reason.
|
|
||||||
@Test func testsDoNotAssertUpperBoundsOnElapsedTime() throws {
|
|
||||||
let lines = try Self.swiftLines()
|
|
||||||
|
|
||||||
let elapsedAssertion = try NSRegularExpression(
|
|
||||||
pattern: #"(?:XCTAssertLessThan|XCTAssertLessThanOrEqual)\s*\(\s*(?:Date\(\)\.timeIntervalSince|[A-Za-z_][A-Za-z0-9_]*\.timeIntervalSince|ContinuousClock)"#
|
|
||||||
)
|
|
||||||
|
|
||||||
var offenders: [String] = []
|
|
||||||
for line in lines where !Self.isWaived(line) {
|
|
||||||
let range = NSRange(line.text.startIndex..., in: line.text)
|
|
||||||
guard elapsedAssertion.firstMatch(in: line.text, range: range) != nil else { continue }
|
|
||||||
offenders.append("\(line.file):\(line.number) — \(line.text.trimmingCharacters(in: .whitespaces))")
|
|
||||||
}
|
|
||||||
|
|
||||||
#expect(
|
|
||||||
offenders.isEmpty,
|
|
||||||
"""
|
|
||||||
An upper bound on elapsed wall-clock time cannot distinguish the \
|
|
||||||
behaviour under test from a slow machine. Assert the property where \
|
|
||||||
it is computable — inject a clock, or test the pure logic — or add \
|
|
||||||
"\(Self.waiver) <reason>".
|
|
||||||
|
|
||||||
\(offenders.joined(separator: "\n"))
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The floor must stay meaningfully above the operations being waited on,
|
|
||||||
/// and the default must satisfy the rule this file enforces.
|
|
||||||
@Test func settleTimeoutsAreSelfConsistent() {
|
|
||||||
#expect(TestConstants.settleTimeout >= TestConstants.minimumSettleTimeout)
|
|
||||||
#expect(TestConstants.minimumSettleTimeout > TestConstants.defaultTimeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -101,7 +101,7 @@ struct VoiceCaptureSessionTests {
|
|||||||
_ condition: () -> Bool,
|
_ condition: () -> Bool,
|
||||||
sourceLocation: SourceLocation = #_sourceLocation
|
sourceLocation: SourceLocation = #_sourceLocation
|
||||||
) async {
|
) async {
|
||||||
let deadline = ContinuousClock.now.advanced(by: .seconds(TestConstants.settleTimeout))
|
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
|
||||||
while !condition(), ContinuousClock.now < deadline {
|
while !condition(), ContinuousClock.now < deadline {
|
||||||
await Task.yield()
|
await Task.yield()
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000)
|
try? await Task.sleep(nanoseconds: 1_000_000)
|
||||||
|
|||||||
@@ -59,24 +59,11 @@ struct VoiceNotePlaybackControllerTests {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits for an async settle, then asserts.
|
|
||||||
///
|
|
||||||
/// The deadline is deliberately far larger than the work it waits on. Every
|
|
||||||
/// condition here depends on a `@MainActor` Task that playback schedules
|
|
||||||
/// (the session acquire and its failure path), and on a CI runner executing
|
|
||||||
/// many suites in parallel that Task can simply not be scheduled for
|
|
||||||
/// seconds. At five seconds this timed out on CI and reported *two*
|
|
||||||
/// failures — the wait itself, and the `!isPlaying` that the un-run failure
|
|
||||||
/// path had not yet reset — which reads like a playback bug rather than a
|
|
||||||
/// starved scheduler.
|
|
||||||
///
|
|
||||||
/// A generous deadline costs nothing when the condition holds, since this
|
|
||||||
/// returns as soon as it does; it only extends the genuine-failure case.
|
|
||||||
private func waitUntil(
|
private func waitUntil(
|
||||||
_ condition: () -> Bool,
|
_ condition: () -> Bool,
|
||||||
sourceLocation: SourceLocation = #_sourceLocation
|
sourceLocation: SourceLocation = #_sourceLocation
|
||||||
) async {
|
) async {
|
||||||
let deadline = ContinuousClock.now.advanced(by: .seconds(TestConstants.settleTimeout))
|
let deadline = ContinuousClock.now.advanced(by: .seconds(5))
|
||||||
while !condition(), ContinuousClock.now < deadline {
|
while !condition(), ContinuousClock.now < deadline {
|
||||||
await Task.yield()
|
await Task.yield()
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000)
|
try? await Task.sleep(nanoseconds: 1_000_000)
|
||||||
|
|||||||
@@ -26,13 +26,22 @@ Signed announces can expose:
|
|||||||
|
|
||||||
- Nickname, persistent Noise public key, and Ed25519 signing public key
|
- Nickname, persistent Noise public key, and Ed25519 signing public key
|
||||||
- Capability flags
|
- Capability flags
|
||||||
- A bounded set of short direct-neighbor identifiers
|
|
||||||
- A coarse rendezvous geohash when the bridge capability is enabled
|
- A coarse rendezvous geohash when the bridge capability is enabled
|
||||||
|
|
||||||
|
Announces no longer advertise this device's direct neighbours. That TLV carried up to ten peer IDs, so a single passive receiver could reconstruct the local adjacency graph — who is standing next to whom — with no need for multiple receivers or signal-strength trilateration. It is off by default (`TransportConfig.announceIncludesDirectNeighbors`). Neighbour lists from other peers are still parsed, so a mixed network behaves sensibly. The cost is source routing: its adjacency map comes from these lists, so directed traffic falls back to flooding, which is already the documented fallback whenever a route fails.
|
||||||
|
|
||||||
The app does not advertise the device's assigned name. iOS manages BLE address randomization; bitchat does not attempt to create a stable MAC address.
|
The app does not advertise the device's assigned name. iOS manages BLE address randomization; bitchat does not attempt to create a stable MAC address.
|
||||||
|
|
||||||
That randomization does not deliver the unlinkability it might suggest, because the application layer publishes stable identifiers above it. The 8-byte peer ID in every packet header is the first 8 bytes of the Noise static key fingerprint, so it does not rotate; announces carry the static keys themselves; and the fixed service UUID makes any bitchat device detectable as such by a passive scanner. A receiver in radio range can therefore recognise a specific device across sessions and locations, and detect that the app is in use at all. RSSI, timing, traffic volume, and radio fingerprints remain observable as well.
|
That randomization does not deliver the unlinkability it might suggest, because the application layer publishes stable identifiers above it. The 8-byte peer ID in every packet header is the first 8 bytes of the Noise static key fingerprint, so it does not rotate; announces carry the static keys themselves; and the fixed service UUID makes any bitchat device detectable as such by a passive scanner. A receiver in radio range can therefore recognise a specific device across sessions and locations, and detect that the app is in use at all. RSSI, timing, traffic volume, and radio fingerprints remain observable as well.
|
||||||
|
|
||||||
|
Public broadcasts that carry authored content — public and group messages, broadcast files, board posts, live voice, and leave — are originated with a TTL drawn from a range rather than always at the maximum. A fixed maximum made `ttl == default` a reliable "this device wrote it" marker to any direct listener, disclosing authorship rather than mere presence. Live voice draws once per talk burst, not per frame; at roughly 15 frames a second a per-frame draw would surrender the range maximum almost immediately.
|
||||||
|
|
||||||
|
This reduces the marker, it does not remove it. Relays strictly decrement, so the top of the range can still only come from an origin — one message in three, or about an 87% chance of self-identifying within five messages. It protects an occasional sender considerably and a chatty one little. Eliminating it needs relays to sometimes not decrement, which trades against TTL's role as the loop bound, so it is deliberately left as follow-up rather than claimed here.
|
||||||
|
|
||||||
|
Announces keep the fixed TTL: link binding reads a maximum-TTL announce as a direct link, and an announce already identifies its sender. Directed traffic, prekey bundles, and gateway carriers are also excluded — for directed traffic because fewer hops means fewer deliveries, and for the others because the payload already identifies its owner.
|
||||||
|
|
||||||
|
Payload length remains observable for most traffic: only Noise frames are padded, and the padding itself is inside the signed bytes, so widening its coverage or fixing its length-marker gap is a coordinated cross-platform change rather than a local one.
|
||||||
|
|
||||||
Ingress validates announce structure, sender binding, signatures, payload sizes, and freshness. Current-link Noise authentication is required before destructive courier handoff or strict directed delivery. Floods, queues, fragments, ingress work, and per-peer state are bounded.
|
Ingress validates announce structure, sender binding, signatures, payload sizes, and freshness. Current-link Noise authentication is required before destructive courier handoff or strict directed delivery. Floods, queues, fragments, ingress work, and per-peer state are bounded.
|
||||||
|
|
||||||
## Private Messaging and Courier Delivery
|
## Private Messaging and Courier Delivery
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Foundation
|
|||||||
// Kept local until the test-helper module is split out.
|
// Kept local until the test-helper module is split out.
|
||||||
struct TestConstants {
|
struct TestConstants {
|
||||||
static let defaultTimeout: TimeInterval = 5.0
|
static let defaultTimeout: TimeInterval = 5.0
|
||||||
|
static let shortTimeout: TimeInterval = 1.0
|
||||||
static let longTimeout: TimeInterval = 10.0
|
static let longTimeout: TimeInterval = 10.0
|
||||||
|
|
||||||
static let testNickname1 = "Alice"
|
static let testNickname1 = "Alice"
|
||||||
|
|||||||
Reference in New Issue
Block a user