From e1defbf28b825966f06412a5768bc393f89caa84 Mon Sep 17 00:00:00 2001 From: mojganii Date: Thu, 28 Nov 2024 15:42:15 +0100 Subject: [PATCH] Fix log redaction loading issue --- ios/CHANGELOG.md | 1 + ios/MullvadSettings/SettingsManager.swift | 2 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 16 +++ .../Classes/ConsolidatedApplicationLog.swift | 119 ++++++++++------ .../ProblemReportInteractor.swift | 83 ++++++++---- .../ProblemReportReviewViewController.swift | 61 +++++---- .../ProblemReportViewController.swift | 4 +- .../Log/ConsolidatedApplicationLogTests.swift | 127 ++++++++++++++++++ ios/Shared/LaunchArguments.swift | 2 +- 9 files changed, 315 insertions(+), 100 deletions(-) create mode 100644 ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 4cf9df3c25ec..717e1209b53a 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -26,6 +26,7 @@ Line wrap the file at 100 chars. Th ## [2024.10 - 2024-11-20] ### Fixed - Removed deadlock when losing connectivity without entering offline state. +- Improved log reporting. ## [2024.9 - 2024-11-07] ### Added diff --git a/ios/MullvadSettings/SettingsManager.swift b/ios/MullvadSettings/SettingsManager.swift index 7a6d4ba2e98f..28e2451d35d4 100644 --- a/ios/MullvadSettings/SettingsManager.swift +++ b/ios/MullvadSettings/SettingsManager.swift @@ -47,7 +47,7 @@ public enum SettingsManager { public static func getLastUsedAccount() throws -> String { let data = try store.read(key: .lastUsedAccount) - return String(decoding: data, as: UTF8.self) + return String(bytes: data, encoding: .utf8) ?? "" } public static func setLastUsedAccount(_ string: String?) throws { diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 8e60b70042e7..e6265ff5194c 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -871,6 +871,7 @@ F0164EC32B4C49D30020268D /* ShadowsocksLoaderStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */; }; F0164ED12B4F2DCB0020268D /* AccessMethodIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0164ED02B4F2DCB0020268D /* AccessMethodIterator.swift */; }; F01DAE332C2B032A00521E46 /* RelaySelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01DAE322C2B032A00521E46 /* RelaySelection.swift */; }; + F022EBA62CF0C6AE009484B9 /* ConsolidatedApplicationLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB95254ADE4E0051A0A4 /* ConsolidatedApplicationLog.swift */; }; F028A56A2A34D4E700C0CAA3 /* RedeemVoucherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A5692A34D4E700C0CAA3 /* RedeemVoucherViewController.swift */; }; F028A56C2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F028A56B2A34D8E600C0CAA3 /* AddCreditSucceededViewController.swift */; }; F02F41A02B9723AF00625A4F /* AddLocationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02F419A2B9723AE00625A4F /* AddLocationsViewController.swift */; }; @@ -946,6 +947,8 @@ F09D04C12AF39EA2003D4F89 /* OutgoingConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F09D04BC2AEBB7C5003D4F89 /* OutgoingConnectionService.swift */; }; F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */; }; F0A1638A2C47B77300592300 /* ServerRelaysResponse+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACE3342BE51745006D5333 /* ServerRelaysResponse+Stubs.swift */; }; + F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */; }; + F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; F0ACE30D2BE4E478006D5333 /* MullvadMockData.h in Headers */ = {isa = PBXBuildFile; fileRef = F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */; settings = {ATTRIBUTES = (Public, ); }; }; F0ACE3102BE4E478006D5333 /* MullvadMockData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; }; F0ACE3112BE4E478006D5333 /* MullvadMockData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -2166,6 +2169,7 @@ F09D04BF2AF39D63003D4F89 /* OutgoingConnectionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingConnectionServiceTests.swift; sourceTree = ""; }; F0A0868F2C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettingsStrategyTests.swift; sourceTree = ""; }; F0A163882C47B46300592300 /* SingleHopEphemeralPeerExchangerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleHopEphemeralPeerExchangerTests.swift; sourceTree = ""; }; + F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsolidatedApplicationLogTests.swift; sourceTree = ""; }; F0ACE3082BE4E478006D5333 /* MullvadMockData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MullvadMockData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F0ACE30A2BE4E478006D5333 /* MullvadMockData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MullvadMockData.h; sourceTree = ""; }; F0ACE32E2BE4EA8B006D5333 /* MockProxyFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProxyFactory.swift; sourceTree = ""; }; @@ -2425,6 +2429,7 @@ 440E9EF02BDA93CB00B1FD11 /* MullvadVPN */ = { isa = PBXGroup; children = ( + F0A7EBB02CEF6C5F005BB671 /* Log */, 440E9EF62BDA957300B1FD11 /* Classes */, 440E9F002BDA997C00B1FD11 /* Extensions */, 440E9EFB2BDA97C600B1FD11 /* GeneralAPIs */, @@ -4205,6 +4210,14 @@ path = GeneralAPIs; sourceTree = ""; }; + F0A7EBB02CEF6C5F005BB671 /* Log */ = { + isa = PBXGroup; + children = ( + F0A7EBB12CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift */, + ); + path = Log; + sourceTree = ""; + }; F0ACE3092BE4E478006D5333 /* MullvadMockData */ = { isa = PBXGroup; children = ( @@ -5378,6 +5391,7 @@ A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */, 58B07C182AEFDD6C00A09625 /* StoreTransactionLog.swift in Sources */, A9A5FA382ACB05600083449F /* InputTextFormatter.swift in Sources */, + F022EBA62CF0C6AE009484B9 /* ConsolidatedApplicationLog.swift in Sources */, A9A5FA372ACB052D0083449F /* ApplicationTarget.swift in Sources */, A9A5F9E12ACB05160083449F /* AddressCacheTracker.swift in Sources */, A9A5F9E22ACB05160083449F /* BackgroundTask.swift in Sources */, @@ -5488,6 +5502,7 @@ A9A5FA282ACB05160083449F /* WgKeyRotation.swift in Sources */, 449872E42B7CB96300094DDC /* TunnelSettingsUpdateTests.swift in Sources */, A9A5FA292ACB05160083449F /* AddressCacheTests.swift in Sources */, + F0A7EBB22CEF6C79005BB671 /* ConsolidatedApplicationLogTests.swift in Sources */, A9B6AC182ADE8F4300F7802A /* MigrationManagerTests.swift in Sources */, 7A9BE5AB2B909A1700E2A7D0 /* LocationDataSourceProtocol.swift in Sources */, A9A5FA2A2ACB05160083449F /* CoordinatesTests.swift in Sources */, @@ -5515,6 +5530,7 @@ A9A5FA342ACB05160083449F /* StringTests.swift in Sources */, 7A52F96C2C17450C00B133B9 /* RelaySelectorWrapperTests.swift in Sources */, A9A5FA352ACB05160083449F /* WgKeyRotationTests.swift in Sources */, + F0A7EBB62CF092CC005BB671 /* ApplicationConfiguration.swift in Sources */, 7AB4CCB92B69097E006037F5 /* IPOverrideTests.swift in Sources */, A9A5FA362ACB05160083449F /* TunnelManagerTests.swift in Sources */, ); diff --git a/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift index 48dbf6a72349..0aca353d5c26 100644 --- a/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift +++ b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift @@ -15,6 +15,7 @@ private let kRedactedContainerPlaceholder = "[REDACTED CONTAINER PATH]" class ConsolidatedApplicationLog: TextOutputStreamable { typealias Metadata = KeyValuePairs + private let bufferSize: UInt64 enum MetadataKey: String { case id, os @@ -30,14 +31,17 @@ class ConsolidatedApplicationLog: TextOutputStreamable { let applicationGroupContainers: [URL] let metadata: Metadata + private let logQueue = DispatchQueue(label: "com.mullvad.consolidation.logs.queue") private var logs: [LogAttachment] = [] init( redactCustomStrings: [String], - redactContainerPathsForSecurityGroupIdentifiers securityGroupIdentifiers: [String] + redactContainerPathsForSecurityGroupIdentifiers securityGroupIdentifiers: [String], + bufferSize: UInt64 ) { metadata = Self.makeMetadata() self.redactCustomStrings = redactCustomStrings + self.bufferSize = bufferSize applicationGroupContainers = securityGroupIdentifiers .compactMap { securityGroupIdentifier -> URL? in @@ -46,37 +50,65 @@ class ConsolidatedApplicationLog: TextOutputStreamable { } } - func addLogFiles(fileURLs: [URL]) { - for fileURL in fileURLs { - addSingleLogFile(fileURL) + func addLogFiles(fileURLs: [URL], completion: (() -> Void)? = nil) { + logQueue.async(flags: .barrier) { + for fileURL in fileURLs { + self.addSingleLogFile(fileURL) + } + DispatchQueue.main.async { + completion?() + } } } - func addError(message: String, error: String) { + func addError(message: String, error: String, completion: (() -> Void)? = nil) { let redactedError = redact(string: error) - - logs.append(LogAttachment(label: message, content: redactedError)) + logQueue.async(flags: .barrier) { + self.logs.append(LogAttachment(label: message, content: redactedError)) + DispatchQueue.main.async { + completion?() + } + } } var string: String { - var body = "" - write(to: &body) - return body + var result = "" + logQueue.sync { + if !logs.isEmpty { + result += "System information:\n" + for (key, value) in metadata { + result += "\(key.rawValue): \(value)\n" + } + result += "\n" + for attachment in logs { + result += "\(kLogDelimiter)\n" + result += "\(attachment.label)\n" + result += "\(kLogDelimiter)\n" + result += "\(attachment.content)\n\n" + } + } + } + return result } func write(to stream: inout some TextOutputStream) { - print("System information:", to: &stream) - for (key, value) in metadata { - print("\(key.rawValue): \(value)", to: &stream) - } - print("", to: &stream) - - for attachment in logs { - print(kLogDelimiter, to: &stream) - print(attachment.label, to: &stream) - print(kLogDelimiter, to: &stream) - print(attachment.content, to: &stream) - print("", to: &stream) + logQueue.sync { + var localOutput = "" + if !logs.isEmpty { + localOutput += "System information:\n" + for (key, value) in metadata { + localOutput += "\(key.rawValue): \(value)\n" + } + localOutput += "\n" + for attachment in logs { + localOutput += "\(kLogDelimiter)\n" + localOutput += "\(attachment.label)\n" + localOutput += "\(kLogDelimiter)\n" + localOutput += "\(attachment.content)\n\n" + } + + stream.write(localOutput) + } } } @@ -92,10 +124,11 @@ class ConsolidatedApplicationLog: TextOutputStreamable { let path = fileURL.path let redactedPath = redact(string: path) - if let lossyString = Self.readFileLossy(path: path, maxBytes: ApplicationConfiguration.logMaximumFileSize) { + if let lossyString = readFileLossy(path: path, maxBytes: bufferSize) { let redactedString = redact(string: lossyString) - - logs.append(LogAttachment(label: redactedPath, content: redactedString)) + logQueue.async(flags: .barrier) { + self.logs.append(LogAttachment(label: redactedPath, content: redactedString)) + } } else { addError(message: redactedPath, error: "Log file does not exist: \(path).") } @@ -113,7 +146,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { ] } - private static func readFileLossy(path: String, maxBytes: UInt64) -> String? { + private func readFileLossy(path: String, maxBytes: UInt64) -> String? { guard let fileHandle = FileHandle(forReadingAtPath: path) else { return nil } @@ -125,17 +158,17 @@ class ConsolidatedApplicationLog: TextOutputStreamable { fileHandle.seek(toFileOffset: 0) } - let data = fileHandle.readData(ofLength: Int(ApplicationConfiguration.logMaximumFileSize)) let replacementCharacter = Character(UTF8.decode(UTF8.encodedReplacementCharacter)) - let lossyString = String( - String(decoding: data, as: UTF8.self) - .drop { ch in - // Drop leading replacement characters produced when decoding data - ch == replacementCharacter - } - ) - - return lossyString + if let data = try? fileHandle.read(upToCount: Int(bufferSize)), + let lossyString = String(bytes: data, encoding: .utf8) { + let resultString = lossyString.drop { ch in + // Drop leading replacement characters produced when decoding data + ch == replacementCharacter + } + return String(resultString) + } else { + return nil + } } private func redactCustomStrings(string: String) -> String { @@ -147,9 +180,9 @@ class ConsolidatedApplicationLog: TextOutputStreamable { private func redact(string: String) -> String { [ redactContainerPaths, - Self.redactAccountNumber, - Self.redactIPv4Address, - Self.redactIPv6Address, + redactAccountNumber, + redactIPv4Address, + redactIPv6Address, redactCustomStrings, ].reduce(string) { resultString, transform -> String in transform(resultString) @@ -165,7 +198,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { } } - private static func redactAccountNumber(string: String) -> String { + private func redactAccountNumber(string: String) -> String { redact( // swiftlint:disable:next force_try regularExpression: try! NSRegularExpression(pattern: #"\d{16}"#), @@ -174,7 +207,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { ) } - private static func redactIPv4Address(string: String) -> String { + private func redactIPv4Address(string: String) -> String { redact( regularExpression: NSRegularExpression.ipv4RegularExpression, string: string, @@ -182,7 +215,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { ) } - private static func redactIPv6Address(string: String) -> String { + private func redactIPv6Address(string: String) -> String { redact( regularExpression: NSRegularExpression.ipv6RegularExpression, string: string, @@ -190,7 +223,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { ) } - private static func redact( + private func redact( regularExpression: NSRegularExpression, string: String, replacementString: String diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift index bbc740f453ec..25de817940f4 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift @@ -14,51 +14,78 @@ import Operations final class ProblemReportInteractor { private let apiProxy: APIQuerying private let tunnelManager: TunnelManager + private let consolidatedLog: ConsolidatedApplicationLog + private var reportedString = "" - private lazy var consolidatedLog: ConsolidatedApplicationLog = { - let securityGroupIdentifier = ApplicationConfiguration.securityGroupIdentifier - let redactStrings = [tunnelManager.deviceState.accountData?.number].compactMap { $0 } - - let report = ConsolidatedApplicationLog( - redactCustomStrings: redactStrings, - redactContainerPathsForSecurityGroupIdentifiers: [securityGroupIdentifier] + init(apiProxy: APIQuerying, tunnelManager: TunnelManager) { + self.apiProxy = apiProxy + self.tunnelManager = tunnelManager + self.consolidatedLog = ConsolidatedApplicationLog( + redactCustomStrings: [tunnelManager.deviceState.accountData?.number].compactMap { $0 }, + redactContainerPathsForSecurityGroupIdentifiers: [ApplicationConfiguration.securityGroupIdentifier], + bufferSize: ApplicationConfiguration.logMaximumFileSize ) + } - let logFileURLs = ApplicationTarget.allCases.flatMap { + func fetchReportString(completion: @escaping (String) -> Void) { + consolidatedLog.addLogFiles(fileURLs: ApplicationTarget.allCases.flatMap { ApplicationConfiguration.logFileURLs(for: $0, in: ApplicationConfiguration.containerURL) + }) { [weak self] in + guard let self else { return } + completion(consolidatedLog.string) } - report.addLogFiles(fileURLs: logFileURLs) - - return report - }() - - var reportString: String { - consolidatedLog.string } - init(apiProxy: APIQuerying, tunnelManager: TunnelManager) { - self.apiProxy = apiProxy - self.tunnelManager = tunnelManager + func sendReport( + email: String, + message: String, + completion: @escaping (Result) -> Void + ) { + let logString = self.consolidatedLog.string + + if logString.isEmpty { + fetchReportString { [weak self] updatedLogString in + self?.sendProblemReport( + email: email, + message: message, + logString: updatedLogString, + completion: completion + ) + } + } else { + sendProblemReport( + email: email, + message: message, + logString: logString, + completion: completion + ) + } } - func sendReport( + private func sendProblemReport( email: String, message: String, + logString: String, completion: @escaping (Result) -> Void - ) -> Cancellable { + ) { + let metadataDict = self.consolidatedLog.metadata.reduce(into: [:]) { output, entry in + output[entry.key.rawValue] = entry.value + } + let request = REST.ProblemReportRequest( address: email, message: message, - log: consolidatedLog.string, - metadata: consolidatedLog.metadata.reduce(into: [:]) { output, entry in - output[entry.key.rawValue] = entry.value - } + log: logString, + metadata: metadataDict ) - return apiProxy.sendProblemReport( + _ = self.apiProxy.sendProblemReport( request, - retryStrategy: .default, - completionHandler: completion - ) + retryStrategy: .default + ) { result in + DispatchQueue.main.async { + completion(result) + } + } } } diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift index a12d32362a74..d7b6f54c38e4 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift @@ -9,8 +9,14 @@ import UIKit class ProblemReportReviewViewController: UIViewController { + private let spinnerView = SpinnerActivityIndicatorView(style: .large) private var textView = UITextView() private let interactor: ProblemReportInteractor + private lazy var spinnerContainerView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.5) + return view + }() init(interactor: ProblemReportInteractor) { self.interactor = interactor @@ -23,7 +29,7 @@ class ProblemReportReviewViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - + view.backgroundColor = .secondaryColor view.accessibilityIdentifier = .appLogsView navigationItem.title = NSLocalizedString( @@ -60,14 +66,21 @@ class ProblemReportReviewViewController: UIViewController { ) textView.backgroundColor = .systemBackground - view.addSubview(textView) + view.addConstrainedSubviews([textView]) { + textView.pinEdgesToSuperview(.all().excluding(.top)) + textView.pinEdgeToSuperviewMargin(.top(0)) + } - NSLayoutConstraint.activate([ - textView.topAnchor.constraint(equalTo: view.topAnchor), - textView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - textView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - textView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) + textView.addConstrainedSubviews([spinnerContainerView]) { + spinnerContainerView.pinEdgesToSuperview() + spinnerContainerView.widthAnchor.constraint(equalTo: textView.widthAnchor) + spinnerContainerView.heightAnchor.constraint(equalTo: textView.heightAnchor) + } + + spinnerContainerView.addConstrainedSubviews([spinnerView]) { + spinnerView.centerXAnchor.constraint(equalTo: view.centerXAnchor) + spinnerView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + } // Used to layout constraints so that navigation controller could properly adjust the text // view insets. @@ -81,30 +94,28 @@ class ProblemReportReviewViewController: UIViewController { } private func loadLogs() { - let presentation = AlertPresentation( - id: "problem-report-load", - icon: .spinner, - buttons: [] - ) - - let alertController = AlertViewController(presentation: presentation) - - present(alertController, animated: true) { - self.textView.text = self.interactor.reportString - self.dismiss(animated: true) + spinnerView.startAnimating() + interactor.fetchReportString { [weak self] reportString in + guard let self else { return } + textView.text = reportString + spinnerView.stopAnimating() + spinnerContainerView.isHidden = true } } #if DEBUG private func share() { - let activityController = UIActivityViewController( - activityItems: [interactor.reportString], - applicationActivities: nil - ) + interactor.fetchReportString { [weak self] reportString in + guard let self,!reportString.isEmpty else { return } + let activityController = UIActivityViewController( + activityItems: [reportString], + applicationActivities: nil + ) - activityController.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem + activityController.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem - present(activityController, animated: true) + present(activityController, animated: true) + } } #endif } diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift index 15d21ee9dbe1..47ba3225e963 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift @@ -131,7 +131,7 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { @objc func handleViewLogsButtonTap() { let reviewController = ProblemReportReviewViewController(interactor: interactor) - let navigationController = UINavigationController(rootViewController: reviewController) + let navigationController = CustomNavigationController(rootViewController: reviewController) present(navigationController, animated: true) } @@ -258,7 +258,7 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { willSendProblemReport() - _ = interactor.sendReport( + interactor.sendReport( email: viewModel.email, message: viewModel.message ) { completion in diff --git a/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift b/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift new file mode 100644 index 000000000000..523c02bebf28 --- /dev/null +++ b/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift @@ -0,0 +1,127 @@ +// +// ConsolidatedApplicationLogTests.swift +// MullvadVPNTests +// +// Created by Mojgan on 2024-11-21. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class ConsolidatedApplicationLogTests: XCTestCase { + var consolidatedLog: ConsolidatedApplicationLog! + let mockRedactStrings = ["sensitive", "secret"] + let mockSecurityGroupIdentifiers = ["group1", "group2"] + var createdMockFiles: [URL] = [] + let kRedactedPlaceholder = "[REDACTED]" + + override func setUp() { + super.setUp() + consolidatedLog = ConsolidatedApplicationLog( + redactCustomStrings: mockRedactStrings, + redactContainerPathsForSecurityGroupIdentifiers: mockSecurityGroupIdentifiers, + bufferSize: 65_536 + ) + createdMockFiles = [] + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + consolidatedLog = nil + // Remove all mock files created during tests + for file in createdMockFiles { + try FileManager.default.removeItem(at: file) + } + createdMockFiles = [] + } + + func testAddLogFiles() { + var string = "" + let expectation = self.expectation(description: "Log files added") + let mockFile = createMockFile(content: content, fileName: "\(generateRandomName()).txt") + + consolidatedLog.addLogFiles(fileURLs: [mockFile]) { + expectation.fulfill() + } + + waitForExpectations(timeout: 1) + consolidatedLog.write(to: &string) + XCTAssertTrue( + consolidatedLog.string.contains(string), + "Log should contain the file content." + ) + } + + func testAddError() { + let expectation = self.expectation(description: "Error added to log") + let errorMessage = "Test error" + let errorDetails = "A sensitive error occurred" + + consolidatedLog.addError(message: errorMessage, error: errorDetails) { + expectation.fulfill() + } + + waitForExpectations(timeout: 1) + XCTAssertTrue( + consolidatedLog.string.contains(errorMessage), + "Log should include the error message." + ) + } + + func testStringOutput() { + let expectation = self.expectation(description: "Log files added") + let mockFile = createMockFile(content: content, fileName: "\(generateRandomName()).txt") + consolidatedLog.addLogFiles(fileURLs: [mockFile]) { + expectation.fulfill() + let output = self.consolidatedLog.string + XCTAssertFalse(output.isEmpty, "Output string should include redacted log content.") + } + waitForExpectations(timeout: 1) + } + + // MARK: - Private functions + + private func createMockFile(content: String, fileName: String) -> URL { + let tempDirectory = FileManager.default.temporaryDirectory + let fileURL = tempDirectory.appendingPathComponent(fileName) + + do { + try content.write(to: fileURL, atomically: true, encoding: .utf8) + createdMockFiles.append(fileURL) + } catch { + XCTFail("Failed to create mock file: \(error)") + } + return fileURL + } + + private func generateRandomName() -> String { + let characterSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + let randomName = (0 ..< 6).compactMap { _ in characterSet.randomElement() } + return String(randomName) + } +} + +extension ConsolidatedApplicationLogTests { + private var content: String { + return """ + MullvadVPN version xxxx.x + [22/11/2024 @ 08:52:22][AppDelegate][debug] Registered app refresh task. + [22/11/2024 @ 08:52:22][AppDelegate][debug] Registered address cache update task. + [22/11/2024 @ 08:52:22][AppDelegate][debug] Registered private key rotation task. + [22/11/2024 @ 08:52:23][TunnelManager][debug] Refresh device state + and tunnel status + due to application becoming active. + [22/11/2024 @ 08:52:23][RelayCacheTracker][debug] Start periodic relay updates. + [22/11/2024 @ 08:52:23][AddressCache.Tracker][debug] Start periodic address cache updates. + [22/11/2024 @ 08:52:23][AddressCache.Tracker][debug] Schedule address cache update at 23/11/2024 @ 08:49:52. + [22/11/2024 @ 08:52:23][AppDelegate][debug] Attempted migration from UI Process, but found nothing to do. + [22/11/2024 @ 08:52:23][TunnelManager][debug] Refresh tunnel status for new tunnel. + [22/11/2024 @ 08:52:23][REST.NetworkOperation][debug] name=get-access-token.2 + Send request + to /auth/v1/token via 127.0.0.1 using encrypted-dns-url-session. + [22/11/2024 @ 08:52:23][ApplicationRouter][debug] Presenting .main. + [22/11/2024 @ 08:52:23][REST.NetworkOperation][debug] name=get-access-token.2 Response: 200. + [22/11/2024 @ 08:52:23][AppDelegate][debug] Finished initialization. + """ + } +} diff --git a/ios/Shared/LaunchArguments.swift b/ios/Shared/LaunchArguments.swift index ab75e73b694e..69c656a9b27b 100644 --- a/ios/Shared/LaunchArguments.swift +++ b/ios/Shared/LaunchArguments.swift @@ -46,7 +46,7 @@ public extension ProcessInfo { extension Encodable { public func toJSON(_ encoder: JSONEncoder = JSONEncoder()) throws -> String { let data = try encoder.encode(self) - let result = String(decoding: data, as: UTF8.self) + let result = String(bytes: data, encoding: .utf8) ?? "" return result } }