mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Improve user session overview tests
* Add sessions overview UI tests * Expose static methods from the UserSession name and lastActivity formatters; cleaned up the UserSessionsOverview a bit * Add UserSessionsOverviewViewModel unit tests * Add UserSessionsOverviewService unit tests
This commit is contained in:
parent
f4da3e2448
commit
52c4ff65db
21 changed files with 650 additions and 143 deletions
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotTests
|
- RiotTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- Riot
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
Riot:
|
Riot:
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Enables to build last activity date string
|
/// Enables to build last activity date string
|
||||||
class UserSessionLastActivityFormatter {
|
enum UserSessionLastActivityFormatter {
|
||||||
private static var lastActivityDateFormatter: DateFormatter = {
|
private static var lastActivityDateFormatter: DateFormatter = {
|
||||||
let dateFormatter = DateFormatter()
|
let dateFormatter = DateFormatter()
|
||||||
dateFormatter.locale = Locale.current
|
dateFormatter.locale = Locale.current
|
||||||
|
@ -28,9 +28,9 @@ class UserSessionLastActivityFormatter {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
/// Session last activity string
|
/// Session last activity string
|
||||||
func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
static func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String {
|
||||||
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
let date = Date(timeIntervalSince1970: lastActivityTimestamp)
|
||||||
|
|
||||||
return UserSessionLastActivityFormatter.lastActivityDateFormatter.string(from: date)
|
return Self.lastActivityDateFormatter.string(from: date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Enables to build user session name
|
/// Enables to build user session name
|
||||||
class UserSessionNameFormatter {
|
enum UserSessionNameFormatter {
|
||||||
/// Session name with client name and session display name
|
/// Session name with client name and session display name
|
||||||
func sessionName(deviceType: DeviceType, sessionDisplayName: String?) -> String {
|
static func sessionName(deviceType: DeviceType, sessionDisplayName: String?) -> String {
|
||||||
let sessionName: String
|
let sessionName: String
|
||||||
|
|
||||||
let clientName = deviceType.name
|
let clientName = deviceType.name
|
||||||
|
|
|
@ -109,12 +109,14 @@ struct UserSessionCardView: View {
|
||||||
.buttonStyle(PrimaryActionButtonStyle())
|
.buttonStyle(PrimaryActionButtonStyle())
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
.padding(.bottom, 3)
|
.padding(.bottom, 3)
|
||||||
|
.accessibilityIdentifier("userSessionCardVerifyButton")
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewData.isCurrentSessionDisplayMode {
|
if viewData.isCurrentSessionDisplayMode {
|
||||||
Text(VectorL10n.userSessionViewDetails)
|
Text(VectorL10n.userSessionViewDetails)
|
||||||
.font(theme.fonts.body)
|
.font(theme.fonts.body)
|
||||||
.foregroundColor(theme.colors.accent)
|
.foregroundColor(theme.colors.accent)
|
||||||
|
.accessibilityIdentifier("userSessionCardViewDetails")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(24)
|
.padding(24)
|
||||||
|
|
|
@ -18,9 +18,6 @@ import Foundation
|
||||||
|
|
||||||
/// View data for UserSessionCardView
|
/// View data for UserSessionCardView
|
||||||
struct UserSessionCardViewData {
|
struct UserSessionCardViewData {
|
||||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
|
||||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
sessionId
|
sessionId
|
||||||
}
|
}
|
||||||
|
@ -48,13 +45,13 @@ struct UserSessionCardViewData {
|
||||||
lastSeenIP: String?,
|
lastSeenIP: String?,
|
||||||
isCurrentSessionDisplayMode: Bool = false) {
|
isCurrentSessionDisplayMode: Bool = false) {
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
sessionName = UserSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
||||||
self.isVerified = isVerified
|
self.isVerified = isVerified
|
||||||
|
|
||||||
var lastActivityDateString: String?
|
var lastActivityDateString: String?
|
||||||
|
|
||||||
if let lastActivityTimestamp = lastActivityTimestamp {
|
if let lastActivityTimestamp = lastActivityTimestamp {
|
||||||
lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityTimestamp)
|
lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.lastActivityDateString = lastActivityDateString
|
self.lastActivityDateString = lastActivityDateString
|
||||||
|
|
|
@ -37,7 +37,8 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||||
init(parameters: UserSessionsOverviewCoordinatorParameters) {
|
init(parameters: UserSessionsOverviewCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
service = UserSessionsOverviewService(mxSession: parameters.session)
|
let dataProvider = UserSessionsDataProvider(session: parameters.session)
|
||||||
|
service = UserSessionsOverviewService(dataProvider: dataProvider)
|
||||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||||
hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context))
|
hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context))
|
||||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController)
|
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController)
|
||||||
|
|
|
@ -20,31 +20,39 @@ import SwiftUI
|
||||||
/// Using an enum for the screen allows you define the different state cases with
|
/// Using an enum for the screen allows you define the different state cases with
|
||||||
/// the relevant associated data for each case.
|
/// the relevant associated data for each case.
|
||||||
enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable {
|
enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable {
|
||||||
case verifiedSession
|
case currentSessionUnverified
|
||||||
|
case currentSessionVerified
|
||||||
|
case onlyUnverifiedSessions
|
||||||
|
case onlyInactiveSessions
|
||||||
|
case noOtherSessions
|
||||||
|
|
||||||
/// The associated screen
|
/// The associated screen
|
||||||
var screenType: Any.Type {
|
var screenType: Any.Type {
|
||||||
UserSessionsOverview.self
|
UserSessionsOverview.self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of screen state definitions
|
|
||||||
static var allCases: [MockUserSessionsOverviewScreenState] {
|
|
||||||
// Each of the presence statuses
|
|
||||||
[.verifiedSession]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate the view struct for the screen state.
|
/// Generate the view struct for the screen state.
|
||||||
var screenView: ([Any], AnyView) {
|
var screenView: ([Any], AnyView) {
|
||||||
let service = MockUserSessionsOverviewService()
|
var service: UserSessionsOverviewServiceProtocol?
|
||||||
switch self {
|
switch self {
|
||||||
case .verifiedSession:
|
case .currentSessionUnverified:
|
||||||
break
|
service = MockUserSessionsOverviewService(mode: .currentSessionUnverified)
|
||||||
|
case .currentSessionVerified:
|
||||||
|
service = MockUserSessionsOverviewService(mode: .currentSessionVerified)
|
||||||
|
case .onlyUnverifiedSessions:
|
||||||
|
service = MockUserSessionsOverviewService(mode: .onlyUnverifiedSessions)
|
||||||
|
case .onlyInactiveSessions:
|
||||||
|
service = MockUserSessionsOverviewService(mode: .onlyInactiveSessions)
|
||||||
|
case .noOtherSessions:
|
||||||
|
service = MockUserSessionsOverviewService(mode: .noOtherSessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let service = service else {
|
||||||
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||||
|
|
||||||
// can simulate service and viewModel actions here if needs be.
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[service, viewModel],
|
[service, viewModel],
|
||||||
AnyView(UserSessionsOverview(viewModel: viewModel.context)
|
AnyView(UserSessionsOverview(viewModel: viewModel.context)
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
import MatrixSDK
|
||||||
|
|
||||||
|
class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
||||||
|
private let session: MXSession
|
||||||
|
|
||||||
|
init(session: MXSession) {
|
||||||
|
self.session = session
|
||||||
|
}
|
||||||
|
|
||||||
|
var myDeviceId: String {
|
||||||
|
session.myDeviceId
|
||||||
|
}
|
||||||
|
|
||||||
|
var myUserId: String? {
|
||||||
|
session.myUserId
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeAccounts: [MXKAccount] {
|
||||||
|
MXKAccountManager.shared().activeAccounts
|
||||||
|
}
|
||||||
|
|
||||||
|
func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void) {
|
||||||
|
session.matrixRestClient.devices(completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo {
|
||||||
|
session.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountData(for eventType: String) -> [AnyHashable : Any]? {
|
||||||
|
session.accountData.accountData(forEventType: eventType)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
import MatrixSDK
|
||||||
|
|
||||||
|
protocol UserSessionsDataProviderProtocol {
|
||||||
|
var myDeviceId: String { get }
|
||||||
|
|
||||||
|
var myUserId: String? { get }
|
||||||
|
|
||||||
|
var activeAccounts: [MXKAccount] { get }
|
||||||
|
|
||||||
|
func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void)
|
||||||
|
|
||||||
|
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo
|
||||||
|
|
||||||
|
func accountData(for eventType: String) -> [AnyHashable: Any]?
|
||||||
|
}
|
|
@ -21,12 +21,12 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
/// Delay after which session is considered inactive, 90 days
|
/// Delay after which session is considered inactive, 90 days
|
||||||
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
||||||
|
|
||||||
private let mxSession: MXSession
|
private let dataProvider: UserSessionsDataProviderProtocol
|
||||||
|
|
||||||
private(set) var overviewData: UserSessionsOverviewData
|
private(set) var overviewData: UserSessionsOverviewData
|
||||||
|
|
||||||
init(mxSession: MXSession) {
|
init(dataProvider: UserSessionsDataProviderProtocol) {
|
||||||
self.mxSession = mxSession
|
self.dataProvider = dataProvider
|
||||||
|
|
||||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||||
unverifiedSessions: [],
|
unverifiedSessions: [],
|
||||||
|
@ -39,7 +39,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||||
mxSession.matrixRestClient.devices { response in
|
dataProvider.devices { response in
|
||||||
switch response {
|
switch response {
|
||||||
case .success(let devices):
|
case .success(let devices):
|
||||||
self.overviewData = self.sessionsOverviewData(from: devices)
|
self.overviewData = self.sessionsOverviewData(from: devices)
|
||||||
|
@ -61,16 +61,18 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func setupInitialOverviewData() {
|
private func setupInitialOverviewData() {
|
||||||
let currentSessionInfo = currentSessionInfo()
|
guard let currentSessionInfo = getCurrentSessionInfo() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||||
unverifiedSessions: [],
|
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
|
||||||
inactiveSessions: [],
|
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||||
otherSessions: [])
|
otherSessions: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func currentSessionInfo() -> UserSessionInfo? {
|
private func getCurrentSessionInfo() -> UserSessionInfo? {
|
||||||
guard let mainAccount = MXKAccountManager.shared().activeAccounts.first,
|
guard let mainAccount = dataProvider.activeAccounts.first,
|
||||||
let device = mainAccount.device else {
|
let device = mainAccount.device else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -80,7 +82,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
||||||
let allSessions = devices
|
let allSessions = devices
|
||||||
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
||||||
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == mxSession.myDeviceId) }
|
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) }
|
||||||
|
|
||||||
return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||||
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
||||||
|
@ -92,7 +94,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||||
|
|
||||||
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||||
let appData = mxSession.accountData.accountData(forEventType: eventType)
|
let appData = dataProvider.accountData(for: eventType)
|
||||||
var userAgent: UserAgent?
|
var userAgent: UserAgent?
|
||||||
var isSessionActive = true
|
var isSessionActive = true
|
||||||
|
|
||||||
|
@ -114,11 +116,11 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deviceInfo(for deviceId: String) -> MXDeviceInfo? {
|
private func deviceInfo(for deviceId: String) -> MXDeviceInfo? {
|
||||||
guard let userId = mxSession.myUserId else {
|
guard let userId = dataProvider.myUserId else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return mxSession.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
return dataProvider.device(withDeviceId: deviceId, ofUser: userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,28 +17,70 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
let overviewData: UserSessionsOverviewData
|
enum Mode {
|
||||||
|
case currentSessionUnverified
|
||||||
|
case currentSessionVerified
|
||||||
|
case onlyUnverifiedSessions
|
||||||
|
case onlyInactiveSessions
|
||||||
|
case noOtherSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
private let mode: Mode
|
||||||
|
|
||||||
|
var overviewData: UserSessionsOverviewData
|
||||||
|
|
||||||
|
init(mode: Mode = .currentSessionUnverified) {
|
||||||
|
self.mode = mode
|
||||||
|
|
||||||
|
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||||
|
unverifiedSessions: [],
|
||||||
|
inactiveSessions: [],
|
||||||
|
otherSessions: [])
|
||||||
|
}
|
||||||
|
|
||||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||||
|
let unverifiedSessions = buildSessions(verified: false, active: true)
|
||||||
|
let inactiveSessions = buildSessions(verified: true, active: false)
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .noOtherSessions:
|
||||||
|
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||||
|
unverifiedSessions: [],
|
||||||
|
inactiveSessions: [],
|
||||||
|
otherSessions: [])
|
||||||
|
case .onlyUnverifiedSessions:
|
||||||
|
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||||
|
unverifiedSessions: unverifiedSessions + [currentSession],
|
||||||
|
inactiveSessions: [],
|
||||||
|
otherSessions: unverifiedSessions)
|
||||||
|
case .onlyInactiveSessions:
|
||||||
|
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||||
|
unverifiedSessions: [],
|
||||||
|
inactiveSessions: inactiveSessions,
|
||||||
|
otherSessions: inactiveSessions)
|
||||||
|
default:
|
||||||
|
let otherSessions = unverifiedSessions + inactiveSessions + buildSessions(verified: true, active: true)
|
||||||
|
|
||||||
|
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||||
|
unverifiedSessions: unverifiedSessions,
|
||||||
|
inactiveSessions: inactiveSessions,
|
||||||
|
otherSessions: otherSessions)
|
||||||
|
}
|
||||||
|
|
||||||
completion(.success(overviewData))
|
completion(.success(overviewData))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
||||||
nil
|
overviewData.otherSessions.first { $0.id == sessionId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
init() {
|
private var currentSession: UserSessionInfo {
|
||||||
overviewData = UserSessionsOverviewData(currentSession: Self.allSessions.filter(\.isCurrent).first,
|
|
||||||
unverifiedSessions: Self.allSessions.filter { !$0.isVerified },
|
|
||||||
inactiveSessions: Self.allSessions.filter { !$0.isActive },
|
|
||||||
otherSessions: Self.allSessions.filter { !$0.isCurrent })
|
|
||||||
}
|
|
||||||
|
|
||||||
static let allSessions = [
|
|
||||||
UserSessionInfo(id: "alice",
|
UserSessionInfo(id: "alice",
|
||||||
name: "iOS",
|
name: "iOS",
|
||||||
deviceType: .mobile,
|
deviceType: .mobile,
|
||||||
isVerified: false,
|
isVerified: mode == .currentSessionVerified,
|
||||||
lastSeenIP: "10.0.0.10",
|
lastSeenIP: "10.0.0.10",
|
||||||
lastSeenTimestamp: nil,
|
lastSeenTimestamp: nil,
|
||||||
applicationName: "Element iOS",
|
applicationName: "Element iOS",
|
||||||
|
@ -49,51 +91,54 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||||
lastSeenIPLocation: nil,
|
lastSeenIPLocation: nil,
|
||||||
deviceName: "My iPhone",
|
deviceName: "My iPhone",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isCurrent: true),
|
isCurrent: true)
|
||||||
UserSessionInfo(id: "1",
|
}
|
||||||
name: "macOS",
|
|
||||||
deviceType: .desktop,
|
private func buildSessions(verified: Bool, active: Bool) -> [UserSessionInfo] {
|
||||||
isVerified: true,
|
[UserSessionInfo(id: "1 verified: \(verified) active: \(active)",
|
||||||
lastSeenIP: "1.0.0.1",
|
name: "macOS verified: \(verified) active: \(active)",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
deviceType: .desktop,
|
||||||
applicationName: "Element MacOS",
|
isVerified: verified,
|
||||||
applicationVersion: "1.0.0",
|
lastSeenIP: "1.0.0.1",
|
||||||
applicationURL: nil,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
|
||||||
deviceModel: nil,
|
applicationName: "Element MacOS",
|
||||||
deviceOS: "macOS 12.5.1",
|
applicationVersion: "1.0.0",
|
||||||
lastSeenIPLocation: nil,
|
applicationURL: nil,
|
||||||
deviceName: "My Mac",
|
deviceModel: nil,
|
||||||
isActive: false,
|
deviceOS: "macOS 12.5.1",
|
||||||
isCurrent: false),
|
lastSeenIPLocation: nil,
|
||||||
UserSessionInfo(id: "2",
|
deviceName: "My Mac",
|
||||||
name: "Firefox on Windows",
|
isActive: active,
|
||||||
deviceType: .web,
|
isCurrent: false),
|
||||||
isVerified: true,
|
UserSessionInfo(id: "2 verified: \(verified) active: \(active)",
|
||||||
lastSeenIP: "2.0.0.2",
|
name: "Firefox on Windows verified: \(verified) active: \(active)",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
deviceType: .web,
|
||||||
applicationName: "Element Web",
|
isVerified: verified,
|
||||||
applicationVersion: "1.0.0",
|
lastSeenIP: "2.0.0.2",
|
||||||
applicationURL: nil,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||||
deviceModel: nil,
|
applicationName: "Element Web",
|
||||||
deviceOS: "Windows 10",
|
applicationVersion: "1.0.0",
|
||||||
lastSeenIPLocation: nil,
|
applicationURL: nil,
|
||||||
deviceName: "My Windows",
|
deviceModel: nil,
|
||||||
isActive: true,
|
deviceOS: "Windows 10",
|
||||||
isCurrent: false),
|
lastSeenIPLocation: nil,
|
||||||
UserSessionInfo(id: "3",
|
deviceName: "My Windows",
|
||||||
name: "Android",
|
isActive: active,
|
||||||
deviceType: .mobile,
|
isCurrent: false),
|
||||||
isVerified: false,
|
UserSessionInfo(id: "3 verified: \(verified) active: \(active)",
|
||||||
lastSeenIP: "3.0.0.3",
|
name: "Android verified: \(verified) active: \(active)",
|
||||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
deviceType: .mobile,
|
||||||
applicationName: "Element Android",
|
isVerified: verified,
|
||||||
applicationVersion: "1.0.0",
|
lastSeenIP: "3.0.0.3",
|
||||||
applicationURL: nil,
|
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||||
deviceModel: nil,
|
applicationName: "Element Android",
|
||||||
deviceOS: "Android 4.0",
|
applicationVersion: "1.0.0",
|
||||||
lastSeenIPLocation: nil,
|
applicationURL: nil,
|
||||||
deviceName: "My Phone",
|
deviceModel: nil,
|
||||||
isActive: true,
|
deviceOS: "Android 4.0",
|
||||||
isCurrent: false)
|
lastSeenIPLocation: nil,
|
||||||
]
|
deviceName: "My Phone",
|
||||||
|
isActive: active,
|
||||||
|
isCurrent: false)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,38 @@ import RiotSwiftUI
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class UserSessionsOverviewUITests: MockScreenTestCase {
|
class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||||
// TODO:
|
func testCurrentSessionUnverified() {
|
||||||
|
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionUnverified.title)
|
||||||
|
|
||||||
|
XCTAssertTrue(app.buttons["userSessionCardVerifyButton"].exists)
|
||||||
|
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCurrentSessionVerified() {
|
||||||
|
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionVerified.title)
|
||||||
|
|
||||||
|
XCTAssertFalse(app.buttons["userSessionCardVerifyButton"].exists)
|
||||||
|
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOnlyUnverifiedSessions() {
|
||||||
|
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyUnverifiedSessions.title)
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||||
|
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOnlyInactiveSessions() {
|
||||||
|
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyInactiveSessions.title)
|
||||||
|
|
||||||
|
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||||
|
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNoOtherSessions() {
|
||||||
|
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.noOtherSessions.title)
|
||||||
|
|
||||||
|
XCTAssertFalse(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||||
|
XCTAssertFalse(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,71 @@ import XCTest
|
||||||
@testable import RiotSwiftUI
|
@testable import RiotSwiftUI
|
||||||
|
|
||||||
class UserSessionsOverviewViewModelTests: XCTestCase {
|
class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||||
var service: MockUserSessionsOverviewService!
|
func testInitialStateEmpty() {
|
||||||
var viewModel: UserSessionsOverviewViewModelProtocol!
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||||
var context: UserSessionsOverviewViewModelType.Context!
|
|
||||||
|
XCTAssertNil(viewModel.state.currentSessionViewData)
|
||||||
|
XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||||
|
XCTAssertTrue(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||||
|
XCTAssertTrue(viewModel.state.otherSessionsViewData.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
func testLoadOnDidAppear() {
|
||||||
service = MockUserSessionsOverviewService()
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
viewModel.process(viewAction: .viewAppeared)
|
||||||
context = viewModel.context
|
|
||||||
|
XCTAssertNotNil(viewModel.state.currentSessionViewData)
|
||||||
|
XCTAssertFalse(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||||
|
XCTAssertFalse(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||||
|
XCTAssertFalse(viewModel.state.otherSessionsViewData.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSimpleActionProcessing() {
|
||||||
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||||
|
|
||||||
|
var result: UserSessionsOverviewViewModelResult?
|
||||||
|
viewModel.completion = { action in
|
||||||
|
result = action
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.process(viewAction: .verifyCurrentSession)
|
||||||
|
XCTAssertEqual(result, .verifyCurrentSession)
|
||||||
|
|
||||||
|
viewModel.process(viewAction: .viewAllUnverifiedSessions)
|
||||||
|
XCTAssertEqual(result, .showAllUnverifiedSessions)
|
||||||
|
|
||||||
|
viewModel.process(viewAction: .viewAllInactiveSessions)
|
||||||
|
XCTAssertEqual(result, .showAllInactiveSessions)
|
||||||
|
|
||||||
|
viewModel.process(viewAction: .viewAllOtherSessions)
|
||||||
|
XCTAssertEqual(result, .showAllOtherSessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShowSessionDetails() {
|
||||||
|
let service = MockUserSessionsOverviewService()
|
||||||
|
service.updateOverviewData { _ in }
|
||||||
|
|
||||||
|
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||||
|
|
||||||
|
var result: UserSessionsOverviewViewModelResult?
|
||||||
|
viewModel.completion = { action in
|
||||||
|
result = action
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let currentSession = service.overviewData.currentSession else {
|
||||||
|
XCTFail("The current session should be valid at this point")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.process(viewAction: .viewCurrentSessionDetails)
|
||||||
|
XCTAssertEqual(result, .showCurrentSessionOverview(session: currentSession))
|
||||||
|
|
||||||
|
guard let randomSession = service.overviewData.otherSessions.randomElement() else {
|
||||||
|
XCTFail("There should be other sessions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.process(viewAction: .tapUserSession(randomSession.id))
|
||||||
|
XCTAssertEqual(result, .showUserSessionOverview(session: randomSession))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ enum UserSessionsOverviewCoordinatorResult {
|
||||||
|
|
||||||
// MARK: View model
|
// MARK: View model
|
||||||
|
|
||||||
enum UserSessionsOverviewViewModelResult {
|
enum UserSessionsOverviewViewModelResult: Equatable {
|
||||||
case showAllUnverifiedSessions
|
case showAllUnverifiedSessions
|
||||||
case showAllInactiveSessions
|
case showAllInactiveSessions
|
||||||
case verifyCurrentSession
|
case verifyCurrentSession
|
||||||
|
|
|
@ -18,9 +18,6 @@ import Foundation
|
||||||
|
|
||||||
/// View data for UserSessionListItem
|
/// View data for UserSessionListItem
|
||||||
struct UserSessionListItemViewData: Identifiable {
|
struct UserSessionListItemViewData: Identifiable {
|
||||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
|
||||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
sessionId
|
sessionId
|
||||||
}
|
}
|
||||||
|
@ -39,7 +36,7 @@ struct UserSessionListItemViewData: Identifiable {
|
||||||
isVerified: Bool,
|
isVerified: Bool,
|
||||||
lastActivityDate: TimeInterval?) {
|
lastActivityDate: TimeInterval?) {
|
||||||
self.sessionId = sessionId
|
self.sessionId = sessionId
|
||||||
sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
sessionName = UserSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName)
|
||||||
sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate)
|
sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate)
|
||||||
deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified)
|
deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified)
|
||||||
}
|
}
|
||||||
|
@ -54,7 +51,7 @@ struct UserSessionListItemViewData: Identifiable {
|
||||||
var lastActivityDateString: String?
|
var lastActivityDateString: String?
|
||||||
|
|
||||||
if let lastActivityDate = lastActivityDate {
|
if let lastActivityDate = lastActivityDate {
|
||||||
lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate)
|
lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false {
|
if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false {
|
||||||
|
|
|
@ -23,11 +23,13 @@ struct UserSessionsOverview: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
securityRecommendationsSection
|
if hasSecurityRecommendations {
|
||||||
|
securityRecommendationsSection
|
||||||
|
}
|
||||||
|
|
||||||
currentSessionsSection
|
currentSessionsSection
|
||||||
|
|
||||||
if viewModel.viewState.otherSessionsViewData.isEmpty == false {
|
if !viewModel.viewState.otherSessionsViewData.isEmpty {
|
||||||
otherSessionsSection
|
otherSessionsSection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,41 +42,39 @@ struct UserSessionsOverview: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var securityRecommendationsSection: some View {
|
private var securityRecommendationsSection: some View {
|
||||||
if hasSecurityRecommendations {
|
SwiftUI.Section {
|
||||||
SwiftUI.Section {
|
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
||||||
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
SecurityRecommendationCard(style: .unverified,
|
||||||
SecurityRecommendationCard(style: .unverified,
|
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
||||||
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
viewModel.send(viewAction: .viewAllUnverifiedSessions)
|
||||||
viewModel.send(viewAction: .viewAllUnverifiedSessions)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viewModel.viewState.inactiveSessionsViewData.isEmpty {
|
|
||||||
SecurityRecommendationCard(style: .inactive,
|
|
||||||
sessionCount: viewModel.viewState.inactiveSessionsViewData.count) {
|
|
||||||
viewModel.send(viewAction: .viewAllInactiveSessions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionTitle)
|
|
||||||
.textCase(.uppercase)
|
|
||||||
.font(theme.fonts.footnote)
|
|
||||||
.foregroundColor(theme.colors.secondaryContent)
|
|
||||||
.padding(.bottom, 8.0)
|
|
||||||
|
|
||||||
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionInfo)
|
|
||||||
.font(theme.fonts.footnote)
|
|
||||||
.foregroundColor(theme.colors.secondaryContent)
|
|
||||||
.padding(.bottom, 12.0)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.top, 24)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
|
||||||
|
if !viewModel.viewState.inactiveSessionsViewData.isEmpty {
|
||||||
|
SecurityRecommendationCard(style: .inactive,
|
||||||
|
sessionCount: viewModel.viewState.inactiveSessionsViewData.count) {
|
||||||
|
viewModel.send(viewAction: .viewAllInactiveSessions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionTitle)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.font(theme.fonts.footnote)
|
||||||
|
.foregroundColor(theme.colors.secondaryContent)
|
||||||
|
.padding(.bottom, 8.0)
|
||||||
|
|
||||||
|
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionInfo)
|
||||||
|
.font(theme.fonts.footnote)
|
||||||
|
.foregroundColor(theme.colors.secondaryContent)
|
||||||
|
.padding(.bottom, 12.0)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.top, 24)
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.accessibilityIdentifier("userSessionsOverviewSecurityRecommendationsSection")
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasSecurityRecommendations: Bool {
|
var hasSecurityRecommendations: Bool {
|
||||||
|
@ -102,10 +102,9 @@ struct UserSessionsOverview: View {
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var otherSessionsSection: some View {
|
private var otherSessionsSection: some View {
|
||||||
SwiftUI.Section {
|
SwiftUI.Section {
|
||||||
// Device list
|
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(viewModel.viewState.otherSessionsViewData) { viewData in
|
ForEach(viewModel.viewState.otherSessionsViewData) { viewData in
|
||||||
UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
|
UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
|
||||||
|
@ -131,6 +130,7 @@ struct UserSessionsOverview: View {
|
||||||
.padding(.horizontal, 16.0)
|
.padding(.horizontal, 16.0)
|
||||||
.padding(.top, 24.0)
|
.padding(.top, 24.0)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier("userSessionsOverviewOtherSection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotSwiftUnitTests
|
- RiotSwiftUnitTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- RiotSwiftUI
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotSwiftUI:
|
RiotSwiftUI:
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotSwiftUITests
|
- RiotSwiftUITests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- RiotSwiftUI
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotSwiftUITests:
|
RiotSwiftUITests:
|
||||||
|
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotSwiftUnitTests
|
- RiotSwiftUnitTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- RiotSwiftUI
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotSwiftUnitTests:
|
RiotSwiftUnitTests:
|
||||||
|
|
267
RiotTests/UserSessionsOverviewServiceTests.swift
Normal file
267
RiotTests/UserSessionsOverviewServiceTests.swift
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
@testable import MatrixSDK
|
||||||
|
|
||||||
|
private let currentDeviceId = "deviceId"
|
||||||
|
private let unverifiedDeviceId = "unverifiedDeviceId"
|
||||||
|
private let currentUserId = "userId"
|
||||||
|
|
||||||
|
class UserSessionsOverviewServiceTests: XCTestCase {
|
||||||
|
func testInitialSessionUnverified() {
|
||||||
|
let dataProvider = MockUserSessionsDataProvider(mode: .currentSessionUnverified)
|
||||||
|
let service = UserSessionsOverviewService(dataProvider: dataProvider)
|
||||||
|
|
||||||
|
XCTAssertNotNil(service.overviewData.currentSession)
|
||||||
|
XCTAssertFalse(service.overviewData.currentSession?.isVerified ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false)
|
||||||
|
XCTAssertFalse(service.overviewData.unverifiedSessions.isEmpty)
|
||||||
|
XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty)
|
||||||
|
|
||||||
|
XCTAssertEqual(service.sessionForIdentifier(currentDeviceId), service.overviewData.currentSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitialSessionVerified() {
|
||||||
|
let dataProvider = MockUserSessionsDataProvider(mode: .currentSessionVerified)
|
||||||
|
let service = UserSessionsOverviewService(dataProvider: dataProvider)
|
||||||
|
|
||||||
|
XCTAssertNotNil(service.overviewData.currentSession)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.unverifiedSessions.isEmpty)
|
||||||
|
XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithAllSessionsVerified() {
|
||||||
|
let service = setupServiceWithMode(.allOtherSessionsValid)
|
||||||
|
|
||||||
|
XCTAssertNotNil(service.overviewData.currentSession)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false)
|
||||||
|
|
||||||
|
XCTAssertTrue(service.overviewData.unverifiedSessions.isEmpty)
|
||||||
|
XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty)
|
||||||
|
XCTAssertFalse(service.overviewData.otherSessions.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithSomeUnverifiedSessions() {
|
||||||
|
let service = setupServiceWithMode(.someUnverifiedSessions)
|
||||||
|
|
||||||
|
XCTAssertNotNil(service.overviewData.currentSession)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false)
|
||||||
|
|
||||||
|
XCTAssertFalse(service.overviewData.unverifiedSessions.isEmpty)
|
||||||
|
XCTAssertTrue(service.overviewData.inactiveSessions.isEmpty)
|
||||||
|
XCTAssertFalse(service.overviewData.otherSessions.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithSomeInactiveSessions() {
|
||||||
|
let service = setupServiceWithMode(.someInactiveSessions)
|
||||||
|
|
||||||
|
XCTAssertNotNil(service.overviewData.currentSession)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false)
|
||||||
|
|
||||||
|
XCTAssertTrue(service.overviewData.unverifiedSessions.isEmpty)
|
||||||
|
XCTAssertFalse(service.overviewData.inactiveSessions.isEmpty)
|
||||||
|
XCTAssertFalse(service.overviewData.otherSessions.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWithSomeUnverifiedAndInactiveSessions() {
|
||||||
|
let service = setupServiceWithMode(.someUnverifiedAndInactiveSessions)
|
||||||
|
|
||||||
|
XCTAssertNotNil(service.overviewData.currentSession)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isVerified ?? false)
|
||||||
|
XCTAssertTrue(service.overviewData.currentSession?.isActive ?? false)
|
||||||
|
|
||||||
|
XCTAssertFalse(service.overviewData.unverifiedSessions.isEmpty)
|
||||||
|
XCTAssertFalse(service.overviewData.inactiveSessions.isEmpty)
|
||||||
|
XCTAssertFalse(service.overviewData.otherSessions.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func setupServiceWithMode(_ mode: MockUserSessionsDataProvider.Mode) -> UserSessionsOverviewServiceProtocol {
|
||||||
|
let dataProvider = MockUserSessionsDataProvider(mode: mode)
|
||||||
|
let service = UserSessionsOverviewService(dataProvider: dataProvider)
|
||||||
|
|
||||||
|
let expectation = expectation(description: "Wait for service update")
|
||||||
|
service.updateOverviewData { _ in
|
||||||
|
expectation.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 1.0)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockUserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
||||||
|
enum Mode {
|
||||||
|
case currentSessionUnverified
|
||||||
|
case currentSessionVerified
|
||||||
|
case allOtherSessionsValid
|
||||||
|
case someUnverifiedSessions
|
||||||
|
case someInactiveSessions
|
||||||
|
case someUnverifiedAndInactiveSessions
|
||||||
|
}
|
||||||
|
|
||||||
|
private let mode: Mode
|
||||||
|
|
||||||
|
var myDeviceId = currentDeviceId
|
||||||
|
|
||||||
|
var myUserId: String? = currentUserId
|
||||||
|
|
||||||
|
var activeAccounts: [MXKAccount] {
|
||||||
|
[MockAccount()]
|
||||||
|
}
|
||||||
|
|
||||||
|
init(mode: Mode) {
|
||||||
|
self.mode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch self.mode {
|
||||||
|
case .currentSessionUnverified:
|
||||||
|
return
|
||||||
|
case .currentSessionVerified:
|
||||||
|
return
|
||||||
|
case .allOtherSessionsValid:
|
||||||
|
completion(.success(self.verifiedSessions))
|
||||||
|
case .someUnverifiedSessions:
|
||||||
|
completion(.success(self.verifiedSessions + self.unverifiedSessions))
|
||||||
|
case .someInactiveSessions:
|
||||||
|
completion(.success(self.verifiedSessions + self.inactiveSessions))
|
||||||
|
case .someUnverifiedAndInactiveSessions:
|
||||||
|
completion(.success(self.verifiedSessions + self.unverifiedSessions + self.inactiveSessions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo {
|
||||||
|
guard deviceId == currentDeviceId else {
|
||||||
|
return MockDeviceInfo(verified: deviceId != unverifiedDeviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case .currentSessionUnverified:
|
||||||
|
return MockDeviceInfo(verified: false)
|
||||||
|
default:
|
||||||
|
return MockDeviceInfo(verified: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountData(for eventType: String) -> [AnyHashable : Any]? {
|
||||||
|
[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
var verifiedSessions: [MXDevice] {
|
||||||
|
[MockDevice(identifier: currentDeviceId, sessionActive: true),
|
||||||
|
MockDevice(identifier: UUID().uuidString, sessionActive: true)]
|
||||||
|
}
|
||||||
|
|
||||||
|
var unverifiedSessions: [MXDevice] {
|
||||||
|
[MockDevice(identifier: unverifiedDeviceId, sessionActive: true)]
|
||||||
|
}
|
||||||
|
|
||||||
|
var inactiveSessions: [MXDevice] {
|
||||||
|
[MockDevice(identifier: UUID().uuidString, sessionActive: false)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockAccount: MXKAccount {
|
||||||
|
override var device: MXDevice? {
|
||||||
|
MockDevice(identifier: currentDeviceId, sessionActive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockDevice: MXDevice {
|
||||||
|
private let identifier: String
|
||||||
|
private let sessionActive: Bool
|
||||||
|
|
||||||
|
init(identifier: String, sessionActive: Bool) {
|
||||||
|
self.identifier = identifier
|
||||||
|
self.sessionActive = sessionActive
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var deviceId: String {
|
||||||
|
get {
|
||||||
|
identifier
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var lastSeenTs: UInt64 {
|
||||||
|
get {
|
||||||
|
if sessionActive {
|
||||||
|
return UInt64(Date().timeIntervalSince1970 * 1000)
|
||||||
|
} else {
|
||||||
|
let ninetyDays: Double = 90 * 86400
|
||||||
|
return UInt64((Date().timeIntervalSince1970 - ninetyDays) * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockDeviceInfo: MXDeviceInfo {
|
||||||
|
private let verified: Bool
|
||||||
|
|
||||||
|
init(verified: Bool) {
|
||||||
|
self.verified = verified
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var trustLevel: MXDeviceTrustLevel! {
|
||||||
|
MockDeviceTrustLevel(verified: verified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MockDeviceTrustLevel: MXDeviceTrustLevel {
|
||||||
|
private let verified: Bool
|
||||||
|
|
||||||
|
init(verified: Bool) {
|
||||||
|
self.verified = verified
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override var isVerified: Bool {
|
||||||
|
verified
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,9 @@ schemes:
|
||||||
disableMainThreadChecker: true
|
disableMainThreadChecker: true
|
||||||
targets:
|
targets:
|
||||||
- RiotTests
|
- RiotTests
|
||||||
|
gatherCoverageData: true
|
||||||
|
coverageTargets:
|
||||||
|
- Riot
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
RiotTests:
|
RiotTests:
|
||||||
|
|
Loading…
Reference in a new issue