mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 04:45:20 +00:00
174 lines
7.6 KiB
Swift
174 lines
7.6 KiB
Swift
import BitFoundation
|
|
import Foundation
|
|
import Testing
|
|
@testable import bitchat
|
|
|
|
@Suite("Share extension handoff", .serialized)
|
|
struct SharedContentHandoffTests {
|
|
private func makeStore() -> (suite: String, defaults: UserDefaults, store: SharedContentStore) {
|
|
let suite = "SharedContentHandoffTests.\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suite)!
|
|
defaults.removePersistentDomain(forName: suite)
|
|
return (suite, defaults, SharedContentStore(defaults: defaults))
|
|
}
|
|
|
|
@Test("A staged share survives an inactive app and a late open")
|
|
func stagedShareSurvivesLateOpen() throws {
|
|
let context = makeStore()
|
|
defer { context.defaults.removePersistentDomain(forName: context.suite) }
|
|
let stagedAt = Date(timeIntervalSince1970: 1_000_000)
|
|
let payload = SharedContentPayload.text("review me later", createdAt: stagedAt)
|
|
|
|
try context.store.stage(payload, now: stagedAt)
|
|
|
|
#expect(context.store.pending(now: stagedAt.addingTimeInterval(60 * 60)) == payload)
|
|
#expect(context.defaults.data(forKey: SharedContentStore.storageKey) != nil)
|
|
}
|
|
|
|
@Test("Malformed, oversized, unsupported, and expired payloads are rejected and cleared")
|
|
func invalidPayloadsAreRejectedAndCleared() throws {
|
|
let context = makeStore()
|
|
defer { context.defaults.removePersistentDomain(forName: context.suite) }
|
|
let now = Date(timeIntervalSince1970: 2_000_000)
|
|
|
|
context.defaults.set(Data("not-json".utf8), forKey: SharedContentStore.storageKey)
|
|
#expect(context.store.pending(now: now) == nil)
|
|
#expect(context.defaults.object(forKey: SharedContentStore.storageKey) == nil)
|
|
|
|
context.defaults.set(
|
|
Data(repeating: 0x41, count: SharedContentPayload.maxEnvelopeBytes + 1),
|
|
forKey: SharedContentStore.storageKey
|
|
)
|
|
#expect(context.store.pending(now: now) == nil)
|
|
#expect(context.defaults.object(forKey: SharedContentStore.storageKey) == nil)
|
|
|
|
let oversized = SharedContentPayload.text(
|
|
String(repeating: "x", count: SharedContentPayload.maxContentBytes + 1),
|
|
createdAt: now
|
|
)
|
|
#expect(throws: SharedContentHandoffError.contentTooLarge) {
|
|
try context.store.stage(oversized, now: now)
|
|
}
|
|
#expect(context.defaults.object(forKey: SharedContentStore.storageKey) == nil)
|
|
|
|
let unsupportedURL = SharedContentPayload(
|
|
kind: .url,
|
|
content: "file:///private/tmp/secret.txt",
|
|
createdAt: now
|
|
)
|
|
#expect(throws: SharedContentHandoffError.unsupportedURL) {
|
|
try context.store.stage(unsupportedURL, now: now)
|
|
}
|
|
|
|
let misleadingControl = SharedContentPayload.text("safe\u{202E}txt", createdAt: now)
|
|
#expect(throws: SharedContentHandoffError.invalidCharacters) {
|
|
try context.store.stage(misleadingControl, now: now)
|
|
}
|
|
|
|
let expired = SharedContentPayload.text(
|
|
"too old",
|
|
createdAt: now.addingTimeInterval(-SharedContentPayload.retentionSeconds - 1)
|
|
)
|
|
context.defaults.set(try JSONEncoder().encode(expired), forKey: SharedContentStore.storageKey)
|
|
#expect(context.store.pending(now: now) == nil)
|
|
#expect(context.defaults.object(forKey: SharedContentStore.storageKey) == nil)
|
|
}
|
|
|
|
@Test("Mesh, geohash, and stale private selections resolve to explicit destinations")
|
|
func destinationsAreExplicit() {
|
|
let geohashChannel = ChannelID.location(
|
|
GeohashChannel(level: .city, geohash: "9Q8YY")
|
|
)
|
|
let stalePeer = PeerID(str: "0011223344556677")
|
|
|
|
#expect(SharedContentDestination.resolve(
|
|
selectedPrivatePeerID: nil,
|
|
privateDisplayName: nil,
|
|
activeChannel: .mesh
|
|
) == .mesh)
|
|
#expect(SharedContentDestination.resolve(
|
|
selectedPrivatePeerID: nil,
|
|
privateDisplayName: nil,
|
|
activeChannel: geohashChannel
|
|
) == .geohash("9q8yy"))
|
|
#expect(SharedContentDestination.resolve(
|
|
selectedPrivatePeerID: stalePeer,
|
|
privateDisplayName: "alice",
|
|
activeChannel: geohashChannel
|
|
) == .privateConversation(peerID: stalePeer, displayName: "alice"))
|
|
}
|
|
|
|
@Test("A destination change requires a new confirmation and never consumes on the stale tap")
|
|
@MainActor
|
|
func staleDestinationCannotBeConfirmed() throws {
|
|
let context = makeStore()
|
|
defer { context.defaults.removePersistentDomain(forName: context.suite) }
|
|
let now = Date(timeIntervalSince1970: 3_000_000)
|
|
let payload = SharedContentPayload.text("do not auto-send", createdAt: now)
|
|
let peer = PeerID(str: "8899aabbccddeeff")
|
|
let privateDestination = SharedContentDestination.privateConversation(
|
|
peerID: peer,
|
|
displayName: "alice"
|
|
)
|
|
let model = SharedContentImportModel(store: context.store)
|
|
try context.store.stage(payload, now: now)
|
|
model.refresh(destination: privateDestination, now: now)
|
|
|
|
#expect(model.confirm(destination: .mesh, now: now) == nil)
|
|
#expect(model.offer?.destination == .mesh)
|
|
#expect(context.store.pending(now: now) == payload)
|
|
|
|
#expect(model.confirm(destination: .mesh, now: now) == payload.content)
|
|
#expect(model.offer == nil)
|
|
#expect(context.store.pending(now: now) == nil)
|
|
}
|
|
|
|
@Test("Confirmation consumes once and cancellation explicitly clears without producing composer text")
|
|
@MainActor
|
|
func oneTimeConfirmationAndCancellation() throws {
|
|
let context = makeStore()
|
|
defer { context.defaults.removePersistentDomain(forName: context.suite) }
|
|
let now = Date(timeIntervalSince1970: 4_000_000)
|
|
let model = SharedContentImportModel(store: context.store)
|
|
|
|
let first = SharedContentPayload.text("confirmed", createdAt: now)
|
|
try context.store.stage(first, now: now)
|
|
model.refresh(destination: .geohash("u4pruy"), now: now)
|
|
#expect(model.confirm(destination: .geohash("u4pruy"), now: now) == "confirmed")
|
|
#expect(model.confirm(destination: .geohash("u4pruy"), now: now) == nil)
|
|
|
|
let second = SharedContentPayload.text("cancelled", createdAt: now)
|
|
try context.store.stage(second, now: now)
|
|
model.refresh(destination: .mesh, now: now)
|
|
model.cancel(destination: .mesh, now: now)
|
|
#expect(model.offer == nil)
|
|
#expect(context.store.pending(now: now) == nil)
|
|
|
|
let third = SharedContentPayload.text("panic-wiped", createdAt: now)
|
|
try context.store.stage(third, now: now)
|
|
model.refresh(destination: .mesh, now: now)
|
|
model.discardAll()
|
|
#expect(model.offer == nil)
|
|
#expect(context.store.pending(now: now) == nil)
|
|
}
|
|
|
|
@Test("Cancelling an old review never deletes a newer staged share")
|
|
@MainActor
|
|
func cancellationPreservesNewerShare() throws {
|
|
let context = makeStore()
|
|
defer { context.defaults.removePersistentDomain(forName: context.suite) }
|
|
let now = Date(timeIntervalSince1970: 5_000_000)
|
|
let model = SharedContentImportModel(store: context.store)
|
|
let old = SharedContentPayload.text("old", createdAt: now)
|
|
let newer = SharedContentPayload.text("new", createdAt: now)
|
|
|
|
try context.store.stage(old, now: now)
|
|
model.refresh(destination: .mesh, now: now)
|
|
try context.store.stage(newer, now: now)
|
|
model.cancel(destination: .mesh, now: now)
|
|
|
|
#expect(model.offer?.payload == newer)
|
|
#expect(context.store.pending(now: now) == newer)
|
|
}
|
|
}
|