mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
c41a38159b
* Initial plan * Offer only joined rooms in destination pickers Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Clean whitespace in share picker test Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Restrict the shared picker room list to joined rooms The alternate room summary provider, which backs the forward, share and Add rooms pickers and global search, now always includes the SDK's joined filter in its base filter. This replaces the per-call joinedOnly flags, so Add rooms no longer offers invited rooms or spaces and no screen can forget the flag. A filter set before the provider has its room list is now applied once the list is set, instead of being reset to the default. The share picker hit this on a cold launch from the share extension. The forward and share pickers also hide upgraded rooms, matching the Suggestions section. The SDK has no filter for these. Also registers the share picker's test file in the project, fixes the blank-line indentation SwiftFormat flagged, and re-records the picker snapshots without the upgraded room. * Check room membership for forwarding suggestions and search breadcrumbs RoomSummary now has isJoined, built from the room's membership. The forwarding suggestions and the search breadcrumbs used a missing joinRequestType to mean joined, which is also the case for rooms that have been left or banned from. It only worked because the room list's base filter already drops those. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: Mauro Romito <mauro.romito@element.io>
352 lines
17 KiB
Swift
352 lines
17 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
@testable import ElementX
|
|
import Foundation
|
|
@testable import MatrixRustSDK
|
|
import MatrixRustSDKMocks
|
|
import Testing
|
|
|
|
@MainActor
|
|
final class LoggingTests {
|
|
deinit {
|
|
Tracing.logsDirectoryOverride = nil
|
|
do {
|
|
try reloadTracingFileWriter(configuration: .init(path: URL.appGroupLogsDirectory.path(percentEncoded: false),
|
|
filePrefix: "console-tests",
|
|
fileSuffix: ".log",
|
|
maxTotalSizeBytes: 1000,
|
|
maxAgeSeconds: 1000))
|
|
} catch {
|
|
Issue.record(error)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func fileLogging() throws {
|
|
try setupTest()
|
|
|
|
let infoLog = UUID().uuidString
|
|
MXLog.info(infoLog)
|
|
|
|
let logFile = try #require(Tracing.logFiles.first)
|
|
|
|
#expect(try String(contentsOf: logFile, encoding: .utf8).contains(infoLog))
|
|
}
|
|
|
|
@Test
|
|
func logLevels() throws {
|
|
try setupTest()
|
|
|
|
let verboseLog = UUID().uuidString
|
|
MXLog.verbose(verboseLog)
|
|
|
|
let logFile = try #require(Tracing.logFiles.first)
|
|
|
|
#expect(try !String(contentsOf: logFile, encoding: .utf8).contains(verboseLog))
|
|
}
|
|
|
|
/// This is meant to test the `Target.tests.configure(…)`, but at this stage the test is somewhat pointless
|
|
/// as it is unlikely to have been called before `tearDown` has manually set the file prefix 😕.
|
|
@Test
|
|
func targetName() throws {
|
|
MXLog.info(UUID().uuidString)
|
|
let logFile = try #require(Tracing.logFiles.first)
|
|
|
|
let target = "tests"
|
|
#expect(logFile.lastPathComponent.contains(target))
|
|
}
|
|
|
|
@Test
|
|
func roomSummaryContentIsRedacted() throws {
|
|
try setupTest()
|
|
|
|
// Given a room summary that contains sensitive information
|
|
let roomName = "Private Conversation"
|
|
let lastMessage = "Secret information"
|
|
let heroName = "Pseudonym"
|
|
let roomSummary = RoomSummary(room: RoomSDKMock(),
|
|
id: "myroomid",
|
|
joinRequestType: nil,
|
|
isJoined: true,
|
|
name: roomName,
|
|
isDirect: true,
|
|
isSpace: false,
|
|
avatarURL: nil,
|
|
heroes: [.init(userID: "", displayName: heroName)],
|
|
activeMembersCount: 0,
|
|
lastMessage: AttributedString(lastMessage),
|
|
lastMessageDate: .mock,
|
|
lastMessageState: nil,
|
|
unreadMessagesCount: 0,
|
|
unreadMentionsCount: 0,
|
|
unreadNotificationsCount: 0,
|
|
notificationMode: nil,
|
|
canonicalAlias: nil,
|
|
alternativeAliases: [],
|
|
hasOngoingCall: false,
|
|
activeCallIntent: nil,
|
|
isMarkedUnread: false,
|
|
isFavourite: false,
|
|
isTombstoned: false)
|
|
|
|
// When logging that value
|
|
MXLog.info(roomSummary)
|
|
|
|
// Then the log file should not include the sensitive information
|
|
let logFile = try #require(Tracing.logFiles.first)
|
|
|
|
let content = try String(contentsOf: logFile, encoding: .utf8)
|
|
#expect(content.contains(roomSummary.id))
|
|
#expect(!content.contains(roomName))
|
|
#expect(!content.contains(lastMessage))
|
|
#expect(!content.contains(heroName))
|
|
}
|
|
|
|
@Test
|
|
func timelineContentIsRedacted() throws {
|
|
try setupTest()
|
|
|
|
// Given timeline items that contain text
|
|
let textAttributedString = "TextAttributed"
|
|
let textMessage = TextRoomTimelineItem(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "sender"),
|
|
content: .init(body: "TextString", formattedBody: AttributedString(textAttributedString)))
|
|
let noticeAttributedString = "NoticeAttributed"
|
|
let noticeMessage = NoticeRoomTimelineItem(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "sender"),
|
|
content: .init(body: "NoticeString", formattedBody: AttributedString(noticeAttributedString)))
|
|
let emoteAttributedString = "EmoteAttributed"
|
|
let emoteMessage = EmoteRoomTimelineItem(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "sender"),
|
|
content: .init(body: "EmoteString", formattedBody: AttributedString(emoteAttributedString)))
|
|
let imageMessage = ImageRoomTimelineItem(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "sender"),
|
|
content: .init(filename: "ImageString",
|
|
caption: "ImageString",
|
|
imageInfo: .mockImage,
|
|
thumbnailInfo: nil))
|
|
let videoMessage = VideoRoomTimelineItem(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "sender"),
|
|
content: .init(filename: "VideoString",
|
|
caption: "VideoString",
|
|
videoInfo: .mockVideo,
|
|
thumbnailInfo: nil))
|
|
let fileMessage = FileRoomTimelineItem(id: .randomEvent,
|
|
timestamp: .mock,
|
|
isOutgoing: false,
|
|
isEditable: false,
|
|
canBeRepliedTo: true,
|
|
sender: .init(id: "sender"),
|
|
content: .init(filename: "FileString",
|
|
caption: "FileString",
|
|
source: nil,
|
|
fileSize: nil,
|
|
thumbnailSource: nil,
|
|
contentType: nil))
|
|
|
|
// When logging that value
|
|
MXLog.info(textMessage)
|
|
MXLog.info(noticeMessage)
|
|
MXLog.info(emoteMessage)
|
|
MXLog.info(imageMessage)
|
|
MXLog.info(videoMessage)
|
|
MXLog.info(fileMessage)
|
|
|
|
// Then the log file should not include the text content
|
|
let logFile = try #require(Tracing.logFiles.first)
|
|
|
|
let content = try String(contentsOf: logFile, encoding: .utf8)
|
|
#expect(content.contains(textMessage.id.uniqueID.value))
|
|
#expect(!content.contains(textMessage.body))
|
|
#expect(!content.contains(textAttributedString))
|
|
|
|
#expect(content.contains(noticeMessage.id.uniqueID.value))
|
|
#expect(!content.contains(noticeMessage.body))
|
|
#expect(!content.contains(noticeAttributedString))
|
|
|
|
#expect(content.contains(emoteMessage.id.uniqueID.value))
|
|
#expect(!content.contains(emoteMessage.body))
|
|
#expect(!content.contains(emoteAttributedString))
|
|
|
|
#expect(content.contains(imageMessage.id.uniqueID.value))
|
|
#expect(!content.contains(imageMessage.body))
|
|
|
|
#expect(content.contains(videoMessage.id.uniqueID.value))
|
|
#expect(!content.contains(videoMessage.body))
|
|
|
|
#expect(content.contains(fileMessage.id.uniqueID.value))
|
|
#expect(!content.contains(fileMessage.body))
|
|
}
|
|
|
|
@Test
|
|
func rustMessageContentIsRedacted() throws {
|
|
try setupTest()
|
|
|
|
// Given message content that contain text
|
|
let textString = "TextString"
|
|
let rustTextMessage = TextMessageContent(body: "",
|
|
formatted: FormattedBody(format: .html, body: "<b>\(textString)</b>"))
|
|
let noticeString = "NoticeString"
|
|
let rustNoticeMessage = NoticeMessageContent(body: noticeString,
|
|
formatted: FormattedBody(format: .html, body: "<b>\(noticeString)</b>"))
|
|
let emoteString = "EmoteString"
|
|
let rustEmoteMessage = EmoteMessageContent(body: emoteString,
|
|
formatted: FormattedBody(format: .html, body: "<b>\(emoteString)</b>"))
|
|
|
|
let rustImageMessage = ImageMessageContent(filename: "ImageString",
|
|
caption: "ImageString",
|
|
formattedCaption: nil,
|
|
source: MediaSourceSDKMock(),
|
|
info: nil)
|
|
|
|
let rustVideoMessage = VideoMessageContent(filename: "VideoString",
|
|
caption: "VideoString",
|
|
formattedCaption: nil,
|
|
source: MediaSourceSDKMock(),
|
|
info: nil)
|
|
|
|
let rustFileMessage = FileMessageContent(filename: "FileString",
|
|
caption: "FileString",
|
|
formattedCaption: nil,
|
|
source: MediaSourceSDKMock(),
|
|
info: nil)
|
|
|
|
// When logging that value
|
|
MXLog.info(rustTextMessage)
|
|
MXLog.info(rustNoticeMessage)
|
|
MXLog.info(rustEmoteMessage)
|
|
MXLog.info(rustImageMessage)
|
|
MXLog.info(rustVideoMessage)
|
|
MXLog.info(rustFileMessage)
|
|
|
|
// Then the log file should not include the text content
|
|
let logFile = try #require(Tracing.logFiles.first)
|
|
|
|
let content = try String(contentsOf: logFile, encoding: .utf8)
|
|
#expect(content.contains(String(describing: TextMessageContent.self)))
|
|
#expect(!content.contains(textString))
|
|
|
|
#expect(content.contains(String(describing: NoticeMessageContent.self)))
|
|
#expect(!content.contains(noticeString))
|
|
|
|
#expect(content.contains(String(describing: EmoteMessageContent.self)))
|
|
#expect(!content.contains(emoteString))
|
|
|
|
#expect(content.contains(String(describing: ImageMessageContent.self)))
|
|
#expect(!content.contains(rustImageMessage.filename))
|
|
|
|
#expect(content.contains(String(describing: VideoMessageContent.self)))
|
|
#expect(!content.contains(rustVideoMessage.filename))
|
|
|
|
#expect(content.contains(String(describing: FileMessageContent.self)))
|
|
#expect(!content.contains(rustFileMessage.filename))
|
|
}
|
|
|
|
@Test
|
|
func logFileSorting() throws {
|
|
try setupTest(redirectTracingFileWriter: false)
|
|
|
|
// Given a collection of log files.
|
|
#expect(Tracing.logFiles.isEmpty)
|
|
|
|
// When creating new logs.
|
|
let logsFileDirectory = Tracing.logsDirectory
|
|
for i in 1...5 {
|
|
let filename = "console.\(i).log"
|
|
try "console".write(to: logsFileDirectory.appending(path: filename), atomically: true, encoding: .utf8)
|
|
}
|
|
|
|
for i in 1...5 {
|
|
let nseFilename = "console-nse.\(i).log"
|
|
try "nse".write(to: logsFileDirectory.appending(path: nseFilename), atomically: true, encoding: .utf8)
|
|
}
|
|
|
|
// Then the logs should be sorted chronologically (newest first) and not alphabetically.
|
|
#expect(Tracing.logFiles.map(\.lastPathComponent) ==
|
|
["console-nse.5.log",
|
|
"console-nse.4.log",
|
|
"console-nse.3.log",
|
|
"console-nse.2.log",
|
|
"console-nse.1.log",
|
|
"console.5.log",
|
|
"console.4.log",
|
|
"console.3.log",
|
|
"console.2.log",
|
|
"console.1.log"])
|
|
|
|
// When updating the oldest log file.
|
|
let currentLogFile = logsFileDirectory.appending(path: "console.1.log")
|
|
let fileHandle = try FileHandle(forWritingTo: currentLogFile)
|
|
try fileHandle.seekToEnd()
|
|
try fileHandle.write(contentsOf: Data("newline".utf8))
|
|
try fileHandle.close()
|
|
|
|
// Then that file should now be the first log file.
|
|
#expect(Tracing.logFiles.map(\.lastPathComponent) ==
|
|
["console.1.log",
|
|
"console-nse.5.log",
|
|
"console-nse.4.log",
|
|
"console-nse.3.log",
|
|
"console-nse.2.log",
|
|
"console-nse.1.log",
|
|
"console.5.log",
|
|
"console.4.log",
|
|
"console.3.log",
|
|
"console.2.log"])
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
/// There is something weird with Rust logging where the file writing handle won't notice that the file it is writing
|
|
/// to has been deleted. So in order to run the tests that validate the file output, we must use a new directory
|
|
/// to start with a fresh state (as calling ``Tracing.deleteLogFiles`` would trigger the bug).
|
|
private func setupTest(name: String = #function, redirectTracingFileWriter: Bool = true) throws {
|
|
let testDirectory = URL.appGroupLogsDirectory.appending(component: name, directoryHint: .isDirectory)
|
|
|
|
// CI retries re-run the suite on the same simulator, so clear out any log files left
|
|
// behind by a previous attempt. No writer points at the directory yet, so this is safe.
|
|
try? FileManager.default.removeItem(at: testDirectory)
|
|
|
|
Tracing.logsDirectoryOverride = testDirectory
|
|
try? FileManager.default.createDirectory(at: testDirectory, withIntermediateDirectories: true)
|
|
|
|
// Make an assertion before redirecting the logs as it the SDK is likely to put an empty file
|
|
// in the directory, ready to be written to.
|
|
#expect(Tracing.logFiles.isEmpty)
|
|
|
|
if redirectTracingFileWriter {
|
|
try reloadTracingFileWriter(configuration: .init(path: testDirectory.path(percentEncoded: false),
|
|
filePrefix: "console",
|
|
fileSuffix: ".log",
|
|
maxTotalSizeBytes: 1000,
|
|
maxAgeSeconds: 1000))
|
|
}
|
|
}
|
|
}
|