-
Notifications
You must be signed in to change notification settings - Fork 37
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
BIT-162: Add sync API #22
Changes from 1 commit
a208b47
2651360
d7f1145
994844f
a7f11ec
0126ca1
ad5068c
01413ae
dc4f668
da256f4
c314070
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,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,75 @@ | ||
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 | ||
|
||
XCTAssertThrowsError( | ||
try subject.decode(Date.self, from: Data(#""2023-08-23""#.utf8)) | ||
) { error in | ||
XCTAssertTrue(error is DecodingError) | ||
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 '2023-08-23'") | ||
} | ||
|
||
XCTAssertThrowsError( | ||
try subject.decode(Date.self, from: Data(#""๐""#.utf8)) | ||
) { error in | ||
XCTAssertTrue(error is DecodingError) | ||
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 '๐'") | ||
} | ||
|
||
XCTAssertThrowsError( | ||
try subject.decode(Date.self, from: Data(#""date""#.utf8)) | ||
) { error in | ||
XCTAssertTrue(error is DecodingError) | ||
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 '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 } | ||
} |
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: Int? | ||
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. ๐ค Just in case, bear in mind that currently the 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. Ok, I'll convert it to a string. |
||
|
||
/// | ||
let sizeName: String? | ||
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. @fedemkr I wasn't sure what this field was used for based on the name. Is this a formatted string of the size (e.g. "5 MB") or something else? 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. |
||
} |
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? | ||
} |
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,45 @@ | ||
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, | ||
object: String? = nil, | ||
password: String? = nil, | ||
revisionDate: Date, | ||
text: SendTextModel? = nil, | ||
type: SendType? = nil | ||
) -> 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, | ||
object: object, | ||
password: password, | ||
revisionDate: revisionDate, | ||
text: text, | ||
type: type | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
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 | ||
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. โ๏ธ This can be 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. Good to know, I'll probably make it optional for now, but may either change that in the future or default it to false when transforming it into an app model. |
||
|
||
/// 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? | ||
|
||
/// The response object type. | ||
let object: 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? | ||
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. โ๏ธ |
||
} |
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? | ||
} |
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? | ||
} |
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? | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import Foundation | ||
|
||
/// API model for a cipher login. | ||
/// | ||
struct CipherLoginModel: Codable, Equatable { | ||
// MARK: Properties | ||
|
||
/// Whether the login should be autofilled when the page loads. | ||
let autofillOnPageLoad: Bool? | ||
|
||
/// The login's password. | ||
let password: String? | ||
|
||
/// The date of the password's last revision. | ||
let passwordRevisionDate: Date? | ||
|
||
/// The login's TOTP details. | ||
let totp: String? | ||
|
||
/// The login's URI. | ||
let uri: String? | ||
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. โ๏ธ This can be removed (or transformed into a computed property). It's just a computed property based on the first one on |
||
|
||
/// The login's list of URI details. | ||
let uris: [CipherLoginUriModel]? | ||
|
||
/// The login's username. | ||
let username: String? | ||
} |
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.
๐จ Instead of repeating each time with different values what do you think on having something like:
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.
Good suggestion! I'll update this.