This commit is contained in:
wt committed 2026-07-30 20:07:51 +07:00
commit 92660a4ba0
228 files changed
+24496

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import XCTest
@testable import Luma
final class AccountConfigurationTests: XCTestCase {
func testValidAccountIsNormalized() throws {
let account = AccountConfiguration(
jid: " Alice@Example.ORG ",
resource: "",
manualHost: " xmpp.example.org ",
manualPort: 5222
)
let validated = try account.validated()
XCTAssertEqual(validated.normalizedJID, "alice@example.org")
XCTAssertEqual(validated.domain, "example.org")
XCTAssertEqual(validated.effectiveResource, "Luma")
XCTAssertEqual(validated.manualHost, "xmpp.example.org")
}
func testInvalidJIDIsRejected() {
XCTAssertThrowsError(try AccountConfiguration(jid: "not-a-jid").validated())
XCTAssertThrowsError(try AccountConfiguration(jid: "@example.org").validated())
XCTAssertThrowsError(try AccountConfiguration(jid: "alice@").validated())
}
func testInvalidPortIsRejected() {
XCTAssertThrowsError(
try AccountConfiguration(jid: "alice@example.org", manualPort: 70_000).validated()
)
}
}
@@ -0,0 +1,22 @@
import XCTest
@testable import Luma
final class ArchiveMessageBatchPolicyTests: XCTestCase {
func testMamPagesStaySmallEnoughForInteractiveUI() {
XCTAssertLessThanOrEqual(ArchiveMessageBatchPolicy.pageSize, 32)
XCTAssertGreaterThan(ArchiveMessageBatchPolicy.pageSize, 0)
}
func testOnlyOneStanzaIsDecodedPerMainActorSlice() {
XCTAssertEqual(ArchiveMessageBatchPolicy.decodeSliceSize, 1)
XCTAssertGreaterThan(ArchiveMessageBatchPolicy.interSliceDelayNanoseconds, 0)
}
func testInboxCanHoldAProtocolViolatingDoublePageWithoutGrowingForever() {
XCTAssertGreaterThanOrEqual(
ArchiveMessageBatchPolicy.maximumBufferedStanzas,
ArchiveMessageBatchPolicy.pageSize * 2
)
XCTAssertLessThanOrEqual(ArchiveMessageBatchPolicy.maximumBufferedStanzas, 128)
}
}
+108
View File
@@ -0,0 +1,108 @@
import XCTest
@testable import Luma
final class ArchiveSyncCheckpointTests: XCTestCase {
func testStoredCursorIsPreferredOverTimestampOverlap() {
let checkpoint = ArchiveSyncCheckpoint(
timestamp: Date(timeIntervalSince1970: 1_700_000_000),
cursor: " mam-42 "
)
let position = ArchiveSyncCursorPolicy.requestPosition(
checkpoint: checkpoint,
resumeAfter: nil,
overlap: 60
)
XCTAssertEqual(position.after, "mam-42")
XCTAssertNil(position.start)
}
func testLegacyTimestampUsesBoundedOverlap() {
let timestamp = Date(timeIntervalSince1970: 1_700_000_000)
let position = ArchiveSyncCursorPolicy.requestPosition(
checkpoint: ArchiveSyncCheckpoint(timestamp: timestamp),
resumeAfter: nil,
overlap: 60
)
XCTAssertNil(position.after)
XCTAssertEqual(position.start, timestamp.addingTimeInterval(-60))
}
func testResumeCursorWinsOverDurableCursor() {
let position = ArchiveSyncCursorPolicy.requestPosition(
checkpoint: ArchiveSyncCheckpoint(
timestamp: Date(timeIntervalSince1970: 1_700_000_000),
cursor: "saved"
),
resumeAfter: "page-in-flight",
overlap: 60
)
XCTAssertEqual(position.after, "page-in-flight")
XCTAssertNil(position.start)
}
func testCaughtUpCheckpointKeepsCursorAndAdvancesTimestamp() {
let previous = ArchiveSyncCheckpoint(
timestamp: Date(timeIntervalSince1970: 100),
cursor: "known"
)
let queryStart = Date(timeIntervalSince1970: 200)
let resolved = ArchiveSyncCursorPolicy.resolvedCheckpoint(
previous: previous,
cursor: nil,
highWatermark: nil,
queryStartedAt: queryStart,
caughtUp: true
)
XCTAssertEqual(resolved.cursor, "known")
XCTAssertEqual(resolved.timestamp, queryStart)
}
func testSameTimestampStillAdvancesWhenCursorChanges() {
let previous = ArchiveSyncCheckpoint(
timestamp: Date(timeIntervalSince1970: 100),
cursor: "old"
)
let resolved = ArchiveSyncCursorPolicy.resolvedCheckpoint(
previous: previous,
cursor: "new",
highWatermark: previous.timestamp,
queryStartedAt: Date(timeIntervalSince1970: 200),
caughtUp: false
)
XCTAssertTrue(resolved.advances(over: previous))
XCTAssertEqual(resolved.cursor, "new")
}
func testStoredCursorFallbackCanRunOnlyOnFirstPageAndOnlyOnce() {
let checkpoint = ArchiveSyncCheckpoint(
timestamp: Date(timeIntervalSince1970: 100),
cursor: "stored"
)
XCTAssertTrue(ArchiveSyncCursorPolicy.shouldFallbackFromStoredCursor(
requestedAfter: "stored",
checkpoint: checkpoint,
hasCompletedPage: false,
fallbackAlreadyUsed: false
))
XCTAssertFalse(ArchiveSyncCursorPolicy.shouldFallbackFromStoredCursor(
requestedAfter: "stored",
checkpoint: checkpoint,
hasCompletedPage: true,
fallbackAlreadyUsed: false
))
XCTAssertFalse(ArchiveSyncCursorPolicy.shouldFallbackFromStoredCursor(
requestedAfter: "stored",
checkpoint: checkpoint,
hasCompletedPage: false,
fallbackAlreadyUsed: true
))
}
}
+90
View File
@@ -0,0 +1,90 @@
import XCTest
@testable import Luma
final class ArchiveSyncPaginationTests: XCTestCase {
func testCompleteResponseFinishesEvenWhenCursorExists() {
var pagination = ArchiveSyncPagination()
XCTAssertEqual(
pagination.decision(
complete: true,
lastCursor: "server-last",
requestedAfter: nil
),
.finished
)
}
func testIncompleteResponseContinuesWithNewCursor() {
var pagination = ArchiveSyncPagination()
XCTAssertEqual(
pagination.decision(
complete: false,
lastCursor: "page-1-last",
requestedAfter: nil
),
.next("page-1-last")
)
}
func testRepeatedCursorStopsPaginationLoop() {
var pagination = ArchiveSyncPagination()
_ = pagination.decision(
complete: false,
lastCursor: "repeated",
requestedAfter: nil
)
XCTAssertEqual(
pagination.decision(
complete: false,
lastCursor: "repeated",
requestedAfter: "repeated"
),
.invalidCursor
)
}
func testCursorCycleStopsPaginationLoop() {
var pagination = ArchiveSyncPagination()
XCTAssertEqual(
pagination.decision(
complete: false,
lastCursor: "page-a",
requestedAfter: nil
),
.next("page-a")
)
XCTAssertEqual(
pagination.decision(
complete: false,
lastCursor: "page-b",
requestedAfter: "page-a"
),
.next("page-b")
)
XCTAssertEqual(
pagination.decision(
complete: false,
lastCursor: "page-a",
requestedAfter: "page-b"
),
.invalidCursor
)
}
func testIncompleteResponseWithoutCursorDoesNotAdvanceCheckpoint() {
var pagination = ArchiveSyncPagination()
XCTAssertEqual(
pagination.decision(
complete: false,
lastCursor: nil,
requestedAfter: nil
),
.invalidCursor
)
}
}
@@ -0,0 +1,35 @@
import XCTest
@testable import Luma
final class ArchiveSyncRecoveryPolicyTests: XCTestCase {
func testIncrementalOverlapDoesNotReplaySeveralMinutesPerPass() {
XCTAssertEqual(ArchiveSyncRecoveryPolicy.incrementalOverlap, 60)
}
func testQueryTimeoutIsFinite() {
XCTAssertGreaterThan(ArchiveSyncRecoveryPolicy.queryTimeoutNanoseconds, 0)
XCTAssertLessThanOrEqual(
ArchiveSyncRecoveryPolicy.queryTimeoutNanoseconds,
15_000_000_000
)
}
func testPageApplicationAlsoHasAFiniteWatchdog() {
XCTAssertGreaterThan(ArchiveSyncRecoveryPolicy.pageApplyTimeoutNanoseconds, 0)
XCTAssertLessThanOrEqual(
ArchiveSyncRecoveryPolicy.pageApplyTimeoutNanoseconds,
ArchiveSyncRecoveryPolicy.queryTimeoutNanoseconds
)
}
func testFailedPagesHaveAtMostOneAutomaticRetry() {
XCTAssertEqual(ArchiveSyncRecoveryPolicy.pageRetryLimit, 1)
}
func testCaptureResumeDelayLetsCameraReleaseResources() {
XCTAssertGreaterThan(
ArchiveSyncRecoveryPolicy.resumeAfterCaptureDelayNanoseconds,
ArchiveSyncRecoveryPolicy.interPageDelayNanoseconds
)
}
}
+25
View File
@@ -0,0 +1,25 @@
import XCTest
@testable import Luma
final class ArchiveSyncWorkBudgetTests: XCTestCase {
func testStopsAfterFinitePageBudget() {
let start = Date(timeIntervalSince1970: 1_700_000_000)
var budget = ArchiveSyncWorkBudget(startedAt: start)
for page in 1..<ArchiveSyncWorkBudget.maximumCompletedPages {
XCTAssertFalse(
budget.recordCompletedPage(at: start.addingTimeInterval(Double(page)))
)
}
XCTAssertTrue(budget.recordCompletedPage(at: start.addingTimeInterval(6)))
}
func testStopsAfterRuntimeBudgetEvenWithFewPages() {
let start = Date(timeIntervalSince1970: 1_700_000_000)
var budget = ArchiveSyncWorkBudget(startedAt: start)
XCTAssertTrue(budget.recordCompletedPage(
at: start.addingTimeInterval(ArchiveSyncWorkBudget.maximumRuntime)
))
}
}
+132
View File
@@ -0,0 +1,132 @@
import XCTest
@testable import Luma
final class CallSDPOrderingTests: XCTestCase {
func testAnswerKeepsOfferMediaOrder() {
XCTAssertEqual(
CallSDPOrdering.answerIndices(
localOrder: ["audio", "video"],
remoteOrder: ["audio", "video"]
),
[0, 1]
)
}
func testAnswerWithReversedJingleContentsIsReordered() {
XCTAssertEqual(
CallSDPOrdering.answerIndices(
localOrder: ["audio", "video"],
remoteOrder: ["video", "audio"]
),
[1, 0]
)
}
func testAnswerWithMissingOrDuplicateContentIsRejected() {
XCTAssertNil(
CallSDPOrdering.answerIndices(
localOrder: ["audio", "video"],
remoteOrder: ["audio"]
)
)
XCTAssertNil(
CallSDPOrdering.answerIndices(
localOrder: ["audio", "video"],
remoteOrder: ["audio", "audio"]
)
)
}
func testJingleCandidateIsNormalizedForWebRTC() {
XCTAssertEqual(
CallSDPOrdering.webRTCCandidateLine("a=candidate:1 1 UDP 1 192.0.2.1 5000 typ host"),
"candidate:1 1 UDP 1 192.0.2.1 5000 typ host"
)
XCTAssertEqual(
CallSDPOrdering.webRTCCandidateLine("candidate:2 1 UDP 1 192.0.2.2 5002 typ host"),
"candidate:2 1 UDP 1 192.0.2.2 5002 typ host"
)
}
func testMartinParseInputPreservesFinalMSIDCharacter() {
let raw = [
"v=0",
"m=video 9 UDP/TLS/RTP/SAVPF 96",
"a=ssrc-group:FID 2408145738 2025569816",
"a=ssrc:2408145738 msid:luma-stream luma-video-0435",
"a=ssrc:2025569816 msid:luma-stream luma-video-0435"
].joined(separator: "\r\n") + "\n"
let parseInput = CallSDPNormalization.martinParseInput(raw)
XCTAssertTrue(parseInput.hasSuffix("luma-video-0435\r\n"))
XCTAssertTrue(String(parseInput.dropLast(2)).hasSuffix("luma-video-0435"))
}
func testMartinParseInputUsesExactlyOneTrailingCRLF() {
for suffix in ["", "\n", "\r\n", "\n\n"] {
let parseInput = CallSDPNormalization.martinParseInput(
"v=0\nm=audio 9 UDP/TLS/RTP/SAVPF 111\na=mid:0" + suffix
)
XCTAssertTrue(parseInput.hasSuffix("a=mid:0\r\n"))
XCTAssertFalse(parseInput.hasSuffix("\r\n\r\n"))
}
}
func testFIDMSIDsUseTheCompleteTrackIdentifier() {
let repaired = CallSDPCompatibility.canonicalFIDMSIDs(
groups: [
.init(semantics: "FID", sources: ["primary", "rtx"])
],
msids: [
"primary": "luma-stream luma-video-9FBECB58-45C5-445B-9F4E-0598594F4F2D",
"rtx": "luma-stream luma-video-9FBECB58-45C5-445B-9F4E-0598594F4F2"
]
)
XCTAssertEqual(repaired["primary"], repaired["rtx"])
XCTAssertTrue(repaired["rtx"]?.hasSuffix("F4F2D") == true)
}
func testFIDMSIDRepairAlsoHandlesMissingRTXMetadata() {
let repaired = CallSDPCompatibility.canonicalFIDMSIDs(
groups: [
.init(semantics: "fid", sources: ["primary", "rtx"]),
.init(semantics: "SIM", sources: ["sim-a", "sim-b"])
],
msids: [
"primary": "stream video-track",
"sim-a": "stream camera-a",
"sim-b": "stream camera-b"
]
)
XCTAssertEqual(repaired["rtx"], "stream video-track")
XCTAssertEqual(repaired["sim-a"], "stream camera-a")
XCTAssertEqual(repaired["sim-b"], "stream camera-b")
}
func testCandidateDispatchWaitsForRemoteAnswerAndFullJID() {
XCTAssertFalse(CallCandidateDispatchGate.canSend(
initialDescriptionWasSignaled: true,
hasLocalDescription: true,
hasRemoteDescription: false,
hasFullPeerJID: true
))
XCTAssertFalse(CallCandidateDispatchGate.canSend(
initialDescriptionWasSignaled: true,
hasLocalDescription: true,
hasRemoteDescription: true,
hasFullPeerJID: false
))
}
func testCandidateDispatchOpensAfterSessionAccept() {
XCTAssertTrue(CallCandidateDispatchGate.canSend(
initialDescriptionWasSignaled: true,
hasLocalDescription: true,
hasRemoteDescription: true,
hasFullPeerJID: true
))
}
}
+115
View File
@@ -0,0 +1,115 @@
import Foundation
import XCTest
@testable import Luma
final class CallSnapshotTests: XCTestCase {
func testIncomingVideoCallProjection() {
let snapshot = CallSnapshot(
id: UUID(),
peerJID: "alice@example.org",
direction: .incoming,
media: [.audio, .video],
phase: .ringing,
connectedAt: nil,
isMuted: false,
isCameraEnabled: true,
isSpeakerEnabled: true,
hasLocalVideo: false,
hasRemoteVideo: false
)
XCTAssertTrue(snapshot.isVideoCall)
XCTAssertTrue(snapshot.isIncomingRinging)
}
func testConnectedAudioCallIsNotRinging() {
let snapshot = CallSnapshot(
id: UUID(),
peerJID: "bob@example.org",
direction: .outgoing,
media: [.audio],
phase: .connected,
connectedAt: Date(),
isMuted: true,
isCameraEnabled: false,
isSpeakerEnabled: false,
hasLocalVideo: false,
hasRemoteVideo: false
)
XCTAssertFalse(snapshot.isVideoCall)
XCTAssertFalse(snapshot.isIncomingRinging)
}
func testConnectedCallAlwaysBecomesCompletedHistory() {
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .outgoing,
phase: .connected,
connectedAt: Date(),
cause: .failed
),
.completed
)
}
func testDeclinedCallClassification() {
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .incoming,
phase: .ringing,
connectedAt: nil,
cause: .localRejected
),
.declined
)
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .outgoing,
phase: .ringing,
connectedAt: nil,
cause: .remoteRejected
),
.declined
)
}
func testMissedCancelledAndUnansweredClassification() {
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .incoming,
phase: .ringing,
connectedAt: nil,
cause: .remoteCancelled
),
.missed
)
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .outgoing,
phase: .ringing,
connectedAt: nil,
cause: .localEnded
),
.cancelled
)
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .outgoing,
phase: .ringing,
connectedAt: nil,
cause: .timedOut
),
.unanswered
)
XCTAssertEqual(
CallHistoryPolicy.outcome(
direction: .incoming,
phase: .connecting,
connectedAt: nil,
cause: .remoteEnded
),
.failed
)
}
}
+53
View File
@@ -0,0 +1,53 @@
import XCTest
@testable import Luma
final class ChatArchiveTests: XCTestCase {
func testSnapshotRoundTrip() async throws {
let jid = "test-\(UUID().uuidString)@example.org"
let archive = ChatArchive(accountJID: jid)
let conversation = Conversation(jid: "bob@example.org", displayName: "Bob")
let message = ChatMessage(
conversationID: conversation.id,
senderJID: jid,
body: "Encrypted hello",
direction: .outgoing,
delivery: .sent,
security: .omemo
)
try await archive.save(.init(
conversations: [conversation],
messages: [message],
locallyDeletedMessageIDs: ["deleted-message-id"],
rosterContactJIDs: [conversation.jid],
lastSuccessfulMAMSync: Date(timeIntervalSince1970: 1_700_000_000),
lastSuccessfulMAMCursor: "mam-cursor-42"
))
let loaded = await archive.load()
XCTAssertEqual(loaded.conversations, [conversation])
XCTAssertEqual(loaded.messages, [message])
XCTAssertEqual(loaded.locallyDeletedMessageIDs, Set(["deleted-message-id"]))
XCTAssertEqual(loaded.rosterContactJIDs, Set([conversation.jid]))
XCTAssertEqual(
loaded.lastSuccessfulMAMSync,
Date(timeIntervalSince1970: 1_700_000_000)
)
XCTAssertEqual(loaded.lastSuccessfulMAMCursor, "mam-cursor-42")
XCTAssertEqual(loaded.schemaVersion, ChatArchive.Snapshot.currentSchemaVersion)
try await archive.erase()
}
func testLegacySnapshotDefaultsToSchemaOne() throws {
let data = Data(
#"{"conversations":[],"messages":[],"locallyDeletedMessageIDs":[]}"#.utf8
)
let snapshot = try JSONDecoder().decode(ChatArchive.Snapshot.self, from: data)
XCTAssertEqual(snapshot.schemaVersion, 1)
XCTAssertTrue(snapshot.rosterContactJIDs.isEmpty)
XCTAssertNil(snapshot.lastSuccessfulMAMSync)
XCTAssertNil(snapshot.lastSuccessfulMAMCursor)
}
}
+29
View File
@@ -0,0 +1,29 @@
import XCTest
@testable import Luma
final class ChatScrollPositionPolicyTests: XCTestCase {
func testBottomSentinelCrossesOnlyTheTelegramButtonThreshold() {
XCTAssertTrue(ChatScrollPositionPolicy.isNearBottom(
bottomY: 796,
viewportHeight: 700
))
XCTAssertFalse(ChatScrollPositionPolicy.isNearBottom(
bottomY: 797,
viewportHeight: 700
))
}
func testInvalidFirstLayoutDoesNotFlashTheButton() {
XCTAssertTrue(ChatScrollPositionPolicy.isNearBottom(
bottomY: 0,
viewportHeight: 0
))
}
func testNativeScrollDistanceCanHideAndRestoreButtonRepeatedly() {
XCTAssertFalse(ChatScrollPositionPolicy.isNearBottom(distanceFromBottom: 180))
XCTAssertTrue(ChatScrollPositionPolicy.isNearBottom(distanceFromBottom: 0))
XCTAssertFalse(ChatScrollPositionPolicy.isNearBottom(distanceFromBottom: 220))
XCTAssertTrue(ChatScrollPositionPolicy.isNearBottom(distanceFromBottom: -12))
}
}
+50
View File
@@ -0,0 +1,50 @@
import XCTest
@testable import Luma
final class ChatTimelineEntryTests: XCTestCase {
func testDayBoundariesArePrecomputedOnceForStableRows() {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
let first = ChatMessage(
id: "first",
conversationID: "friend@example.org",
senderJID: "friend@example.org",
body: "one",
timestamp: Date(timeIntervalSince1970: 1_704_067_200),
direction: .incoming,
delivery: .delivered,
security: .plaintext
)
var sameDay = first
sameDay.body = "two"
sameDay.timestamp = first.timestamp.addingTimeInterval(60)
let sameDayMessage = ChatMessage(
id: "same-day",
conversationID: sameDay.conversationID,
senderJID: sameDay.senderJID,
body: sameDay.body,
timestamp: sameDay.timestamp,
direction: sameDay.direction,
delivery: sameDay.delivery,
security: sameDay.security
)
let nextDay = ChatMessage(
id: "next-day",
conversationID: first.conversationID,
senderJID: first.senderJID,
body: "three",
timestamp: first.timestamp.addingTimeInterval(86_400),
direction: .incoming,
delivery: .delivered,
security: .plaintext
)
let entries = ChatTimelineEntry.make(
from: [first, sameDayMessage, nextDay],
calendar: calendar
)
XCTAssertEqual(entries.map(\.id), ["first", "same-day", "next-day"])
XCTAssertEqual(entries.map(\.startsNewDay), [true, false, true])
}
}
+26
View File
@@ -0,0 +1,26 @@
import XCTest
@testable import Luma
final class ChatTypingPolicyTests: XCTestCase {
func testDirectChatUsesCompactTypingText() {
XCTAssertEqual(
ChatTypingPolicy.displayText(names: ["Alice"], isGroup: false),
"печатает…"
)
}
func testGroupChatNamesOneOrTwoParticipants() {
XCTAssertEqual(
ChatTypingPolicy.displayText(names: ["Alice"], isGroup: true),
"Alice печатает…"
)
XCTAssertEqual(
ChatTypingPolicy.displayText(names: ["Bob", "Alice"], isGroup: true),
"Alice и Bob печатают…"
)
}
func testEmptyTypingSetHasNoIndicator() {
XCTAssertNil(ChatTypingPolicy.displayText(names: [], isGroup: false))
}
}
+81
View File
@@ -0,0 +1,81 @@
import CoreGraphics
import XCTest
@testable import Luma
final class ComposerRecordingGestureTests: XCTestCase {
func testShortMovementKeepsRecording() {
XCTAssertEqual(
ComposerRecordingGesturePolicy.resolution(
for: CGSize(width: -35, height: -24)
),
.continueRecording
)
}
func testHorizontalSwipeCancelsRecording() {
XCTAssertEqual(
ComposerRecordingGesturePolicy.resolution(
for: CGSize(width: -110, height: -18)
),
.cancel
)
}
func testVerticalSwipeLocksRecording() {
XCTAssertEqual(
ComposerRecordingGesturePolicy.resolution(
for: CGSize(width: -12, height: -95)
),
.lock
)
}
func testDominantDiagonalDirectionWins() {
XCTAssertEqual(
ComposerRecordingGesturePolicy.resolution(
for: CGSize(width: -105, height: -82)
),
.cancel
)
XCTAssertEqual(
ComposerRecordingGesturePolicy.resolution(
for: CGSize(width: -94, height: -105)
),
.lock
)
}
func testCancelHintOpacityUsesCGFloatMathAndReturnsDouble() {
XCTAssertEqual(
ComposerRecordingGesturePolicy.cancelHintOpacity(for: .zero),
1,
accuracy: 0.0001
)
XCTAssertEqual(
ComposerRecordingGesturePolicy.cancelHintOpacity(
for: CGSize(width: -75, height: 0)
),
0.5,
accuracy: 0.0001
)
XCTAssertEqual(
ComposerRecordingGesturePolicy.cancelHintOpacity(
for: CGSize(width: -200, height: 0)
),
0.28,
accuracy: 0.0001
)
XCTAssertEqual(
ComposerRecordingGesturePolicy.cancelHintOpacity(
for: CGSize(width: 50, height: 0)
),
1,
accuracy: 0.0001
)
}
func testDelayedMainActorStillRecognizesPhysicalLongPress() {
XCTAssertFalse(ComposerRecordingGesturePolicy.isLongPress(elapsed: 0.1))
XCTAssertTrue(ComposerRecordingGesturePolicy.isLongPress(elapsed: 0.3))
}
}
+33
View File
@@ -0,0 +1,33 @@
import XCTest
@testable import Luma
final class CryptoKitAESGCMEngineTests: XCTestCase {
func testRoundTrip() {
let engine = CryptoKitAESGCMEngine()
let key = Data(repeating: 0x31, count: 16)
let iv = Data(repeating: 0x22, count: 12)
let plaintext = Data("hello omemo".utf8)
var ciphertext = Data()
var tag = Data()
XCTAssertTrue(engine.encrypt(iv: iv, key: key, message: plaintext, output: &ciphertext, tag: &tag))
var decrypted = Data()
XCTAssertTrue(engine.decrypt(iv: iv, key: key, encoded: ciphertext, auth: tag, output: &decrypted))
XCTAssertEqual(decrypted, plaintext)
}
func testModifiedTagFails() {
let engine = CryptoKitAESGCMEngine()
let key = Data(repeating: 0x31, count: 16)
let iv = Data(repeating: 0x22, count: 12)
var ciphertext = Data()
var tag = Data()
XCTAssertTrue(engine.encrypt(iv: iv, key: key, message: Data("secret".utf8), output: &ciphertext, tag: &tag))
tag[0] ^= 0x01
var decrypted = Data()
XCTAssertFalse(engine.decrypt(iv: iv, key: key, encoded: ciphertext, auth: tag, output: &decrypted))
}
}
+38
View File
@@ -0,0 +1,38 @@
import Foundation
import XCTest
@testable import Luma
final class EncryptionPreferenceTests: XCTestCase {
func testResolutionUsesGlobalDefaultAndPerChatOverride() {
XCTAssertTrue(EncryptionPreference.inheritGlobal.resolved(globalEnabled: true))
XCTAssertFalse(EncryptionPreference.inheritGlobal.resolved(globalEnabled: false))
XCTAssertTrue(EncryptionPreference.enabled.resolved(globalEnabled: false))
XCTAssertFalse(EncryptionPreference.disabled.resolved(globalEnabled: true))
}
func testConversationWithoutEncryptionFieldStillDecodes() throws {
let conversation = Conversation(jid: "bob@example.org", displayName: "Bob")
let encoded = try JSONEncoder().encode(conversation)
var object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
object.removeValue(forKey: "encryptionPreference")
let legacyData = try JSONSerialization.data(withJSONObject: object)
let decoded = try JSONDecoder().decode(Conversation.self, from: legacyData)
XCTAssertEqual(decoded.encryptionPreference, .inheritGlobal)
XCTAssertEqual(decoded.jid, conversation.jid)
}
func testGlobalSettingIsStoredPerAccount() throws {
let suite = "LumaTests.\(UUID().uuidString)"
let defaults = try XCTUnwrap(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let preferences = AccountPreferences(defaults: defaults)
XCTAssertTrue(preferences.encryptionEnabled(for: "alice@example.org"))
preferences.setEncryptionEnabled(false, for: "alice@example.org")
XCTAssertFalse(preferences.encryptionEnabled(for: "alice@example.org"))
XCTAssertTrue(preferences.encryptionEnabled(for: "bob@example.org"))
}
}
+33
View File
@@ -0,0 +1,33 @@
import XCTest
@testable import Luma
final class GeoLocationTests: XCTestCase {
func testGeoURIFormattingAndParsing() throws {
let location = try XCTUnwrap(
GeoLocation(latitude: 59.3293, longitude: 18.0686, uncertainty: 14.4)
)
XCTAssertEqual(location.uriString, "geo:59.329300,18.068600;u=14")
let decoded = try XCTUnwrap(GeoLocation(uri: location.uriString))
XCTAssertEqual(decoded.latitude, 59.3293, accuracy: 0.000_001)
XCTAssertEqual(decoded.longitude, 18.0686, accuracy: 0.000_001)
XCTAssertEqual(try XCTUnwrap(decoded.uncertainty), 14, accuracy: 0.001)
}
func testGeoURIWithoutUncertainty() throws {
let location = try XCTUnwrap(GeoLocation(uri: "geo:-33.8688,151.2093"))
XCTAssertEqual(location.latitude, -33.8688, accuracy: 0.000_001)
XCTAssertEqual(location.longitude, 151.2093, accuracy: 0.000_001)
XCTAssertNil(location.uncertainty)
}
func testInvalidCoordinatesAreRejected() {
XCTAssertNil(GeoLocation(latitude: 91, longitude: 0))
XCTAssertNil(GeoLocation(latitude: 0, longitude: -181))
XCTAssertNil(GeoLocation(uri: "https://maps.example.org/59,18"))
XCTAssertNil(GeoLocation(uri: "geo:not-a-number,18"))
XCTAssertNil(GeoLocation(uri: "geo:59,18;u=unknown"))
}
}
+56
View File
@@ -0,0 +1,56 @@
import Foundation
import XCTest
@testable import Luma
final class GroupConversationTests: XCTestCase {
func testGroupConversationRoundTrips() throws {
let conversation = Conversation(
jid: "team@conference.example.org",
displayName: "Команда",
encryptionPreference: .disabled,
kind: .group,
groupNickname: "Alice",
isGroupJoined: true,
shouldAutojoin: true,
occupantCount: 4,
invitedBy: "bob@example.org"
)
let decoded = try JSONDecoder().decode(
Conversation.self,
from: JSONEncoder().encode(conversation)
)
XCTAssertTrue(decoded.isGroup)
XCTAssertEqual(decoded.groupNickname, "Alice")
XCTAssertEqual(decoded.occupantCount, 4)
XCTAssertEqual(decoded.invitedBy, "bob@example.org")
}
func testLegacyConversationDefaultsToDirect() throws {
let conversation = Conversation(jid: "bob@example.org", displayName: "Bob")
let encoded = try JSONEncoder().encode(conversation)
var object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
for key in ["kind", "groupNickname", "isGroupJoined", "shouldAutojoin", "occupantCount", "invitedBy"] {
object.removeValue(forKey: key)
}
let decoded = try JSONDecoder().decode(
Conversation.self,
from: JSONSerialization.data(withJSONObject: object)
)
XCTAssertFalse(decoded.isGroup)
XCTAssertFalse(decoded.isGroupJoined)
XCTAssertEqual(decoded.occupantCount, 0)
}
func testNewGroupInheritsGlobalEncryptionByDefault() {
let conversation = Conversation(
jid: "team@conference.example.org",
kind: .group
)
XCTAssertEqual(conversation.encryptionPreference, .inheritGlobal)
}
}
+42
View File
@@ -0,0 +1,42 @@
import Foundation
import XCTest
@testable import Luma
final class MediaFileIOTests: XCTestCase {
func testStagedDraftSurvivesThePickerURLBeingRemoved() async throws {
let sourceDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(
at: sourceDirectory,
withIntermediateDirectories: true
)
let source = sourceDirectory.appendingPathComponent("photo.jpg")
let payload = Data([0xFF, 0xD8, 0xFF, 0xD9])
try payload.write(to: source)
defer { try? FileManager.default.removeItem(at: sourceDirectory) }
let staged = try await MediaFileIO().stageAttachmentDraft(from: source)
defer { try? FileManager.default.removeItem(at: staged.url) }
try FileManager.default.removeItem(at: source)
XCTAssertEqual(staged.filename, "photo.jpg")
XCTAssertEqual(staged.byteCount, payload.count)
XCTAssertEqual(try Data(contentsOf: staged.url), payload)
}
func testEmptyPickerFileIsRejectedBeforePreviewOrUpload() async throws {
let source = FileManager.default.temporaryDirectory
.appendingPathComponent("empty-\(UUID().uuidString).mov")
try Data().write(to: source)
defer { try? FileManager.default.removeItem(at: source) }
do {
_ = try await MediaFileIO().stageAttachmentDraft(from: source)
XCTFail("An empty media file must not become a sendable draft")
} catch MediaFileIOError.emptyFile(_) {
// Expected.
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}
+86
View File
@@ -0,0 +1,86 @@
import Foundation
import XCTest
@testable import Luma
final class MediaMetadataTests: XCTestCase {
func testVoiceFilenameRoundTrip() throws {
let filename = MediaMetadata.transportFilename(
originalFilename: "voice-123.m4a",
kind: .voice,
duration: 12.345
)
let inferred = MediaMetadata.infer(from: filename)
XCTAssertEqual(filename, "luma-voice-12345-voice-123.m4a")
XCTAssertEqual(inferred.kind, .voice)
XCTAssertEqual(try XCTUnwrap(inferred.duration), 12.345, accuracy: 0.001)
XCTAssertNotNil(inferred.mimeType)
}
func testVideoNoteFilenameRoundTrip() throws {
let filename = MediaMetadata.transportFilename(
originalFilename: "video-note.mov",
kind: .videoNote,
duration: 59.9
)
let inferred = MediaMetadata.infer(from: filename)
XCTAssertEqual(inferred.kind, .videoNote)
XCTAssertEqual(try XCTUnwrap(inferred.duration), 59.9, accuracy: 0.001)
}
func testStandardExtensionsAreClassified() {
XCTAssertEqual(MediaMetadata.infer(from: "image.jpg").kind, .photo)
XCTAssertEqual(MediaMetadata.infer(from: "clip.mov").kind, .video)
XCTAssertEqual(MediaMetadata.infer(from: "track.m4a").kind, .audio)
XCTAssertEqual(MediaMetadata.infer(from: "archive.zip").kind, .attachment)
}
func testEncryptedTransportNameKeepsOriginalExtension() {
let photo = MediaMetadata.transportFilename(
originalFilename: "holiday.jpg",
kind: .photo,
duration: nil
)
let voice = MediaMetadata.transportFilename(
originalFilename: "voice.m4a",
kind: .voice,
duration: 2.5
)
XCTAssertEqual(URL(fileURLWithPath: photo).pathExtension, "jpg")
XCTAssertEqual(URL(fileURLWithPath: voice).pathExtension, "m4a")
XCTAssertFalse(photo.hasSuffix(".aesgcm"))
XCTAssertFalse(voice.hasSuffix(".aesgcm"))
}
func testLegacyMessageWithoutMediaMetadataStillDecodes() throws {
let message = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "old attachment",
direction: .incoming,
delivery: .delivered,
security: .omemo,
kind: .attachment,
remoteAttachmentURL: "aesgcm://upload.example.org/file.aesgcm#key",
localFilename: "file.pdf"
)
let encoded = try JSONEncoder().encode(message)
var object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
object.removeValue(forKey: "mimeType")
object.removeValue(forKey: "duration")
object.removeValue(forKey: "byteCount")
let decoded = try JSONDecoder().decode(
ChatMessage.self,
from: JSONSerialization.data(withJSONObject: object)
)
XCTAssertEqual(decoded.localFilename, "file.pdf")
XCTAssertNil(decoded.mimeType)
XCTAssertNil(decoded.duration)
}
}
@@ -0,0 +1,24 @@
import XCTest
@testable import Luma
final class MediaPickerSelectionPolicyTests: XCTestCase {
func testLivePhotoPrefersStillImageButRetainsVideoFallback() {
XCTAssertEqual(
MediaPickerSelectionPolicy.preferredOrder(
supportsImage: true,
supportsVideo: true
),
[.photo, .video]
)
}
func testVideoOnlyAssetPrefersMovieButRetainsPhotoFallback() {
XCTAssertEqual(
MediaPickerSelectionPolicy.preferredOrder(
supportsImage: false,
supportsVideo: true
),
[.video, .photo]
)
}
}
+39
View File
@@ -0,0 +1,39 @@
import XCTest
@testable import Luma
final class MediaSendActivityTrackerTests: XCTestCase {
func testOverlappingOperationsDoNotRestoreStaleBusyState() {
var tracker = MediaSendActivityTracker()
let first = tracker.begin()
let second = tracker.begin()
XCTAssertTrue(tracker.isActive)
tracker.end(first)
XCTAssertTrue(tracker.isActive)
tracker.end(second)
XCTAssertFalse(tracker.isActive)
}
func testEndingSameOperationTwiceIsSafe() {
var tracker = MediaSendActivityTracker()
let token = tracker.begin()
tracker.end(token)
tracker.end(token)
XCTAssertFalse(tracker.isActive)
XCTAssertTrue(tracker.activeTokens.isEmpty)
}
func testResetRecoversFromEveryOutstandingOperation() {
var tracker = MediaSendActivityTracker()
_ = tracker.begin()
_ = tracker.begin()
tracker.reset()
XCTAssertFalse(tracker.isActive)
}
}
+15
View File
@@ -0,0 +1,15 @@
import XCTest
@testable import Luma
final class MediaTimeFormatterTests: XCTestCase {
func testFormatsShortAndLongMediaDurations() {
XCTAssertEqual(MediaTimeFormatter.string(0), "0:00")
XCTAssertEqual(MediaTimeFormatter.string(65.9), "1:05")
XCTAssertEqual(MediaTimeFormatter.string(3_661), "1:01:01")
}
func testInvalidDurationFallsBackToZero() {
XCTAssertEqual(MediaTimeFormatter.string(.infinity), "0:00")
XCTAssertEqual(MediaTimeFormatter.string(-4), "0:00")
}
}
@@ -0,0 +1,55 @@
import CoreGraphics
import XCTest
@testable import Luma
final class MediaViewerDismissGestureTests: XCTestCase {
func testLongDownwardSwipeDismissesViewer() {
XCTAssertTrue(
MediaViewerDismissGesturePolicy.shouldDismiss(
translation: CGSize(width: 12, height: 130),
predictedEndTranslation: CGSize(width: 14, height: 168),
containerHeight: 800
)
)
}
func testLongUpwardSwipeDismissesViewer() {
XCTAssertTrue(
MediaViewerDismissGesturePolicy.shouldDismiss(
translation: CGSize(width: -9, height: -130),
predictedEndTranslation: CGSize(width: -12, height: -176),
containerHeight: 800
)
)
}
func testFastShortVerticalFlickDismissesViewer() {
XCTAssertTrue(
MediaViewerDismissGesturePolicy.shouldDismiss(
translation: CGSize(width: 5, height: 54),
predictedEndTranslation: CGSize(width: 9, height: 180),
containerHeight: 800
)
)
}
func testShortVerticalDragReturnsToViewer() {
XCTAssertFalse(
MediaViewerDismissGesturePolicy.shouldDismiss(
translation: CGSize(width: 6, height: 52),
predictedEndTranslation: CGSize(width: 8, height: 84),
containerHeight: 800
)
)
}
func testHorizontalMediaGestureDoesNotDismissViewer() {
XCTAssertFalse(
MediaViewerDismissGesturePolicy.shouldDismiss(
translation: CGSize(width: 190, height: 72),
predictedEndTranslation: CGSize(width: 260, height: 110),
containerHeight: 800
)
)
}
}
+69
View File
@@ -0,0 +1,69 @@
import Foundation
import XCTest
@testable import Luma
final class MessageCorrectionTests: XCTestCase {
func testOnlyDeliveredOrSentOutgoingTextCanBeEdited() {
let editable = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "До исправления",
direction: .outgoing,
delivery: .sent,
security: .omemo
)
var incoming = editable
incoming.direction = .incoming
var attachment = editable
attachment.kind = .attachment
var failed = editable
failed.delivery = .failed
var retracted = editable
retracted.retractedAt = Date()
XCTAssertTrue(editable.canBeEdited)
XCTAssertFalse(incoming.canBeEdited)
XCTAssertFalse(attachment.canBeEdited)
XCTAssertFalse(failed.canBeEdited)
XCTAssertFalse(retracted.canBeEdited)
}
func testEditedTimestampRoundTrips() throws {
let editedAt = Date(timeIntervalSince1970: 1_700_000_000)
let message = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "После исправления",
direction: .outgoing,
delivery: .delivered,
security: .plaintext,
editedAt: editedAt
)
let data = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(ChatMessage.self, from: data)
XCTAssertEqual(decoded.editedAt, editedAt)
}
func testLegacyMessageWithoutEditedTimestampStillDecodes() throws {
let message = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "Старое сообщение",
direction: .incoming,
delivery: .delivered,
security: .omemo
)
let encoded = try JSONEncoder().encode(message)
var object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
object.removeValue(forKey: "editedAt")
let decoded = try JSONDecoder().decode(
ChatMessage.self,
from: JSONSerialization.data(withJSONObject: object)
)
XCTAssertNil(decoded.editedAt)
}
}
+120
View File
@@ -0,0 +1,120 @@
import Foundation
import XCTest
@testable import Luma
final class MessageInteractionTests: XCTestCase {
func testReplyMetadataRoundTrips() throws {
let message = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "Ответ",
direction: .outgoing,
delivery: .sent,
security: .omemo,
replyToID: "original-id",
replyToJID: "bob@example.org",
replyPreview: "Исходный текст"
)
let data = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(ChatMessage.self, from: data)
XCTAssertEqual(decoded.replyToID, "original-id")
XCTAssertEqual(decoded.replyToJID, "bob@example.org")
XCTAssertEqual(decoded.replyPreview, "Исходный текст")
}
func testRetractedMessageCannotBeEditedRepliedOrForwarded() {
let message = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "Удалено",
direction: .outgoing,
delivery: .delivered,
security: .omemo,
retractedAt: Date()
)
XCTAssertFalse(message.canBeEdited)
XCTAssertFalse(message.canBeRepliedTo)
XCTAssertFalse(message.canBeForwarded)
XCTAssertFalse(message.canBeRetracted)
XCTAssertEqual(message.previewText, "🚫 Сообщение удалено")
}
func testLegacyMessageWithoutInteractionFieldsStillDecodes() throws {
let original = ChatMessage(
conversationID: "bob@example.org",
senderJID: "alice@example.org",
body: "Старый архив",
direction: .incoming,
delivery: .delivered,
security: .omemo
)
let encoded = try JSONEncoder().encode(original)
var object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any])
for key in [
"replyToID", "replyToJID", "replyPreview", "forwardedFrom", "retractedAt",
"stanzaID", "senderDisplayName", "isGroupMessage", "callHistory"
] {
object.removeValue(forKey: key)
}
let decoded = try JSONDecoder().decode(
ChatMessage.self,
from: JSONSerialization.data(withJSONObject: object)
)
XCTAssertNil(decoded.replyToID)
XCTAssertNil(decoded.retractedAt)
XCTAssertEqual(decoded.body, "Старый архив")
XCTAssertFalse(decoded.isGroupMessage)
}
func testGroupReplyRequiresRoomAssignedStanzaID() {
var message = ChatMessage(
conversationID: "team@conference.example.org",
senderJID: "team@conference.example.org/alice",
body: "Сообщение группы",
direction: .incoming,
delivery: .delivered,
security: .omemo,
senderDisplayName: "Alice",
isGroupMessage: true
)
XCTAssertFalse(message.canBeRepliedTo)
message.stanzaID = "room-stable-id"
XCTAssertTrue(message.canBeRepliedTo)
XCTAssertEqual(message.replyIdentifier, "room-stable-id")
XCTAssertEqual(message.security, .omemo)
XCTAssertFalse(message.canBeEdited)
XCTAssertFalse(message.canBeRetracted)
}
func testCompletedVideoCallHistoryRoundTripsWithDuration() throws {
let metadata = CallHistoryMetadata(isVideo: true, outcome: .completed)
let message = ChatMessage(
id: "call-alice-123",
conversationID: "alice@example.org",
senderJID: "me@example.org",
body: "Видеозвонок",
direction: .outgoing,
delivery: .sent,
security: .plaintext,
kind: .system,
duration: 65,
callHistory: metadata
)
let data = try JSONEncoder().encode(message)
let decoded = try JSONDecoder().decode(ChatMessage.self, from: data)
XCTAssertEqual(decoded.callHistory, metadata)
XCTAssertEqual(decoded.callTitle, "Исходящий видеозвонок")
XCTAssertEqual(decoded.callSubtitle, "Длительность 1:05")
XCTAssertEqual(decoded.previewText, "🎥 Исходящий видеозвонок")
XCTAssertFalse(decoded.canBeRepliedTo)
XCTAssertFalse(decoded.canBeForwarded)
}
}
+48
View File
@@ -0,0 +1,48 @@
import XCTest
@testable import Luma
final class MessageReactionTests: XCTestCase {
func testReactionUpdateKeepsOneCopyOfEachEmoji() {
XCTAssertEqual(
MessageReactionPolicy.sanitized(["👍", " 👍 ", "❤️", "a", "not emoji"]),
["👍", "❤️"]
)
}
func testSummariesCountSendersAndMarkOwnReaction() {
let reactions = [
MessageReaction(senderJID: "me@example.org/device", emojis: ["👍", "❤️"]),
MessageReaction(senderJID: "you@example.org", emojis: ["👍"])
]
let summaries = MessageReactionPolicy.summaries(
reactions: reactions,
ownJID: "me@example.org"
)
XCTAssertEqual(
summaries.first(where: { $0.emoji == "👍" }),
MessageReactionSummary(emoji: "👍", count: 2, includesOwnReaction: true)
)
XCTAssertEqual(
summaries.first(where: { $0.emoji == "❤️" }),
MessageReactionSummary(emoji: "❤️", count: 1, includesOwnReaction: true)
)
}
func testOlderArchivesDecodeWithoutReactionField() throws {
let message = ChatMessage(
conversationID: "peer@example.org",
senderJID: "peer@example.org",
body: "hello",
direction: .incoming,
delivery: .delivered,
security: .plaintext
)
let encoded = try JSONEncoder().encode(message)
var object = try XCTUnwrap(
JSONSerialization.jsonObject(with: encoded) as? [String: Any]
)
object.removeValue(forKey: "reactions")
let legacyData = try JSONSerialization.data(withJSONObject: object)
XCTAssertEqual(try JSONDecoder().decode(ChatMessage.self, from: legacyData).reactions, [])
}
}
+45
View File
@@ -0,0 +1,45 @@
import XCTest
@testable import Luma
final class MessageReplyFallbackTests: XCTestCase {
func testXEP0428ScalarRangeRemovesUnicodeQuote() {
let fallback = MessageReplyFallback.make(
author: "алиса@example.org",
preview: "Привет 👋\nКак дела?"
)
let body = fallback.prefix + "Отлично!"
let parsed = MessageReplyFallback.parse(
body: body,
fallbackStart: 0,
fallbackEnd: fallback.scalarCount
)
XCTAssertEqual(parsed.body, "Отлично!")
XCTAssertEqual(parsed.preview, "Привет 👋\nКак дела?")
XCTAssertEqual(fallback.scalarCount, fallback.prefix.unicodeScalars.count)
}
func testLegacyConversationsQuoteBecomesReplyPreview() {
let parsed = MessageReplyFallback.parse(
body: "> Старое сообщение\n> в двух строках\n\nНовый ответ",
fallbackStart: nil,
fallbackEnd: nil
)
XCTAssertEqual(parsed.body, "Новый ответ")
XCTAssertEqual(parsed.preview, "Старое сообщение\nв двух строках")
}
func testPlainBodyWithoutQuoteIsUnchanged() {
let body = "Обычное сообщение"
let parsed = MessageReplyFallback.parse(
body: body,
fallbackStart: nil,
fallbackEnd: nil
)
XCTAssertEqual(parsed.body, body)
XCTAssertNil(parsed.preview)
}
}
+55
View File
@@ -0,0 +1,55 @@
import CoreGraphics
import XCTest
@testable import Luma
final class MessageReplySwipeTests: XCTestCase {
func testHorizontalSwipeInEitherDirectionReplies() {
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
translation: CGSize(width: 70, height: 8),
predictedEndTranslation: CGSize(width: 72, height: 9)
))
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
translation: CGSize(width: -70, height: 8),
predictedEndTranslation: CGSize(width: -72, height: 9)
))
}
func testVerticalTimelineScrollDoesNotReply() {
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
translation: CGSize(width: 42, height: 80),
predictedEndTranslation: CGSize(width: 50, height: 105)
))
}
func testDiagonalTimelineScrollDoesNotReply() {
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
translation: CGSize(width: 54, height: 44),
predictedEndTranslation: CGSize(width: 76, height: 58)
))
XCTAssertEqual(
MessageReplySwipePolicy.offset(for: CGSize(width: 54, height: 44)),
0
)
}
func testShortSwipeDoesNotReply() {
XCTAssertFalse(MessageReplySwipePolicy.shouldReply(
translation: CGSize(width: 36, height: 4),
predictedEndTranslation: CGSize(width: 48, height: 5)
))
}
func testFastHorizontalFlickUsesPredictedEnd() {
XCTAssertTrue(MessageReplySwipePolicy.shouldReply(
translation: CGSize(width: -35, height: 3),
predictedEndTranslation: CGSize(width: -90, height: 7)
))
}
func testVisualOffsetIsCapped() {
XCTAssertEqual(
MessageReplySwipePolicy.offset(for: CGSize(width: 500, height: 0)),
MessageReplySwipePolicy.maximumOffset
)
}
}
+51
View File
@@ -0,0 +1,51 @@
import XCTest
@testable import Luma
final class NotificationPolicyTests: XCTestCase {
func testForegroundMessageFromAnotherConversationIsPresented() {
XCTAssertTrue(NotificationPolicy.shouldPresentMessage(
inserted: true,
isOutgoing: false,
appIsActive: true,
selectedConversationID: "bob@example.org",
conversationID: "alice@example.org"
))
}
func testVisibleConversationDoesNotPresentDuplicateBanner() {
XCTAssertFalse(NotificationPolicy.shouldPresentMessage(
inserted: true,
isOutgoing: false,
appIsActive: true,
selectedConversationID: "alice@example.org",
conversationID: "ALICE@example.org"
))
}
func testBackgroundIncomingMessageIsPresented() {
XCTAssertTrue(NotificationPolicy.shouldPresentMessage(
inserted: true,
isOutgoing: false,
appIsActive: false,
selectedConversationID: "alice@example.org",
conversationID: "alice@example.org"
))
}
func testOutgoingOrDuplicateMessageIsNotPresented() {
XCTAssertFalse(NotificationPolicy.shouldPresentMessage(
inserted: true,
isOutgoing: true,
appIsActive: true,
selectedConversationID: nil,
conversationID: "alice@example.org"
))
XCTAssertFalse(NotificationPolicy.shouldPresentMessage(
inserted: false,
isOutgoing: false,
appIsActive: true,
selectedConversationID: nil,
conversationID: "alice@example.org"
))
}
}
@@ -0,0 +1,80 @@
import AVFoundation
import XCTest
@testable import Luma
final class VideoNoteRecordingCompletionPolicyTests: XCTestCase {
func testNilErrorKeepsRecording() {
XCTAssertTrue(VideoNoteRecordingCompletionPolicy.shouldKeepOutput(for: nil))
}
func testSuccessfulFinishedFlagKeepsRecordingDespiteError() {
let error = NSError(
domain: AVFoundationErrorDomain,
code: -11_800,
userInfo: [AVErrorRecordingSuccessfullyFinishedKey: true]
)
XCTAssertTrue(VideoNoteRecordingCompletionPolicy.shouldKeepOutput(for: error))
}
func testFailedFinishedFlagRejectsRecording() {
let error = NSError(
domain: AVFoundationErrorDomain,
code: -11_800,
userInfo: [AVErrorRecordingSuccessfullyFinishedKey: false]
)
XCTAssertFalse(VideoNoteRecordingCompletionPolicy.shouldKeepOutput(for: error))
}
func testNSNumberSuccessfulFinishedFlagKeepsRecording() {
let error = NSError(
domain: AVFoundationErrorDomain,
code: -11_800,
userInfo: [AVErrorRecordingSuccessfullyFinishedKey: NSNumber(value: true)]
)
XCTAssertTrue(VideoNoteRecordingCompletionPolicy.shouldKeepOutput(for: error))
}
func testMaximumDurationStillKeepsRecording() {
let error = NSError(
domain: AVFoundationErrorDomain,
code: AVError.Code.maximumDurationReached.rawValue
)
XCTAssertTrue(VideoNoteRecordingCompletionPolicy.shouldKeepOutput(for: error))
}
func testExplicitStopAllowsCompletedRecordingToSend() {
XCTAssertTrue(VideoNoteRecordingCompletionPolicy.wasRequestedOrReachedLimit(
stopRequested: true,
error: nil,
recordedDuration: 3,
maximumDuration: 60
))
}
func testUnexpectedCameraCompletionDoesNotSendPartialRecording() {
XCTAssertFalse(VideoNoteRecordingCompletionPolicy.wasRequestedOrReachedLimit(
stopRequested: false,
error: nil,
recordedDuration: 3,
maximumDuration: 60
))
}
func testAutomaticMaximumDurationCompletionMaySend() {
let error = NSError(
domain: AVFoundationErrorDomain,
code: AVError.Code.maximumDurationReached.rawValue
)
XCTAssertTrue(VideoNoteRecordingCompletionPolicy.wasRequestedOrReachedLimit(
stopRequested: false,
error: error,
recordedDuration: 59.8,
maximumDuration: 60
))
}
}
@@ -0,0 +1,47 @@
import Foundation
import XCTest
@testable import Luma
final class VideoNoteRecordingLifecycleTests: XCTestCase {
func testOnlyBusyPhasesBlockASecondRecording() {
XCTAssertFalse(VideoNoteRecordingLifecycle.idle.isBusy)
XCTAssertFalse(VideoNoteRecordingLifecycle.prepared.isBusy)
XCTAssertTrue(VideoNoteRecordingLifecycle.starting.isBusy)
XCTAssertTrue(VideoNoteRecordingLifecycle.recording.isBusy)
XCTAssertTrue(VideoNoteRecordingLifecycle.stopping.isBusy)
}
func testStaleCompletionCannotFinishANewerRecording() {
let oldURL = URL(fileURLWithPath: "/tmp/old.mov")
let currentURL = URL(fileURLWithPath: "/tmp/current.mov")
XCTAssertFalse(VideoNoteRecordingLifecycle.acceptsCompletion(
activeURL: currentURL,
outputURL: oldURL
))
XCTAssertTrue(VideoNoteRecordingLifecycle.acceptsCompletion(
activeURL: currentURL,
outputURL: currentURL
))
}
func testMacOSTemporaryPathAliasesIdentifyTheSameRecording() {
let suppliedURL = URL(fileURLWithPath: "/var/folders/luma/video-note-id.mov")
let callbackURL = URL(fileURLWithPath: "/private/var/folders/luma/video-note-id.mov")
XCTAssertTrue(VideoNoteRecordingLifecycle.acceptsCompletion(
activeURL: suppliedURL,
outputURL: callbackURL
))
}
func testTemporaryPathAliasDoesNotAcceptAnotherRecording() {
let currentURL = URL(fileURLWithPath: "/var/folders/luma/current.mov")
let oldURL = URL(fileURLWithPath: "/private/var/folders/luma/old.mov")
XCTAssertFalse(VideoNoteRecordingLifecycle.acceptsCompletion(
activeURL: currentURL,
outputURL: oldURL
))
}
}
+52
View File
@@ -0,0 +1,52 @@
import Foundation
import XCTest
@testable import Luma
final class VideoNoteStopPolicyTests: XCTestCase {
func testEarlyReleaseWaitsForRecordedMedia() {
let remaining = VideoNoteStopPolicy.remainingRecordedDuration(
recordedDuration: 0.25,
minimumDuration: VideoNoteStopPolicy.minimumCaptureDuration
)
XCTAssertEqual(
remaining,
VideoNoteStopPolicy.minimumCaptureDuration - 0.25,
accuracy: 0.001
)
}
func testStopDoesNotWaitAfterMinimumMediaDurationElapsed() {
XCTAssertEqual(
VideoNoteStopPolicy.remainingRecordedDuration(
recordedDuration: 2,
minimumDuration: VideoNoteStopPolicy.minimumCaptureDuration
),
0,
accuracy: 0.001
)
}
func testRecordingFloorIsLongerThanLegacyShortClipThreshold() {
XCTAssertGreaterThanOrEqual(VideoNoteStopPolicy.minimumCaptureDuration, 1.1)
}
func testRecordedMediaTimeControlsTheFinalStop() {
XCTAssertEqual(
VideoNoteStopPolicy.remainingRecordedDuration(
recordedDuration: 0.35,
minimumDuration: 1.2
),
0.85,
accuracy: 0.001
)
XCTAssertEqual(
VideoNoteStopPolicy.remainingRecordedDuration(
recordedDuration: 1.3,
minimumDuration: 1.2
),
0,
accuracy: 0.001
)
}
}
+20
View File
@@ -0,0 +1,20 @@
import Foundation
import XCTest
@testable import Luma
final class WatchVoiceMessageTests: XCTestCase {
func testTransferUsesStableXMPPMessageIDAcrossRetries() {
let message = WatchVoiceMessage(
transferID: "9B4D98BA-7EB0-487A-96A2-608052AFCA2E",
jid: "friend@example.org",
filename: "voice.m4a",
duration: 2.5,
data: Data([0x01])
)
XCTAssertEqual(
message.stableMessageID,
"watch-9B4D98BA-7EB0-487A-96A2-608052AFCA2E"
)
}
}