Codable.swift 8.2 KB

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