Codable.swift 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import FirebaseRemoteConfig
  15. import XCTest
  16. #if compiler(>=5.5.2) && canImport(_Concurrency)
  17. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  18. class CodableTests: APITestBase {
  19. // MARK: - Test decoding Remote Config JSON values
  20. // Contrast this test with the subsequent one to see the value of the Codable API.
  21. func testFetchAndActivateWithoutCodable() async throws {
  22. let status = try await config.fetchAndActivate()
  23. XCTAssertEqual(status, .successFetchedFromRemote)
  24. let dict = try XCTUnwrap(config[Constants.jsonKey].jsonValue as? [String: AnyHashable])
  25. XCTAssertEqual(dict["recipeName"], "PB&J")
  26. XCTAssertEqual(dict["ingredients"], ["bread", "peanut butter", "jelly"])
  27. XCTAssertEqual(dict["cookTime"], 7)
  28. XCTAssertEqual(
  29. config[Constants.jsonKey].jsonValue as! [String: AnyHashable],
  30. Constants.jsonValue
  31. )
  32. }
  33. struct Recipe: Decodable {
  34. var recipeName: String
  35. var ingredients: [String]
  36. var cookTime: Int
  37. }
  38. func testFetchAndActivateWithCodable() async throws {
  39. let status = try await config.fetchAndActivate()
  40. XCTAssertEqual(status, .successFetchedFromRemote)
  41. let recipe = try XCTUnwrap(config[Constants.jsonKey].decoded(asType: Recipe.self))
  42. XCTAssertEqual(recipe.recipeName, "PB&J")
  43. XCTAssertEqual(recipe.ingredients, ["bread", "peanut butter", "jelly"])
  44. XCTAssertEqual(recipe.cookTime, 7)
  45. }
  46. func testFetchAndActivateWithCodableAlternativeAPI() async throws {
  47. let status = try await config.fetchAndActivate()
  48. XCTAssertEqual(status, .successFetchedFromRemote)
  49. let recipe: Recipe = try XCTUnwrap(config[Constants.jsonKey].decoded())
  50. XCTAssertEqual(recipe.recipeName, "PB&J")
  51. XCTAssertEqual(recipe.ingredients, ["bread", "peanut butter", "jelly"])
  52. XCTAssertEqual(recipe.cookTime, 7)
  53. }
  54. func testFetchAndActivateWithCodableBadJson() async throws {
  55. let status = try await config.fetchAndActivate()
  56. XCTAssertEqual(status, .successFetchedFromRemote)
  57. do {
  58. _ = try config[Constants.nonJsonKey].decoded(asType: Recipe.self)
  59. } catch let DecodingError.typeMismatch(_, context) {
  60. XCTAssertEqual(context.debugDescription,
  61. "Expected to decode Dictionary<String, Any> but found " +
  62. "FirebaseRemoteConfigValueDecoderHelper instead.")
  63. return
  64. }
  65. XCTFail("Failed to catch trying to decode non-JSON key as JSON")
  66. }
  67. // MARK: - Test setting Remote Config defaults via an encodable struct
  68. struct DataTestDefaults: Encodable {
  69. var bool: Bool
  70. var int: Int32
  71. var long: Int64
  72. var string: String
  73. }
  74. func testSetEncodeableDefaults() throws {
  75. let data = DataTestDefaults(
  76. bool: true,
  77. int: 2,
  78. long: 9_876_543_210,
  79. string: "four"
  80. )
  81. try config.setDefaults(from: data)
  82. let boolValue = try XCTUnwrap(config.defaultValue(forKey: "bool")).numberValue.boolValue
  83. XCTAssertTrue(boolValue)
  84. let intValue = try XCTUnwrap(config.defaultValue(forKey: "int")).numberValue.intValue
  85. XCTAssertEqual(intValue, 2)
  86. let longValue = try XCTUnwrap(config.defaultValue(forKey: "long")).numberValue.int64Value
  87. XCTAssertEqual(longValue, 9_876_543_210)
  88. let stringValue = try XCTUnwrap(config.defaultValue(forKey: "string")).stringValue
  89. XCTAssertEqual(stringValue, "four")
  90. }
  91. func testSetEncodeableDefaultsInvalid() throws {
  92. do {
  93. _ = try config.setDefaults(from: 7)
  94. } catch let RemoteConfigCodableError.invalidSetDefaultsInput(message) {
  95. XCTAssertEqual(message,
  96. "The setDefaults input: 7, must be a Struct that encodes to a Dictionary")
  97. return
  98. }
  99. XCTFail("Failed to catch trying to encode an invalid input to setDefaults.")
  100. }
  101. // MARK: - Test extracting config to an decodable struct.
  102. struct MyConfig: Decodable {
  103. var Recipe: Recipe
  104. var notJSON: String
  105. var myInt: Int
  106. var myFloat: Float
  107. var myDecimal: Decimal
  108. var myTrue: Bool
  109. var myData: Data
  110. }
  111. func testExtractConfig() async throws {
  112. let status = try await config.fetchAndActivate()
  113. XCTAssertEqual(status, .successFetchedFromRemote)
  114. let myConfig: MyConfig = try config.decoded()
  115. XCTAssertEqual(myConfig.notJSON, Constants.nonJsonValue)
  116. XCTAssertEqual(myConfig.myInt, Constants.intValue)
  117. XCTAssertEqual(myConfig.myTrue, true)
  118. XCTAssertEqual(myConfig.myFloat, Constants.floatValue)
  119. XCTAssertEqual(myConfig.myDecimal, Constants.decimalValue)
  120. XCTAssertEqual(myConfig.myData, Constants.dataValue)
  121. XCTAssertEqual(myConfig.Recipe.recipeName, "PB&J")
  122. XCTAssertEqual(myConfig.Recipe.ingredients, ["bread", "peanut butter", "jelly"])
  123. XCTAssertEqual(myConfig.Recipe.cookTime, 7)
  124. }
  125. // Additional fields in config are ignored.
  126. func testExtractConfigExtra() async throws {
  127. guard APITests.useFakeConfig else { return }
  128. fakeConsole.config["extra"] = "extra Value"
  129. let status = try await config.fetchAndActivate()
  130. XCTAssertEqual(status, .successFetchedFromRemote)
  131. let myConfig: MyConfig = try config.decoded()
  132. XCTAssertEqual(myConfig.notJSON, Constants.nonJsonValue)
  133. XCTAssertEqual(myConfig.Recipe.recipeName, "PB&J")
  134. XCTAssertEqual(myConfig.Recipe.ingredients, ["bread", "peanut butter", "jelly"])
  135. XCTAssertEqual(myConfig.Recipe.cookTime, 7)
  136. }
  137. // Failure if requested field does not exist.
  138. func testExtractConfigMissing() async throws {
  139. struct MyConfig: Decodable {
  140. var missing: String
  141. var Recipe: String
  142. var notJSON: String
  143. }
  144. let status = try await config.fetchAndActivate()
  145. XCTAssertEqual(status, .successFetchedFromRemote)
  146. do {
  147. let _: MyConfig = try config.decoded()
  148. } catch let DecodingError.keyNotFound(codingKey, context) {
  149. XCTAssertEqual(codingKey.stringValue, "missing")
  150. print(codingKey, context)
  151. return
  152. }
  153. XCTFail("Failed to throw on missing field")
  154. }
  155. func testCodableAfterPlistDefaults() throws {
  156. struct Defaults: Codable {
  157. let format: String
  158. let isPaidUser: Bool
  159. let newItem: Double
  160. let Languages: String
  161. let dictValue: [String: String]
  162. let arrayValue: [String]
  163. let arrayIntValue: [Int]
  164. }
  165. // setDefaults(fromPlist:) doesn't work because of dynamic linking.
  166. // More details in RCNRemoteConfigTest.m
  167. var findPlist: String?
  168. #if SWIFT_PACKAGE
  169. findPlist = Bundle.module.path(forResource: "Defaults-testInfo", ofType: "plist")
  170. #else
  171. for b in Bundle.allBundles {
  172. findPlist = b.path(forResource: "Defaults-testInfo", ofType: "plist")
  173. if findPlist != nil {
  174. break
  175. }
  176. }
  177. #endif
  178. let plistFile = try XCTUnwrap(findPlist)
  179. let defaults = NSDictionary(contentsOfFile: plistFile)
  180. config.setDefaults(defaults as? [String: NSObject])
  181. let readDefaults: Defaults = try config.decoded()
  182. XCTAssertEqual(readDefaults.format, "key to value.")
  183. XCTAssertEqual(readDefaults.isPaidUser, true)
  184. XCTAssertEqual(readDefaults.newItem, 2.4)
  185. XCTAssertEqual(readDefaults.Languages, "English")
  186. XCTAssertEqual(readDefaults.dictValue, ["foo": "foo",
  187. "bar": "bar",
  188. "baz": "baz"])
  189. XCTAssertEqual(readDefaults.arrayValue, ["foo", "bar", "baz"])
  190. XCTAssertEqual(readDefaults.arrayIntValue, [1, 2, 0, 3])
  191. }
  192. }
  193. #endif