ServerValueCodingTests.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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 FirebaseDatabase
  17. import FirebaseDatabaseSwift
  18. import Foundation
  19. import XCTest
  20. class ServerTimestampTests: XCTestCase {
  21. func testDateRoundTrip() throws {
  22. struct Model: Codable, Equatable {
  23. @ServerTimestamp var timestamp: Date?
  24. }
  25. let model = Model(timestamp: Date(timeIntervalSince1970: 123_456_789.123))
  26. let dict = ["timestamp": 123_456_789_123]
  27. assertThat(model).roundTrips(to: dict)
  28. }
  29. func testTimestampEncoding() throws {
  30. struct Model: Codable, Equatable {
  31. @ServerTimestamp var timestamp: Date?
  32. }
  33. let model = Model()
  34. let dict = ["timestamp": [".sv": "timestamp"]]
  35. // We can't round trip since we only decode values as Ints
  36. // (for Date conversion) and never as the magic value.
  37. assertThat(model).encodes(to: dict)
  38. }
  39. }
  40. private struct CurrencyAmount: Codable, Equatable, Hashable, AdditiveArithmetic {
  41. static var zero: Self = CurrencyAmount(value: 0)
  42. static func + (lhs: Self, rhs: Self) -> Self {
  43. return Self(value: lhs.value + rhs.value)
  44. }
  45. static func += (lhs: inout Self, rhs: Self) {
  46. lhs.value += rhs.value
  47. }
  48. static func - (lhs: Self, rhs: Self) -> Self {
  49. return CurrencyAmount(value: lhs.value - rhs.value)
  50. }
  51. static func -= (lhs: inout Self, rhs: Self) {
  52. lhs.value -= rhs.value
  53. }
  54. var value: Decimal
  55. func encode(to encoder: Encoder) throws {
  56. var container = encoder.singleValueContainer()
  57. try container.encode(value)
  58. }
  59. init(value: Decimal) {
  60. self.value = value
  61. }
  62. init(from decoder: Decoder) throws {
  63. let container = try decoder.singleValueContainer()
  64. value = try container.decode(Decimal.self)
  65. }
  66. }
  67. extension CurrencyAmount: ExpressibleByIntegerLiteral {
  68. public init(integerLiteral value: Int) {
  69. self.value = Decimal(value)
  70. }
  71. }
  72. extension CurrencyAmount: ExpressibleByFloatLiteral {
  73. public init(floatLiteral value: Double) {
  74. self.value = Decimal(value)
  75. }
  76. }
  77. private func assertThat(_ dictionary: [String: Any],
  78. file: StaticString = #file,
  79. line: UInt = #line) -> DictionarySubject {
  80. return DictionarySubject(dictionary, file: file, line: line)
  81. }
  82. func assertThat<X: Equatable & Codable>(_ model: X, file: StaticString = #file,
  83. line: UInt = #line) -> CodableSubject<X> {
  84. return CodableSubject(model, file: file, line: line)
  85. }
  86. func assertThat<X: Equatable & Encodable>(_ model: X, file: StaticString = #file,
  87. line: UInt = #line) -> EncodableSubject<X> {
  88. return EncodableSubject(model, file: file, line: line)
  89. }
  90. class EncodableSubject<X: Equatable & Encodable> {
  91. var subject: X
  92. var file: StaticString
  93. var line: UInt
  94. init(_ subject: X, file: StaticString, line: UInt) {
  95. self.subject = subject
  96. self.file = file
  97. self.line = line
  98. }
  99. @discardableResult
  100. func encodes(to expected: [String: Any],
  101. using encoder: Database.Encoder = .init()) -> DictionarySubject {
  102. let encoded = assertEncodes(to: expected, using: encoder)
  103. return DictionarySubject(encoded, file: file, line: line)
  104. }
  105. func failsToEncode() {
  106. do {
  107. let encoder = Database.Encoder()
  108. encoder.keyEncodingStrategy = .convertToSnakeCase
  109. _ = try encoder.encode(subject)
  110. } catch {
  111. return
  112. }
  113. XCTFail("Failed to throw")
  114. }
  115. func failsEncodingAtTopLevel() {
  116. do {
  117. let encoder = Database.Encoder()
  118. encoder.keyEncodingStrategy = .convertToSnakeCase
  119. _ = try encoder.encode(subject)
  120. XCTFail("Failed to throw", file: file, line: line)
  121. } catch EncodingError.invalidValue(_, _) {
  122. return
  123. } catch {
  124. XCTFail("Unrecognized error: \(error)", file: file, line: line)
  125. }
  126. }
  127. private func assertEncodes(to expected: [String: Any],
  128. using encoder: Database.Encoder = .init()) -> [String: Any] {
  129. do {
  130. let enc = try encoder.encode(subject)
  131. XCTAssertEqual(enc as? NSDictionary, expected as NSDictionary, file: file, line: line)
  132. return (enc as! NSDictionary) as! [String: Any]
  133. } catch {
  134. XCTFail("Failed to encode \(X.self): error: \(error)")
  135. return ["": -1]
  136. }
  137. }
  138. }
  139. class CodableSubject<X: Equatable & Codable>: EncodableSubject<X> {
  140. func roundTrips(to expected: [String: Any],
  141. using encoder: Database.Encoder = .init(),
  142. decoder: Database.Decoder = .init()) {
  143. let reverseSubject = encodes(to: expected, using: encoder)
  144. reverseSubject.decodes(to: subject, using: decoder)
  145. }
  146. }
  147. class DictionarySubject {
  148. var subject: [String: Any]
  149. var file: StaticString
  150. var line: UInt
  151. init(_ subject: [String: Any], file: StaticString, line: UInt) {
  152. self.subject = subject
  153. self.file = file
  154. self.line = line
  155. }
  156. func decodes<X: Equatable & Codable>(to expected: X,
  157. using decoder: Database.Decoder = .init()) -> Void {
  158. do {
  159. let decoded = try decoder.decode(X.self, from: subject)
  160. XCTAssertEqual(decoded, expected)
  161. } catch {
  162. XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line)
  163. }
  164. }
  165. }