-
Notifications
You must be signed in to change notification settings - Fork 82
[PM-19305] Enforce session timeout policy #2127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e65850f
f8268e6
b69af43
1937e2d
9a80efe
b81b871
b2acdf2
e404cb3
8bb7d69
b663543
69a91ff
03bacf1
c2ebc53
1077368
1fde466
66c918f
76c4f85
ade0228
3823fc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /// An object that represents a session timeout policy | ||
| /// | ||
| public struct SessionTimeoutPolicy { | ||
| // MARK: Properties | ||
|
|
||
| /// The action to perform on session timeout. | ||
| public let timeoutAction: SessionTimeoutAction? | ||
|
|
||
| /// An enumeration of session timeout types to choose from. | ||
| public let timeoutType: SessionTimeoutType? | ||
|
|
||
| /// An enumeration of session timeout values to choose from. | ||
| public let timeoutValue: SessionTimeoutValue? | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| /// Initialize `SessionTimeoutPolicy` with the specified values. | ||
| /// | ||
| /// - Parameters: | ||
| /// - timeoutAction: The action to perform on session timeout. | ||
| /// - timeoutType: The type of session timeout. | ||
| /// - timeoutValue: The session timeout value. | ||
| public init( | ||
| timeoutAction: SessionTimeoutAction?, | ||
| timeoutType: SessionTimeoutType?, | ||
| timeoutValue: SessionTimeoutValue?, | ||
| ) { | ||
| self.timeoutAction = timeoutAction | ||
| self.timeoutType = timeoutType | ||
| self.timeoutValue = timeoutValue | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import BitwardenResources | ||
|
|
||
| /// The action to perform on session timeout. | ||
| /// | ||
| public enum SessionTimeoutAction: Int, CaseIterable, Codable, Equatable, Menuable, Sendable { | ||
| /// Lock the vault. | ||
| case lock = 0 | ||
|
|
||
| /// Log the user out. | ||
| case logout = 1 | ||
|
|
||
| /// All of the cases to show in the menu. | ||
| public static let allCases: [SessionTimeoutAction] = [.lock, .logout] | ||
|
|
||
| public var localizedName: String { | ||
| switch self { | ||
| case .lock: | ||
| Localizations.lock | ||
| case .logout: | ||
| Localizations.logOut | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // MARK: - SessionTimeoutType | ||
|
|
||
| /// An enumeration of session timeout types to choose from. | ||
| /// | ||
| public enum SessionTimeoutType: Codable, Equatable, Hashable, Sendable { | ||
| /// Time out immediately. | ||
| case immediately | ||
|
|
||
| /// Time out on app restart. | ||
| case onAppRestart | ||
|
|
||
| /// Never time out the session. | ||
| case never | ||
|
|
||
| /// A custom timeout value. | ||
| case custom | ||
|
|
||
| // MARK: Properties | ||
|
|
||
| /// The string representation of a session timeout type. | ||
| public var rawValue: String { | ||
| switch self { | ||
| case .immediately: | ||
| "immediately" | ||
| case .onAppRestart: | ||
| "onAppRestart" | ||
| case .never: | ||
| "never" | ||
| case .custom: | ||
| "custom" | ||
| } | ||
| } | ||
|
|
||
| /// A safe string representation of the timeout type. | ||
| public var timeoutType: String { | ||
| switch self { | ||
| case .immediately: | ||
| "immediately" | ||
| case .onAppRestart: | ||
| "on app restart" | ||
| case .never: | ||
| "never" | ||
| case .custom: | ||
| "custom" | ||
| } | ||
| } | ||
|
|
||
| // MARK: Initialization | ||
|
|
||
| /// Initialize a `SessionTimeoutType` using a string of the raw value. | ||
| /// | ||
| /// - Parameter rawValue: The string representation of the type raw value. | ||
| /// | ||
| public init(rawValue: String?) { | ||
| switch rawValue { | ||
| case "custom": | ||
| self = .custom | ||
| case "immediately": | ||
| self = .immediately | ||
| case "never": | ||
| self = .never | ||
| case "onAppRestart", | ||
| "onSystemLock": | ||
| self = .onAppRestart | ||
| default: | ||
| self = .custom | ||
| } | ||
| } | ||
|
|
||
| /// Initialize a `SessionTimeoutType` using a SessionTimeoutValue that belongs to that type. | ||
| /// | ||
| /// - Parameter value: The SessionTimeoutValue that belongs to the type. | ||
| /// | ||
| public init(value: SessionTimeoutValue) { | ||
| switch value { | ||
| case .custom: | ||
| self = .custom | ||
| case .immediately: | ||
| self = .immediately | ||
| case .never: | ||
| self = .never | ||
| case .onAppRestart: | ||
| self = .onAppRestart | ||
| case .fifteenMinutes, | ||
| .fiveMinutes, | ||
| .fourHours, | ||
| .oneHour, | ||
| .oneMinute, | ||
| .thirtyMinutes: | ||
| self = .custom | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import BitwardenKit | ||
| import XCTest | ||
|
|
||
| final class SessionTimeoutTypeTests: BitwardenTestCase { | ||
| // MARK: Tests | ||
|
|
||
| /// `init(rawValue:)` returns the correct case for the given raw value string. | ||
| func test_initFromRawValue() { | ||
| XCTAssertEqual(SessionTimeoutType.immediately, SessionTimeoutType(rawValue: "immediately")) | ||
| XCTAssertEqual(SessionTimeoutType.onAppRestart, SessionTimeoutType(rawValue: "onAppRestart")) | ||
| // `onSystemLock` value maps to `onAppRestart` on mobile. | ||
| XCTAssertEqual(SessionTimeoutType.onAppRestart, SessionTimeoutType(rawValue: "onSystemLock")) | ||
| XCTAssertEqual(SessionTimeoutType.never, SessionTimeoutType(rawValue: "never")) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "custom")) | ||
| } | ||
|
|
||
| /// `init(rawValue:)` returns `.custom` for `nil` and unknown values (default case). | ||
| func test_initFromRawValue_defaultCase() { | ||
| // `nil` value maps to `custom` on mobile in support to legacy. | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: nil)) | ||
| // Unknown/invalid strings map to `custom` (default case). | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "unknown")) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "invalid")) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(rawValue: "")) | ||
| } | ||
|
|
||
| /// `init(value:)` returns the correct case for the given `SessionTimeoutValue`. | ||
| func test_initFromSessionTimeoutValue() { | ||
| XCTAssertEqual(SessionTimeoutType.immediately, SessionTimeoutType(value: .immediately)) | ||
| XCTAssertEqual(SessionTimeoutType.onAppRestart, SessionTimeoutType(value: .onAppRestart)) | ||
| XCTAssertEqual(SessionTimeoutType.never, SessionTimeoutType(value: .never)) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .custom(123))) | ||
| } | ||
|
|
||
| /// `init(value:)` returns `.custom` for all predefined timeout values. | ||
| func test_initFromSessionTimeoutValue_predefined() { | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .oneMinute)) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .fiveMinutes)) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .fifteenMinutes)) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .thirtyMinutes)) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .oneHour)) | ||
| XCTAssertEqual(SessionTimeoutType.custom, SessionTimeoutType(value: .fourHours)) | ||
| } | ||
|
|
||
| /// `rawValue` returns the correct string values. | ||
| func test_rawValues() { | ||
| XCTAssertEqual(SessionTimeoutType.immediately.rawValue, "immediately") | ||
| XCTAssertEqual(SessionTimeoutType.onAppRestart.rawValue, "onAppRestart") | ||
| XCTAssertEqual(SessionTimeoutType.never.rawValue, "never") | ||
| XCTAssertEqual(SessionTimeoutType.custom.rawValue, "custom") | ||
| } | ||
|
|
||
| /// `timeoutType` returns the correct string representation values. | ||
| func test_timeoutType() { | ||
| XCTAssertEqual(SessionTimeoutType.immediately.timeoutType, "immediately") | ||
| XCTAssertEqual(SessionTimeoutType.onAppRestart.timeoutType, "on app restart") | ||
| XCTAssertEqual(SessionTimeoutType.never.timeoutType, "never") | ||
| XCTAssertEqual(SessionTimeoutType.custom.timeoutType, "custom") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| // swiftlint:disable:this file_name | ||
| import SnapshotTesting | ||
| import SwiftUI | ||
| import XCTest | ||
|
|
||
| @testable import BitwardenKit | ||
|
|
||
| class SettingsPickerFieldTests: BitwardenTestCase { | ||
| // MARK: Properties | ||
|
|
||
| var subject: SettingsPickerField! | ||
|
|
||
| // MARK: Setup & Teardown | ||
|
|
||
| override func setUp() { | ||
| super.setUp() | ||
| } | ||
|
|
||
| override func tearDown() { | ||
| super.tearDown() | ||
| subject = nil | ||
| } | ||
|
|
||
| // MARK: Tests | ||
|
|
||
| /// Test a snapshot of the picker field in light mode without footer. | ||
| func disabletest_snapshot_lightMode_noFooter() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. โ Finding 1: All snapshot tests are disabled All 7 snapshot tests have the Required action: Remove the
|
||
| subject = SettingsPickerField( | ||
| title: "Title", | ||
| customTimeoutValue: "1:00", | ||
| pickerValue: .constant(3600), | ||
| customTimeoutAccessibilityLabel: "one hour, zero minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortrait) | ||
| } | ||
|
|
||
| /// Test a snapshot of the picker field in dark mode without footer. | ||
| func disabletest_snapshot_darkMode_noFooter() { | ||
| subject = SettingsPickerField( | ||
| title: "Title", | ||
| customTimeoutValue: "1:00", | ||
| pickerValue: .constant(3600), | ||
| customTimeoutAccessibilityLabel: "one hour, zero minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitDark) | ||
| } | ||
|
|
||
| /// Test a snapshot of the picker field with large dynamic type without footer. | ||
| func disabletest_snapshot_largeDynamicType_noFooter() { | ||
| subject = SettingsPickerField( | ||
| title: "Title", | ||
| customTimeoutValue: "1:00", | ||
| pickerValue: .constant(3600), | ||
| customTimeoutAccessibilityLabel: "one hour, zero minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitAX5) | ||
| } | ||
|
|
||
| /// Test a snapshot of the picker field in light mode with footer. | ||
| func disabletest_snapshot_lightMode_withFooter() { | ||
| subject = SettingsPickerField( | ||
| title: "Title", | ||
| footer: "Your organization has set the default session timeout to 1 hour", | ||
| customTimeoutValue: "1:00", | ||
| pickerValue: .constant(3600), | ||
| customTimeoutAccessibilityLabel: "one hour, zero minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortrait) | ||
| } | ||
|
|
||
| /// Test a snapshot of the picker field in dark mode with footer. | ||
| func disabletest_snapshot_darkMode_withFooter() { | ||
| subject = SettingsPickerField( | ||
| title: "Title", | ||
| footer: "Your organization has set the default session timeout to 1 hour", | ||
| customTimeoutValue: "1:00", | ||
| pickerValue: .constant(3600), | ||
| customTimeoutAccessibilityLabel: "one hour, zero minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitDark) | ||
| } | ||
|
|
||
| /// Test a snapshot of the picker field with large dynamic type with footer. | ||
| func disabletest_snapshot_largeDynamicType_withFooter() { | ||
| subject = SettingsPickerField( | ||
| title: "Title", | ||
| footer: "Your organization has set the default session timeout to 1 hour", | ||
| customTimeoutValue: "1:00", | ||
| pickerValue: .constant(3600), | ||
| customTimeoutAccessibilityLabel: "one hour, zero minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortraitAX5) | ||
| } | ||
|
|
||
| /// Test a snapshot of the picker field with an empty title. | ||
| func disabletest_snapshot_lightMode_emptyTitle() { | ||
| subject = SettingsPickerField( | ||
| title: "", | ||
| footer: "This setting is managed by your organization", | ||
| customTimeoutValue: "1:30", | ||
| pickerValue: .constant(5400), | ||
| customTimeoutAccessibilityLabel: "one hour, thirty minutes", | ||
| ) | ||
|
|
||
| assertSnapshot(of: subject, as: .defaultPortrait) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This enum lacks dedicated unit tests, particularly for:
init(rawValue:)method with all possible string values.custominit(value:)method with all SessionTimeoutValue casesThese initialization paths are critical for policy enforcement compatibility with legacy servers. Add tests in a new file:
BitwardenKit/Core/Platform/Models/Enum/SessionTimeoutTypeTests.swift