Files
bitchat/bitchatTests/WifiBulk/WifiBulkPolicyTests.swift
T
jackandClaude Fable 5 e4b3cff5fa Wi-Fi bulk transport: AWDL data plane for large media with BLE fallback
BLE stays the control plane: a sender with a queued file > 64 KiB to a
directly connected peer advertising the wifiBulk capability sends a
bulkTransferOffer (0x04) inside the established Noise session — transferID,
file size, payload SHA-256, a fresh 32-byte token, and a random per-transfer
Bonjour instance name. The receiver answers bulkTransferResponse (0x05) with
its own token half, then both sides meet on a per-transfer
_bitchat-bulk._tcp channel over peer-to-peer Wi-Fi (AWDL).

The TCP stream is secured independently of TLS: both tokens traveled inside
Noise, so only the two peers can derive the ChaChaPoly channel key
(HKDF-SHA256, domain "bitchat-bulk-v1", transferID as salt). Frames are
length-prefixed sealed boxes with structured direction+counter nonces; the
first frame must prove knowledge of the key or the client is disconnected,
and the final hash is verified against the offer before delivery.

Decline, timeout, or any mid-transfer error falls back to BLE fragmentation
exactly once, driving the same TransferProgressManager stream so the UI is
unchanged. Wi-Fi-negotiated transfers may carry up to 8 MiB (new
FileTransferLimits.maxWifiBulkPayloadBytes, enforced by the receiver from
the accepted offer); the BLE path keeps its existing caps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:35:40 +02:00

151 lines
5.8 KiB
Swift

//
// WifiBulkPolicyTests.swift
// bitchatTests
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import BitFoundation
import Foundation
import Testing
@testable import bitchat
@Suite("WifiBulk policy")
struct WifiBulkPolicyTests {
private func candidate(
payloadBytes: Int = 300_000,
capabilities: PeerCapabilities = [.wifiBulk],
direct: Bool = true,
session: Bool = true
) -> WifiBulkPolicy.SendCandidate {
WifiBulkPolicy.SendCandidate(
payloadBytes: payloadBytes,
peerCapabilities: capabilities,
isDirectlyConnected: direct,
hasEstablishedNoiseSession: session
)
}
@Test("Eligible large transfer to a direct wifiBulk peer is offered")
func eligibleTransferOffered() {
#expect(WifiBulkPolicy.shouldOffer(candidate(), enabled: true))
}
@Test("Fallback matrix: every failed gate keeps the transfer on BLE")
func fallbackMatrix() {
// Feature disabled.
#expect(!WifiBulkPolicy.shouldOffer(candidate(), enabled: false))
// Small file: negotiation overhead not worth it.
#expect(!WifiBulkPolicy.shouldOffer(candidate(payloadBytes: 64 * 1024), enabled: true))
// Peer doesn't advertise the capability.
#expect(!WifiBulkPolicy.shouldOffer(candidate(capabilities: []), enabled: true))
#expect(!WifiBulkPolicy.shouldOffer(candidate(capabilities: [.prekeys, .gateway]), enabled: true))
// Multi-hop recipient (reachable but not directly connected).
#expect(!WifiBulkPolicy.shouldOffer(candidate(direct: false), enabled: true))
// No established Noise session to carry the offer.
#expect(!WifiBulkPolicy.shouldOffer(candidate(session: false), enabled: true))
// Payload beyond even the Wi-Fi ceiling.
#expect(!WifiBulkPolicy.shouldOffer(
candidate(payloadBytes: FileTransferLimits.maxWifiBulkPayloadBytes + 1),
enabled: true
))
}
@Test("Offer threshold is strictly greater than the minimum")
func offerThresholdBoundary() {
#expect(!WifiBulkPolicy.shouldOffer(
candidate(payloadBytes: TransportConfig.wifiBulkMinPayloadBytes),
enabled: true
))
#expect(WifiBulkPolicy.shouldOffer(
candidate(payloadBytes: TransportConfig.wifiBulkMinPayloadBytes + 1),
enabled: true
))
}
private func offer(fileSize: UInt64) -> WifiBulkOffer {
WifiBulkOffer(
transferID: Data(repeating: 1, count: WifiBulkWire.transferIDLength),
fileSize: fileSize,
payloadHash: Data(repeating: 2, count: WifiBulkWire.hashLength),
token: Data(repeating: 3, count: WifiBulkWire.tokenLength),
serviceName: "0011223344556677"
)
}
@Test("Receiver accepts an in-cap offer from a direct peer")
func receiverAccepts() {
#expect(WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: 1_000_000),
senderIsDirectlyConnected: true,
activeIncomingTransfers: 0,
enabled: true
))
}
@Test("Receiver enforces its own size cap, not the sender's word")
func receiverSizeCap() {
#expect(!WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: UInt64(FileTransferLimits.maxWifiBulkPayloadBytes) + 1),
senderIsDirectlyConnected: true,
activeIncomingTransfers: 0,
enabled: true
))
#expect(WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: UInt64(FileTransferLimits.maxWifiBulkPayloadBytes)),
senderIsDirectlyConnected: true,
activeIncomingTransfers: 0,
enabled: true
))
#expect(!WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: 0),
senderIsDirectlyConnected: true,
activeIncomingTransfers: 0,
enabled: true
))
}
@Test("Receiver declines when disabled, indirect, or saturated")
func receiverDeclines() {
#expect(!WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: 1000),
senderIsDirectlyConnected: true,
activeIncomingTransfers: 0,
enabled: false
))
#expect(!WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: 1000),
senderIsDirectlyConnected: false,
activeIncomingTransfers: 0,
enabled: true
))
#expect(!WifiBulkPolicy.shouldAccept(
offer: offer(fileSize: 1000),
senderIsDirectlyConnected: true,
activeIncomingTransfers: TransportConfig.wifiBulkMaxConcurrentIncoming,
enabled: true
))
}
@Test("This build advertises the wifiBulk capability when enabled")
func localCapabilityAdvertised() {
#expect(PeerCapabilities.localSupported.contains(.wifiBulk) == TransportConfig.wifiBulkEnabled)
}
@Test("File packets above the BLE cap encode/decode only with the Wi-Fi limit")
func filePacketWifiLimit() throws {
let content = Data(repeating: 0x5A, count: 2 * 1024 * 1024) // 2 MiB
let packet = BitchatFilePacket(fileName: "big.jpg", fileSize: UInt64(content.count), mimeType: "image/jpeg", content: content)
// BLE cap unchanged.
#expect(packet.encode() == nil)
let encoded = try #require(packet.encode(limit: FileTransferLimits.maxWifiBulkPayloadBytes))
#expect(BitchatFilePacket.decode(encoded) == nil) // BLE-cap decode still rejects
let decoded = try #require(BitchatFilePacket.decode(encoded, limit: FileTransferLimits.maxWifiBulkPayloadBytes))
#expect(decoded.content == content)
#expect(decoded.fileName == "big.jpg")
}
}