Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e65850f
Merge branch 'main' into cmcg/pm-19305-vault-timeout-policy
LRNcardozoWDF Oct 29, 2025
f8268e6
pm-19305 Add support for timeout policy type
LRNcardozoWDF Nov 6, 2025
b69af43
pm-19305 Fix tests
LRNcardozoWDF Nov 10, 2025
1937e2d
Merge branch 'main' into cmcg/pm-19305-vault-timeout-policy
LRNcardozoWDF Nov 10, 2025
9a80efe
pm-19305 Applied plurals
LRNcardozoWDF Nov 11, 2025
b81b871
Merge branch 'main' into cmcg/pm-19305-vault-timeout-policy
LRNcardozoWDF Nov 11, 2025
b2acdf2
pm-19305 Fix pr comments
LRNcardozoWDF Nov 12, 2025
e404cb3
pm-19305 Fix test coverage
LRNcardozoWDF Nov 12, 2025
8bb7d69
Merge branch 'main' into cmcg/pm-19305-vault-timeout-policy
LRNcardozoWDF Nov 13, 2025
b663543
pm-19305 Fix tests
LRNcardozoWDF Nov 13, 2025
69a91ff
pm-19305 Add missing tests
LRNcardozoWDF Nov 17, 2025
03bacf1
Merge branch 'main' into cmcg/pm-19305-vault-timeout-policy
LRNcardozoWDF Nov 18, 2025
c2ebc53
Merge remote-tracking branch 'origin/main' into cmcg/pm-19305-vault-tโ€ฆ
LRNcardozoWDF Nov 21, 2025
1077368
Merge remote-tracking branch 'origin/main' into cmcg/pm-19305-vault-tโ€ฆ
LRNcardozoWDF Nov 24, 2025
1fde466
pm-19305 Fix pr comments
LRNcardozoWDF Nov 24, 2025
66c918f
pm-19305 Fix PR comments
LRNcardozoWDF Nov 25, 2025
76c4f85
Merge remote-tracking branch 'origin/main' into cmcg/pm-19305-vault-tโ€ฆ
LRNcardozoWDF Nov 26, 2025
ade0228
pm-19305 Fix pr comments
LRNcardozoWDF Nov 27, 2025
3823fc3
Merge remote-tracking branch 'origin/main' into cmcg/pm-19305-vault-tโ€ฆ
LRNcardozoWDF Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
}
93 changes: 93 additions & 0 deletions BitwardenKit/Core/Platform/Models/Enum/SessionTimeoutType.swift
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?) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Finding 6: Missing test coverage for SessionTimeoutType

This enum lacks dedicated unit tests, particularly for:

  1. The init(rawValue:) method with all possible string values
  2. The legacy "onSystemLock" โ†’ "onAppRestart" mapping (line 70)
  3. The default case handling (line 73) which falls back to .custom
  4. The init(value:) method with all SessionTimeoutValue cases

These initialization paths are critical for policy enforcement compatibility with legacy servers. Add tests in a new file: BitwardenKit/Core/Platform/Models/Enum/SessionTimeoutTypeTests.swift

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() {
Copy link

Choose a reason for hiding this comment

The 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 disabletest_ prefix instead of test_, which means they never execute. This violates the testing guidelines (Testing.md:172-174) requiring snapshot tests for light mode, dark mode, and large dynamic type.

Required action: Remove the disable prefix from all test function names:

  • test_snapshot_lightMode_noFooter()
  • test_snapshot_darkMode_noFooter()
  • test_snapshot_largeDynamicType_noFooter()
  • etc.

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)
}
}
Loading