mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 15:22:39 +00:00
Extended device info (PSG-772) (#6766)
This commit is contained in:
parent
46a975b9dc
commit
2f689f4557
17 changed files with 825 additions and 156 deletions
|
@ -2398,12 +2398,18 @@ To enable access, tap Settings> Location and select Always";
|
|||
|
||||
"user_session_details_title" = "Session details";
|
||||
"user_session_details_session_section_header" = "Session";
|
||||
"user_session_details_application_section_header" = "Application";
|
||||
"user_session_details_device_section_header" = "Device";
|
||||
"user_session_details_session_name" = "Session name";
|
||||
"user_session_details_session_id" = "Session ID";
|
||||
"user_session_details_session_section_footer" = "Copy any data by tapping on it and holding it down.";
|
||||
"user_session_details_device_ip_address" = "IP address";
|
||||
|
||||
"user_session_details_device_ip_location" = "IP location";
|
||||
"user_session_details_device_model" = "Model";
|
||||
"user_session_details_device_os" = "Operating System";
|
||||
"user_session_details_application_name" = "Name";
|
||||
"user_session_details_application_version" = "Version";
|
||||
"user_session_details_application_url" = "URL";
|
||||
"user_session_overview_current_session_title" = "Current session";
|
||||
"user_session_overview_session_title" = "Session";
|
||||
"user_session_overview_session_details_button_title" = "Session details";
|
||||
|
|
|
@ -8471,10 +8471,38 @@ public class VectorL10n: NSObject {
|
|||
public static var userIdTitle: String {
|
||||
return VectorL10n.tr("Vector", "user_id_title")
|
||||
}
|
||||
/// Name
|
||||
public static var userSessionDetailsApplicationName: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_application_name")
|
||||
}
|
||||
/// Application
|
||||
public static var userSessionDetailsApplicationSectionHeader: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_application_section_header")
|
||||
}
|
||||
/// URL
|
||||
public static var userSessionDetailsApplicationUrl: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_application_url")
|
||||
}
|
||||
/// Version
|
||||
public static var userSessionDetailsApplicationVersion: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_application_version")
|
||||
}
|
||||
/// IP address
|
||||
public static var userSessionDetailsDeviceIpAddress: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_device_ip_address")
|
||||
}
|
||||
/// IP location
|
||||
public static var userSessionDetailsDeviceIpLocation: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_device_ip_location")
|
||||
}
|
||||
/// Model
|
||||
public static var userSessionDetailsDeviceModel: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_device_model")
|
||||
}
|
||||
/// Operating System
|
||||
public static var userSessionDetailsDeviceOs: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_device_os")
|
||||
}
|
||||
/// Device
|
||||
public static var userSessionDetailsDeviceSectionHeader: String {
|
||||
return VectorL10n.tr("Vector", "user_session_details_device_section_header")
|
||||
|
|
24
RiotSwiftUI/Modules/Common/Extensions/Collection.swift
Normal file
24
RiotSwiftUI/Modules/Common/Extensions/Collection.swift
Normal file
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Collection {
|
||||
/// Returns the element at the specified index if it is within bounds, otherwise nil.
|
||||
subscript(safe index: Index) -> Element? {
|
||||
indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
}
|
201
RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift
Normal file
201
RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift
Normal file
|
@ -0,0 +1,201 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct UserAgent {
|
||||
let deviceType: DeviceType
|
||||
let deviceModel: String?
|
||||
let deviceOS: String?
|
||||
let clientName: String?
|
||||
let clientVersion: String?
|
||||
|
||||
static let unknown = UserAgent(deviceType: .unknown,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
}
|
||||
|
||||
extension UserAgent: Equatable { }
|
||||
|
||||
enum UserAgentParser {
|
||||
private enum Constants {
|
||||
static let deviceInfoRegexPattern = "\\((?:[^)(]+|\\((?:[^)(]+|\\([^)(]*\\))*\\))*\\)"
|
||||
|
||||
static let androidKeyword = "; MatrixAndroidSdk2"
|
||||
static let iosKeyword = "; iOS "
|
||||
static let desktopKeyword = " Electron/"
|
||||
static let webKeyword = "Mozilla/"
|
||||
}
|
||||
|
||||
static func parse(_ userAgent: String) -> UserAgent {
|
||||
if userAgent.vc_caseInsensitiveContains(Constants.androidKeyword) {
|
||||
return parseAndroid(userAgent)
|
||||
} else if userAgent.vc_caseInsensitiveContains(Constants.iosKeyword) {
|
||||
return parseIOS(userAgent)
|
||||
} else if userAgent.vc_caseInsensitiveContains(Constants.desktopKeyword) {
|
||||
return parseDesktop(userAgent)
|
||||
} else if userAgent.vc_caseInsensitiveContains(Constants.webKeyword) {
|
||||
return parseWeb(userAgent)
|
||||
}
|
||||
return .unknown
|
||||
}
|
||||
|
||||
// Legacy: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)
|
||||
// New: Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0)
|
||||
private static func parseAndroid(_ userAgent: String) -> UserAgent {
|
||||
var deviceModel: String?
|
||||
var deviceOS: String?
|
||||
var clientName: String?
|
||||
var clientVersion: String?
|
||||
|
||||
let (beforeSlash, afterSlash) = userAgent.splitByFirst("/")
|
||||
clientName = beforeSlash
|
||||
if let afterSlash = afterSlash {
|
||||
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ")
|
||||
clientVersion = beforeSpace
|
||||
if let afterSpace = afterSpace {
|
||||
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
let isLegacy = deviceInfoComponents[safe: 0] == "Linux"
|
||||
if isLegacy {
|
||||
// find the segment starting with "Android"
|
||||
if let osSegmentIndex = deviceInfoComponents.firstIndex(where: { $0.hasPrefix("Android") }) {
|
||||
deviceOS = deviceInfoComponents[safe: osSegmentIndex]
|
||||
deviceModel = deviceInfoComponents[safe: osSegmentIndex + 1]
|
||||
}
|
||||
} else {
|
||||
deviceModel = deviceInfoComponents[safe: 0]
|
||||
deviceOS = deviceInfoComponents[safe: 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .mobile,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion)
|
||||
}
|
||||
|
||||
// Legacy: Riot/1.8.21 (iPhone; iOS 15.2; Scale/3.00)
|
||||
// New: Riot/1.8.21 (iPhone X; iOS 15.2; Scale/3.00)
|
||||
private static func parseIOS(_ userAgent: String) -> UserAgent {
|
||||
var deviceModel: String?
|
||||
var deviceOS: String?
|
||||
var clientName: String?
|
||||
var clientVersion: String?
|
||||
|
||||
let (beforeSlash, afterSlash) = userAgent.splitByFirst("/")
|
||||
clientName = beforeSlash
|
||||
if let afterSlash = afterSlash {
|
||||
let (beforeSpace, afterSpace) = afterSlash.splitByFirst(" ")
|
||||
clientVersion = beforeSpace
|
||||
if let afterSpace = afterSpace {
|
||||
if let deviceInfo = findFirstDeviceInfo(in: afterSpace) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
deviceModel = deviceInfoComponents[safe: 0]
|
||||
deviceOS = deviceInfoComponents[safe: 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .mobile,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion)
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36
|
||||
private static func parseDesktop(_ userAgent: String) -> UserAgent {
|
||||
var deviceOS: String?
|
||||
let browserName = browserName(for: userAgent)
|
||||
|
||||
if let deviceInfo = findFirstDeviceInfo(in: userAgent) {
|
||||
let deviceInfoComponents = deviceInfo.components(separatedBy: "; ")
|
||||
deviceOS = deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true ? deviceInfoComponents[safe: 1] : deviceInfoComponents.first
|
||||
}
|
||||
|
||||
return UserAgent(deviceType: .desktop,
|
||||
deviceModel: browserName,
|
||||
deviceOS: deviceOS,
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
}
|
||||
|
||||
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
|
||||
private static func parseWeb(_ userAgent: String) -> UserAgent {
|
||||
let desktopUserAgent = parseDesktop(userAgent)
|
||||
|
||||
return UserAgent(deviceType: .web,
|
||||
deviceModel: desktopUserAgent.deviceModel,
|
||||
deviceOS: desktopUserAgent.deviceOS,
|
||||
clientName: desktopUserAgent.clientName,
|
||||
clientVersion: desktopUserAgent.clientVersion)
|
||||
}
|
||||
|
||||
private static func findFirstDeviceInfo(in string: String) -> String? {
|
||||
guard let regex = try? NSRegularExpression(pattern: Constants.deviceInfoRegexPattern,
|
||||
options: .caseInsensitive) else {
|
||||
return nil
|
||||
}
|
||||
var range = regex.rangeOfFirstMatch(in: string, range: NSRange(string.startIndex..., in: string))
|
||||
if range.location != NSNotFound {
|
||||
range.location += 1
|
||||
range.length -= 2
|
||||
return string[range]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func browserName(for userAgent: String) -> String? {
|
||||
let components = userAgent.components(separatedBy: " ")
|
||||
if components.last?.hasPrefix("Firefox") == true {
|
||||
return "Firefox"
|
||||
} else if components.last?.hasPrefix("Safari") == true
|
||||
&& components[safe:components.count - 2]?.hasPrefix("Mobile") == true {
|
||||
// mobile browser
|
||||
let possibleBrowserName = components[safe:components.count - 3]?.components(separatedBy: "/").first
|
||||
return possibleBrowserName == "Version" ? "Safari" : possibleBrowserName
|
||||
} else if components.last?.hasPrefix("Safari") == true && components[safe:components.count - 2]?.hasPrefix("Version") == true {
|
||||
return "Safari"
|
||||
} else {
|
||||
// regular browser
|
||||
return components[safe:components.count - 2]?.components(separatedBy: "/").first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
subscript(_ range: NSRange) -> String {
|
||||
let start = index(startIndex, offsetBy: range.lowerBound)
|
||||
let end = index(startIndex, offsetBy: range.upperBound)
|
||||
let subString = self[start..<end]
|
||||
return String(subString)
|
||||
}
|
||||
|
||||
func splitByFirst(_ delimiter: Character) -> (String?, String?) {
|
||||
guard let delimiterIndex = firstIndex(of: delimiter) else {
|
||||
return (nil, nil)
|
||||
}
|
||||
let before = String(prefix(upTo: delimiterIndex))
|
||||
let after = String(suffix(from: index(after: delimiterIndex)))
|
||||
return (before, after)
|
||||
}
|
||||
}
|
|
@ -35,16 +35,41 @@ struct UserSessionInfo: Identifiable {
|
|||
|
||||
/// Last time the session was active
|
||||
let lastSeenTimestamp: TimeInterval?
|
||||
|
||||
|
||||
// MARK: - Application Properties
|
||||
|
||||
/// Application name used by the session
|
||||
let applicationName: String?
|
||||
|
||||
/// Application version used by the session
|
||||
let applicationVersion: String?
|
||||
|
||||
/// Application URL used by the session. Only applicable for web sessions.
|
||||
let applicationURL: String?
|
||||
|
||||
// MARK: - Device Properties
|
||||
|
||||
/// Device model
|
||||
let deviceModel: String?
|
||||
|
||||
/// Device OS
|
||||
let deviceOS: String?
|
||||
|
||||
/// Last seen IP location
|
||||
let lastSeenIPLocation: String?
|
||||
|
||||
/// Device name
|
||||
let deviceName: String?
|
||||
|
||||
/// True to indicate that session has been used under `inactiveSessionDurationTreshold` value
|
||||
let isActive: Bool
|
||||
|
||||
|
||||
/// True to indicate that this is current user session
|
||||
let isCurrent: Bool
|
||||
}
|
||||
|
||||
extension UserSessionInfo: Equatable {
|
||||
static func == (lhs: UserSessionInfo, rhs: UserSessionInfo) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,17 +134,23 @@ struct UserSessionCardViewPreview: View {
|
|||
@Environment(\.theme) var theme: ThemeSwiftUI
|
||||
|
||||
let viewData: UserSessionCardViewData
|
||||
|
||||
init(isCurrentSessionInfo: Bool = false) {
|
||||
|
||||
init(isCurrent: Bool = false) {
|
||||
let session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: isCurrentSessionInfo)
|
||||
|
||||
isCurrent: isCurrent)
|
||||
viewData = UserSessionCardViewData(session: session)
|
||||
}
|
||||
|
||||
|
@ -161,8 +167,8 @@ struct UserSessionCardViewPreview: View {
|
|||
struct UserSessionCardView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview(isCurrentSessionInfo: true).theme(.dark).preferredColorScheme(.dark)
|
||||
UserSessionCardViewPreview(isCurrent: true).theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview(isCurrent: true).theme(.dark).preferredColorScheme(.dark)
|
||||
UserSessionCardViewPreview().theme(.light).preferredColorScheme(.light)
|
||||
UserSessionCardViewPreview().theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
|
|
|
@ -41,21 +41,35 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
|
|||
let session: UserSessionInfo
|
||||
switch self {
|
||||
case .allSections:
|
||||
session = UserSessionInfo(id: "session",
|
||||
session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
case .sessionSectionOnly:
|
||||
session = UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
session = UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: nil,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Phone",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
}
|
||||
|
|
|
@ -20,17 +20,20 @@ import XCTest
|
|||
|
||||
class UserSessionDetailsViewModelTests: XCTestCase {
|
||||
func test_whenSessionNameAndLastSeenIPNil_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: nil,
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: nil,
|
||||
lastSeenIP: nil)
|
||||
|
||||
let sessionItems = [
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
|
||||
|
@ -38,18 +41,20 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_whenSessionNameNotNilLastSeenIPNil_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: "session name",
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: "session name",
|
||||
lastSeenIP: nil)
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
|
||||
let sessionItems = [
|
||||
sessionNameItem(sessionName: "session name"),
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
|
@ -58,56 +63,98 @@ class UserSessionDetailsViewModelTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_whenUserSessionInfoContainsAllValues_viewStateCorrect() {
|
||||
let userSessionInfo = createUserSessionInfo(sessionId: "session",
|
||||
sessionName: "session name",
|
||||
lastSeenIP: "0.0.0.0")
|
||||
let userSessionInfo = createUserSessionInfo(id: "session",
|
||||
name: "session name",
|
||||
lastSeenIP: "0.0.0.0",
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0")
|
||||
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
sessionItems.append(sessionNameItem(sessionName: "session name"))
|
||||
sessionItems.append(sessionIdItem(sessionId: userSessionInfo.id))
|
||||
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems))
|
||||
|
||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: "0.0.0.0"))
|
||||
sections.append(UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems))
|
||||
let sessionItems = [
|
||||
sessionNameItem(sessionName: "session name"),
|
||||
sessionIdItem(sessionId: "session")
|
||||
]
|
||||
let appItems = [
|
||||
appNameItem(appName: "Element iOS"),
|
||||
appVersionItem(appVersion: "1.0.0")
|
||||
]
|
||||
let deviceItems = [
|
||||
ipAddressItem(ipAddress: "0.0.0.0")
|
||||
]
|
||||
|
||||
let sections = [
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems),
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsApplicationSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: appItems),
|
||||
UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceItems)
|
||||
]
|
||||
|
||||
let expectedModel = UserSessionDetailsViewState(sections: sections)
|
||||
let sut = UserSessionDetailsViewModel(session: userSessionInfo)
|
||||
|
||||
XCTAssertEqual(sut.state, expectedModel)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func createUserSessionInfo(sessionId: String,
|
||||
sessionName: String?,
|
||||
private func createUserSessionInfo(id: String,
|
||||
name: String?,
|
||||
deviceType: DeviceType = .mobile,
|
||||
isVerified: Bool = false,
|
||||
lastSeenIP: String?,
|
||||
lastSeenTimestamp: TimeInterval = Date().timeIntervalSince1970,
|
||||
isCurrentSession: Bool = true) -> UserSessionInfo {
|
||||
UserSessionInfo(id: sessionId,
|
||||
name: sessionName,
|
||||
applicationName: String? = nil,
|
||||
applicationVersion: String? = nil,
|
||||
applicationURL: String? = nil,
|
||||
deviceModel: String? = nil,
|
||||
deviceOS: String? = nil,
|
||||
lastSeenIPLocation: String? = nil,
|
||||
deviceName: String? = nil,
|
||||
isActive: Bool = true,
|
||||
isCurrent: Bool = true) -> UserSessionInfo {
|
||||
UserSessionInfo(id: id,
|
||||
name: name,
|
||||
deviceType: deviceType,
|
||||
isVerified: isVerified,
|
||||
lastSeenIP: lastSeenIP,
|
||||
lastSeenTimestamp: lastSeenTimestamp,
|
||||
isActive: true,
|
||||
isCurrent: isCurrentSession)
|
||||
applicationName: applicationName,
|
||||
applicationVersion: applicationVersion,
|
||||
applicationURL: applicationURL,
|
||||
deviceModel: deviceModel,
|
||||
deviceOS: deviceOS,
|
||||
lastSeenIPLocation: lastSeenIPLocation,
|
||||
deviceName: deviceName,
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
|
||||
private func sessionNameItem(sessionName: String) -> UserSessionDetailsSectionItemViewData {
|
||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName)
|
||||
.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName)
|
||||
}
|
||||
|
||||
private func sessionIdItem(sessionId: String) -> UserSessionDetailsSectionItemViewData {
|
||||
UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: sessionId)
|
||||
.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: sessionId)
|
||||
}
|
||||
|
||||
private func appNameItem(appName: String) -> UserSessionDetailsSectionItemViewData {
|
||||
.init(title: VectorL10n.userSessionDetailsApplicationName,
|
||||
value: appName)
|
||||
}
|
||||
|
||||
private func appVersionItem(appVersion: String) -> UserSessionDetailsSectionItemViewData {
|
||||
.init(title: VectorL10n.userSessionDetailsApplicationVersion,
|
||||
value: appVersion)
|
||||
}
|
||||
|
||||
private func ipAddressItem(ipAddress: String) -> UserSessionDetailsSectionItemViewData {
|
||||
.init(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: ipAddress)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,12 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
|||
|
||||
private func updateViewState(session: UserSessionInfo) {
|
||||
var sections = [UserSessionDetailsSectionViewData]()
|
||||
|
||||
|
||||
sections.append(sessionSection(session: session))
|
||||
|
||||
if let applicationSection = applicationSection(session: session) {
|
||||
sections.append(applicationSection)
|
||||
}
|
||||
|
||||
if let deviceSection = deviceSection(session: session) {
|
||||
sections.append(deviceSection)
|
||||
|
@ -43,31 +47,68 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD
|
|||
}
|
||||
|
||||
private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData {
|
||||
var sessionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
|
||||
var sessionItems: [UserSessionDetailsSectionItemViewData] = []
|
||||
|
||||
if let sessionName = session.name {
|
||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName))
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsSessionName,
|
||||
value: sessionName))
|
||||
}
|
||||
|
||||
sessionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: session.id))
|
||||
sessionItems.append(.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle,
|
||||
value: session.id))
|
||||
|
||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
return .init(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(),
|
||||
footer: VectorL10n.userSessionDetailsSessionSectionFooter,
|
||||
items: sessionItems)
|
||||
}
|
||||
|
||||
private func applicationSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? {
|
||||
var sessionItems: [UserSessionDetailsSectionItemViewData] = []
|
||||
|
||||
if let name = session.applicationName {
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationName,
|
||||
value: name))
|
||||
}
|
||||
if let version = session.applicationVersion {
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationVersion,
|
||||
value: version))
|
||||
}
|
||||
if let url = session.applicationURL {
|
||||
sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationUrl,
|
||||
value: url))
|
||||
}
|
||||
|
||||
guard !sessionItems.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return .init(header: VectorL10n.userSessionDetailsApplicationSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: sessionItems)
|
||||
}
|
||||
|
||||
private func deviceSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? {
|
||||
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
|
||||
|
||||
if let model = session.deviceModel {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceModel,
|
||||
value: model))
|
||||
}
|
||||
if let deviceOS = session.deviceOS {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceOs,
|
||||
value: deviceOS))
|
||||
}
|
||||
if let lastSeenIP = session.lastSeenIP {
|
||||
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: lastSeenIP))
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpAddress,
|
||||
value: lastSeenIP))
|
||||
}
|
||||
if let lastSeenIPLocation = session.lastSeenIPLocation {
|
||||
deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpLocation,
|
||||
value: lastSeenIPLocation))
|
||||
}
|
||||
if deviceSectionItems.count > 0 {
|
||||
return UserSessionDetailsSectionViewData(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems)
|
||||
return .init(header: VectorL10n.userSessionDetailsDeviceSectionHeader.uppercased(),
|
||||
footer: nil,
|
||||
items: deviceSectionItems)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -38,30 +38,43 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
|||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: UserSessionOverviewViewModel
|
||||
let session: UserSessionInfo
|
||||
switch self {
|
||||
case .currentSession:
|
||||
let session = UserSessionInfo(id: "session",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
viewModel = UserSessionOverviewViewModel(session: session)
|
||||
session = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
case .otherSession:
|
||||
let session = UserSessionInfo(id: "session",
|
||||
name: "Mac",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
viewModel = UserSessionOverviewViewModel(session: session)
|
||||
session = UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "macOS 12.5.1",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Mac",
|
||||
isActive: false,
|
||||
isCurrent: false)
|
||||
}
|
||||
|
||||
|
||||
let viewModel = UserSessionOverviewViewModel(session: session)
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
return ([viewModel], AnyView(UserSessionOverview(viewModel: viewModel.context)))
|
||||
}
|
||||
|
|
|
@ -52,6 +52,13 @@ class UserSessionOverviewViewModelTests: XCTestCase {
|
|||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: "Element",
|
||||
applicationVersion: "1.9.7",
|
||||
applicationURL: nil,
|
||||
deviceModel: "iPhone XS",
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "Mobile",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ struct UserSessionOverview: View {
|
|||
UserSessionCardView(viewData: viewModel.viewState.cardViewData, onVerifyAction: { _ in
|
||||
viewModel.send(viewAction: .verifyCurrentSession)
|
||||
},
|
||||
onViewDetailsAction: { _ in
|
||||
onViewDetailsAction: { _ in
|
||||
viewModel.send(viewAction: .viewSessionDetails)
|
||||
})
|
||||
.padding(16)
|
||||
|
@ -39,8 +39,8 @@ struct UserSessionOverview: View {
|
|||
.background(theme.colors.system.ignoresSafeArea())
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(viewModel.viewState.isCurrentSession ?
|
||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||
VectorL10n.userSessionOverviewSessionTitle)
|
||||
VectorL10n.userSessionOverviewCurrentSessionTitle :
|
||||
VectorL10n.userSessionOverviewSessionTitle)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
|||
private func showCurrentSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
}
|
||||
|
||||
|
||||
private func showUserSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
}
|
||||
return sessionInfo(from: device, isCurrentSession: true)
|
||||
}
|
||||
|
||||
|
||||
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
||||
let allSessions = devices
|
||||
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
||||
|
@ -90,24 +90,25 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
|
||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||
|
||||
var lastSeenTs: TimeInterval?
|
||||
if device.lastSeenTs > 0 {
|
||||
lastSeenTs = TimeInterval(device.lastSeenTs / 1000)
|
||||
}
|
||||
|
||||
|
||||
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||
let appData = mxSession.accountData.accountData(forEventType: eventType)
|
||||
var userAgent: UserAgent?
|
||||
var isSessionActive = true
|
||||
if let lastSeenTimestamp = lastSeenTs {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp
|
||||
|
||||
if let lastSeenUserAgent = device.lastSeenUserAgent {
|
||||
userAgent = UserAgentParser.parse(lastSeenUserAgent)
|
||||
}
|
||||
|
||||
if device.lastSeenTs > 0 {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - TimeInterval(device.lastSeenTs / 1000)
|
||||
isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
|
||||
}
|
||||
|
||||
return UserSessionInfo(id: device.deviceId,
|
||||
name: device.displayName,
|
||||
deviceType: .unknown,
|
||||
isVerified: isSessionVerified,
|
||||
lastSeenIP: device.lastSeenIp,
|
||||
lastSeenTimestamp: lastSeenTs,
|
||||
|
||||
return UserSessionInfo(withDevice: device,
|
||||
applicationData: appData as? [String: String],
|
||||
userAgent: userAgent,
|
||||
isSessionVerified: isSessionVerified,
|
||||
isActive: isSessionActive,
|
||||
isCurrent: isCurrentSession)
|
||||
}
|
||||
|
@ -120,3 +121,28 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
return mxSession.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||
}
|
||||
}
|
||||
|
||||
extension UserSessionInfo {
|
||||
init(withDevice device: MXDevice,
|
||||
applicationData: [String: String]?,
|
||||
userAgent: UserAgent?,
|
||||
isSessionVerified: Bool,
|
||||
isActive: Bool,
|
||||
isCurrent: Bool) {
|
||||
self.init(id: device.deviceId,
|
||||
name: device.displayName,
|
||||
deviceType: userAgent?.deviceType ?? .unknown,
|
||||
isVerified: isSessionVerified,
|
||||
lastSeenIP: device.lastSeenIp,
|
||||
lastSeenTimestamp: device.lastSeenTs > 0 ? TimeInterval(device.lastSeenTs / 1000) : nil,
|
||||
applicationName: applicationData?["name"],
|
||||
applicationVersion: applicationData?["version"],
|
||||
applicationURL: applicationData?["url"],
|
||||
deviceModel: userAgent?.deviceModel,
|
||||
deviceOS: userAgent?.deviceOS,
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: userAgent?.clientName,
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import Foundation
|
||||
|
||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
var overviewData: UserSessionsOverviewData
|
||||
let overviewData: UserSessionsOverviewData
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
completion(.success(overviewData))
|
||||
|
@ -34,38 +34,66 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
|||
otherSessions: Self.allSessions.filter { !$0.isCurrent })
|
||||
}
|
||||
|
||||
static var allSessions: [UserSessionInfo] = {
|
||||
[UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
isActive: true,
|
||||
isCurrent: true),
|
||||
UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||
isActive: false,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "2",
|
||||
name: "Firefox on Windows",
|
||||
deviceType: .web,
|
||||
isVerified: true,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
isActive: true,
|
||||
isCurrent: false)]
|
||||
}()
|
||||
static let allSessions = [
|
||||
UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true),
|
||||
UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "macOS 12.5.1",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Mac",
|
||||
isActive: false,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "2",
|
||||
name: "Firefox on Windows",
|
||||
deviceType: .web,
|
||||
isVerified: true,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: "Element Web",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Windows 10",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Windows",
|
||||
isActive: true,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Phone",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
]
|
||||
}
|
||||
|
|
202
RiotTests/UserAgentParserTests.swift
Normal file
202
RiotTests/UserAgentParserTests.swift
Normal file
|
@ -0,0 +1,202 @@
|
|||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import Element
|
||||
|
||||
class UserAgentParserTests: XCTestCase {
|
||||
|
||||
func testAndroidUserAgents() throws {
|
||||
let uaStrings = [
|
||||
// New User Agent Implementation
|
||||
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
|
||||
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||
// Legacy User Agent Implementation
|
||||
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
|
||||
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)"
|
||||
]
|
||||
let userAgents = uaStrings.map { UserAgentParser.parse($0) }
|
||||
|
||||
let expected = [
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "Xiaomi Mi 9T",
|
||||
deviceOS: "Android 11",
|
||||
clientName: "Element dbg",
|
||||
clientVersion: "1.5.0-dev"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "Samsung SM-G960F",
|
||||
deviceOS: "Android 6.0.1",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.5.0"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "Google Nexus 5",
|
||||
deviceOS: "Android 7.0",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.5.0"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "SM-A510F Build/MMB29",
|
||||
deviceOS: "Android 6.0.1",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "SM-G610M Build/NRD90M",
|
||||
deviceOS: "Android 7.0",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0")
|
||||
]
|
||||
|
||||
XCTAssertEqual(userAgents, expected)
|
||||
}
|
||||
|
||||
func testIOSUserAgents() throws {
|
||||
let uaStrings = [
|
||||
// New User Agent Implementation
|
||||
"Element/1.9.8 (iPhone X; iOS 15.2; Scale/3.00)",
|
||||
"Element/1.9.9 (iPhone XS; iOS 15.5; Scale/3.00)",
|
||||
"Element/1.9.7 (iPad Pro (12.9-inch) (3rd generation); iOS 15.5; Scale/3.00)",
|
||||
// Legacy User Agent Implementation
|
||||
"Element/1.8.21 (iPhone; iOS 15.0; Scale/2.00)",
|
||||
"Element/1.8.19 (iPhone; iOS 15.2; Scale/3.00)",
|
||||
// Simulator User Agent
|
||||
"Element/1.9.7 (Simulator (iPhone 13 Pro Max); iOS 15.5; Scale/3.00)"
|
||||
]
|
||||
let userAgents = uaStrings.map { UserAgentParser.parse($0) }
|
||||
|
||||
let expected = [
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "iPhone X",
|
||||
deviceOS: "iOS 15.2",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.9.8"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "iPhone XS",
|
||||
deviceOS: "iOS 15.5",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.9.9"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "iPad Pro (12.9-inch) (3rd generation)",
|
||||
deviceOS: "iOS 15.5",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.9.7"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "iPhone",
|
||||
deviceOS: "iOS 15.0",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.8.21"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "iPhone",
|
||||
deviceOS: "iOS 15.2",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.8.19"),
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: "Simulator (iPhone 13 Pro Max)",
|
||||
deviceOS: "iOS 15.5",
|
||||
clientName: "Element",
|
||||
clientVersion: "1.9.7")
|
||||
]
|
||||
|
||||
XCTAssertEqual(userAgents, expected)
|
||||
}
|
||||
|
||||
func testDesktopUserAgents() {
|
||||
let uaStrings = [
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36"
|
||||
]
|
||||
let userAgents = uaStrings.map { UserAgentParser.parse($0) }
|
||||
|
||||
let expected = [
|
||||
UserAgent(deviceType: .desktop,
|
||||
deviceModel: "Electron",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .desktop,
|
||||
deviceModel: "Electron",
|
||||
deviceOS: "Windows NT 10.0",
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
]
|
||||
|
||||
XCTAssertEqual(userAgents, expected)
|
||||
}
|
||||
|
||||
func testWebUserAgents() throws {
|
||||
let uaStrings = [
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
|
||||
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36"
|
||||
]
|
||||
let userAgents = uaStrings.map { UserAgentParser.parse($0) }
|
||||
|
||||
let expected = [
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Chrome",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Chrome",
|
||||
deviceOS: "Windows NT 10.0",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Firefox",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Safari",
|
||||
deviceOS: "Macintosh",
|
||||
clientName: nil,
|
||||
clientVersion: nil),
|
||||
UserAgent(deviceType: .web,
|
||||
deviceModel: "Chrome",
|
||||
deviceOS: "Android 9",
|
||||
clientName: nil,
|
||||
clientVersion: nil)
|
||||
]
|
||||
|
||||
XCTAssertEqual(userAgents, expected)
|
||||
}
|
||||
|
||||
func testInvalidUserAgents() throws {
|
||||
let uaStrings = [
|
||||
"Element (iPhone X; OS 15.2; 3.00)",
|
||||
"Element/1.9.9; iOS",
|
||||
"Element/1.9.7 Android",
|
||||
"Element/1.9.9; iOS "
|
||||
]
|
||||
let userAgents = uaStrings.map { UserAgentParser.parse($0) }
|
||||
|
||||
let expected = [
|
||||
.unknown,
|
||||
.unknown,
|
||||
.unknown,
|
||||
UserAgent(deviceType: .mobile,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.9.9;")
|
||||
]
|
||||
|
||||
XCTAssertEqual(userAgents, expected)
|
||||
}
|
||||
|
||||
}
|
1
changelog.d/pr-6766.change
Normal file
1
changelog.d/pr-6766.change
Normal file
|
@ -0,0 +1 @@
|
|||
UserSessions: Extended device information (PSG-772).
|
Loading…
Reference in a new issue