Files
mobile-ios/RosettaTests/SchemaParityTests.swift

280 lines
11 KiB
Swift

import XCTest
@testable import Rosetta
@MainActor
final class SchemaParityTests: XCTestCase {
private var ctx: DBTestContext!
override func setUpWithError() throws {
ctx = DBTestContext()
}
override func tearDownWithError() throws {
ctx.teardown()
ctx = nil
}
func testSchemaContainsRequiredTablesColumnsAndIndexes() async throws {
try await ctx.bootstrap()
let schema = try ctx.schemaSnapshot()
let requiredTables: Set<String> = [
"messages",
"dialogs",
"accounts_sync_times",
"sync_cursors",
"groups",
"pinned_messages",
"avatar_cache",
"blacklist",
]
XCTAssertTrue(requiredTables.isSubset(of: schema.tables), "Missing required tables")
let messagesColumns = schema.columnsByTable["messages"] ?? []
let requiredMessageColumns: Set<String> = [
"account", "from_public_key", "to_public_key", "message_id", "dialog_key",
"timestamp", "is_read", "read", "delivery_status", "delivered", "text", "plain_message",
"attachments", "reply_to_message_id",
]
XCTAssertTrue(requiredMessageColumns.isSubset(of: messagesColumns), "Missing messages columns")
let dialogsColumns = schema.columnsByTable["dialogs"] ?? []
let requiredDialogColumns: Set<String> = [
"account", "opponent_key", "dialog_id", "is_request", "last_timestamp", "last_message_id",
"last_message_timestamp", "i_have_sent", "unread_count",
]
XCTAssertTrue(requiredDialogColumns.isSubset(of: dialogsColumns), "Missing dialogs columns")
let requiredIndexes: Set<String> = [
"idx_messages_account_message_id",
"idx_messages_account_dialog_key_timestamp",
"idx_messages_account_dialog_fromme_isread",
"idx_messages_account_dialog_fromme_timestamp",
"idx_dialogs_account_opponent_key",
]
XCTAssertTrue(requiredIndexes.isSubset(of: schema.indexes), "Missing required indexes")
}
func testUnreadAndSentQueriesUseParityIndexes() async throws {
try await ctx.bootstrap()
try await ctx.runScenario(FixtureScenario(name: "seed", events: [
.incoming(opponent: "02peer_a", messageId: "m1", timestamp: 1, text: "hello"),
.outgoing(opponent: "02peer_a", messageId: "m2", timestamp: 2, text: "yo"),
]))
let sqlite = try ctx.openSQLite()
let unreadPlanRows = try sqlite.query(
"""
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM messages
WHERE account = ? AND dialog_key = ? AND from_me = 0 AND is_read = 0
""",
[.text(ctx.account), .text(DatabaseManager.dialogKey(account: ctx.account, opponentKey: "02peer_a"))]
)
let unreadPlan = unreadPlanRows.compactMap { $0["detail"] }.joined(separator: " | ")
XCTAssertTrue(
unreadPlan.contains("idx_messages_account_dialog_fromme_isread"),
"Unread query plan did not use idx_messages_account_dialog_fromme_isread: \(unreadPlan)"
)
let sentPlanRows = try sqlite.query(
"""
EXPLAIN QUERY PLAN
SELECT message_id FROM messages
WHERE account = ? AND dialog_key = ? AND from_me = 1
ORDER BY timestamp DESC
LIMIT 1
""",
[.text(ctx.account), .text(DatabaseManager.dialogKey(account: ctx.account, opponentKey: "02peer_a"))]
)
let sentPlan = sentPlanRows.compactMap { $0["detail"] }.joined(separator: " | ")
XCTAssertTrue(
sentPlan.contains("idx_messages_account_dialog_fromme_timestamp"),
"Sent query plan did not use idx_messages_account_dialog_fromme_timestamp: \(sentPlan)"
)
}
func testPacketRegistrySupportsMessagingAndGroupsCoreIds() throws {
let packets: [(Int, any Packet)] = [
(0x00, PacketHandshake()),
(0x01, PacketUserInfo()),
(0x02, PacketResult()),
(0x03, PacketSearch()),
(0x04, PacketOnlineSubscribe()),
(0x05, PacketOnlineState()),
(0x06, PacketMessage()),
(0x07, PacketRead()),
(0x08, PacketDelivery()),
(0x09, PacketDeviceNew()),
(0x0A, PacketRequestUpdate()),
(0x0B, PacketTyping()),
(0x0F, PacketRequestTransport()),
(0x10, PacketPushNotification()),
(0x11, PacketCreateGroup()),
(0x12, PacketGroupInfo()),
(0x13, PacketGroupInviteInfo()),
(0x14, PacketGroupJoin()),
(0x15, PacketGroupLeave()),
(0x16, PacketGroupBan()),
(0x17, PacketDeviceList()),
(0x18, PacketDeviceResolve()),
(0x19, PacketSync()),
]
for (expectedId, packet) in packets {
let encoded = PacketRegistry.encode(packet)
guard let decoded = PacketRegistry.decode(from: encoded) else {
XCTFail("Failed to decode packet 0x\(String(expectedId, radix: 16))")
continue
}
XCTAssertEqual(decoded.packetId, expectedId)
}
}
func testAttachmentTypeCallRoundTripDecoding() throws {
var packet = PacketMessage()
packet.fromPublicKey = "02from"
packet.toPublicKey = "02to"
packet.content = ""
packet.chachaKey = ""
packet.timestamp = 123
packet.privateKey = "hash"
packet.messageId = "msg-call"
packet.attachments = [MessageAttachment(id: "call-1", preview: "", blob: "", type: .call)]
packet.aesChachaKey = ""
let encoded = PacketRegistry.encode(packet)
guard let decoded = PacketRegistry.decode(from: encoded),
let decodedMessage = decoded.packet as? PacketMessage
else {
XCTFail("Failed to decode call attachment packet")
return
}
XCTAssertEqual(decoded.packetId, 0x06)
XCTAssertEqual(decodedMessage.attachments.first?.type, .call)
XCTAssertFalse(decodedMessage.isMalformed)
}
func testPacketMessageDecodeSupportsAttachmentMeta4Compatibility() throws {
let encoded = makePacketMessageData(attachmentMetaFieldCount: 4)
guard let decoded = PacketRegistry.decode(from: encoded),
let message = decoded.packet as? PacketMessage else {
XCTFail("Failed to decode packet with 4 attachment meta fields")
return
}
XCTAssertEqual(decoded.packetId, 0x06)
XCTAssertFalse(message.isMalformed)
XCTAssertEqual(message.fromPublicKey, "02from")
XCTAssertEqual(message.toPublicKey, "02to")
XCTAssertEqual(message.messageId, "msg-compat")
XCTAssertEqual(message.aesChachaKey, "aes-key")
XCTAssertEqual(message.attachments.count, 1)
XCTAssertEqual(message.attachments[0].transportTag, "tag-1")
XCTAssertEqual(message.attachments[0].transportServer, "cdn.rosetta.im")
}
func testPacketMessageDecodeSupportsAttachmentMeta0Compatibility() throws {
let encoded = makePacketMessageData(attachmentMetaFieldCount: 0)
guard let decoded = PacketRegistry.decode(from: encoded),
let message = decoded.packet as? PacketMessage else {
XCTFail("Failed to decode packet with 0 attachment meta fields")
return
}
XCTAssertEqual(decoded.packetId, 0x06)
XCTAssertFalse(message.isMalformed)
XCTAssertEqual(message.messageId, "msg-compat")
XCTAssertEqual(message.aesChachaKey, "aes-key")
XCTAssertEqual(message.attachments.count, 1)
XCTAssertEqual(message.attachments[0].transportTag, "")
XCTAssertEqual(message.attachments[0].transportServer, "")
}
func testPacketMessageDecodeMarksMalformedForTruncatedOrMisalignedPayload() throws {
let canonical = makePacketMessageData(attachmentMetaFieldCount: 2)
let truncated = canonical.dropLast(3)
let withTrailingByte = canonical + Data([0x00])
guard let decodedTruncated = PacketRegistry.decode(from: Data(truncated)),
let messageTruncated = decodedTruncated.packet as? PacketMessage else {
XCTFail("Failed to decode truncated packet wrapper")
return
}
XCTAssertTrue(messageTruncated.isMalformed)
XCTAssertFalse(messageTruncated.malformedFingerprint.isEmpty)
guard let decodedTrailing = PacketRegistry.decode(from: withTrailingByte),
let messageTrailing = decodedTrailing.packet as? PacketMessage else {
XCTFail("Failed to decode trailing-byte packet wrapper")
return
}
XCTAssertTrue(messageTrailing.isMalformed)
XCTAssertFalse(messageTrailing.malformedFingerprint.isEmpty)
}
func testSessionPacketContextResolverAcceptsGroupWireShape() throws {
let own = "02my_account"
var groupMessage = PacketMessage()
groupMessage.fromPublicKey = "02group_member"
groupMessage.toPublicKey = "#group:alpha"
let messageContext = SessionManager.testResolveMessagePacketContext(groupMessage, ownKey: own)
XCTAssertEqual(messageContext?.kind, "group")
XCTAssertEqual(messageContext?.dialogKey, "#group:alpha")
XCTAssertEqual(messageContext?.fromMe, false)
var groupRead = PacketRead()
groupRead.fromPublicKey = "02group_member"
groupRead.toPublicKey = "#group:alpha"
let readContext = SessionManager.testResolveReadPacketContext(groupRead, ownKey: own)
XCTAssertEqual(readContext?.kind, "group")
XCTAssertEqual(readContext?.dialogKey, "#group:alpha")
XCTAssertEqual(readContext?.fromMe, false)
var groupTyping = PacketTyping()
groupTyping.fromPublicKey = "02group_member"
groupTyping.toPublicKey = "#group:alpha"
let typingContext = SessionManager.testResolveTypingPacketContext(groupTyping, ownKey: own)
XCTAssertEqual(typingContext?.kind, "group")
XCTAssertEqual(typingContext?.dialogKey, "#group:alpha")
XCTAssertEqual(typingContext?.fromMe, false)
}
func testStreamEncodingSmoke() {
let stream = Rosetta.Stream()
_ = stream
XCTAssertTrue(true)
}
private func makePacketMessageData(attachmentMetaFieldCount: Int) -> Data {
let stream = Rosetta.Stream()
stream.writeInt16(PacketMessage.packetId)
stream.writeString("02from")
stream.writeString("02to")
stream.writeString("ciphertext")
stream.writeString("chacha-key")
stream.writeInt64(1_710_000_000_000)
stream.writeString("hash")
stream.writeString("msg-compat")
stream.writeInt8(1)
stream.writeString("att-1")
stream.writeString("preview")
stream.writeString("blob")
stream.writeInt8(AttachmentType.image.rawValue)
if attachmentMetaFieldCount >= 2 {
stream.writeString("tag-1")
stream.writeString("cdn.rosetta.im")
}
if attachmentMetaFieldCount >= 4 {
stream.writeString("02encoded-for")
stream.writeString("desktop")
}
stream.writeString("aes-key")
return stream.toData()
}
}