@react-native-firebase/auth 26.3.3 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/RNFBAuth.podspec +8 -2
  3. package/android/build.gradle +5 -0
  4. package/android/src/main/java/io/invertase/firebase/auth/AuthListenerHandle.java +26 -0
  5. package/android/src/main/java/io/invertase/firebase/auth/NativeRNFBTurboAuth.java +68 -102
  6. package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthCacheRegistry.kt +77 -0
  7. package/android/src/main/java/io/invertase/firebase/auth/RNFBAuthListenerRegistry.kt +76 -0
  8. package/android/src/test/java/io/invertase/firebase/auth/RNFBAuthCacheRegistryTest.java +184 -0
  9. package/android/src/test/java/io/invertase/firebase/auth/RNFBAuthListenerRegistryTest.java +171 -0
  10. package/dist/module/ActionCodeURL.js +1 -1
  11. package/dist/module/ActionCodeURL.js.map +1 -1
  12. package/dist/module/index.js +1 -2
  13. package/dist/module/index.js.map +1 -1
  14. package/dist/module/version.js +1 -1
  15. package/dist/typescript/lib/ActionCodeURL.d.ts.map +1 -1
  16. package/dist/typescript/lib/types/internal.d.ts +2 -3
  17. package/dist/typescript/lib/types/internal.d.ts.map +1 -1
  18. package/dist/typescript/lib/version.d.ts +1 -1
  19. package/ios/RNFBAuth/RNFBAuthCacheRegistry.h +41 -0
  20. package/ios/RNFBAuth/RNFBAuthCacheRegistry.m +84 -0
  21. package/ios/RNFBAuth/RNFBAuthCacheRegistryStorage.swift +53 -0
  22. package/ios/RNFBAuth/RNFBAuthErrorCodeMapper.swift +333 -0
  23. package/ios/RNFBAuth/RNFBAuthHelper.m +147 -262
  24. package/ios/RNFBAuth/RNFBAuthListenerRegistry.h +39 -0
  25. package/ios/RNFBAuth/RNFBAuthListenerRegistry.m +83 -0
  26. package/ios/RNFBAuth/RNFBAuthListenerRegistryStorage.swift +76 -0
  27. package/ios/RNFBAuth/RNFBAuthNullSentinelDecoder.h +40 -0
  28. package/ios/RNFBAuth/RNFBAuthNullSentinelDecoder.m +49 -0
  29. package/ios/RNFBAuth.xcodeproj/project.pbxproj +22 -0
  30. package/ios/RNFBAuthUnitTests/RNFBAuthCacheRegistryStorageTests.swift +72 -0
  31. package/ios/RNFBAuthUnitTests/RNFBAuthCacheRegistryTests.m +131 -0
  32. package/ios/RNFBAuthUnitTests/RNFBAuthErrorCodeMapperTests.swift +250 -0
  33. package/ios/RNFBAuthUnitTests/RNFBAuthListenerRegistryStorageTests.swift +122 -0
  34. package/ios/RNFBAuthUnitTests/RNFBAuthListenerRegistryTests.m +152 -0
  35. package/ios/RNFBAuthUnitTests/RNFBAuthNullSentinelDecodeTests.m +78 -0
  36. package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/project.pbxproj +311 -0
  37. package/ios/RNFBAuthUnitTests/RNFBAuthUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBAuthUnitTests.xcscheme +69 -0
  38. package/lib/ActionCodeURL.ts +1 -2
  39. package/lib/index.ts +1 -1
  40. package/lib/types/internal.ts +2 -2
  41. package/lib/version.ts +1 -1
  42. package/package.json +5 -4
  43. package/plugin/build/index.js +4 -0
  44. package/plugin/build/ios/index.d.ts +2 -1
  45. package/plugin/build/ios/index.js +3 -1
  46. package/plugin/build/ios/openUrlFix.js +12 -0
  47. package/plugin/build/ios/sceneDelegate.d.ts +34 -0
  48. package/plugin/build/ios/sceneDelegate.js +127 -0
  49. package/plugin/build/ios/sceneLifecycle.d.ts +32 -0
  50. package/plugin/build/ios/sceneLifecycle.js +115 -0
  51. package/plugin/src/index.ts +9 -1
  52. package/plugin/src/ios/index.ts +2 -1
  53. package/plugin/src/ios/openUrlFix.ts +15 -0
  54. package/plugin/src/ios/sceneDelegate.ts +159 -0
  55. package/plugin/src/ios/sceneLifecycle.ts +134 -0
  56. package/plugin/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library 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
+
17
+ import XCTest
18
+
19
+ final class RNFBAuthErrorCodeMapperTests: XCTestCase {
20
+ /// Sample of known FIRAuthErrorCode raw values → JS codes.
21
+ func testSampleKnownCodes() {
22
+ XCTAssertEqual(
23
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17000),
24
+ "invalid-custom-token"
25
+ )
26
+ XCTAssertEqual(
27
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17008),
28
+ "invalid-email"
29
+ )
30
+ XCTAssertEqual(
31
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17020),
32
+ "network-request-failed"
33
+ )
34
+ XCTAssertEqual(
35
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17057),
36
+ "cancelled-popup-request"
37
+ )
38
+ XCTAssertEqual(
39
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17078),
40
+ "multi-factor-auth-required"
41
+ )
42
+ XCTAssertEqual(
43
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17999),
44
+ "internal-error"
45
+ )
46
+ }
47
+
48
+ /// Pre-port quirk: MissingEmail (17034) maps to invalid-email, not missing-email.
49
+ func testMissingEmailMapsToInvalidEmail() {
50
+ XCTAssertEqual(
51
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17034),
52
+ "invalid-email"
53
+ )
54
+ XCTAssertEqual(
55
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17008),
56
+ "invalid-email"
57
+ )
58
+ }
59
+
60
+ func testUnknownCodeReturnsNil() {
61
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 0))
62
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 17001))
63
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: 99999))
64
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: -1))
65
+ }
66
+
67
+ /// Sample of known FIRAuthErrorCode raw values → JS message overrides.
68
+ func testSampleKnownMessageOverrides() {
69
+ XCTAssertEqual(
70
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17000),
71
+ "The custom token format is incorrect. Please check the documentation."
72
+ )
73
+ XCTAssertEqual(
74
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17008),
75
+ "The email address is badly formatted."
76
+ )
77
+ XCTAssertEqual(
78
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17020),
79
+ "A network error has occurred, please try again."
80
+ )
81
+ XCTAssertEqual(
82
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17078),
83
+ "Please complete a second factor challenge to finish signing into this account."
84
+ )
85
+ XCTAssertEqual(
86
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17999),
87
+ "An internal error has occurred, please try again."
88
+ )
89
+ XCTAssertEqual(
90
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17042),
91
+ "The format of the phone number provided is incorrect. "
92
+ + "Please enter the phone number in a format that can be parsed into E.164 format. "
93
+ + "E.164 phone numbers are written in the format [+][country code][subscriber "
94
+ + "number including area code]."
95
+ )
96
+ }
97
+
98
+ func testUnknownMessageOverrideReturnsNil() {
99
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 0))
100
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17001))
101
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 17034))
102
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: 99999))
103
+ XCTAssertNil(RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: -1))
104
+ }
105
+
106
+ /// Full table — every pre-port `getJSError:` message override (coverage of switch DA).
107
+ func testAllPrePortMessageOverrides() {
108
+ let expected: [(Int, String)] = [
109
+ (
110
+ 17000,
111
+ "The custom token format is incorrect. Please check the documentation."
112
+ ),
113
+ (17002, "The custom token corresponds to a different audience."),
114
+ (17004, "The supplied auth credential is malformed or has expired."),
115
+ (17008, "The email address is badly formatted."),
116
+ (17009, "The password is invalid or the user does not have a password."),
117
+ (
118
+ 17024,
119
+ "The supplied credentials do not correspond to the previously signed in user."
120
+ ),
121
+ (
122
+ 17014,
123
+ "This operation is sensitive and requires recent authentication. Log in again "
124
+ + "before retrying this request."
125
+ ),
126
+ (
127
+ 17078,
128
+ "Please complete a second factor challenge to finish signing into this account."
129
+ ),
130
+ (
131
+ 17012,
132
+ "An account already exists with the same email address but different sign-in "
133
+ + "credentials. Sign in using a provider associated with this email address."
134
+ ),
135
+ (17007, "The email address is already in use by another account."),
136
+ (
137
+ 17025,
138
+ "This credential is already associated with a different user account."
139
+ ),
140
+ (17005, "The user account has been disabled by an administrator."),
141
+ (
142
+ 17021,
143
+ "The user's credential is no longer valid. The user must sign in again."
144
+ ),
145
+ (
146
+ 17011,
147
+ "There is no user record corresponding to this identifier. The user may have been "
148
+ + "deleted."
149
+ ),
150
+ (
151
+ 17017,
152
+ "The user's credential is no longer valid. The user must sign in again."
153
+ ),
154
+ (17026, "The given password is invalid."),
155
+ (
156
+ 17006,
157
+ "This operation is not allowed. You must enable this service in the console."
158
+ ),
159
+ (17020, "A network error has occurred, please try again."),
160
+ (17999, "An internal error has occurred, please try again."),
161
+ (
162
+ 17042,
163
+ "The format of the phone number provided is incorrect. "
164
+ + "Please enter the phone number in a format that can be parsed into E.164 format. "
165
+ + "E.164 phone numbers are written in the format [+][country code][subscriber "
166
+ + "number including area code]."
167
+ ),
168
+ ]
169
+
170
+ for (code, message) in expected {
171
+ XCTAssertEqual(
172
+ RNFBAuthErrorCodeMapper.jsErrorMessage(forAuthErrorCode: code),
173
+ message,
174
+ "FIRAuthErrorCode raw \(code) message override"
175
+ )
176
+ }
177
+ }
178
+
179
+ /// Full table — preserves every pre-port sparse-array entry (coverage of switch DA).
180
+ func testAllPrePortMappings() {
181
+ let expected: [(Int, String)] = [
182
+ (17000, "invalid-custom-token"),
183
+ (17002, "custom-token-mismatch"),
184
+ (17004, "invalid-credential"),
185
+ (17005, "user-disabled"),
186
+ (17006, "operation-not-allowed"),
187
+ (17007, "email-already-in-use"),
188
+ (17008, "invalid-email"),
189
+ (17009, "wrong-password"),
190
+ (17010, "too-many-requests"),
191
+ (17011, "user-not-found"),
192
+ (17012, "account-exists-with-different-credential"),
193
+ (17014, "requires-recent-login"),
194
+ (17015, "provider-already-linked"),
195
+ (17016, "no-such-provider"),
196
+ (17017, "invalid-user-token"),
197
+ (17020, "network-request-failed"),
198
+ (17021, "user-token-expired"),
199
+ (17023, "invalid-api-key"),
200
+ (17024, "user-mismatch"),
201
+ (17025, "credential-already-in-use"),
202
+ (17026, "weak-password"),
203
+ (17028, "app-not-authorized"),
204
+ (17029, "expired-action-code"),
205
+ (17030, "invalid-action-code"),
206
+ (17031, "invalid-message-payload"),
207
+ (17032, "invalid-sender"),
208
+ (17033, "invalid-recipient-email"),
209
+ (17034, "invalid-email"),
210
+ (17036, "missing-ios-bundle-id"),
211
+ (17037, "missing-android-pkg-name"),
212
+ (17038, "unauthorized-domain"),
213
+ (17039, "invalid-continue-uri"),
214
+ (17040, "missing-continue-uri"),
215
+ (17041, "missing-phone-number"),
216
+ (17042, "invalid-phone-number"),
217
+ (17043, "missing-verification-code"),
218
+ (17044, "invalid-verification-code"),
219
+ (17045, "missing-verification-id"),
220
+ (17046, "invalid-verification-id"),
221
+ (17047, "missing-app-credential"),
222
+ (17048, "invalid-app-credential"),
223
+ (17051, "code-expired"),
224
+ (17052, "quota-exceeded"),
225
+ (17053, "missing-apns-token"),
226
+ (17054, "notification-not-forwarded"),
227
+ (17055, "app-not-verified"),
228
+ (17056, "captcha-check-failed"),
229
+ (17057, "cancelled-popup-request"),
230
+ (17058, "popup-closed-by-user"),
231
+ (17059, "app-verification-user-interaction-failure"),
232
+ (17060, "invalid-oauth-client-id"),
233
+ (17061, "network-request-failed"),
234
+ (17062, "internal-error"),
235
+ (17067, "null-user"),
236
+ (17995, "keychain-error"),
237
+ (17999, "internal-error"),
238
+ (18000, "malformed-jwt"),
239
+ (17078, "multi-factor-auth-required"),
240
+ ]
241
+
242
+ for (code, js) in expected {
243
+ XCTAssertEqual(
244
+ RNFBAuthErrorCodeMapper.jsErrorCode(forAuthErrorCode: code),
245
+ js,
246
+ "FIRAuthErrorCode raw \(code)"
247
+ )
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library 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
+
17
+ import XCTest
18
+
19
+ private final class FakeRemovableHandle: NSObject {
20
+ private(set) var removeCount = 0
21
+
22
+ @objc func remove() {
23
+ removeCount += 1
24
+ }
25
+ }
26
+
27
+ final class RNFBAuthListenerRegistryStorageTests: XCTestCase {
28
+ private var storage: RNFBAuthListenerRegistryStorage!
29
+
30
+ override func setUp() {
31
+ super.setUp()
32
+ storage = RNFBAuthListenerRegistryStorage()
33
+ }
34
+
35
+ func testPutIfAbsent_storesWhenFree() {
36
+ let handle = FakeRemovableHandle()
37
+ XCTAssertTrue(storage.putIfAbsent("k1" as NSString, value: handle))
38
+ XCTAssertTrue(storage.get("k1" as NSString) === handle)
39
+ XCTAssertEqual(handle.removeCount, 0)
40
+ }
41
+
42
+ func testPutIfAbsent_collision_keepsExistingWithoutRemove() {
43
+ let first = FakeRemovableHandle()
44
+ let second = FakeRemovableHandle()
45
+ XCTAssertTrue(storage.putIfAbsent("k1" as NSString, value: first))
46
+ XCTAssertFalse(storage.putIfAbsent("k1" as NSString, value: second))
47
+ XCTAssertTrue(storage.get("k1" as NSString) === first)
48
+ XCTAssertEqual(first.removeCount, 0)
49
+ XCTAssertEqual(second.removeCount, 0)
50
+ }
51
+
52
+ func testPutOrDiscard_storesWhenFree() {
53
+ let handle = FakeRemovableHandle()
54
+ XCTAssertTrue(storage.putOrDiscard("k1" as NSString, value: handle))
55
+ XCTAssertTrue(storage.get("k1" as NSString) === handle)
56
+ XCTAssertEqual(handle.removeCount, 0)
57
+ }
58
+
59
+ func testPutOrDiscard_collision_removesIncoming() {
60
+ let first = FakeRemovableHandle()
61
+ let duplicate = FakeRemovableHandle()
62
+ XCTAssertTrue(storage.putOrDiscard("k1" as NSString, value: first))
63
+ XCTAssertFalse(storage.putOrDiscard("k1" as NSString, value: duplicate))
64
+ XCTAssertEqual(duplicate.removeCount, 1)
65
+ XCTAssertEqual(first.removeCount, 0)
66
+ XCTAssertTrue(storage.get("k1" as NSString) === first)
67
+ }
68
+
69
+ func testGet_whenFree_isNil() {
70
+ XCTAssertNil(storage.get("missing" as NSString))
71
+ }
72
+
73
+ func testTake_removesAndReturnsWithoutCallingRemove() {
74
+ let handle = FakeRemovableHandle()
75
+ XCTAssertTrue(storage.putIfAbsent("k1" as NSString, value: handle))
76
+ XCTAssertTrue(storage.take("k1" as NSString) === handle)
77
+ XCTAssertNil(storage.get("k1" as NSString))
78
+ XCTAssertEqual(handle.removeCount, 0)
79
+ }
80
+
81
+ func testTake_whenFree_isNil() {
82
+ XCTAssertNil(storage.take("missing" as NSString))
83
+ }
84
+
85
+ func testTakeAndRemove_removesAfterTake() {
86
+ let handle = FakeRemovableHandle()
87
+ XCTAssertTrue(storage.putIfAbsent("k1" as NSString, value: handle))
88
+ storage.takeAndRemove("k1" as NSString)
89
+ XCTAssertEqual(handle.removeCount, 1)
90
+ XCTAssertNil(storage.get("k1" as NSString))
91
+ }
92
+
93
+ func testTakeAndRemove_missingKey_isNoOp() {
94
+ storage.takeAndRemove("missing" as NSString)
95
+ }
96
+
97
+ func testTakeAndRemove_objectWithoutRemove_doesNotCrash() {
98
+ let plain = NSObject()
99
+ XCTAssertTrue(storage.putIfAbsent("plain" as NSString, value: plain))
100
+ storage.takeAndRemove("plain" as NSString)
101
+ XCTAssertNil(storage.get("plain" as NSString))
102
+ }
103
+
104
+ func testRemoveAll_removesSnapshotAndLeavesEmpty() {
105
+ let a = FakeRemovableHandle()
106
+ let b = FakeRemovableHandle()
107
+ XCTAssertTrue(storage.putIfAbsent("a" as NSString, value: a))
108
+ XCTAssertTrue(storage.putIfAbsent("b" as NSString, value: b))
109
+ storage.removeAll()
110
+ XCTAssertEqual(a.removeCount, 1)
111
+ XCTAssertEqual(b.removeCount, 1)
112
+ XCTAssertNil(storage.get("a" as NSString))
113
+ XCTAssertNil(storage.get("b" as NSString))
114
+ }
115
+
116
+ func testRemoveAll_objectWithoutRemove_doesNotCrash() {
117
+ let plain = NSObject()
118
+ XCTAssertTrue(storage.putIfAbsent("plain" as NSString, value: plain))
119
+ storage.removeAll()
120
+ XCTAssertNil(storage.get("plain" as NSString))
121
+ }
122
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library 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
+ */
17
+
18
+ #import <XCTest/XCTest.h>
19
+
20
+ #import "RNFBAuthListenerRegistry.h"
21
+ #import "RNFBHandleMap.h"
22
+
23
+ @interface FakeAuthListenerHandle : NSObject
24
+ @property(nonatomic, assign) NSInteger removeCount;
25
+ - (void)remove;
26
+ @end
27
+
28
+ @implementation FakeAuthListenerHandle
29
+ - (void)remove {
30
+ self.removeCount += 1;
31
+ }
32
+ @end
33
+
34
+ @interface RNFBAuthListenerRegistryTests : XCTestCase
35
+ @property(nonatomic, strong) RNFBAuthListenerRegistry *registry;
36
+ @end
37
+
38
+ @implementation RNFBAuthListenerRegistryTests
39
+
40
+ - (void)setUp {
41
+ [super setUp];
42
+ self.registry = [[RNFBAuthListenerRegistry alloc] init];
43
+ }
44
+
45
+ - (void)testPutGetTake_happyPath {
46
+ FakeAuthListenerHandle *handle = [[FakeAuthListenerHandle alloc] init];
47
+ NSError *error = nil;
48
+ XCTAssertTrue([self.registry put:@"app" value:handle error:&error]);
49
+ XCTAssertNil(error);
50
+ XCTAssertEqual(handle, [self.registry get:@"app"]);
51
+ XCTAssertEqual(handle, [self.registry take:@"app"]);
52
+ XCTAssertNil([self.registry get:@"app"]);
53
+ XCTAssertEqual(handle.removeCount, 0);
54
+ }
55
+
56
+ - (void)testPut_occupiedId_returnsCollision {
57
+ FakeAuthListenerHandle *first = [[FakeAuthListenerHandle alloc] init];
58
+ XCTAssertTrue([self.registry put:@"app" value:first error:nil]);
59
+
60
+ NSError *error = nil;
61
+ XCTAssertFalse([self.registry put:@"app"
62
+ value:[[FakeAuthListenerHandle alloc] init]
63
+ error:&error]);
64
+ XCTAssertNotNil(error);
65
+ XCTAssertEqualObjects(error.domain, RNFBHandleMapErrorDomain);
66
+ XCTAssertEqual(error.code, RNFBHandleMapErrorCollision);
67
+ XCTAssertEqual(first, [self.registry get:@"app"]);
68
+ }
69
+
70
+ - (void)testPut_occupiedId_nilErrorOut_returnsNo {
71
+ FakeAuthListenerHandle *first = [[FakeAuthListenerHandle alloc] init];
72
+ XCTAssertTrue([self.registry put:@"app" value:first error:nil]);
73
+ XCTAssertFalse([self.registry put:@"app" value:[[FakeAuthListenerHandle alloc] init] error:nil]);
74
+ XCTAssertEqual(first, [self.registry get:@"app"]);
75
+ }
76
+
77
+ - (void)testGet_whenFree_isNil {
78
+ XCTAssertNil([self.registry get:@"app"]);
79
+ }
80
+
81
+ - (void)testGet_whenOccupied_returnsHandle {
82
+ FakeAuthListenerHandle *first = [[FakeAuthListenerHandle alloc] init];
83
+ XCTAssertTrue([self.registry put:@"app" value:first error:nil]);
84
+ XCTAssertEqual(first, [self.registry get:@"app"]);
85
+ }
86
+
87
+ - (void)testPutOrDiscard_collision_removesIncoming {
88
+ FakeAuthListenerHandle *first = [[FakeAuthListenerHandle alloc] init];
89
+ FakeAuthListenerHandle *duplicate = [[FakeAuthListenerHandle alloc] init];
90
+ XCTAssertTrue([self.registry put:@"app" value:first error:nil]);
91
+ XCTAssertFalse([self.registry putOrDiscard:@"app" value:duplicate]);
92
+ XCTAssertEqual(duplicate.removeCount, 1);
93
+ XCTAssertEqual(first.removeCount, 0);
94
+ XCTAssertEqual(first, [self.registry get:@"app"]);
95
+ }
96
+
97
+ - (void)testPutOrDiscard_storesWhenFree {
98
+ FakeAuthListenerHandle *handle = [[FakeAuthListenerHandle alloc] init];
99
+ XCTAssertTrue([self.registry putOrDiscard:@"app" value:handle]);
100
+ XCTAssertEqual(handle, [self.registry get:@"app"]);
101
+ XCTAssertEqual(handle.removeCount, 0);
102
+ }
103
+
104
+ - (void)testTakeAndRemove_removesAfterTake {
105
+ FakeAuthListenerHandle *handle = [[FakeAuthListenerHandle alloc] init];
106
+ XCTAssertTrue([self.registry put:@"app" value:handle error:nil]);
107
+ [self.registry takeAndRemove:@"app"];
108
+ XCTAssertEqual(handle.removeCount, 1);
109
+ XCTAssertNil([self.registry get:@"app"]);
110
+ }
111
+
112
+ - (void)testTakeAndRemove_missingKey_isNoOp {
113
+ [self.registry takeAndRemove:@"missing"];
114
+ }
115
+
116
+ - (void)testRemoveAll_removesSnapshotAndLeavesEmpty {
117
+ FakeAuthListenerHandle *a = [[FakeAuthListenerHandle alloc] init];
118
+ FakeAuthListenerHandle *b = [[FakeAuthListenerHandle alloc] init];
119
+ XCTAssertTrue([self.registry put:@"a" value:a error:nil]);
120
+ XCTAssertTrue([self.registry put:@"b" value:b error:nil]);
121
+ [self.registry removeAll];
122
+ XCTAssertEqual(a.removeCount, 1);
123
+ XCTAssertEqual(b.removeCount, 1);
124
+ XCTAssertNil([self.registry get:@"a"]);
125
+ XCTAssertNil([self.registry get:@"b"]);
126
+ }
127
+
128
+ - (void)testPut_afterTake_allowsReuse {
129
+ FakeAuthListenerHandle *first = [[FakeAuthListenerHandle alloc] init];
130
+ FakeAuthListenerHandle *second = [[FakeAuthListenerHandle alloc] init];
131
+ XCTAssertTrue([self.registry put:@"app" value:first error:nil]);
132
+ XCTAssertEqual(first, [self.registry take:@"app"]);
133
+ XCTAssertTrue([self.registry put:@"app" value:second error:nil]);
134
+ XCTAssertEqual(second, [self.registry get:@"app"]);
135
+ XCTAssertEqual(first.removeCount, 0);
136
+ }
137
+
138
+ - (void)testRemoveAll_objectWithoutRemove_doesNotCrash {
139
+ NSObject *plain = [[NSObject alloc] init];
140
+ XCTAssertTrue([self.registry put:@"plain" value:plain error:nil]);
141
+ [self.registry removeAll];
142
+ XCTAssertNil([self.registry get:@"plain"]);
143
+ }
144
+
145
+ - (void)testTakeAndRemove_objectWithoutRemove_doesNotCrash {
146
+ NSObject *plain = [[NSObject alloc] init];
147
+ XCTAssertTrue([self.registry put:@"plain" value:plain error:nil]);
148
+ [self.registry takeAndRemove:@"plain"];
149
+ XCTAssertNil([self.registry get:@"plain"]);
150
+ }
151
+
152
+ @end
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library 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
+ */
17
+
18
+ #import <XCTest/XCTest.h>
19
+
20
+ #import "RNFBAuthNullSentinelDecoder.h"
21
+
22
+ @interface RNFBAuthNullSentinelDecodeTests : XCTestCase
23
+ @end
24
+
25
+ @implementation RNFBAuthNullSentinelDecodeTests
26
+
27
+ - (NSDictionary *)nullSentinel {
28
+ return @{@"__rnfbNull" : @YES};
29
+ }
30
+
31
+ - (NSDictionary *)unrelatedOneKeyDictionary {
32
+ return @{@".sv" : @"timestamp"};
33
+ }
34
+
35
+ - (void)testUpdateProfile_displayNameNullSentinel_becomesNil {
36
+ NSDictionary *props = @{@"displayName" : [self nullSentinel]};
37
+ NSDictionary *decoded = [RNFBAuthNullSentinelDecoder decodedProfileProps:props];
38
+ XCTAssertNil(decoded[@"displayName"]);
39
+ XCTAssertFalse([decoded[@"displayName"] isKindOfClass:[NSDictionary class]]);
40
+ }
41
+
42
+ - (void)testUpdateProfile_photoURLNullSentinel_becomesNil {
43
+ NSDictionary *props = @{@"photoURL" : [self nullSentinel]};
44
+ NSDictionary *decoded = [RNFBAuthNullSentinelDecoder decodedProfileProps:props];
45
+ XCTAssertNil(decoded[@"photoURL"]);
46
+ XCTAssertFalse([decoded[@"photoURL"] isKindOfClass:[NSDictionary class]]);
47
+ // Same path updateProfile uses: seam omit → subscript nil → clear arm (nil, not URLWithString:).
48
+ XCTAssertNil([RNFBAuthNullSentinelDecoder photoURLFromDecodedValue:decoded[@"photoURL"]]);
49
+ }
50
+
51
+ - (void)testUpdateProfile_photoURLFromDecodedValue_nilClearsWithoutURLWithString {
52
+ XCTAssertNil([RNFBAuthNullSentinelDecoder photoURLFromDecodedValue:nil]);
53
+ }
54
+
55
+ - (void)testUpdateProfile_photoURLFromDecodedValue_stringBecomesNSURL {
56
+ NSURL *url = [RNFBAuthNullSentinelDecoder photoURLFromDecodedValue:@"https://example.com/a.jpg"];
57
+ XCTAssertEqualObjects(url.absoluteString, @"https://example.com/a.jpg");
58
+ }
59
+
60
+ - (void)testUpdateProfile_displayNameNSNull_becomesNil {
61
+ NSDictionary *props = @{@"displayName" : [NSNull null]};
62
+ NSDictionary *decoded = [RNFBAuthNullSentinelDecoder decodedProfileProps:props];
63
+ XCTAssertNil(decoded[@"displayName"]);
64
+ }
65
+
66
+ - (void)testUpdateProfile_photoURLNSNull_becomesNil {
67
+ NSDictionary *props = @{@"photoURL" : [NSNull null]};
68
+ NSDictionary *decoded = [RNFBAuthNullSentinelDecoder decodedProfileProps:props];
69
+ XCTAssertNil(decoded[@"photoURL"]);
70
+ }
71
+
72
+ - (void)testUpdateProfile_unrelatedOneKeyDictionary_unchanged {
73
+ NSDictionary *props = @{@"displayName" : [self unrelatedOneKeyDictionary]};
74
+ NSDictionary *decoded = [RNFBAuthNullSentinelDecoder decodedProfileProps:props];
75
+ XCTAssertEqualObjects(decoded[@"displayName"], [self unrelatedOneKeyDictionary]);
76
+ }
77
+
78
+ @end