Procházet zdrojové kódy

Manually created dynamic links should be subject to allowed/blocked check (#5853)

* Fix implementation.

Fix unit tests.

Remove duplicate method.

Add comment.

Add unit tests.

Use deprecated annotation.

Fix the comment.

* Make changes additive.

* Address review comments.

* Review changes.

* Check data before serialization.

* Minor fix.

* Post rebase fix.

* Post rebase fix 2.

* Fix styling.

* Fix type.

* Post rebase fix.

* Remove kClientID.

* Code review comment addressed. Get rid of code duplication.
Alexander Perepelitsyn před 5 roky
rodič
revize
5035a99113

+ 1 - 0
FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.h

@@ -31,6 +31,7 @@ typedef void (^FIRPostInstallAttributionCompletionHandler)(
 
 /** A definition for a block used to return data and errors after an asynchronous task. */
 typedef void (^FIRNetworkRequestCompletionHandler)(NSData *_Nullable data,
+                                                   NSURLResponse *_Nullable response,
                                                    NSError *_Nullable error);
 
 // these enums must be in sync with google/firebase/dynamiclinks/v1/dynamic_links.proto

+ 78 - 36
FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.m

@@ -41,6 +41,7 @@ static NSString *const kFDLAnalyticsDataSourceKey = @"utmSource";
 static NSString *const kFDLAnalyticsDataMediumKey = @"utmMedium";
 static NSString *const kFDLAnalyticsDataCampaignKey = @"utmCampaign";
 static NSString *const kHeaderIosBundleIdentifier = @"X-Ios-Bundle-Identifier";
+static NSString *const kGenericErrorDomain = @"com.firebase.dynamicLinks";
 
 typedef NSDictionary *_Nullable (^FIRDLNetworkingParserBlock)(
     NSString *requestURLString,
@@ -67,7 +68,7 @@ void FIRMakeHTTPRequest(NSURLRequest *request, FIRNetworkRequestCompletionHandle
       [session dataTaskWithRequest:request
                  completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
                                      NSError *_Nullable error) {
-                   completion(data, error);
+                   completion(data, response, error);
                  }];
   [dataTask resume];
 }
@@ -91,6 +92,41 @@ NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nul
   return self;
 }
 
++ (nullable NSError *)extractErrorForShortLink:(NSURL *)url
+                                          data:(NSData *)data
+                                      response:(NSURLResponse *)response
+                                         error:(nullable NSError *)error {
+  if (error) {
+    return error;
+  }
+
+  NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
+  NSError *customError = nil;
+
+  if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
+    customError =
+        [NSError errorWithDomain:kGenericErrorDomain
+                            code:0
+                        userInfo:@{@"message" : @"Response should be of type NSHTTPURLResponse."}];
+  } else if ((statusCode < 200 || statusCode >= 300) && data) {
+    NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
+    if ([result isKindOfClass:[NSDictionary class]] && [result objectForKey:@"error"]) {
+      id err = [result objectForKey:@"error"];
+      customError = [NSError errorWithDomain:kGenericErrorDomain code:statusCode userInfo:err];
+    } else {
+      customError = [NSError
+          errorWithDomain:kGenericErrorDomain
+                     code:0
+                 userInfo:@{
+                   @"message" :
+                       [NSString stringWithFormat:@"Failed to resolve link: %@", url.absoluteString]
+                 }];
+    }
+  }
+
+  return customError;
+}
+
 #pragma mark - Public interface
 
 - (void)resolveShortLink:(NSURL *)url
@@ -108,34 +144,39 @@ NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nul
     @"sdk_version" : FDLSDKVersion
   };
 
-  FIRNetworkRequestCompletionHandler resolveLinkCallback = ^(NSData *data, NSError *error) {
-    NSURL *resolvedURL;
-
-    if (!error && data) {
-      NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
-      if ([result isKindOfClass:[NSDictionary class]]) {
-        id invitationIDObject = [result objectForKey:@"invitationId"];
-
-        NSString *invitationIDString;
-        if ([invitationIDObject isKindOfClass:[NSDictionary class]]) {
-          NSDictionary *invitationIDDictionary = invitationIDObject;
-          invitationIDString = invitationIDDictionary[@"id"];
-        } else if ([invitationIDObject isKindOfClass:[NSString class]]) {
-          invitationIDString = invitationIDObject;
+  FIRNetworkRequestCompletionHandler resolveLinkCallback =
+      ^(NSData *data, NSURLResponse *response, NSError *error) {
+        NSURL *resolvedURL = nil;
+        NSError *extractedError = [FIRDynamicLinkNetworking extractErrorForShortLink:url
+                                                                                data:data
+                                                                            response:response
+                                                                               error:error];
+
+        if (!extractedError && data) {
+          NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
+          if ([result isKindOfClass:[NSDictionary class]]) {
+            id invitationIDObject = [result objectForKey:@"invitationId"];
+
+            NSString *invitationIDString;
+            if ([invitationIDObject isKindOfClass:[NSDictionary class]]) {
+              NSDictionary *invitationIDDictionary = invitationIDObject;
+              invitationIDString = invitationIDDictionary[@"id"];
+            } else if ([invitationIDObject isKindOfClass:[NSString class]]) {
+              invitationIDString = invitationIDObject;
+            }
+
+            NSString *deepLinkString = result[kFDLResolvedLinkDeepLinkURLKey];
+            NSString *minAppVersion = result[kFDLResolvedLinkMinAppVersionKey];
+            NSString *utmSource = result[kFDLAnalyticsDataSourceKey];
+            NSString *utmMedium = result[kFDLAnalyticsDataMediumKey];
+            NSString *utmCampaign = result[kFDLAnalyticsDataCampaignKey];
+            resolvedURL = FIRDLDeepLinkURLWithInviteID(invitationIDString, deepLinkString,
+                                                       utmSource, utmMedium, utmCampaign, NO, nil,
+                                                       minAppVersion, self->_URLScheme, nil);
+          }
         }
-
-        NSString *deepLinkString = result[kFDLResolvedLinkDeepLinkURLKey];
-        NSString *minAppVersion = result[kFDLResolvedLinkMinAppVersionKey];
-        NSString *utmSource = result[kFDLAnalyticsDataSourceKey];
-        NSString *utmMedium = result[kFDLAnalyticsDataMediumKey];
-        NSString *utmCampaign = result[kFDLAnalyticsDataCampaignKey];
-        resolvedURL = FIRDLDeepLinkURLWithInviteID(invitationIDString, deepLinkString, utmSource,
-                                                   utmMedium, utmCampaign, NO, nil, minAppVersion,
-                                                   self->_URLScheme, nil);
-      }
-    }
-    handler(resolvedURL, error);
-  };
+        handler(resolvedURL, extractedError);
+      };
 
   NSString *requestURLString =
       [NSString stringWithFormat:@"%@/reopenAttribution%@", kiOSReopenRestBaseUrl,
@@ -242,13 +283,14 @@ NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nul
     }
   };
 
-  FIRNetworkRequestCompletionHandler convertInvitationCallback = ^(NSData *data, NSError *error) {
-    if (handler) {
-      dispatch_async(dispatch_get_main_queue(), ^{
-        handler(error);
-      });
-    }
-  };
+  FIRNetworkRequestCompletionHandler convertInvitationCallback =
+      ^(NSData *data, NSURLResponse *response, NSError *error) {
+        if (handler) {
+          dispatch_async(dispatch_get_main_queue(), ^{
+            handler(error);
+          });
+        }
+      };
 
   NSString *requestURL = [NSString stringWithFormat:@"%@/convertInvitation%@", kApiaryRestBaseUrl,
                                                     FIRDynamicLinkAPIKeyParameter(_APIKey)];
@@ -270,7 +312,7 @@ NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nul
       stringWithFormat:@"%@/%@%@", baseURL, endpointPath, FIRDynamicLinkAPIKeyParameter(_APIKey)];
 
   FIRNetworkRequestCompletionHandler completeInvitationByDeviceCallback =
-      ^(NSData *data, NSError *error) {
+      ^(NSData *data, NSURLResponse *response, NSError *error) {
         if (error || !data) {
           dispatch_async(dispatch_get_main_queue(), ^{
             handler(nil, nil, error);

+ 25 - 10
FirebaseDynamicLinks/Sources/FIRDynamicLinks.m

@@ -397,7 +397,10 @@ static const NSInteger FIRErrorCodeDurableDeepLinkFailed = -119;
   return nil;
 }
 
-- (nullable FIRDynamicLink *)dynamicLinkFromUniversalLinkURL:(NSURL *)url {
+- (nullable FIRDynamicLink *)
+    dynamicLinkInternalFromUniversalLinkURL:(NSURL *)url
+                                 completion:
+                                     (nullable FIRDynamicLinkUniversalLinkHandler)completion {
   if ([self canParseUniversalLinkURL:url]) {
     if (url.query.length > 0) {
       NSDictionary *parameters = FIRDLDictionaryFromQuery(url.query);
@@ -414,8 +417,10 @@ static const NSInteger FIRErrorCodeDurableDeepLinkFailed = -119;
           [self.dynamicLinkNetworking
               resolveShortLink:url
                  FDLSDKVersion:FIRFirebaseVersion()
-                    completion:^(NSURL *_Nullable resolverURL, NSError *_Nullable resolverError){
-                        // Nothing to do
+                    completion:^(NSURL *_Nullable resolverURL, NSError *_Nullable resolverError) {
+                      if (completion) {
+                        completion(dynamicLink, resolverError);
+                      }
                     }];
 #ifdef GIN_SCION_LOGGING
           FIRDLLogEventToScion(FIRDLLogEventAppOpen, parameters[kFIRDLParameterSource],
@@ -427,9 +432,21 @@ static const NSInteger FIRErrorCodeDurableDeepLinkFailed = -119;
       }
     }
   }
+  if (completion) {
+    completion(nil, nil);
+  }
   return nil;
 }
 
+- (nullable FIRDynamicLink *)dynamicLinkFromUniversalLinkURL:(NSURL *)url {
+  return [self dynamicLinkInternalFromUniversalLinkURL:url completion:nil];
+}
+
+- (void)dynamicLinkFromUniversalLinkURL:(NSURL *)url
+                             completion:(FIRDynamicLinkUniversalLinkHandler)completion {
+  [self dynamicLinkInternalFromUniversalLinkURL:url completion:completion];
+}
+
 - (BOOL)handleUniversalLink:(NSURL *)universalLinkURL
                  completion:(FIRDynamicLinkUniversalLinkHandler)completion {
   if ([self matchesShortLinkFormat:universalLinkURL]) {
@@ -448,14 +465,12 @@ static const NSInteger FIRErrorCodeDurableDeepLinkFailed = -119;
                 }];
     return YES;
   } else {
-    FIRDynamicLink *dynamicLink = [self dynamicLinkFromUniversalLinkURL:universalLinkURL];
-    if (dynamicLink) {
-      completion(dynamicLink, nil);
-      return YES;
-    }
+    [self dynamicLinkFromUniversalLinkURL:universalLinkURL completion:completion];
+    BOOL canHandleUniversalLink =
+        [self canParseUniversalLinkURL:universalLinkURL] && universalLinkURL.query.length > 0 &&
+        FIRDLDictionaryFromQuery(universalLinkURL.query)[kFIRDLParameterLink];
+    return canHandleUniversalLink;
   }
-
-  return NO;
 }
 
 - (void)resolveShortLink:(NSURL *)url completion:(FIRDynamicLinkResolverHandler)completion {

+ 18 - 3
FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/FIRDynamicLinks.h

@@ -67,6 +67,21 @@ NS_SWIFT_NAME(DynamicLinks)
 - (nullable FIRDynamicLink *)dynamicLinkFromCustomSchemeURL:(NSURL *)url
     NS_SWIFT_NAME(dynamicLink(fromCustomSchemeURL:));
 
+/**
+ * @method dynamicLinkFromUniversalLinkURL:completion:
+ * @abstract Get a Dynamic Link from a universal link URL. This method parses universal link
+ *     URLs, for instance,
+ *     "https://example.page.link?link=https://www.google.com&ibi=com.google.app&ius=comgoogleapp".
+ *     It is suggested to call it inside your |UIApplicationDelegate|'s
+ *     |application:continueUserActivity:restorationHandler:| method.
+ * @param url Custom scheme URL.
+ * @param completion A block that handles the outcome of attempting to get a Dynamic Link from a
+ * universal link URL.
+ */
+- (void)dynamicLinkFromUniversalLinkURL:(NSURL *)url
+                             completion:(FIRDynamicLinkUniversalLinkHandler)completion
+    NS_SWIFT_NAME(dynamicLink(fromUniversalLink:completion:));
+
 /**
  * @method dynamicLinkFromUniversalLinkURL:
  * @abstract Get a Dynamic Link from a universal link URL. This method parses universal link
@@ -78,12 +93,12 @@ NS_SWIFT_NAME(DynamicLinks)
  * @return Dynamic Link object if the URL is valid and has link parameter, otherwise nil.
  */
 - (nullable FIRDynamicLink *)dynamicLinkFromUniversalLinkURL:(NSURL *)url
-    NS_SWIFT_NAME(dynamicLink(fromUniversalLink:));
+    NS_SWIFT_NAME(dynamicLink(fromUniversalLink:))
+        DEPRECATED_MSG_ATTRIBUTE("Use dynamicLinkFromUniversalLinkURL:completion: instead.");
 
 /**
  * @method handleUniversalLink:completion:
- * @abstract Convenience method to handle a Universal Link whether it is long or short. A long link
- *     will call the handler immediately, but a short link may not.
+ * @abstract Convenience method to handle a Universal Link whether it is long or short.
  * @param url A Universal Link URL.
  * @param completion A block that handles the outcome of attempting to create a FIRDynamicLink.
  * @return YES if FIRDynamicLinks is handling the link, otherwise, NO.

+ 1 - 1
FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinkNetworkingTests.m

@@ -73,7 +73,7 @@ static const NSTimeInterval kAsyncTestTimout = 0.5;
   void (^executeRequestBlock)(id, NSDictionary *, NSString *, FIRNetworkRequestCompletionHandler) =
       ^(id p1, NSDictionary *requestBody, NSString *requestURLString,
         FIRNetworkRequestCompletionHandler handler) {
-        handler(nil, nil);
+        handler(nil, nil, nil);
       };
 
   SEL executeRequestSelector = @selector(executeOnePlatformRequest:forURL:completionHandler:);

+ 322 - 3
FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m

@@ -506,6 +506,31 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   UnswizzleDynamicLinkNetworking();
 }
 
+- (void)testDynamicLinkFromUniversalLinkURLCompletionWithCustomDomainLink {
+  self.service = [[FIRDynamicLinks alloc] init];
+  NSString *durableDeepLinkString = @"https://a.firebase.com/mypath/?link=abcd";
+  NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString];
+
+  SwizzleDynamicLinkNetworkingWithMock();
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:durabledeepLinkURL
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             XCTAssertNotNil(dynamicLink);
+                             NSString *deepLinkURLString = dynamicLink.url.absoluteString;
+
+                             XCTAssertEqualObjects(
+                                 @"abcd", deepLinkURLString,
+                                 @"ddl url parameter and deep link url should be the same");
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+
+  UnswizzleDynamicLinkNetworking();
+}
+
 - (void)testDynamicLinkFromUniversalLinkURLWithSpecialCharacters {
   NSString *durableDeepLinkString =
       [NSString stringWithFormat:@"https://xyz.page.link/?link=%@", kEncodedComplicatedURLString];
@@ -522,6 +547,30 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   UnswizzleDynamicLinkNetworking();
 }
 
+- (void)testDynamicLinkFromUniversalLinkURLCompletionWithSpecialCharacters {
+  NSString *durableDeepLinkString =
+      [NSString stringWithFormat:@"https://xyz.page.link/?link=%@", kEncodedComplicatedURLString];
+  NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString];
+
+  SwizzleDynamicLinkNetworkingWithMock();
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:durabledeepLinkURL
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             NSString *deepLinkURLString = dynamicLink.url.absoluteString;
+
+                             XCTAssertEqualObjects(
+                                 kDecodedComplicatedURLString, deepLinkURLString,
+                                 @"ddl url parameter and deep link url should be the same");
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+
+  UnswizzleDynamicLinkNetworking();
+}
+
 - (void)testDynamicLinkFromUniversalLinkURLWithEncodedCharacters {
   NSString *durableDeepLinkString =
       [NSString stringWithFormat:@"https://xyz.page.link/?link=%@", kEncodedComplicatedURLString];
@@ -538,6 +587,30 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   UnswizzleDynamicLinkNetworking();
 }
 
+- (void)testDynamicLinkFromUniversalLinkURLCompletionWithEncodedCharacters {
+  NSString *durableDeepLinkString =
+      [NSString stringWithFormat:@"https://xyz.page.link/?link=%@", kEncodedComplicatedURLString];
+  NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString];
+
+  SwizzleDynamicLinkNetworkingWithMock();
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:durabledeepLinkURL
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             NSString *deepLinkURLString = dynamicLink.url.absoluteString;
+
+                             XCTAssertEqualObjects(
+                                 kDecodedComplicatedURLString, deepLinkURLString,
+                                 @"ddl url parameter and deep link url should be the same");
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+
+  UnswizzleDynamicLinkNetworking();
+}
+
 - (void)testUniversalLink_DeepLink {
   NSString *deepLinkString = @"https://www.google.com/maps/place/Minneapolis";
   NSString *webPageURLString =
@@ -558,6 +631,34 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   UnswizzleDynamicLinkNetworking();
 }
 
+- (void)testUniversalLinkWithCompletion_DeepLink {
+  NSString *deepLinkString = @"https://www.google.com/maps/place/Minneapolis";
+  NSString *webPageURLString =
+      [NSString stringWithFormat:kStructuredUniversalLinkFmtDeepLink, deepLinkString];
+  NSURL *url = [NSURL URLWithString:webPageURLString];
+
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  SwizzleDynamicLinkNetworkingWithMock();
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:url
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             XCTAssertEqual(dynamicLink.matchConfidence,
+                                            FIRDynamicLinkMatchConfidenceStrong);
+                             XCTAssertEqualObjects(dynamicLink.url.absoluteString, deepLinkString);
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+
+  UnswizzleDynamicLinkNetworking();
+}
+
 - (void)testUniversalLink_DeepLinkWithParameters {
   NSString *deepLinkString = @"https://www.google.com?key1%3Dvalue1%26key2%3Dvalue2";
   NSString *parsedDeepLinkString = @"https://www.google.com?key1=value1&key2=value2";
@@ -578,6 +679,34 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   UnswizzleDynamicLinkNetworking();
 }
 
+- (void)testUniversalLinkWithCompletion_DeepLinkWithParameters {
+  NSString *deepLinkString = @"https://www.google.com?key1%3Dvalue1%26key2%3Dvalue2";
+  NSString *parsedDeepLinkString = @"https://www.google.com?key1=value1&key2=value2";
+  NSString *webPageURLString =
+      [NSString stringWithFormat:kStructuredUniversalLinkFmtDeepLink, deepLinkString];
+  NSURL *url = [NSURL URLWithString:webPageURLString];
+
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  SwizzleDynamicLinkNetworkingWithMock();
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service dynamicLinkFromUniversalLinkURL:url
+                                     completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                                  NSError *_Nullable error) {
+                                       XCTAssertEqual(dynamicLink.matchConfidence,
+                                                      FIRDynamicLinkMatchConfidenceStrong);
+                                       XCTAssertEqualObjects(dynamicLink.url.absoluteString,
+                                                             parsedDeepLinkString);
+                                       [expectation fulfill];
+                                     }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+  UnswizzleDynamicLinkNetworking();
+}
+
 - (void)testResolveLinkReturnsDLWithNilMinAppVersionWhenNotPresent {
   [self.service setUpWithLaunchOptions:nil
                                 apiKey:kAPIKey
@@ -593,7 +722,7 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
         NSDictionary *dictionary = @{kFDLResolvedLinkDeepLinkURLKey : kEncodedComplicatedURLString};
         NSData *data = FIRDataWithDictionary(dictionary, nil);
 
-        handler(data, nil);
+        handler(data, nil, nil);
       };
 
   SEL executeRequestSelector = @selector(executeOnePlatformRequest:forURL:completionHandler:);
@@ -634,8 +763,11 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
           kFDLResolvedLinkMinAppVersionKey : expectedMinVersion,
         };
         NSData *data = FIRDataWithDictionary(dictionary, nil);
-
-        handler(data, nil);
+        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url
+                                                                  statusCode:200
+                                                                 HTTPVersion:nil
+                                                                headerFields:nil];
+        handler(data, response, nil);
       };
 
   SEL executeRequestSelector = @selector(executeOnePlatformRequest:forURL:completionHandler:);
@@ -715,6 +847,28 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   XCTAssertNil(minVersion, @"Min app version was not nil when not set.");
 }
 
+- (void)testDynamicLinkFromUniversalLinkURLCompletionReturnsDLWithNilMinimumVersion {
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  NSURL *url = FIRDLDeepLinkURLWithInviteID(nil, kEncodedComplicatedURLString, nil, nil, nil, NO,
+                                            nil, nil, kURLScheme, nil);
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:url
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             NSString *minVersion = dynamicLink.minimumAppVersion;
+
+                             XCTAssertNil(minVersion, @"Min app version was not nil when not set.");
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+}
+
 - (void)testDynamicLinkFromUniversalLinkURLReturnsDLMinimumVersion {
   NSString *expectedMinVersion = @"03-9g03hfd";
   NSString *urlSuffix =
@@ -735,6 +889,33 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   XCTAssertEqualObjects(expectedMinVersion, minVersion, @"Min version didn't match imv= parameter");
 }
 
+- (void)testDynamicLinkFromUniversalLinkURLCompletionReturnsDLMinimumVersion {
+  NSString *expectedMinVersion = @"03-9g03hfd";
+  NSString *urlSuffix =
+      [NSString stringWithFormat:@"%@&imv=%@", kEncodedComplicatedURLString, expectedMinVersion];
+  NSString *urlString =
+      [NSString stringWithFormat:kStructuredUniversalLinkFmtSubdomainDeepLink, urlSuffix];
+  NSURL *url = [NSURL URLWithString:urlString];
+
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:url
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             NSString *minVersion = dynamicLink.minimumAppVersion;
+
+                             XCTAssertEqualObjects(expectedMinVersion, minVersion,
+                                                   @"Min version didn't match imv= parameter");
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+}
+
 - (void)testUniversalLinkWithSubdomain_DeepLink {
   NSString *deepLinkString = @"https://www.google.com/maps/place/Minneapolis";
   NSString *webPageURLString =
@@ -754,6 +935,34 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   UnswizzleDynamicLinkNetworking();
 }
 
+- (void)testUniversalLinkWithCompletionWithSubdomain_DeepLink {
+  NSString *deepLinkString = @"https://www.google.com/maps/place/Minneapolis";
+  NSString *webPageURLString =
+      [NSString stringWithFormat:kStructuredUniversalLinkFmtSubdomainDeepLink, deepLinkString];
+  NSURL *url = [NSURL URLWithString:webPageURLString];
+
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  SwizzleDynamicLinkNetworkingWithMock();
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service
+      dynamicLinkFromUniversalLinkURL:url
+                           completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                        NSError *_Nullable error) {
+                             XCTAssertEqual(dynamicLink.matchConfidence,
+                                            FIRDynamicLinkMatchConfidenceStrong);
+                             XCTAssertEqualObjects(dynamicLink.url.absoluteString, deepLinkString);
+                             [expectation fulfill];
+                           }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+
+  UnswizzleDynamicLinkNetworking();
+}
+
 - (void)testUniversalLinkWithSubdomain_DeepLinkWithParameters {
   NSString *deepLinkString = @"https://www.google.com?key1%3Dvalue1%26key2%3Dvalue2";
   NSString *parsedDeepLinkString = @"https://www.google.com?key1=value1&key2=value2";
@@ -771,6 +980,116 @@ static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustom
   XCTAssertEqualObjects(dynamicLink.url.absoluteString, parsedDeepLinkString);
 }
 
+- (void)testUniversalLinkWithCompletionWithSubdomain_DeepLinkWithParameters {
+  NSString *deepLinkString = @"https://www.google.com?key1%3Dvalue1%26key2%3Dvalue2";
+  NSString *parsedDeepLinkString = @"https://www.google.com?key1=value1&key2=value2";
+  NSString *webPageURLString =
+      [NSString stringWithFormat:kStructuredUniversalLinkFmtSubdomainDeepLink, deepLinkString];
+  NSURL *url = [NSURL URLWithString:webPageURLString];
+
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"];
+  [self.service dynamicLinkFromUniversalLinkURL:url
+                                     completion:^(FIRDynamicLink *_Nullable dynamicLink,
+                                                  NSError *_Nullable error) {
+                                       XCTAssertEqual(dynamicLink.matchConfidence,
+                                                      FIRDynamicLinkMatchConfidenceStrong);
+                                       XCTAssertEqualObjects(dynamicLink.url.absoluteString,
+                                                             parsedDeepLinkString);
+                                       [expectation fulfill];
+                                     }];
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+}
+
+- (void)testResolveLinkRespectsResponseSuccessStatusCode {
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  NSString *urlString = @"http://domain";
+  NSURL *url = [NSURL URLWithString:urlString];
+
+  void (^executeRequestBlock)(id, NSDictionary *, NSString *, FIRNetworkRequestCompletionHandler) =
+      ^(id p1, NSDictionary *requestBody, NSString *requestURLString,
+        FIRNetworkRequestCompletionHandler handler) {
+        NSData *data = FIRDataWithDictionary(@{}, nil);
+        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url
+                                                                  statusCode:200
+                                                                 HTTPVersion:nil
+                                                                headerFields:nil];
+        handler(data, response, nil);
+      };
+
+  SEL executeRequestSelector = @selector(executeOnePlatformRequest:forURL:completionHandler:);
+  [GULSwizzler swizzleClass:[FIRDynamicLinkNetworking class]
+                   selector:executeRequestSelector
+            isClassSelector:NO
+                  withBlock:executeRequestBlock];
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"handler called"];
+
+  [self.service resolveShortLink:url
+                      completion:^(NSURL *_Nullable url, NSError *_Nullable error) {
+                        XCTAssertNotNil(url);
+                        XCTAssertNil(error);
+                        [expectation fulfill];
+                      }];
+
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+}
+
+- (void)testResolveLinkRespectsResponseErrorStatusCode {
+  [self.service setUpWithLaunchOptions:nil
+                                apiKey:kAPIKey
+                             urlScheme:kURLScheme
+                          userDefaults:self.userDefaults];
+
+  NSString *urlString = @"http://domain";
+  NSURL *url = [NSURL URLWithString:urlString];
+
+  NSError *expectedError = [NSError
+      errorWithDomain:@"com.firebase.dynamicLinks"
+                 code:0
+             userInfo:@{
+               @"message" : [NSString stringWithFormat:@"Failed to resolve link: %@", urlString]
+             }];
+
+  void (^executeRequestBlock)(id, NSDictionary *, NSString *, FIRNetworkRequestCompletionHandler) =
+      ^(id p1, NSDictionary *requestBody, NSString *requestURLString,
+        FIRNetworkRequestCompletionHandler handler) {
+        NSData *data = FIRDataWithDictionary(@{}, nil);
+        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url
+                                                                  statusCode:400
+                                                                 HTTPVersion:nil
+                                                                headerFields:nil];
+        handler(data, response, nil);
+      };
+
+  SEL executeRequestSelector = @selector(executeOnePlatformRequest:forURL:completionHandler:);
+  [GULSwizzler swizzleClass:[FIRDynamicLinkNetworking class]
+                   selector:executeRequestSelector
+            isClassSelector:NO
+                  withBlock:executeRequestBlock];
+
+  XCTestExpectation *expectation = [self expectationWithDescription:@"handler called"];
+
+  [self.service resolveShortLink:url
+                      completion:^(NSURL *_Nullable url, NSError *_Nullable error) {
+                        XCTAssertNil(url);
+                        XCTAssertNotNil(error);
+                        XCTAssertEqualObjects(error, expectedError,
+                                              @"Handle universal link returned unexpected error");
+                        [expectation fulfill];
+                      }];
+
+  [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil];
+}
+
 - (void)testMatchesShortLinkFormat {
   NSArray<NSString *> *urlStrings =
       @[ @"https://test.app.goo.gl/xyz", @"https://test.app.goo.gl/xyz?link=" ];