Skip to content

Commit

Permalink
BIT-162: Add sync API (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-livefront authored Sep 15, 2023
1 parent 6d0e4dd commit 6e07b78
Show file tree
Hide file tree
Showing 46 changed files with 1,512 additions and 5 deletions.
12 changes: 12 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ analyzer_rules:
- unused_declaration
- unused_import

disabled_rules:
- todo

opt_in_rules:
- anonymous_argument_in_multiline_closure
- attributes
Expand Down Expand Up @@ -75,3 +78,12 @@ identifier_name:
inclusive_language:
override_allowed_terms:
- masterPassword

custom_rules:
todo_without_jira:
name: "TODO without JIRA"
regex: "(TODO|TO DO|FIX|FIXME|FIX ME|todo)(?!: BIT-[0-9]{1,})" # "TODO: BIT-123"
message: "All TODOs must be followed by a JIRA reference, for example: \"TODO: BIT-123\""
match_kinds:
- comment
severity: warning
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

extension JSONDecoder {
/// The default `JSONDecoder` used to decode JSON payloads throughout the app.
static let defaultDecoder: JSONDecoder = {
let dateFormatterWithFractionalSeconds = ISO8601DateFormatter()
dateFormatterWithFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime]

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .custom { dateDecoder in
let container = try dateDecoder.singleValueContainer()
let stringValue = try container.decode(String.self)

// ISO8601DateFormatter supports ISO 8601 dates with or without fractional seconds, but
// not both at the same time 🙃. Since the API contains both formats with the more
// common containing fractional seconds, attempt to parse that first and fall back to
// parsing without fractional seconds.
if let date = dateFormatterWithFractionalSeconds.date(from: stringValue) {
return date
} else if let date = dateFormatter.date(from: stringValue) {
return date
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unable to decode date with value '\(stringValue)'"
)
}
}
return jsonDecoder
}()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import XCTest

@testable import BitwardenShared

class JSONDecoderBitwardenTests: BitwardenTestCase {
// MARK: Tests

/// `JSONDecoder.defaultDecoder` can decode ISO8601 dates with fractional seconds.
func test_decode_iso8601DateWithFractionalSeconds() {
let subject = JSONDecoder.defaultDecoder

XCTAssertEqual(
try subject
.decode(Date.self, from: Data(#""2023-08-18T21:33:31.6366667Z""#.utf8)),
Date(timeIntervalSince1970: 1_692_394_411.636)
)
XCTAssertEqual(
try subject
.decode(Date.self, from: Data(#""2023-06-14T13:51:24.45Z""#.utf8)),
Date(timeIntervalSince1970: 1_686_750_684.450)
)
}

/// `JSONDecoder.defaultDecoder` can decode ISO8601 dates without fractional seconds.
func test_decode_iso8601DateWithoutFractionalSeconds() {
let subject = JSONDecoder.defaultDecoder

XCTAssertEqual(
try subject
.decode(Date.self, from: Data(#""2023-08-25T21:33:00Z""#.utf8)),
Date(timeIntervalSince1970: 1_692_999_180)
)
XCTAssertEqual(
try subject
.decode(Date.self, from: Data(#""2023-07-12T15:46:12Z""#.utf8)),
Date(timeIntervalSince1970: 1_689_176_772)
)
}

/// `JSONDecoder.defaultDecoder` will throw an error if an invalid or unsupported date format is
/// encountered.
func test_decode_invalidDateThrowsError() {
let subject = JSONDecoder.defaultDecoder

func assertThrowsDataCorruptedError(
dateString: String,
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertThrowsError(
try subject.decode(Date.self, from: Data(#""\#(dateString)""#.utf8)),
file: file,
line: line
) { error in
XCTAssertTrue(error is DecodingError, file: file, line: line)
guard case let .dataCorrupted(context) = error as? DecodingError else {
return XCTFail("Expected error to be DecodingError.dataCorrupted")
}
XCTAssertEqual(
context.debugDescription,
"Unable to decode date with value '\(dateString)'",
file: file,
line: line
)
}
}

assertThrowsDataCorruptedError(dateString: "2023-08-23")
assertThrowsDataCorruptedError(dateString: "🔒")
assertThrowsDataCorruptedError(dateString: "date")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation
import Networking

extension JSONResponse {
/// The decoder used by default to decode JSON responses from the API.
static var decoder: JSONDecoder { .defaultDecoder }
}
17 changes: 17 additions & 0 deletions BitwardenShared/Core/Tools/Models/API/SendFileModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// API model for a file send.
///
struct SendFileModel: Codable, Equatable {
// MARK: Properties

/// The filename of the file to send.
let fileName: String?

/// The send file identifier.
let id: String?

/// The size of the file.
let size: String?

/// The human-readable string of the file size.
let sizeName: String?
}
11 changes: 11 additions & 0 deletions BitwardenShared/Core/Tools/Models/API/SendTextModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// API model for a text send.
///
struct SendTextModel: Codable, Equatable {
// MARK: Properties

/// Whether the text is hidden by default.
let hidden: Bool

/// The text in the send.
let text: String?
}
9 changes: 9 additions & 0 deletions BitwardenShared/Core/Tools/Models/Enum/SendType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// An enum describing the type of data in a send.
///
enum SendType: Int, Codable {
/// The send contains text data.
case text = 0

/// The send contains an attached file.
case file = 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

@testable import BitwardenShared

extension SendResponseModel {
static func fixture(
accessCount: Int = 0,
accessId: String? = nil,
deletionDate: Date,
disabled: Bool = false,
expirationDate: Date? = nil,
file: SendFileModel? = nil,
hideEmail: Bool = false,
id: String = UUID().uuidString,
key: String? = nil,
maxAccessCount: Int? = nil,
name: String? = nil,
notes: String? = nil,
password: String? = nil,
revisionDate: Date,
text: SendTextModel? = nil,
type: SendType = .text
) -> SendResponseModel {
self.init(
accessCount: accessCount,
accessId: accessId,
deletionDate: deletionDate,
disabled: disabled,
expirationDate: expirationDate,
file: file,
hideEmail: hideEmail,
id: id,
key: key,
maxAccessCount: maxAccessCount,
name: name,
notes: notes,
password: password,
revisionDate: revisionDate,
text: text,
type: type
)
}
}
55 changes: 55 additions & 0 deletions BitwardenShared/Core/Tools/Models/Response/SendResponseModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation

/// API response model for a send.
///
struct SendResponseModel: Codable, Equatable {
// MARK: Properties

/// The number of times the send has been accessed.
let accessCount: Int

/// The identifier used to access the send.
let accessId: String?

/// The deletion date of the send.
let deletionDate: Date

/// Whether the send is disabled.
let disabled: Bool

/// The expiration date of the send.
let expirationDate: Date?

/// The file included in the send.
let file: SendFileModel?

/// Whether the user's email address should be hidden from recipients.
let hideEmail: Bool?

/// The send's identifier.
let id: String

/// The key used to decrypt the send.
let key: String?

/// The maximum number of times the send can be accessed.
let maxAccessCount: Int?

/// The name of the send.
let name: String?

/// Notes about the send.
let notes: String?

/// An optional password used to access the send.
let password: String?

/// The date of the sends's last revision.
let revisionDate: Date

/// The text included in the send.
let text: SendTextModel?

/// The type of data in the send.
let type: SendType
}
23 changes: 23 additions & 0 deletions BitwardenShared/Core/Vault/Models/API/CipherCardModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// API model for a card cipher.
///
struct CipherCardModel: Codable, Equatable {
// MARK: Properties

/// The card's brand.
let brand: String?

/// The card's cardholder name.
let cardholderName: String?

/// The card's code.
let code: String?

/// The card's expiration month.
let expMonth: String?

/// The card's expiration year.
let expYear: String?

/// The card's number.
let number: String?
}
17 changes: 17 additions & 0 deletions BitwardenShared/Core/Vault/Models/API/CipherFieldModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// API model for a cipher custom field.
///
struct CipherFieldModel: Codable, Equatable {
// MARK: Properties

/// The type of value that the field is linked to for a linked field type.
let linkedId: LinkedIdType?

/// The field's name.
let name: String?

/// The field's type.
let type: FieldType?

/// The field's value.
let value: String?
}
59 changes: 59 additions & 0 deletions BitwardenShared/Core/Vault/Models/API/CipherIdentityModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/// API model for a cipher identity.
///
struct CipherIdentityModel: Codable, Equatable {
// MARK: Properties

/// The identity's address line 1.
let address1: String?

/// The identity's address line 2.
let address2: String?

/// The identity's address line 3.
let address3: String?

/// The identity's city.
let city: String?

/// The identity's company.
let company: String?

/// The identity's country.
let country: String?

/// The identity's email.
let email: String?

/// The identity's first name.
let firstName: String?

/// The identity's last name.
let lastName: String?

/// The identity's license number.
let licenseNumber: String?

/// The identity's middle name.
let middleName: String?

/// The identity's passport number.
let passportNumber: String?

/// The identity's phone number.
let phone: String?

/// The identity's postal code.
let postalCode: String?

/// The identity's SSN.
let ssn: String?

/// The identity's state.
let state: String?

/// The identity's title.
let title: String?

/// The identity's username.
let username: String?
}
Loading

0 comments on commit 6e07b78

Please sign in to comment.