FirebaseDataEncoderTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. /*
  2. * Copyright 2020 Google LLC
  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 FirebaseSharedSwift
  18. import XCTest
  19. class FirebaseFirebaseDataEncoderTests: XCTestCase {
  20. func testInt() {
  21. struct Model: Codable, Equatable {
  22. let x: Int
  23. }
  24. let model = Model(x: 42)
  25. let dict = ["x": 42]
  26. assertThat(model).roundTrips(to: dict)
  27. }
  28. func testNullDecodesAsNil() throws {
  29. let decoder = FirebaseDataDecoder()
  30. let opt = try decoder.decode(Int?.self, from: NSNull())
  31. XCTAssertNil(opt)
  32. }
  33. func testEmpty() {
  34. struct Model: Codable, Equatable {}
  35. assertThat(Model()).roundTrips(to: [String: Any]())
  36. }
  37. func testString() throws {
  38. struct Model: Codable, Equatable {
  39. let s: String
  40. }
  41. assertThat(Model(s: "abc")).roundTrips(to: ["s": "abc"])
  42. }
  43. func testCaseConversion() throws {
  44. struct Model: Codable, Equatable {
  45. let snakeCase: Int
  46. }
  47. let model = Model(snakeCase: 42)
  48. let dict = ["snake_case": 42]
  49. let encoder = FirebaseDataEncoder()
  50. encoder.keyEncodingStrategy = .convertToSnakeCase
  51. let decoder = FirebaseDataDecoder()
  52. decoder.keyDecodingStrategy = .convertFromSnakeCase
  53. assertThat(model).roundTrips(to: dict, using: encoder, decoder: decoder)
  54. }
  55. func testOptional() {
  56. struct Model: Codable, Equatable {
  57. let x: Int
  58. let opt: Int?
  59. }
  60. assertThat(Model(x: 42, opt: nil)).roundTrips(to: ["x": 42])
  61. assertThat(Model(x: 42, opt: 7)).roundTrips(to: ["x": 42, "opt": 7])
  62. assertThat(["x": 42, "opt": 5]).decodes(to: Model(x: 42, opt: 5))
  63. assertThat(["x": 42, "opt": true]).failsDecoding(to: Model.self)
  64. assertThat(["x": 42, "opt": "abc"]).failsDecoding(to: Model.self)
  65. assertThat(["x": 45.55, "opt": 5]).failsDecoding(to: Model.self)
  66. assertThat(["opt": 5]).failsDecoding(to: Model.self)
  67. // TODO: - handle encoding keys with nil values
  68. // See https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder
  69. // and https://bugs.swift.org/browse/SR-9232
  70. // XCTAssertTrue(encodedDict.keys.contains("x"))
  71. }
  72. func testEnum() {
  73. enum MyEnum: Codable, Equatable {
  74. case num(number: Int)
  75. case text(String)
  76. private enum CodingKeys: String, CodingKey {
  77. case num
  78. case text
  79. }
  80. private enum DecodingError: Error {
  81. case decoding(String)
  82. }
  83. init(from decoder: Decoder) throws {
  84. let values = try decoder.container(keyedBy: CodingKeys.self)
  85. if let value = try? values.decode(Int.self, forKey: .num) {
  86. self = .num(number: value)
  87. return
  88. }
  89. if let value = try? values.decode(String.self, forKey: .text) {
  90. self = .text(value)
  91. return
  92. }
  93. throw DecodingError.decoding("Decoding error: \(dump(values))")
  94. }
  95. func encode(to encoder: Encoder) throws {
  96. var container = encoder.container(keyedBy: CodingKeys.self)
  97. switch self {
  98. case let .num(number):
  99. try container.encode(number, forKey: .num)
  100. case let .text(value):
  101. try container.encode(value, forKey: .text)
  102. }
  103. }
  104. }
  105. struct Model: Codable, Equatable {
  106. let x: Int
  107. let e: MyEnum
  108. }
  109. assertThat(Model(x: 42, e: MyEnum.num(number: 4)))
  110. .roundTrips(to: ["x": 42, "e": ["num": 4]])
  111. assertThat(Model(x: 43, e: MyEnum.text("abc")))
  112. .roundTrips(to: ["x": 43, "e": ["text": "abc"]])
  113. }
  114. func testBadValue() {
  115. struct Model: Codable, Equatable {
  116. let x: Int
  117. }
  118. assertThat(["x": "abc"]).failsDecoding(to: Model.self) // Wrong type
  119. }
  120. func testValueTooBig() {
  121. struct Model: Codable, Equatable {
  122. let x: CChar
  123. }
  124. assertThat(Model(x: 42)).roundTrips(to: ["x": 42])
  125. assertThat(["x": 12345]).failsDecoding(to: Model.self) // Overflow
  126. }
  127. // Inspired by https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java
  128. func testBeans() {
  129. struct Model: Codable, Equatable {
  130. let s: String
  131. let d: Double
  132. let f: Float
  133. let l: CLongLong
  134. let i: Int
  135. let b: Bool
  136. let sh: CShort
  137. let byte: CChar
  138. let uchar: CUnsignedChar
  139. let ai: [Int]
  140. let si: [String]
  141. let caseSensitive: String
  142. let casESensitive: String
  143. let casESensitivE: String
  144. }
  145. let model = Model(
  146. s: "abc",
  147. d: 123,
  148. f: -4,
  149. l: 1_234_567_890_123,
  150. i: -4444,
  151. b: false,
  152. sh: 123,
  153. byte: 45,
  154. uchar: 44,
  155. ai: [1, 2, 3, 4],
  156. si: ["abc", "def"],
  157. caseSensitive: "aaa",
  158. casESensitive: "bbb",
  159. casESensitivE: "ccc"
  160. )
  161. let dict = [
  162. "s": "abc",
  163. "d": 123,
  164. "f": -4,
  165. "l": Int64(1_234_567_890_123),
  166. "i": -4444,
  167. "b": false,
  168. "sh": 123,
  169. "byte": 45,
  170. "uchar": 44,
  171. "ai": [1, 2, 3, 4],
  172. "si": ["abc", "def"],
  173. "caseSensitive": "aaa",
  174. "casESensitive": "bbb",
  175. "casESensitivE": "ccc",
  176. ] as [String: Any]
  177. assertThat(model).roundTrips(to: dict)
  178. }
  179. func testCodingKeysCanCustomizeEncodingAndDecoding() throws {
  180. struct Model: Codable, Equatable {
  181. var s: String
  182. var ms: String = "filler"
  183. var d: Double
  184. var md: Double = 42.42
  185. // Use CodingKeys to only encode part of the struct.
  186. enum CodingKeys: String, CodingKey {
  187. case s
  188. case d
  189. }
  190. }
  191. assertThat(Model(s: "abc", ms: "dummy", d: 123.3, md: 0))
  192. .encodes(to: ["s": "abc", "d": 123.3])
  193. .decodes(to: Model(s: "abc", ms: "filler", d: 123.3, md: 42.42))
  194. }
  195. func testNestedObjects() {
  196. struct SecondLevelNestedModel: Codable, Equatable {
  197. var age: Int8
  198. var weight: Double
  199. }
  200. struct NestedModel: Codable, Equatable {
  201. var group: String
  202. var groupList: [SecondLevelNestedModel]
  203. var groupMap: [String: SecondLevelNestedModel]
  204. }
  205. struct Model: Codable, Equatable {
  206. var id: Int64
  207. var group: NestedModel
  208. }
  209. let model = Model(
  210. id: 123,
  211. group: NestedModel(
  212. group: "g1",
  213. groupList: [
  214. SecondLevelNestedModel(age: 20, weight: 80.1),
  215. SecondLevelNestedModel(age: 25, weight: 85.1),
  216. ],
  217. groupMap: [
  218. "name1": SecondLevelNestedModel(age: 30, weight: 64.2),
  219. "name2": SecondLevelNestedModel(age: 35, weight: 79.2),
  220. ]
  221. )
  222. )
  223. let dict = [
  224. "group": [
  225. "group": "g1",
  226. "groupList": [
  227. [
  228. "age": 20,
  229. "weight": 80.1,
  230. ],
  231. [
  232. "age": 25,
  233. "weight": 85.1,
  234. ],
  235. ],
  236. "groupMap": [
  237. "name1": [
  238. "age": 30,
  239. "weight": 64.2,
  240. ],
  241. "name2": [
  242. "age": 35,
  243. "weight": 79.2,
  244. ],
  245. ],
  246. ] as [String: Any],
  247. "id": 123,
  248. ] as [String: Any]
  249. assertThat(model).roundTrips(to: dict)
  250. }
  251. func testCollapsingNestedObjects() {
  252. // The model is flat but the document has a nested Map.
  253. struct Model: Codable, Equatable {
  254. var id: Int64
  255. var name: String
  256. init(id: Int64, name: String) {
  257. self.id = id
  258. self.name = name
  259. }
  260. private enum CodingKeys: String, CodingKey {
  261. case id
  262. case nested
  263. }
  264. private enum NestedCodingKeys: String, CodingKey {
  265. case name
  266. }
  267. init(from decoder: Decoder) throws {
  268. let container = try decoder.container(keyedBy: CodingKeys.self)
  269. try id = container.decode(Int64.self, forKey: .id)
  270. let nestedContainer = try container
  271. .nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
  272. try name = nestedContainer.decode(String.self, forKey: .name)
  273. }
  274. func encode(to encoder: Encoder) throws {
  275. var container = encoder.container(keyedBy: CodingKeys.self)
  276. try container.encode(id, forKey: .id)
  277. var nestedContainer = container
  278. .nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
  279. try nestedContainer.encode(name, forKey: .name)
  280. }
  281. }
  282. assertThat(Model(id: 12345, name: "ModelName"))
  283. .roundTrips(to: [
  284. "id": 12345,
  285. "nested": ["name": "ModelName"],
  286. ])
  287. }
  288. class SuperModel: Codable, Equatable {
  289. var superPower: Double? = 100.0
  290. var superName: String? = "superName"
  291. init(power: Double, name: String) {
  292. superPower = power
  293. superName = name
  294. }
  295. static func == (lhs: SuperModel, rhs: SuperModel) -> Bool {
  296. return (lhs.superName == rhs.superName) && (lhs.superPower == rhs.superPower)
  297. }
  298. private enum CodingKeys: String, CodingKey {
  299. case superPower
  300. case superName
  301. }
  302. required init(from decoder: Decoder) throws {
  303. let container = try decoder.container(keyedBy: CodingKeys.self)
  304. superPower = try container.decode(Double.self, forKey: .superPower)
  305. superName = try container.decode(String.self, forKey: .superName)
  306. }
  307. func encode(to encoder: Encoder) throws {
  308. var container = encoder.container(keyedBy: CodingKeys.self)
  309. try container.encode(superPower, forKey: .superPower)
  310. try container.encode(superName, forKey: .superName)
  311. }
  312. }
  313. class SubModel: SuperModel {
  314. var timestamp: Double? = 123_456_789.123
  315. init(power: Double, name: String, seconds: Double) {
  316. super.init(power: power, name: name)
  317. timestamp = seconds
  318. }
  319. static func == (lhs: SubModel, rhs: SubModel) -> Bool {
  320. return ((lhs as SuperModel) == (rhs as SuperModel)) && (lhs.timestamp == rhs.timestamp)
  321. }
  322. private enum CodingKeys: String, CodingKey {
  323. case timestamp
  324. }
  325. required init(from decoder: Decoder) throws {
  326. let container = try decoder.container(keyedBy: CodingKeys.self)
  327. timestamp = try container.decode(Double.self, forKey: .timestamp)
  328. try super.init(from: container.superDecoder())
  329. }
  330. override func encode(to encoder: Encoder) throws {
  331. var container = encoder.container(keyedBy: CodingKeys.self)
  332. try container.encode(timestamp, forKey: .timestamp)
  333. try super.encode(to: container.superEncoder())
  334. }
  335. }
  336. func testClassHierarchy() {
  337. assertThat(SubModel(power: 100, name: "name", seconds: 123_456_789.123))
  338. .roundTrips(to: [
  339. "super": ["superPower": 100, "superName": "name"] as [String: Any],
  340. "timestamp": 123_456_789.123,
  341. ])
  342. }
  343. }
  344. private func assertThat(_ dictionary: [String: Any],
  345. file: StaticString = #file,
  346. line: UInt = #line) -> DictionarySubject {
  347. return DictionarySubject(dictionary, file: file, line: line)
  348. }
  349. func assertThat<X: Equatable & Codable>(_ model: X, file: StaticString = #file,
  350. line: UInt = #line) -> CodableSubject<X> {
  351. return CodableSubject(model, file: file, line: line)
  352. }
  353. func assertThat<X: Equatable & Encodable>(_ model: X, file: StaticString = #file,
  354. line: UInt = #line) -> EncodableSubject<X> {
  355. return EncodableSubject(model, file: file, line: line)
  356. }
  357. class EncodableSubject<X: Equatable & Encodable> {
  358. var subject: X
  359. var file: StaticString
  360. var line: UInt
  361. init(_ subject: X, file: StaticString, line: UInt) {
  362. self.subject = subject
  363. self.file = file
  364. self.line = line
  365. }
  366. @discardableResult
  367. func encodes(to expected: [String: Any],
  368. using encoder: FirebaseDataEncoder = .init()) -> DictionarySubject {
  369. let encoded = assertEncodes(to: expected, using: encoder)
  370. return DictionarySubject(encoded, file: file, line: line)
  371. }
  372. func failsToEncode() {
  373. do {
  374. let encoder = FirebaseDataEncoder()
  375. encoder.keyEncodingStrategy = .convertToSnakeCase
  376. _ = try encoder.encode(subject)
  377. } catch {
  378. return
  379. }
  380. XCTFail("Failed to throw")
  381. }
  382. func failsEncodingAtTopLevel() {
  383. do {
  384. let encoder = FirebaseDataEncoder()
  385. encoder.keyEncodingStrategy = .convertToSnakeCase
  386. _ = try encoder.encode(subject)
  387. XCTFail("Failed to throw", file: file, line: line)
  388. } catch EncodingError.invalidValue(_, _) {
  389. return
  390. } catch {
  391. XCTFail("Unrecognized error: \(error)", file: file, line: line)
  392. }
  393. }
  394. private func assertEncodes(to expected: [String: Any],
  395. using encoder: FirebaseDataEncoder = .init()) -> [String: Any] {
  396. do {
  397. let enc = try encoder.encode(subject)
  398. XCTAssertEqual(enc as? NSDictionary, expected as NSDictionary, file: file, line: line)
  399. return (enc as! NSDictionary) as! [String: Any]
  400. } catch {
  401. XCTFail("Failed to encode \(X.self): error: \(error)")
  402. return ["": -1]
  403. }
  404. }
  405. }
  406. class CodableSubject<X: Equatable & Codable>: EncodableSubject<X> {
  407. func roundTrips(to expected: [String: Any],
  408. using encoder: FirebaseDataEncoder = .init(),
  409. decoder: FirebaseDataDecoder = .init()) {
  410. let reverseSubject = encodes(to: expected, using: encoder)
  411. reverseSubject.decodes(to: subject, using: decoder)
  412. }
  413. }
  414. class DictionarySubject {
  415. var subject: [String: Any]
  416. var file: StaticString
  417. var line: UInt
  418. init(_ subject: [String: Any], file: StaticString, line: UInt) {
  419. self.subject = subject
  420. self.file = file
  421. self.line = line
  422. }
  423. func decodes<X: Equatable & Codable>(to expected: X,
  424. using decoder: FirebaseDataDecoder = .init()) -> Void {
  425. do {
  426. let decoded = try decoder.decode(X.self, from: subject)
  427. XCTAssertEqual(decoded, expected)
  428. } catch {
  429. XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line)
  430. }
  431. }
  432. func failsDecoding<X: Equatable & Codable>(to _: X.Type,
  433. using decoder: FirebaseDataDecoder = .init()) -> Void {
  434. XCTAssertThrowsError(
  435. try decoder.decode(X.self, from: subject),
  436. file: file,
  437. line: line
  438. )
  439. }
  440. }