Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
30 changes: 30 additions & 0 deletions BitwardenKit/Core/Platform/Utilities/AnyCodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
/// A custom codable type that can be used to encode/decode a variety of primitive types.
///
public enum AnyCodable: Codable, Equatable, Sendable {
/// The wrapped value is an array of `AnyCodable` values.
case array([AnyCodable])

/// The wrapped value is a bool value.
case bool(Bool)

/// The wrapped value is a keyed dictionary of `AnyCodable` values.
case dictionary([String: AnyCodable])

/// The wrapped value is a double value.
case double(Double)

Expand Down Expand Up @@ -34,6 +40,10 @@ public enum AnyCodable: Codable, Equatable, Sendable {
self = .null
} else if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let arrayValue = try? container.decode([AnyCodable].self) {
self = .array(arrayValue)
} else if let dictionaryValue = try? container.decode([String: AnyCodable].self) {
self = .dictionary(dictionaryValue)
} else {
throw DecodingError.dataCorruptedError(
in: container,
Expand All @@ -48,8 +58,12 @@ public enum AnyCodable: Codable, Equatable, Sendable {
var container = encoder.singleValueContainer()

switch self {
case let .array(arrayValue):
try container.encode(arrayValue)
case let .bool(boolValue):
try container.encode(boolValue)
case let .dictionary(dictionaryValue):
try container.encode(dictionaryValue)
case let .double(doubleValue):
try container.encode(doubleValue)
case let .int(intValue):
Expand All @@ -63,15 +77,29 @@ public enum AnyCodable: Codable, Equatable, Sendable {
}

public extension AnyCodable {
/// Returns the associated array value if the type is `array`.
var arrayValue: [AnyCodable]? {
guard case let .array(arrayValue) = self else { return nil }
return arrayValue
}

/// Returns the associated bool value if the type is `bool`.
var boolValue: Bool? {
guard case let .bool(boolValue) = self else { return nil }
return boolValue
}

/// Returns the associated dictionary value if the type is `dictionary`.
var dictionaryValue: [String: AnyCodable]? {
guard case let .dictionary(dictionaryValue) = self else { return nil }
return dictionaryValue
}

/// Returns the associated double value if the type is `double` or could be converted to `Double`.
var doubleValue: Double? {
switch self {
case .array, .dictionary:
nil
case let .bool(boolValue):
boolValue ? 1 : 0
case let .double(doubleValue):
Expand All @@ -88,6 +116,8 @@ public extension AnyCodable {
/// Returns the associated int value if the type is `int` or could be converted to `Int`.
var intValue: Int? {
switch self {
case .array, .dictionary:
nil
case let .bool(boolValue):
boolValue ? 1 : 0
case let .double(doubleValue):
Expand Down
205 changes: 146 additions & 59 deletions BitwardenKit/Core/Platform/Utilities/AnyCodableTests.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import XCTest
import Foundation
import Testing

@testable import BitwardenKit

class AnyCodableTests: BitwardenTestCase {
// MARK: Properties

/// `boolValue` returns the bool associated value if the type is a `bool`.
func test_boolValue() {
XCTAssertEqual(AnyCodable.bool(true).boolValue, true)
XCTAssertEqual(AnyCodable.bool(false).boolValue, false)

XCTAssertNil(AnyCodable.int(2).boolValue)
XCTAssertNil(AnyCodable.null.boolValue)
XCTAssertNil(AnyCodable.string("abc").boolValue)
}
struct AnyCodableTests {
// MARK: Decode Tests

/// `AnyCodable` can be used to decode JSON.
func test_decode() throws {
@Test
func decode() throws {
let json = """
{
"minComplexity": null,
Expand All @@ -31,12 +23,11 @@ class AnyCodableTests: BitwardenTestCase {
}
"""

let jsonData = try XCTUnwrap(json.data(using: .utf8))
let jsonData = try #require(json.data(using: .utf8))
let dictionary = try JSONDecoder().decode([String: AnyCodable].self, from: jsonData)

XCTAssertEqual(
dictionary,
[
#expect(
dictionary == [
"minComplexity": AnyCodable.null,
"minLength": AnyCodable.int(12),
"requireUpper": AnyCodable.bool(false),
Expand All @@ -50,30 +41,43 @@ class AnyCodableTests: BitwardenTestCase {
)
}

/// `doubleValue` returns the double associated value if the type is a `double` or could be
/// converted to `Double`.
func test_doubleValue() {
XCTAssertEqual(AnyCodable.bool(true).doubleValue, 1)
XCTAssertEqual(AnyCodable.bool(false).doubleValue, 0)
/// `AnyCodable` can decode a JSON array of values, e.g. a Send Controls policy's
/// `allowedSendTypes` field.
@Test
func decode_array() throws {
let json = """
{
"disableSend": false,
"whoCanAccess": 1,
"allowedDomains": null,
"disableHideEmail": false,
"allowedSendTypes": [0, 1],
"deletionHours": null
}
"""

let jsonData = try #require(json.data(using: .utf8))
let dictionary = try JSONDecoder().decode([String: AnyCodable].self, from: jsonData)

XCTAssertEqual(AnyCodable.double(2).doubleValue, 2)
XCTAssertEqual(AnyCodable.double(3.1).doubleValue, 3.1)
XCTAssertEqual(AnyCodable.double(3.8).doubleValue, 3.8)
XCTAssertEqual(AnyCodable.double(-5.5).doubleValue, -5.5)
#expect(dictionary["allowedSendTypes"] == .array([.int(0), .int(1)]))
}

XCTAssertEqual(AnyCodable.int(1).doubleValue, 1)
XCTAssertEqual(AnyCodable.int(5).doubleValue, 5)
XCTAssertEqual(AnyCodable.int(15).doubleValue, 15)
/// `AnyCodable` can decode a nested JSON object value.
@Test
func decode_dictionary() throws {
let json = #"{"nested": {"a": 1, "b": true}}"#

XCTAssertNil(AnyCodable.null.doubleValue)
let jsonData = try #require(json.data(using: .utf8))
let dictionary = try JSONDecoder().decode([String: AnyCodable].self, from: jsonData)

XCTAssertEqual(AnyCodable.string("1").doubleValue, 1)
XCTAssertEqual(AnyCodable.string("1.5").doubleValue, 1.5)
XCTAssertNil(AnyCodable.string("abc").doubleValue)
#expect(dictionary["nested"] == .dictionary(["a": .int(1), "b": .bool(true)]))
}

// MARK: Encode Tests

/// `AnyCodable` can be used to encode JSON.
func test_encode() throws {
@Test
func encode() throws {
let dictionary: [String: AnyCodable] = [
"minComplexity": AnyCodable.null,
"minLength": AnyCodable.int(12),
Expand All @@ -91,9 +95,8 @@ class AnyCodableTests: BitwardenTestCase {
let jsonData = try encoder.encode(dictionary)
let json = String(data: jsonData, encoding: .utf8)

XCTAssertEqual(
json,
"""
#expect(
json == """
{
"enforceOnLogin" : false,
"minComplexity" : null,
Expand All @@ -109,35 +112,119 @@ class AnyCodableTests: BitwardenTestCase {
)
}

/// `AnyCodable` can encode a JSON array of values.
@Test
func encode_array() throws {
let dictionary: [String: AnyCodable] = ["allowedSendTypes": .array([.int(0), .int(1)])]

let encoder = JSONEncoder()
let jsonData = try encoder.encode(dictionary)
let json = String(data: jsonData, encoding: .utf8)

#expect(json == #"{"allowedSendTypes":[0,1]}"#)
}

/// `AnyCodable` can encode a nested JSON object value.
@Test
func encode_dictionary() throws {
let dictionary: [String: AnyCodable] = ["nested": .dictionary(["a": .int(1)])]

let encoder = JSONEncoder()
let jsonData = try encoder.encode(dictionary)
let json = String(data: jsonData, encoding: .utf8)

#expect(json == #"{"nested":{"a":1}}"#)
}

// MARK: Value Tests

/// `arrayValue` returns the array associated value if the type is an `array`.
@Test
func arrayValue() {
#expect(AnyCodable.array([.int(0), .int(1)]).arrayValue == [.int(0), .int(1)])

#expect(AnyCodable.bool(true).arrayValue == nil)
#expect(AnyCodable.dictionary(["a": .int(1)]).arrayValue == nil)
#expect(AnyCodable.null.arrayValue == nil)
#expect(AnyCodable.string("abc").arrayValue == nil)
}

/// `boolValue` returns the bool associated value if the type is a `bool`.
@Test
func boolValue() {
#expect(AnyCodable.bool(true).boolValue == true)
#expect(AnyCodable.bool(false).boolValue == false)

#expect(AnyCodable.int(2).boolValue == nil)
#expect(AnyCodable.null.boolValue == nil)
#expect(AnyCodable.string("abc").boolValue == nil)
}

/// `dictionaryValue` returns the dictionary associated value if the type is a `dictionary`.
@Test
func dictionaryValue() {
#expect(AnyCodable.dictionary(["a": .int(1)]).dictionaryValue == ["a": .int(1)])

#expect(AnyCodable.array([.int(1)]).dictionaryValue == nil)
#expect(AnyCodable.bool(true).dictionaryValue == nil)
#expect(AnyCodable.null.dictionaryValue == nil)
#expect(AnyCodable.string("abc").dictionaryValue == nil)
}

/// `doubleValue` returns the double associated value if the type is a `double` or could be
/// converted to `Double`.
@Test
func doubleValue() {
#expect(AnyCodable.bool(true).doubleValue == 1)
#expect(AnyCodable.bool(false).doubleValue == 0)

#expect(AnyCodable.double(2).doubleValue == 2)
#expect(AnyCodable.double(3.1).doubleValue == 3.1)
#expect(AnyCodable.double(3.8).doubleValue == 3.8)
#expect(AnyCodable.double(-5.5).doubleValue == -5.5)

#expect(AnyCodable.int(1).doubleValue == 1)
#expect(AnyCodable.int(5).doubleValue == 5)
#expect(AnyCodable.int(15).doubleValue == 15)

#expect(AnyCodable.null.doubleValue == nil)

#expect(AnyCodable.string("1").doubleValue == 1)
#expect(AnyCodable.string("1.5").doubleValue == 1.5)
#expect(AnyCodable.string("abc").doubleValue == nil)
}

/// `intValue` returns the int associated value if the type is an `int` or could be converted
/// to `Int`.
func test_intValue() {
XCTAssertEqual(AnyCodable.bool(true).intValue, 1)
XCTAssertEqual(AnyCodable.bool(false).intValue, 0)
@Test
func intValue() {
#expect(AnyCodable.bool(true).intValue == 1)
#expect(AnyCodable.bool(false).intValue == 0)

XCTAssertEqual(AnyCodable.double(2).intValue, 2)
XCTAssertEqual(AnyCodable.double(3.1).intValue, 3)
XCTAssertEqual(AnyCodable.double(3.8).intValue, 3)
XCTAssertEqual(AnyCodable.double(-5.5).intValue, -5)
#expect(AnyCodable.double(2).intValue == 2)
#expect(AnyCodable.double(3.1).intValue == 3)
#expect(AnyCodable.double(3.8).intValue == 3)
#expect(AnyCodable.double(-5.5).intValue == -5)

XCTAssertEqual(AnyCodable.int(1).intValue, 1)
XCTAssertEqual(AnyCodable.int(5).intValue, 5)
XCTAssertEqual(AnyCodable.int(15).intValue, 15)
#expect(AnyCodable.int(1).intValue == 1)
#expect(AnyCodable.int(5).intValue == 5)
#expect(AnyCodable.int(15).intValue == 15)

XCTAssertNil(AnyCodable.null.intValue)
#expect(AnyCodable.null.intValue == nil)

XCTAssertEqual(AnyCodable.string("1").intValue, 1)
XCTAssertNil(AnyCodable.string("1.5").intValue)
XCTAssertNil(AnyCodable.string("abc").intValue)
#expect(AnyCodable.string("1").intValue == 1)
#expect(AnyCodable.string("1.5").intValue == nil)
#expect(AnyCodable.string("abc").intValue == nil)
}

/// `stringValue` returns the string associated value if the type is a `string`.
func test_stringValue() {
XCTAssertEqual(AnyCodable.string("abc").stringValue, "abc")
XCTAssertEqual(AnyCodable.string("example").stringValue, "example")

XCTAssertNil(AnyCodable.bool(false).stringValue)
XCTAssertNil(AnyCodable.int(2).stringValue)
XCTAssertNil(AnyCodable.null.stringValue)
@Test
func stringValue() {
#expect(AnyCodable.string("abc").stringValue == "abc")
#expect(AnyCodable.string("example").stringValue == "example")

#expect(AnyCodable.bool(false).stringValue == nil)
#expect(AnyCodable.int(2).stringValue == nil)
#expect(AnyCodable.null.stringValue == nil)
}
}
5 changes: 5 additions & 0 deletions BitwardenShared/Core/Platform/Models/Enum/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ extension FeatureFlag: @retroactive CaseIterable {
/// Flag to enable/disable Premium upgrade path.
static let premiumUpgradePath = FeatureFlag(rawValue: "pm-31697-premium-upgrade-path")

/// Flag to enable/disable the Send Controls policy, which supersedes the `disableSend` and
/// `sendOptions` policies when active.
static let sendControls = FeatureFlag(rawValue: "pm-31885-send-controls")

public static var allCases: [FeatureFlag] {
[
.accountEncryptionV2JITPassword,
Expand All @@ -76,6 +80,7 @@ extension FeatureFlag: @retroactive CaseIterable {
.organizationUserNotificationBanner,
.policiesInAcceptedState,
.premiumUpgradePath,
.sendControls,
]
}
}
5 changes: 5 additions & 0 deletions BitwardenShared/Core/Vault/Models/Enum/PolicyType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ enum PolicyType: Int, Codable {
/// Displays a banner notification to organization users.
case organizationUserNotification = 20

/// Restricts Send creation, access, and default settings for members of an organization;
/// supersedes `disableSend` and `sendOptions` when the `pm-31885-send-controls` feature flag
/// is active on the server.
case sendControls = 21

/// An unknown policy type.
case unknown = -1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ extension BitwardenSdk.PolicyType {
case .requireSSO: self = .requireSso
case .resetPassword: self = .resetPassword
case .restrictItemTypes: self = .restrictedItemTypes
case .sendControls: self = .sendControls
case .sendOptions: self = .sendOptions
case .twoFactorAuthentication: self = .twoFactorAuthentication
// TODO: PM-39144 Add SDK mapping for `organizationUserNotification` PolicyType
Expand Down Expand Up @@ -113,6 +114,7 @@ extension PolicyType {
case .requireSso: self = .requireSSO
case .resetPassword: self = .resetPassword
case .restrictedItemTypes: self = .restrictItemTypes
case .sendControls: self = .sendControls
Comment thread
matt-livefront marked this conversation as resolved.
case .sendOptions: self = .sendOptions
case .singleOrg: self = .onlyOrg
case .twoFactorAuthentication: self = .twoFactorAuthentication
Expand All @@ -122,7 +124,6 @@ extension PolicyType {
.blockClaimedDomainAccountCreation,
.freeFamiliesSponsorship,
.organizationUserNotification,
.sendControls,
.uriMatchDefaults: self = .unknown
}
}
Expand Down
Loading