diff --git a/FirebaseDynamicLinks/CHANGELOG.md b/FirebaseDynamicLinks/CHANGELOG.md index bfac33186a2..565534b4b8f 100644 --- a/FirebaseDynamicLinks/CHANGELOG.md +++ b/FirebaseDynamicLinks/CHANGELOG.md @@ -1,3 +1,6 @@ +# v7.3.0 +- [added] Manually created dynamic links should be subject to allowed/blocked check (#5853) + # v4.3.1 - [changed] Client id usage in api call and respective checks in the code. - [fixed] Fix attempts to connect to invalid ipv6 domain by updating ipv4 and ipv6 to use a single, valid endpoint (#5032) diff --git a/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.h b/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.h index 97a99311c49..0d810400288 100644 --- a/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.h +++ b/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 diff --git a/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.m b/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.m index 72bfa5e2bb0..b202744c257 100644 --- a/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.m +++ b/FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.m @@ -41,6 +41,7 @@ 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 @@ - (instancetype)initWithAPIKey:(NSString *)APIKey URLScheme:(NSString *)URLSchem 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 @@ - (void)resolveShortLink:(NSURL *)url @"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 @@ - (void)convertInvitation:(NSString *)invitationID } }; - 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 @@ - (void)sendRequestWithBaseURLString:(NSString *)baseURL 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); diff --git a/FirebaseDynamicLinks/Sources/FIRDynamicLinks.m b/FirebaseDynamicLinks/Sources/FIRDynamicLinks.m index 778823c11cd..56a893c0fab 100644 --- a/FirebaseDynamicLinks/Sources/FIRDynamicLinks.m +++ b/FirebaseDynamicLinks/Sources/FIRDynamicLinks.m @@ -397,7 +397,10 @@ - (nullable FIRDynamicLink *)dynamicLinkFromCustomSchemeURL:(NSURL *)url { 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 @@ - (nullable FIRDynamicLink *)dynamicLinkFromUniversalLinkURL:(NSURL *)url { [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 @@ - (nullable FIRDynamicLink *)dynamicLinkFromUniversalLinkURL:(NSURL *)url { } } } + 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 @@ - (BOOL)handleUniversalLink:(NSURL *)universalLinkURL }]; 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 { diff --git a/FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/FIRDynamicLinks.h b/FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/FIRDynamicLinks.h index e0da753e26b..326962c404a 100644 --- a/FirebaseDynamicLinks/Sources/Public/FirebaseDynamicLinks/FIRDynamicLinks.h +++ b/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. diff --git a/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinkNetworkingTests.m b/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinkNetworkingTests.m index 02ca28cc0bf..937fc62fb04 100644 --- a/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinkNetworkingTests.m +++ b/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinkNetworkingTests.m @@ -73,7 +73,7 @@ - (void)testResolveShortLinkServiceCompletionDoesntCrashWhenNilDataIsRetrieved { 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:); diff --git a/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m b/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m index ff14cd20638..bcb3154d980 100644 --- a/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m +++ b/FirebaseDynamicLinks/Tests/Unit/FIRDynamicLinksTest.m @@ -506,6 +506,31 @@ - (void)testDynamicLinkFromUniversalLinkURLWithCustomDomainLink { 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 @@ - (void)testDynamicLinkFromUniversalLinkURLWithSpecialCharacters { 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 @@ - (void)testDynamicLinkFromUniversalLinkURLWithEncodedCharacters { 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 @@ - (void)testUniversalLink_DeepLink { 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 @@ - (void)testUniversalLink_DeepLinkWithParameters { 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 @@ - (void)testResolveLinkReturnsDLWithNilMinAppVersionWhenNotPresent { 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 @@ - (void)testResolveLinkReturnsDLWithMinAppVersionWhenPresent { 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 @@ - (void)testDynamicLinkFromUniversalLinkURLReturnsDLWithNilMinimumVersion { 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 @@ - (void)testDynamicLinkFromUniversalLinkURLReturnsDLMinimumVersion { 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 @@ - (void)testUniversalLinkWithSubdomain_DeepLink { 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 @@ - (void)testUniversalLinkWithSubdomain_DeepLinkWithParameters { 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 *urlStrings = @[ @"https://test.app.goo.gl/xyz", @"https://test.app.goo.gl/xyz?link=" ];