FirestoreEncoderTests.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Foundation
  17. import FirebaseFirestore
  18. import FirebaseFirestoreSwift
  19. import XCTest
  20. private func assertRoundTrip<X: Equatable & Codable>(model: X, encoded: [String: Any]) -> Void {
  21. let enc = assertEncodes(model, encoded: encoded)
  22. assertDecodes(enc, encoded: model)
  23. }
  24. private func assertEncodes<X: Equatable & Codable>(_ model: X, encoded: [String: Any]) -> [String: Any] {
  25. do {
  26. let enc = try Firestore.Encoder().encode(model)
  27. XCTAssertEqual(enc as NSDictionary, encoded as NSDictionary)
  28. return enc
  29. } catch {
  30. XCTFail("Failed to encode \(X.self): error: \(error)")
  31. }
  32. return ["": -1]
  33. }
  34. private func assertDecodes<X: Equatable & Codable>(_ model: [String: Any], encoded: X) -> Void {
  35. do {
  36. let decoded = try Firestore.Decoder().decode(X.self, from: model)
  37. XCTAssertEqual(decoded, encoded)
  38. } catch {
  39. XCTFail("Failed to decode \(X.self): \(error)")
  40. }
  41. }
  42. private func assertEncodingThrows<X: Equatable & Codable>(_ model: X) -> Void {
  43. do {
  44. _ = try Firestore.Encoder().encode(model)
  45. } catch {
  46. return
  47. }
  48. XCTFail("Failed to throw")
  49. }
  50. private func assertDecodingThrows<X: Equatable & Codable>(_ model: [String: Any], encoded: X) -> Void {
  51. do {
  52. _ = try Firestore.Decoder().decode(X.self, from: model)
  53. } catch {
  54. return
  55. }
  56. XCTFail("Failed to throw")
  57. }
  58. class FirestoreEncoderTests: XCTestCase {
  59. func testInt() {
  60. struct Model: Codable, Equatable {
  61. let x: Int
  62. }
  63. let model = Model(x: 42)
  64. let dict = ["x": 42]
  65. assertRoundTrip(model: model, encoded: dict)
  66. }
  67. func testEmpty() {
  68. struct Model: Codable, Equatable {}
  69. _ = assertEncodes(Model(), encoded: [String: Any]())
  70. }
  71. func testString() {
  72. struct Model: Codable, Equatable {
  73. let s: String
  74. }
  75. let model = Model(s: "abc")
  76. let encodedDict = try! Firestore.Encoder().encode(model)
  77. XCTAssertEqual(encodedDict["s"] as! String, "abc")
  78. }
  79. func testOptional() {
  80. struct Model: Codable, Equatable {
  81. let x: Int
  82. let opt: Int?
  83. }
  84. assertRoundTrip(model: Model(x: 42, opt: nil), encoded: ["x": 42])
  85. assertRoundTrip(model: Model(x: 42, opt: 7), encoded: ["x": 42, "opt": 7])
  86. assertDecodes(["x": 42, "opt": 5], encoded: Model(x: 42, opt: 5))
  87. assertDecodingThrows(["x": 42, "opt": true], encoded: Model(x: 42, opt: nil))
  88. assertDecodingThrows(["x": 42, "opt": "abc"], encoded: Model(x: 42, opt: nil))
  89. assertDecodingThrows(["x": 45.55, "opt": 5], encoded: Model(x: 42, opt: nil))
  90. assertDecodingThrows(["opt": 5], encoded: Model(x: 42, opt: nil))
  91. // TODO: - handle encoding keys with nil values
  92. // See https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder
  93. // and https://bugs.swift.org/browse/SR-9232
  94. // XCTAssertTrue(encodedDict.keys.contains("x"))
  95. }
  96. func testEnum() {
  97. enum MyEnum: Codable, Equatable {
  98. case num(number: Int)
  99. case text(String)
  100. case timestamp(Timestamp)
  101. private enum CodingKeys: String, CodingKey {
  102. case num
  103. case text
  104. case timestamp
  105. }
  106. private enum DecodingError: Error {
  107. case decoding(String)
  108. }
  109. init(from decoder: Decoder) throws {
  110. let values = try decoder.container(keyedBy: CodingKeys.self)
  111. if let value = try? values.decode(Int.self, forKey: .num) {
  112. self = .num(number: value)
  113. return
  114. }
  115. if let value = try? values.decode(String.self, forKey: .text) {
  116. self = .text(value)
  117. return
  118. }
  119. if let value = try? values.decode(Timestamp.self, forKey: .timestamp) {
  120. self = .timestamp(value)
  121. return
  122. }
  123. throw DecodingError.decoding("Decoding error: \(dump(values))")
  124. }
  125. func encode(to encoder: Encoder) throws {
  126. var container = encoder.container(keyedBy: CodingKeys.self)
  127. switch self {
  128. case let .num(number):
  129. try container.encode(number, forKey: .num)
  130. case let .text(value):
  131. try container.encode(value, forKey: .text)
  132. case let .timestamp(stamp):
  133. try container.encode(stamp, forKey: .timestamp)
  134. }
  135. }
  136. }
  137. struct Model: Codable, Equatable {
  138. let x: Int
  139. let e: MyEnum
  140. }
  141. let model = Model(x: 42, e: MyEnum.num(number: 4))
  142. assertRoundTrip(model: model, encoded: ["x": 42, "e": ["num": 4]])
  143. let model2 = Model(x: 43, e: MyEnum.text("abc"))
  144. assertRoundTrip(model: model2, encoded: ["x": 43, "e": ["text": "abc"]])
  145. let timestamp = Timestamp(date: Date())
  146. let model3 = Model(x: 43, e: MyEnum.timestamp(timestamp))
  147. assertRoundTrip(model: model3, encoded: ["x": 43, "e": ["timestamp": timestamp]])
  148. }
  149. func testGeoPoint() {
  150. struct Model: Codable, Equatable {
  151. let p: GeoPoint
  152. }
  153. let geopoint = GeoPoint(latitude: 1, longitude: -2)
  154. let model = Model(p: geopoint)
  155. assertRoundTrip(model: model, encoded: ["p": geopoint])
  156. }
  157. func testDate() {
  158. struct Model: Codable, Equatable {
  159. let date: Date
  160. }
  161. let date = Date(timeIntervalSinceReferenceDate: 0)
  162. let model = Model(date: date)
  163. assertRoundTrip(model: model, encoded: ["date": date])
  164. }
  165. func testDocumentReference() {
  166. struct Model: Codable, Equatable {
  167. let doc: DocumentReference
  168. }
  169. let d = FSTTestDocRef("abc/xyz")
  170. let model = Model(doc: d)
  171. assertRoundTrip(model: model, encoded: ["doc": d])
  172. }
  173. func testEncodingDocumentReferenceThrowsWithJSONEncoder() {
  174. let doc = FSTTestDocRef("abc/xyz")
  175. do {
  176. _ = try JSONEncoder().encode(doc)
  177. XCTFail("Failed to throw")
  178. } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported {
  179. return
  180. } catch {
  181. XCTFail("Unrecognized error: \(error)")
  182. }
  183. }
  184. func testEncodingDocumentReferenceNotEmbeddedThrows() {
  185. let doc = FSTTestDocRef("abc/xyz")
  186. do {
  187. _ = try Firestore.Encoder().encode(doc)
  188. XCTFail("Failed to throw")
  189. } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported {
  190. return
  191. } catch {
  192. XCTFail("Unrecognized error: \(error)")
  193. }
  194. }
  195. func testTimestamp() {
  196. struct Model: Codable, Equatable {
  197. let timestamp: Timestamp
  198. }
  199. let t = Timestamp(date: Date())
  200. let model = Model(timestamp: t)
  201. assertRoundTrip(model: model, encoded: ["timestamp": t])
  202. }
  203. func testBadValue() {
  204. struct Model: Codable, Equatable {
  205. let x: Int
  206. }
  207. let dict = ["x": "abc"] // Wrong type;
  208. let model = Model(x: 42)
  209. assertDecodingThrows(dict, encoded: model)
  210. }
  211. func testValueTooBig() {
  212. struct Model: Codable, Equatable {
  213. let x: CChar
  214. }
  215. let dict = ["x": 12345] // Overflow
  216. let model = Model(x: 42)
  217. assertDecodingThrows(dict, encoded: model)
  218. assertRoundTrip(model: model, encoded: ["x": 42])
  219. }
  220. // Inspired by https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java
  221. func testBeans() {
  222. struct Model: Codable, Equatable {
  223. let s: String
  224. let d: Double
  225. let f: Float
  226. let l: CLongLong
  227. let i: Int
  228. let b: Bool
  229. let sh: CShort
  230. let byte: CChar
  231. let uchar: CUnsignedChar
  232. let ai: [Int]
  233. let si: [String]
  234. let caseSensitive: String
  235. let casESensitive: String
  236. let casESensitivE: String
  237. }
  238. let model = Model(
  239. s: "abc",
  240. d: 123,
  241. f: -4,
  242. l: 1_234_567_890_123,
  243. i: -4444,
  244. b: false,
  245. sh: 123,
  246. byte: 45,
  247. uchar: 44,
  248. ai: [1, 2, 3, 4],
  249. si: ["abc", "def"],
  250. caseSensitive: "aaa",
  251. casESensitive: "bbb",
  252. casESensitivE: "ccc"
  253. )
  254. let dict = [
  255. "s": "abc",
  256. "d": 123,
  257. "f": -4,
  258. "l": 1_234_567_890_123,
  259. "i": -4444,
  260. "b": false,
  261. "sh": 123,
  262. "byte": 45,
  263. "uchar": 44,
  264. "ai": [1, 2, 3, 4],
  265. "si": ["abc", "def"],
  266. "caseSensitive": "aaa",
  267. "casESensitive": "bbb",
  268. "casESensitivE": "ccc",
  269. ] as [String: Any]
  270. assertRoundTrip(model: model, encoded: dict)
  271. }
  272. func testCodingKeysCanCustomizeEncodingAndDecoding() {
  273. struct Model: Codable, Equatable {
  274. var s: String
  275. var ms: String
  276. var d: Double
  277. var md: Double
  278. // Use CodingKeys to only encode part of the struct.
  279. enum CodingKeys: String, CodingKey {
  280. case s
  281. case d
  282. }
  283. public init(from decoder: Decoder) throws {
  284. let values = try decoder.container(keyedBy: CodingKeys.self)
  285. s = try values.decode(String.self, forKey: .s)
  286. d = try values.decode(Double.self, forKey: .d)
  287. ms = "filler"
  288. md = 42.42
  289. }
  290. public init(ins: String, inms: String, ind: Double, inmd: Double) {
  291. s = ins
  292. d = ind
  293. ms = inms
  294. md = inmd
  295. }
  296. }
  297. let model = Model(
  298. ins: "abc",
  299. inms: "dummy",
  300. ind: 123.3,
  301. inmd: 0
  302. )
  303. let dict = [
  304. "s": "abc",
  305. "d": 123.3,
  306. ] as [String: Any]
  307. let model2 = try! Firestore.Decoder().decode(Model.self, from: dict)
  308. XCTAssertEqual(model.s, model2.s)
  309. XCTAssertEqual(model.d, model2.d)
  310. XCTAssertEqual(model2.ms, "filler")
  311. XCTAssertEqual(model2.md, 42.42)
  312. let encodedDict = try! Firestore.Encoder().encode(model)
  313. XCTAssertEqual(encodedDict["s"] as! String, "abc")
  314. XCTAssertEqual(encodedDict["d"] as! Double, 123.3)
  315. XCTAssertNil(encodedDict["ms"])
  316. XCTAssertNil(encodedDict["md"])
  317. }
  318. func testNestedObjects() {
  319. struct SecondLevelNestedModel: Codable, Equatable {
  320. var age: Int8
  321. var weight: Double
  322. }
  323. struct NestedModel: Codable, Equatable {
  324. var group: String
  325. var groupList: [SecondLevelNestedModel]
  326. var groupMap: [String: SecondLevelNestedModel]
  327. var point: GeoPoint
  328. }
  329. struct Model: Codable, Equatable {
  330. var id: Int64
  331. var group: NestedModel
  332. }
  333. let model = Model(id: 123, group: NestedModel(group: "g1", groupList: [SecondLevelNestedModel(age: 20, weight: 80.1), SecondLevelNestedModel(age: 25, weight: 85.1)], groupMap: ["name1": SecondLevelNestedModel(age: 30, weight: 64.2), "name2": SecondLevelNestedModel(age: 35, weight: 79.2)],
  334. point: GeoPoint(latitude: 12.0, longitude: 9.1)))
  335. let dict = ["group": [
  336. "group": "g1",
  337. "point": GeoPoint(latitude: 12.0, longitude: 9.1),
  338. "groupList": [
  339. [
  340. "age": 20,
  341. "weight": 80.1,
  342. ],
  343. [
  344. "age": 25,
  345. "weight": 85.1,
  346. ],
  347. ],
  348. "groupMap": [
  349. "name1": [
  350. "age": 30,
  351. "weight": 64.2,
  352. ],
  353. "name2": [
  354. "age": 35,
  355. "weight": 79.2,
  356. ],
  357. ],
  358. ], "id": 123] as [String: Any]
  359. assertRoundTrip(model: model, encoded: dict)
  360. }
  361. func testCollapsingNestedObjects() {
  362. // The model is flat but the document has a nested Map.
  363. struct Model: Codable, Equatable {
  364. var id: Int64
  365. var name: String
  366. init(id: Int64, name: String) {
  367. self.id = id
  368. self.name = name
  369. }
  370. private enum CodingKeys: String, CodingKey {
  371. case id
  372. case nested
  373. }
  374. private enum NestedCodingKeys: String, CodingKey {
  375. case name
  376. }
  377. init(from decoder: Decoder) throws {
  378. let container = try decoder.container(keyedBy: CodingKeys.self)
  379. try id = container.decode(Int64.self, forKey: .id)
  380. let nestedContainer = try container.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
  381. try name = nestedContainer.decode(String.self, forKey: .name)
  382. }
  383. func encode(to encoder: Encoder) throws {
  384. var container = encoder.container(keyedBy: CodingKeys.self)
  385. try container.encode(id, forKey: .id)
  386. var nestedContainer = container.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
  387. try nestedContainer.encode(name, forKey: .name)
  388. }
  389. }
  390. let model = Model(id: 12345, name: "ModelName")
  391. let dict = ["id": 12345,
  392. "nested": ["name": "ModelName"]] as [String: Any]
  393. assertRoundTrip(model: model, encoded: dict)
  394. }
  395. class SuperModel: Codable, Equatable {
  396. var superPower: Double? = 100.0
  397. var superName: String? = "superName"
  398. init(power: Double, name: String) {
  399. superPower = power
  400. superName = name
  401. }
  402. static func == (lhs: SuperModel, rhs: SuperModel) -> Bool {
  403. return (lhs.superName == rhs.superName) && (lhs.superPower == rhs.superPower)
  404. }
  405. private enum CodingKeys: String, CodingKey {
  406. case superPower
  407. case superName
  408. }
  409. required init(from decoder: Decoder) throws {
  410. let container = try decoder.container(keyedBy: CodingKeys.self)
  411. superPower = try container.decode(Double.self, forKey: .superPower)
  412. superName = try container.decode(String.self, forKey: .superName)
  413. }
  414. func encode(to encoder: Encoder) throws {
  415. var container = encoder.container(keyedBy: CodingKeys.self)
  416. try container.encode(superPower, forKey: .superPower)
  417. try container.encode(superName, forKey: .superName)
  418. }
  419. }
  420. class SubModel: SuperModel {
  421. var timestamp: Timestamp? = Timestamp(seconds: 848_483_737, nanoseconds: 23423)
  422. init(power: Double, name: String, seconds: Int64, nano: Int32) {
  423. super.init(power: power, name: name)
  424. timestamp = Timestamp(seconds: seconds, nanoseconds: nano)
  425. }
  426. static func == (lhs: SubModel, rhs: SubModel) -> Bool {
  427. return ((lhs as SuperModel) == (rhs as SuperModel)) && (lhs.timestamp == rhs.timestamp)
  428. }
  429. private enum CodingKeys: String, CodingKey {
  430. case timestamp
  431. }
  432. required init(from decoder: Decoder) throws {
  433. let container = try decoder.container(keyedBy: CodingKeys.self)
  434. timestamp = try container.decode(Timestamp.self, forKey: .timestamp)
  435. try super.init(from: container.superDecoder())
  436. }
  437. override func encode(to encoder: Encoder) throws {
  438. var container = encoder.container(keyedBy: CodingKeys.self)
  439. try container.encode(timestamp, forKey: .timestamp)
  440. try super.encode(to: container.superEncoder())
  441. }
  442. }
  443. func testClassHierarchy() {
  444. let model = SubModel(power: 100, name: "name", seconds: 123_456_789, nano: 654_321)
  445. let dict = ["super": ["superPower": 100, "superName": "name"],
  446. "timestamp": Timestamp(seconds: 123_456_789, nanoseconds: 654_321)] as [String: Any]
  447. assertRoundTrip(model: model, encoded: dict)
  448. }
  449. func testEncodingEncodableArrayNotSupported() {
  450. struct Model: Codable, Equatable {
  451. var name: String
  452. }
  453. assertEncodingThrows([Model(name: "1")])
  454. }
  455. func testFieldValuePassthrough() {
  456. struct Model: Encodable, Equatable {
  457. var fieldValue: FieldValue
  458. }
  459. let model = Model(fieldValue: FieldValue.delete())
  460. let dict = ["fieldValue": FieldValue.delete()]
  461. let encoded = try! Firestore.Encoder().encode(model)
  462. XCTAssertEqual(dict, encoded as! [String: FieldValue])
  463. }
  464. func testEncodingFieldValueNotEmbeddedThrows() {
  465. let ts = FieldValue.serverTimestamp()
  466. do {
  467. _ = try Firestore.Encoder().encode(ts)
  468. XCTFail("Failed to throw")
  469. } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported {
  470. return
  471. } catch {
  472. XCTFail("Unrecognized error: \(error)")
  473. }
  474. }
  475. func testServerTimestamp() {
  476. struct Model: Codable {
  477. var timestamp: ServerTimestamp
  478. }
  479. // Encoding `pending`
  480. var encoded = try! Firestore.Encoder().encode(Model(timestamp: .pending))
  481. XCTAssertEqual(encoded["timestamp"] as! FieldValue, FieldValue.serverTimestamp())
  482. // Encoding `resolved`
  483. encoded = try! Firestore.Encoder().encode(Model(timestamp: .resolved(Timestamp(seconds: 123_456_789, nanoseconds: 4321))))
  484. XCTAssertEqual(encoded["timestamp"] as! Timestamp,
  485. Timestamp(seconds: 123_456_789, nanoseconds: 4321))
  486. // Decoding a Timestamp leads to `resolved`
  487. var dict = ["timestamp": Timestamp(seconds: 123_456_789, nanoseconds: 4321)] as [String: Any]
  488. var decoded = try! Firestore.Decoder().decode(Model.self, from: dict)
  489. XCTAssertEqual(decoded.timestamp,
  490. ServerTimestamp.resolved(Timestamp(seconds: 123_456_789, nanoseconds: 4321)))
  491. // Decoding a NSNull() leads to `pending`.
  492. dict = ["timestamp": NSNull()] as [String: Any]
  493. decoded = try! Firestore.Decoder().decode(Model.self, from: dict)
  494. XCTAssertEqual(decoded.timestamp,
  495. ServerTimestamp.pending)
  496. }
  497. func testExplicitNull() throws {
  498. struct Model: Codable, Equatable {
  499. var name: ExplicitNull<String>
  500. }
  501. // Encoding 'none'
  502. let fieldIsNull = Model(name: .none)
  503. var encoded = try Firestore.Encoder().encode(fieldIsNull)
  504. XCTAssertTrue(encoded.keys.contains("name"))
  505. XCTAssertEqual(encoded["name"]! as! NSNull, NSNull())
  506. // Decoding null
  507. var decoded = try Firestore.Decoder().decode(Model.self, from: encoded)
  508. XCTAssertEqual(decoded, fieldIsNull)
  509. // Encoding 'some'
  510. let fieldIsNotNull = Model(name: .some("good name"))
  511. encoded = try Firestore.Encoder().encode(fieldIsNotNull)
  512. XCTAssertEqual(encoded["name"]! as! String, "good name")
  513. // Decoding not-null value
  514. decoded = try Firestore.Decoder().decode(Model.self, from: encoded)
  515. XCTAssertEqual(decoded, fieldIsNotNull)
  516. }
  517. }