Codable.swift 7.8 KB

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