/* * Copyright 2018 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import // This needs to precede the GULSwizzler+Unswizzle.h import for the --use-libraries build. #import #import #import #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" #import "FirebaseDynamicLinks/Sources/FIRDLDefaultRetrievalProcessV2.h" #import "FirebaseDynamicLinks/Sources/FIRDLRetrievalProcessFactory.h" #import "FirebaseDynamicLinks/Sources/FIRDLRetrievalProcessResult+Private.h" #import "FirebaseDynamicLinks/Sources/FIRDynamicLink+Private.h" #import "FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking+Private.h" #import "FirebaseDynamicLinks/Sources/FIRDynamicLinks+FirstParty.h" #import "FirebaseDynamicLinks/Sources/FIRDynamicLinks+Private.h" #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h" #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" static NSString *const kAPIKey = @"myAPIKey"; static NSString *const kStructuredLinkFmtFreeform = @"%@://google/link/%@"; static NSString *const kStructuredLinkFmtDeepLink = @"%@://google/link/?deep_link_id=%@"; static NSString *const kStructuredLinkFmtInvitation = @"%@://google/link/?invitation_id=%@"; static NSString *const kStructuredLinkFmtInvitationWeak = @"%@://google/link/?invitation_id=%@&match_type=weak"; static NSString *const kStructuredLinkFmtDeepLinkAndInvitation = @"%@://google/link/?deep_link_id=%@&invitation_id=%@"; static NSString *const kStructuredUniversalLinkFmtFreeForm = @"https://goo.gl/app/sample%@"; static NSString *const kStructuredUniversalLinkFmtDeepLink = @"https://goo.gl/app/sample?link=%@&isi=585027354"; static NSString *const kStructuredUniversalLinkFmtSubdomain = @"https://sample.page.link%@"; static NSString *const kStructuredUniversalLinkFmtSubdomainDeepLink = @"https://sample.page.link?link=%@&isi=585027354"; static NSString *const kURLScheme = @"gindeeplinkurl"; static const NSTimeInterval kAsyncTestTimout = 0.5; /** * This string was generated by percent-encoding the Tactile URL for the Tokyo American Club in * Tokyo, and then replacing a '%2B' with a '+' to verify that the '+' does not cause our parsing to * fail and double-encoding one value to verify that only one decoding pass is run. */ NSString *kEncodedComplicatedURLString = @"https%3A%2F%2Fwww.google.com%252Fmaps%2Fplace%2FTokyo+Am" @"erican%2BClub%2F%4035.658578%2C139.741588%2C3a%2C75y%2C90t%2Fdata%3D%213m8%211e2%213m6%211s42" @"66698%212e1%213e10%216s%252F%252Fstorage.googleapis.com%252Fstatic.panoramio.com%252Fphotos%2" @"52Fmedium%252F4266698.jpg%217i640%218i480%214m2%213m1%211s0x0000000000000000%3A0x1b8b8130c791" @"48e1%216m1%211e1"; /** This string was generated by percent-decoding kEncodedComplicatedURLString. */ NSString *kDecodedComplicatedURLString = @"https://www.google.com%2Fmaps/place/Tokyo+American+Club/" @"@35.658578,139.741588,3a,75y,90t/data=!3m8!1e2!3m6!1s4266698!2e1!3e10!6s%2F%2Fstorage.googlea" @"pis.com%2Fstatic.panoramio.com%2Fphotos%2Fmedium%2F4266698.jpg!7i640!8i480!4m2!3m1!1s0x000000" @"0000000000:0x1b8b8130c79148e1!6m1!1e1"; static void *kOpenURLHandlerKey = &kOpenURLHandlerKey; typedef NSURL * (^FakeShortLinkResolverHandler)(NSURL *shortLink); @interface FIRDynamicLinks (FIRApp) - (void)configureDynamicLinks:(FIRApp *)app; - (BOOL)setUpWithLaunchOptions:(nullable NSDictionary *)launchOptions apiKey:(NSString *)apiKey urlScheme:(nullable NSString *)urlScheme userDefaults:(nullable NSUserDefaults *)userDefaults; - (BOOL)canParseUniversalLinkURL:(nullable NSURL *)url; - (void)passRetrievedDynamicLinkToApplication:(NSURL *)url; - (BOOL)isOpenUrlMethodPresentInAppDelegate:(id)applicationDelegate; @end @interface FakeShortLinkResolver : FIRDynamicLinkNetworking + (instancetype)resolverWithBlock:(FakeShortLinkResolverHandler)resolverHandler; @end @implementation FakeShortLinkResolver { FakeShortLinkResolverHandler _resolverHandler; } + (instancetype)resolverWithBlock:(FakeShortLinkResolverHandler)resolverHandler { // The parameters don't matter since they aren't validated or used here. FakeShortLinkResolver *resolver = [[self alloc] initWithAPIKey:@"" URLScheme:@""]; resolver->_resolverHandler = [resolverHandler copy]; return resolver; } - (void)resolveShortLink:(NSURL *)url FDLSDKVersion:(NSString *)FDLSDKVersion completion:(FIRDynamicLinkResolverHandler)completion { if (_resolverHandler && completion) { NSURL *resolvedLink = _resolverHandler(url); completion(resolvedLink, nil); } } @end // dummy protocol to prevent compile warning @protocol DummyProtocol @property(atomic, assign) BOOL retrievingPendingDynamicLink; @property(nonatomic, readonly) FIRDynamicLinkNetworking *dynamicLinkNetworking; - (void)handlePendingDynamicLinkRetrievalFailureWithErrorCode:(NSInteger)errorCode errorDescription:(NSString *)errorDescription underlyingError:(nullable NSError *)underlyingError; @end // Swizzle DynamicLinks.dynamicLinkNetworking property to return fake resolver. static void SwizzleDynamicLinkNetworking(id linkResolver) { id (^dynamicLinkNetworkingBlock)(void) = ^id(void) { return linkResolver; }; [GULSwizzler swizzleClass:[FIRDynamicLinks class] selector:@selector(dynamicLinkNetworking) isClassSelector:NO withBlock:dynamicLinkNetworkingBlock]; } static void SwizzleDynamicLinkNetworkingWithMock() { id linkResolver = OCMPartialMock([[FIRDynamicLinkNetworking alloc] initWithAPIKey:kAPIKey URLScheme:kURLScheme]); [[linkResolver stub] resolveShortLink:OCMOCK_ANY FDLSDKVersion:@"1.0.0" completion:OCMOCK_ANY]; SwizzleDynamicLinkNetworking(linkResolver); } static void UnswizzleDynamicLinkNetworking() { [GULSwizzler unswizzleClass:[FIRDynamicLinks class] selector:@selector(dynamicLinkNetworking) isClassSelector:NO]; } @interface FIRDynamicLinksTest : XCTestCase { id _bundleMock; } // An instance of |GINDurableDeepLinkService| used for testing. @property(nonatomic, strong) FIRDynamicLinks *service; // An instance of |NSUserDefaults| that have all default values removed. @property(nonatomic, strong) NSUserDefaults *userDefaults; // FIRAnalytics mock. Necessary because we don't call [FIRAPP configure]. @property(nonatomic, strong) id analytics; @end @implementation FIRDynamicLinksTest // Disable deprecated warning for internal methods. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma mark - Test lifecycle static NSString *const kInfoPlistCustomDomainsKey = @"FirebaseDynamicLinksCustomDomains"; - (void)setUp { [super setUp]; // Mock the mainBundle infoDictionary with version from DL-Info.plist for custom domain testing. NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *filePath = [bundle pathForResource:@"DL-Info" ofType:@"plist"]; _bundleMock = OCMPartialMock([NSBundle mainBundle]); OCMStub([_bundleMock infoDictionary]) .andReturn([NSDictionary dictionaryWithContentsOfFile:filePath]); if (![FIRApp isDefaultAppConfigured]) { XCTAssertNoThrow([FIRApp configureWithOptions:[self appOptions]]); } self.service = [[FIRDynamicLinks alloc] init]; self.userDefaults = [[NSUserDefaults alloc] init]; [self.userDefaults removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]; self.analytics = OCMProtocolMock(@protocol(FIRAnalyticsInterop)); } - (void)tearDown { self.service = nil; self.userDefaults = nil; [self.analytics stopMocking]; self.analytics = nil; [_bundleMock stopMocking]; _bundleMock = nil; [super tearDown]; } - (FIROptions *)appOptions { // TODO: Evaluate if we want to hardcode things here instead. FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc" GCMSenderID:@"correct_gcm_sender_id"]; options.APIKey = @"correct_api_key"; options.projectID = @"abc-xyz-123"; return options; } #pragma mark - Set Up. - (void)testURLScheme_NoApiKey { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" BOOL setUpSucceed = [self.service setUpWithLaunchOptions:nil apiKey:nil urlScheme:nil userDefaults:nil]; #pragma clang diagnostic pop XCTAssertFalse(setUpSucceed, @"Should fail when apiKey is nil."); } - (void)testURLScheme_MinimumParameters { BOOL setUpSucceed = [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:nil userDefaults:nil]; XCTAssertTrue(setUpSucceed, @"Should not fail when apiKey is set."); } - (void)testFactoryMethodReturnsProperClassObject { id service = [FIRDynamicLinks dynamicLinks]; XCTAssertNotNil(service, @"Factory method returned nil"); XCTAssertEqualObjects([service class], [FIRDynamicLinks class], @"Factory returned incorrect class object"); } - (void)testURLScheme_LaunchOptionsWithCustomSchemeURL { NSString *deepLinkString = [NSString stringWithFormat:kStructuredLinkFmtDeepLink, [[NSBundle mainBundle] bundleIdentifier], @"abc123"]; NSDictionary *launchOptions = @{UIApplicationLaunchOptionsURLKey : [NSURL URLWithString:deepLinkString]}; [self.userDefaults setBool:NO forKey:kFIRDLReadDeepLinkAfterInstallKey]; [self.service setUpWithLaunchOptions:launchOptions apiKey:kAPIKey urlScheme:nil userDefaults:self.userDefaults]; XCTAssertTrue([self.userDefaults boolForKey:kFIRDLReadDeepLinkAfterInstallKey]); } - (void)testURLScheme_LaunchOptionsWithUniversalLinkURL { NSString *deepLinkString = [NSString stringWithFormat:kStructuredUniversalLinkFmtDeepLink, @"abc123"]; NSDictionary *launchOptions = @{UIApplicationLaunchOptionsURLKey : [NSURL URLWithString:deepLinkString]}; [self.userDefaults setBool:NO forKey:kFIRDLReadDeepLinkAfterInstallKey]; [self.service setUpWithLaunchOptions:launchOptions apiKey:kAPIKey urlScheme:nil userDefaults:self.userDefaults]; XCTAssertTrue([self.userDefaults boolForKey:kFIRDLReadDeepLinkAfterInstallKey]); } - (void)testURLScheme_LaunchOptionsWithInvalidURLWillNotResetUserDefaultsFlag { NSDictionary *launchOptions = @{UIApplicationLaunchOptionsURLKey : [NSURL URLWithString:@"https://www.google.com"]}; [self.userDefaults setBool:YES forKey:kFIRDLReadDeepLinkAfterInstallKey]; [self.service setUpWithLaunchOptions:launchOptions apiKey:kAPIKey urlScheme:nil userDefaults:self.userDefaults]; XCTAssertTrue([self.userDefaults boolForKey:kFIRDLReadDeepLinkAfterInstallKey]); } - (void)testURLScheme_Nil { BOOL setUpSucceed = [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:nil userDefaults:nil]; XCTAssertTrue(setUpSucceed); XCTAssertEqualObjects(self.service.URLScheme, [NSBundle mainBundle].bundleIdentifier); } - (void)testURLScheme_EmptyString { BOOL setUpSucceed = [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:@"" userDefaults:nil]; XCTAssertTrue(setUpSucceed); XCTAssertEqualObjects(self.service.URLScheme, [NSBundle mainBundle].bundleIdentifier); } - (void)testURLScheme_NonNil { [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; XCTAssertEqualObjects(self.service.URLScheme, kURLScheme, @"URLScheme should be the same as in argument"); } // TODO (b/63079414) Re-enable this - (void)DISABLED_testConfigNamedFIRApp { [self removeAllFIRApps]; id deepLinkServicePartialMock = OCMPartialMock([FIRDynamicLinks dynamicLinks]); [[deepLinkServicePartialMock reject] configureDynamicLinks:[OCMArg any]]; [FIRApp configureWithName:@"NonDefaultName" options:[FIROptions defaultOptions]]; [deepLinkServicePartialMock stopMocking]; } // TODO (b/37855379) re-enable the test - (void)DISABLED_testConfigForFIRApp { [self removeAllFIRApps]; id deepLinkServicePartialMock = OCMPartialMock([FIRDynamicLinks dynamicLinks]); [FIRApp configure]; OCMVerify([deepLinkServicePartialMock configureDynamicLinks:[OCMArg any]]); [deepLinkServicePartialMock stopMocking]; } #pragma mark - dynamicLinkFromCustomSchemeURL - (void)testCustomScheme_NoDeepLink { NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtFreeform, kURLScheme, @"abc123xyz"]; NSURL *url = [NSURL URLWithString:urlString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertNil(dynamicLink, @"invite should be nil since there is no parameter."); } - (void)testCustomScheme_DeepLinkOnly { NSString *deepLinkString = @"https://developers.google.com/products/"; NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtDeepLink, kURLScheme, deepLinkString]; NSURL *url = [NSURL URLWithString:urlString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertEqualObjects(dynamicLink.url.absoluteString, deepLinkString); XCTAssertNil(dynamicLink.inviteId); } - (void)testCustomScheme_InvitationOnly { NSString *invitationId = @"213920940217491274389172947"; NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtInvitation, kURLScheme, invitationId]; NSURL *url = [NSURL URLWithString:urlString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertNil(dynamicLink.url); XCTAssertEqualObjects(dynamicLink.inviteId, invitationId); } - (void)testCustomScheme_DeepLinkAndInvitation { NSString *deepLinkString = @"https://developers.google.com/products/"; NSString *invitationId = @"21392094021749127-4389172947"; NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtDeepLinkAndInvitation, [[NSBundle mainBundle] bundleIdentifier], deepLinkString, invitationId]; NSURL *url = [NSURL URLWithString:urlString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertEqualObjects(dynamicLink.url.absoluteString, deepLinkString); XCTAssertEqualObjects(dynamicLink.inviteId, invitationId); } - (void)testCustomScheme_FirstTimeOpenedWithCustomSchemeShouldGetStrongMatch { NSString *invitationId = @"21392094021749127-4389172947"; NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtInvitation, kURLScheme, invitationId]; NSURL *url = [NSURL URLWithString:urlString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertEqual(dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceStrong, @"matchConfidence should be strong when app is first opened with custom scheme."); XCTAssertNil(dynamicLink.url); XCTAssertEqualObjects(dynamicLink.inviteId, invitationId); } - (void)testCustomScheme_FirstTimeOpenedFromFingerprintCodepathShouldGetWeakMatch { NSString *invitationId = @"21392094021749127-4389172947"; NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtInvitationWeak, kURLScheme, invitationId]; NSURL *url = [NSURL URLWithString:urlString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertEqual( dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceWeak, @"matchConfidence should be weak when app is first opened from fingerprint codepath."); XCTAssertNil(dynamicLink.url); XCTAssertEqualObjects(dynamicLink.inviteId, invitationId); } - (void)testCustomScheme_StrongMatch { NSString *invitationId = @"21392094021749127-4389172947"; NSString *urlString = [NSString stringWithFormat:kStructuredLinkFmtInvitation, kURLScheme, invitationId]; NSURL *url = [NSURL URLWithString:urlString]; // Simulate opening the app. [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kFIRDLReadDeepLinkAfterInstallKey]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertEqual(dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceStrong, @"matchConfidence should be strong when opening an url after app is installed."); XCTAssertNil(dynamicLink.url); XCTAssertEqualObjects(dynamicLink.inviteId, invitationId); } - (void)testLinkParamWithPlus { [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; NSString *durableDeepLinkString = [NSString stringWithFormat:@"gindeeplinkurl://google/link?deep_link_id=%@", kEncodedComplicatedURLString]; NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:durabledeepLinkURL]; NSString *deepLinkURLString = dynamicLink.url.absoluteString; XCTAssertEqualObjects(kDecodedComplicatedURLString, deepLinkURLString, @"ddl url parameter and deep link url should be the same"); } #pragma mark - dynamicLinkFromUniversalLinkURL - (void)testUniversalLink_NoDeepLink { NSString *webPageURLString = [NSString stringWithFormat:kStructuredUniversalLinkFmtFreeForm, @"/abc123"]; NSURL *url = [NSURL URLWithString:webPageURLString]; NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; activity.webpageURL = [NSURL URLWithString:webPageURLString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertNil(dynamicLink, @"invite should be nil since there is no parameter."); } // Custom domain entries in plist file: // https://google.com // https://google.com/one // https://a.firebase.com/mypath - (void)testDynamicLinkFromUniversalLinkURLWithCustomDomainLink { self.service = [[FIRDynamicLinks alloc] init]; NSString *durableDeepLinkString = @"https://a.firebase.com/mypath/?link=abcd"; NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString]; SwizzleDynamicLinkNetworkingWithMock(); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:durabledeepLinkURL]; XCTAssertNotNil(dynamicLink); NSString *deepLinkURLString = dynamicLink.url.absoluteString; XCTAssertEqualObjects(@"abcd", deepLinkURLString, @"ddl url parameter and deep link url should be the same"); 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) { XCTAssertTrue([NSThread isMainThread]); 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]; NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString]; SwizzleDynamicLinkNetworkingWithMock(); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:durabledeepLinkURL]; NSString *deepLinkURLString = dynamicLink.url.absoluteString; XCTAssertEqualObjects(kDecodedComplicatedURLString, deepLinkURLString, @"ddl url parameter and deep link url should be the same"); 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) { XCTAssertTrue([NSThread isMainThread]); 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]; NSURL *durabledeepLinkURL = [NSURL URLWithString:durableDeepLinkString]; SwizzleDynamicLinkNetworkingWithMock(); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:durabledeepLinkURL]; NSString *deepLinkURLString = dynamicLink.url.absoluteString; XCTAssertEqualObjects(kDecodedComplicatedURLString, deepLinkURLString, @"ddl url parameter and deep link url should be the same"); 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) { XCTAssertTrue([NSThread isMainThread]); 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 = [NSString stringWithFormat:kStructuredUniversalLinkFmtDeepLink, deepLinkString]; NSURL *url = [NSURL URLWithString:webPageURLString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; SwizzleDynamicLinkNetworkingWithMock(); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:url]; XCTAssertEqual(dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceStrong); XCTAssertEqualObjects(dynamicLink.url.absoluteString, deepLinkString); 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) { XCTAssertTrue([NSThread isMainThread]); 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"; NSString *webPageURLString = [NSString stringWithFormat:kStructuredUniversalLinkFmtDeepLink, deepLinkString]; NSURL *url = [NSURL URLWithString:webPageURLString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; SwizzleDynamicLinkNetworkingWithMock(); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:url]; XCTAssertEqual(dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceStrong); XCTAssertEqualObjects(dynamicLink.url.absoluteString, parsedDeepLinkString); 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) { XCTAssertTrue([NSThread isMainThread]); 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 urlScheme:kURLScheme userDefaults:self.userDefaults]; NSString *urlString = @"http://reinl.page.link/t4ionvr"; NSURL *url = [NSURL URLWithString:urlString]; void (^executeRequestBlock)(id, NSDictionary *, NSString *, FIRNetworkRequestCompletionHandler) = ^(id p1, NSDictionary *requestBody, NSString *requestURLString, FIRNetworkRequestCompletionHandler handler) { NSDictionary *dictionary = @{kFDLResolvedLinkDeepLinkURLKey : kEncodedComplicatedURLString}; NSData *data = FIRDataWithDictionary(dictionary, nil); handler(data, nil, 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 handleUniversalLink:url completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { XCTAssertNil(dynamicLink.minimumAppVersion, @"Min app version not nil when not present."); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil]; } - (void)testResolveLinkReturnsDLWithMinAppVersionWhenPresent { NSString *expectedMinVersion = @"8g5u3e"; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; NSString *urlString = @"http://reinl.page.link/t4ionvr"; NSURL *url = [NSURL URLWithString:urlString]; void (^executeRequestBlock)(id, NSDictionary *, NSString *, FIRNetworkRequestCompletionHandler) = ^(id p1, NSDictionary *requestBody, NSString *requestURLString, FIRNetworkRequestCompletionHandler handler) { NSDictionary *dictionary = @{ kFDLResolvedLinkDeepLinkURLKey : kEncodedComplicatedURLString, kFDLResolvedLinkMinAppVersionKey : expectedMinVersion, }; NSData *data = FIRDataWithDictionary(dictionary, 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 handleUniversalLink:url completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { XCTAssertEqualObjects(expectedMinVersion, dynamicLink.minimumAppVersion, @"min app version did not match imv parameter."); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil]; } - (void)testUniversalLinkWithSubdomain_NoDeepLink { NSString *webPageURLString = [NSString stringWithFormat:kStructuredUniversalLinkFmtSubdomain, @"/abc123"]; NSURL *url = [NSURL URLWithString:webPageURLString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertNil(dynamicLink, @"deepLink should be nil since there is no parameter."); } - (void)testDynamicLinkFromCustomSchemeURLReturnsDLWithNilMinimumVersion { [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; NSURL *url = FIRDLDeepLinkURLWithInviteID(nil, kEncodedComplicatedURLString, nil, nil, nil, NO, nil, nil, kURLScheme, nil); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; XCTAssertNil(dynamicLink.minimumAppVersion, @"Min app version was not nil when not set."); } - (void)testDynamicLinkFromCustomSchemeURLReturnsDLMinimumVersion { NSString *expectedMinVersion = @"03-9g03hfd"; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; NSURL *url = FIRDLDeepLinkURLWithInviteID(nil, kEncodedComplicatedURLString, nil, nil, nil, NO, nil, expectedMinVersion, kURLScheme, nil); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromCustomSchemeURL:url]; NSString *minVersion = dynamicLink.minimumAppVersion; XCTAssertEqualObjects(expectedMinVersion, minVersion, @"Min version didn't match the min app version parameter"); } - (void)testDynamicLinkFromUniversalLinkURLReturnsDLWithNilMinimumVersion { [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; NSURL *url = FIRDLDeepLinkURLWithInviteID(nil, kEncodedComplicatedURLString, nil, nil, nil, NO, nil, nil, kURLScheme, nil); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:url]; NSString *minVersion = dynamicLink.minimumAppVersion; 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) { XCTAssertTrue([NSThread isMainThread]); 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 = [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]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:url]; NSString *minVersion = dynamicLink.minimumAppVersion; 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) { XCTAssertTrue([NSThread isMainThread]); 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 = [NSString stringWithFormat:kStructuredUniversalLinkFmtSubdomainDeepLink, deepLinkString]; NSURL *url = [NSURL URLWithString:webPageURLString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; SwizzleDynamicLinkNetworkingWithMock(); FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:url]; XCTAssertEqual(dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceStrong); XCTAssertEqualObjects(dynamicLink.url.absoluteString, deepLinkString); 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) { XCTAssertTrue([NSThread isMainThread]); 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"; NSString *webPageURLString = [NSString stringWithFormat:kStructuredUniversalLinkFmtSubdomainDeepLink, deepLinkString]; NSURL *url = [NSURL URLWithString:webPageURLString]; [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; FIRDynamicLink *dynamicLink = [self.service dynamicLinkFromUniversalLinkURL:url]; XCTAssertEqual(dynamicLink.matchConfidence, FIRDynamicLinkMatchConfidenceStrong); 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) { XCTAssertTrue([NSThread isMainThread]); 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=" ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertTrue(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } // Custom domain entries in plist file: // https://google.com // https://google.com/one // https://a.firebase.com/mypath - (void)testFailMatchesShortLinkFormatForCustomDomains { NSArray *urlStrings = @[ @"https://google.com", @"https://google.com?link=", @"https://a.firebase.com", @"https://a.firebase.com/mypath?link=", ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertFalse(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } // Custom domain entries in plist file: // https://google.com // https://google.com/one // https://a.firebase.com/mypath - (void)testPassMatchesShortLinkFormatForCustomDomains { NSArray *urlStrings = @[ @"https://google.com/xyz", @"https://google.com/xyz/?link=", @"https://google.com/xyz?link=", @"https://google.com/one/xyz", @"https://google.com/one/xyz?link=", @"https://google.com/one?utm_campaignlink=", @"https://google.com/mylink", @"https://google.com/one/mylink", @"https://a.firebase.com/mypath/mylink" ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertTrue(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } - (void)testPassMatchesShortLinkFormat { NSArray *urlStrings = @[ @"https://test.app.goo.gl/xyz", @"https://test.app.goo.gl/xyz?link=", ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertTrue(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } - (void)testFailMatchesShortLinkFormat { NSArray *urlStrings = @[ @"https://test.app.goo.gl", @"https://test.app.goo.gl?link=", @"https://test.app.goo.gl/", @"https://sample.page.link?link=https://google.com/test&ibi=com.google.sample&ius=79306483", @"https://sample.page.link/?link=https://google.com/test&ibi=com.google.sample&ius=79306483" ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertFalse(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } - (void)testMatchesUnversalLinkWithShortDurableLink { NSString *urlString = @"https://sample.page.link/79g49s"; NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShort = [self.service matchesShortLinkFormat:url]; XCTAssertTrue(matchesShort, @"Short Durable Link didn't match short link"); } - (void)testMatchesUnversalLinkWithAppInvite { NSString *urlString = @"https://sample.page.link/i/79g49s"; NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShort = [self.service matchesShortLinkFormat:url]; XCTAssertTrue(matchesShort, @"AppInvite didn't match short link"); } - (void)testDoesNotMatchesShortLinkFormatWithNonDDLDomains { NSArray *urlStrings = @[ @"https://www.google.com/maps/place/@1,1/My+Home/", @"https://mydomain.com/t439gfde", @"https://goo.gl/309dht4", @"https://59eh.goo.gl/309dht4", @"https://app.59eh.goo.gl/309dht4", @"https://goo.gl/i/309dht4", @"https://page.link/i/309dht4", @"https://fjo3eh.goo.gl/i/309dht4", @"https://app.fjo3eh.goo.gl/i/309dht4", @"https://1234.page.link/link/dismiss" ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertFalse(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } - (void)testHandleUniversalLinkWithShortLink { NSString *shortLinkString = @"https://sample.page.link/549igo"; NSString *bundleID = [NSBundle mainBundle].bundleIdentifier; NSString *customSchemeURLString = [NSString stringWithFormat:kStructuredLinkFmtDeepLink, bundleID, kEncodedComplicatedURLString]; XCTestExpectation *handleLinkCompletionExpectation = [self expectationWithDescription:@"handleLink"]; XCTestExpectation *linkResolverCompletionExpectation = [self expectationWithDescription:@"linkResolver"]; FakeShortLinkResolver *resolver = [FakeShortLinkResolver resolverWithBlock:^NSURL *(NSURL *shortLink) { [linkResolverCompletionExpectation fulfill]; return [NSURL URLWithString:customSchemeURLString]; }]; SwizzleDynamicLinkNetworking(resolver); [self.service handleUniversalLink:[NSURL URLWithString:shortLinkString] completion:^(FIRDynamicLink *_Nonnull dynamicLink, NSError *_Nullable error) { NSString *returnedURLString = dynamicLink.url.absoluteString; XCTAssertEqualObjects(kDecodedComplicatedURLString, returnedURLString, @"Handle universal link returned unexpected link"); [handleLinkCompletionExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil]; UnswizzleDynamicLinkNetworking(); } - (void)testHandleUniversalLinkWithLongLink { NSString *longLinkString = [NSString stringWithFormat:@"https://sample.page.link?link=%@&ibi=com.google.sample&ius=79306483", kEncodedComplicatedURLString]; XCTestExpectation *handleLinkCompletionExpectation = [self expectationWithDescription:@"handleLink"]; __block NSUInteger resolverInvocationsCount = 0; // should not be used. FakeShortLinkResolver *resolver = [FakeShortLinkResolver resolverWithBlock:^NSURL *(NSURL *shortLink) { resolverInvocationsCount++; return [NSURL URLWithString:kDecodedComplicatedURLString]; }]; id handleUniversalLinkBlock = ^(FIRDynamicLink *_Nonnull dynamicLink, NSError *_Nullable error) { [handleLinkCompletionExpectation fulfill]; NSURL *expectedResolvedLink = [NSURL URLWithString:kDecodedComplicatedURLString]; XCTAssertEqualObjects(expectedResolvedLink, dynamicLink.url, @"Resolve short link returned unexpected link"); }; SwizzleDynamicLinkNetworking(resolver); [self.service handleUniversalLink:[NSURL URLWithString:longLinkString] completion:handleUniversalLinkBlock]; [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil]; // It is expected to call resolveLink once for logging. XCTAssertEqual(resolverInvocationsCount, 1, @"handleUniversalLink called resolveLink more than once"); UnswizzleDynamicLinkNetworking(); } - (void)testHandleUniversalLinkCallsHandleUniversalLinkResolver { XCTestExpectation *handleLinkCompletionExpectation = [self expectationWithDescription:@"handleLink"]; void (^replacementBlock)(void) = ^{ [handleLinkCompletionExpectation fulfill]; }; SEL selectorToSwizzle = @selector(handleUniversalLink:completion:); [GULSwizzler swizzleClass:[FIRDynamicLinks class] selector:selectorToSwizzle isClassSelector:NO withBlock:replacementBlock]; NSURL *url = [NSURL URLWithString:@"https://google.com"]; [self.service handleUniversalLink:url completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error){ }]; [self waitForExpectationsWithTimeout:kAsyncTestTimout handler:nil]; [GULSwizzler unswizzleClass:[FIRDynamicLinks class] selector:selectorToSwizzle isClassSelector:NO]; } - (void)testHandleUniversalLinkCompletionReturnsNoForNonDDL { NSArray *urlStrings = @[ @"https://www.google.com/maps/place/@1,1/My+Home/", @"https://mydomain.com/t439gfde", @"https://goo.gl/309dht4", @"https://59eh.goo.gl/309dht4", @"https://app.59eh.goo.gl/309dht4", @"https://goo.gl/i/309dht4", @"https://page.link/i/309dht4", @"https://fjo3eh.goo.gl/i/309dht4", @"https://app.fjo3eh.goo.gl/i/309dht4", @"https://1234.page.link/link/dismiss" ]; [urlStrings enumerateObjectsUsingBlock:^(NSString *_Nonnull urlString, NSUInteger idx, BOOL *_Nonnull stop) { NSURL *url = [NSURL URLWithString:urlString]; BOOL handled = [self.service handleUniversalLink:url completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { XCTAssertNil(dynamicLink, @"Non DDL returned FIRDynamicLink"); }]; XCTAssertFalse(handled, @"Non DDL Universal Link was handled"); }]; } - (void)test_ensureInternalMethodsNotRenamed { // sanity check to ensure these methods has not been renamed // we relaying on these to be the same for tests to work properly // we are not exposing these methods in internal headers as we do not have "internal headers" // 1P apps can import all of our headers XCTAssertTrue([self.service respondsToSelector:@selector (handlePendingDynamicLinkRetrievalFailureWithErrorCode:errorDescription:underlyingError:)]); XCTAssertTrue([self.service respondsToSelector:@selector(setRetrievingPendingDynamicLink:)]); XCTAssertTrue([self.service respondsToSelector:@selector(dynamicLinkNetworking)]); } - (void)testCheckForPendingDynamicLinkReturnsImmediatelyIfAlreadyRead { id mockService = OCMPartialMock(self.service); [[mockService expect] handlePendingDynamicLinkRetrievalFailureWithErrorCode:-1 errorDescription:[OCMArg any] underlyingError:[OCMArg any]]; [[mockService reject] setRetrievingPendingDynamicLink:YES]; [mockService setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:nil userDefaults:[NSUserDefaults standardUserDefaults]]; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kFIRDLReadDeepLinkAfterInstallKey]; [mockService checkForPendingDynamicLink]; [mockService verify]; [mockService stopMocking]; } - (void)testRetrievalProcessResultURLContainsAllParametersPassedToDynamicLinkInitializer { NSDictionary *linkParameters = @{ @"deep_link_id" : @"https://mmaksym.com/test-app1", @"match_message" : @"Link is uniquely matched for this device.", @"match_type" : @"unique", @"utm_campaign" : @"Maksym M Test", @"utm_medium" : @"test_medium", @"utm_source" : @"test_source", @"a_parameter" : @"a_value" }; FIRDynamicLink *dynamicLink = [[FIRDynamicLink alloc] initWithParametersDictionary:linkParameters]; FIRDLRetrievalProcessResult *result = [[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:dynamicLink error:nil message:nil matchSource:nil]; NSURL *customSchemeURL = [result URLWithCustomURLScheme:@"scheme"]; XCTAssertNotNil(customSchemeURL); // Validate URL parameters NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:customSchemeURL resolvingAgainstBaseURL:NO]; XCTAssertNotNil(urlComponents); XCTAssertEqualObjects(urlComponents.scheme, @"scheme"); NSMutableDictionary *notEncodedParameters = [linkParameters mutableCopy]; for (NSURLQueryItem *queryItem in urlComponents.queryItems) { NSString *expectedValue = notEncodedParameters[queryItem.name]; XCTAssertNotNil(expectedValue, @"Extra parameter encoded: %@ = %@", queryItem.name, queryItem.value); XCTAssertEqualObjects(queryItem.value, expectedValue); [notEncodedParameters removeObjectForKey:queryItem.name]; } XCTAssertEqual(notEncodedParameters.count, 0, @"The parameters must have been encoded: %@", notEncodedParameters); } - (void)test_multipleRequestsToRetrievePendingDeepLinkShouldNotCrash { id mockService = OCMPartialMock(self.service); [[mockService expect] handlePendingDynamicLinkRetrievalFailureWithErrorCode:-1 errorDescription:[OCMArg any] underlyingError:[OCMArg any]]; // swizzle method to prevent actual retrieval, this will ensure that first pending link // retrieval will stuck and second will fail with error id (^replacementBlock)(void) = (id) ^ { return nil; }; SEL selectorToSwizzle = @selector(automaticRetrievalProcess); [GULSwizzler swizzleClass:[FIRDLRetrievalProcessFactory class] selector:selectorToSwizzle isClassSelector:NO withBlock:replacementBlock]; [mockService setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:kURLScheme userDefaults:self.userDefaults]; [mockService checkForPendingDynamicLink]; // we should not crash here [mockService checkForPendingDynamicLink]; [mockService verify]; [mockService stopMocking]; [GULSwizzler unswizzleClass:[FIRDLRetrievalProcessFactory class] selector:selectorToSwizzle isClassSelector:NO]; } - (void)test_retrievePendingDeepLinkShouldSetkFIRDLOpenURLKeyRegardlessOfFailures { [self.service setUpWithLaunchOptions:nil apiKey:kAPIKey urlScheme:nil userDefaults:[NSUserDefaults standardUserDefaults]]; FIRDynamicLinks *delegate = (FIRDynamicLinks *)self.service; // Error Result to pass FIRDLRetrievalProcessResult *result = [[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:nil error:[NSError errorWithDomain:@"unknown domain" code:500 userInfo:nil] message:nil matchSource:nil]; FIRDLDefaultRetrievalProcessV2 *defaultRetrievalProcess = [FIRDLDefaultRetrievalProcessV2 alloc]; [delegate retrievalProcess:defaultRetrievalProcess completedWithResult:result]; NSString *kFIRDLOpenURLKey = @"com.google.appinvite.openURL"; XCTAssertEqual([[NSUserDefaults standardUserDefaults] boolForKey:kFIRDLOpenURLKey], YES, @"kFIRDLOpenURL key should be set regardless of failures"); } - (void)test_passRetrievedDynamicLinkToApplicationDelegatesProperly { // Creating ApplicationDelegate partial mock object. id applicationDelegate = OCMPartialMock([UIApplication sharedApplication].delegate); // Creating FIRDynamicLinks partial mock object. id firebaseDynamicLinks = OCMPartialMock(self.service); // Stubbing Application delegate to return YES when application:openURL:options method is called. // Not sure why this is required as we are not concerned about its return, but without this, the // test will throw NSInvalidArgumentException with message "unrecognized selector sent to // instance". OCMStub([applicationDelegate application:[OCMArg any] openURL:[OCMArg any] options:[OCMArg any]]) .andReturn(YES); // Stubbing firebase dynamiclinks instance to return YES when isOpenUrlMethodPresentInAppDelegate // is called. OCMStub([firebaseDynamicLinks isOpenUrlMethodPresentInAppDelegate:[OCMArg any]]).andReturn(YES); // Executing the function with a URL. NSURL *url = [NSURL URLWithString:@"http://www.google.com"]; [firebaseDynamicLinks passRetrievedDynamicLinkToApplication:url]; // Verifying the application:openURL:options method is called in AppDelegate. OCMVerify([applicationDelegate application:[OCMArg any] openURL:url options:[OCMArg any]]); } #pragma mark - Self-diagnose tests - (void)testSelfDiagnoseWithNilCompletion { [FIRDynamicLinks performDiagnosticsWithCompletion:nil]; } - (void)testSelfDiagnoseCompletionCalled { XCTestExpectation *expectation = [self expectationWithDescription:@"Self diagnose completion block must be called"]; [FIRDynamicLinks performDiagnosticsWithCompletion:^(NSString *_Nonnull diagnosticOutput, BOOL hasErrors) { XCTAssert(diagnosticOutput.length > 0, @"Diagnostic expected to provide output"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:2.0 handler:nil]; } #pragma mark - Custom domain tests - (void)testValidCustomDomainNames { // Entries in plist file: // https://google.com // https://google.com/one // https://a.firebase.com/mypath NSArray *urlStrings = @[ @"https://google.com/mylink", // Short FDL starting with 'https://google.com' @"https://google.com/one", // Short FDL starting with 'https://google.com' @"https://google.com/one/mylink", // Short FDL starting with 'https://google.com/one' @"https://a.firebase.com/mypath/mylink", // Short FDL starting https://a.firebase.com/mypath ]; NSArray *longFDLURLStrings = @[ @"https://a.firebase.com/mypath/?link=abcd&test=1", // Long FDL starting with // https://a.firebase.com/mypath @"https://google.com/?link=abcd", // Long FDL starting with 'https://google.com' ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertTrue(matchesShortLinkFormat, @"URL did not validate as short link: %@", url); } for (NSString *urlString in longFDLURLStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesLongLinkFormat = [self.service canParseUniversalLinkURL:url]; XCTAssertTrue(matchesLongLinkFormat, @"URL did not validate as long link: %@", url); } } - (void)testInvalidCustomDomainNames { // Entries in plist file: // https://google.com // https://google.com/one // https://a.firebase.com/mypath NSArray *urlStrings = @[ @"google.com", // Valid domain. No scheme. @"https://google.com", // Valid domain. No path after domainURIPrefix. @"https://google.com/", // Valid domain. No path after domainURIPrefix. @"https://google.com/one/", // Valid domain. No path after domainURIPrefix. @"https://google.com/one/two/mylink", // domainURIPrefix not exact match. @"https://google.co.in/mylink", // No matching domainURIPrefix. @"https://firebase.com/mypath", // No matching domainURIPrefix: Invalid (sub)domain. @"https://b.firebase.com/mypath", // No matching domainURIPrefix: Invalid subdomain. @"https://a.firebase.com/mypathabc", // No matching domainURIPrefix: Invalid subdomain. @"mydomain.com", // https scheme not specified for domainURIPrefix. @"http://mydomain", // Domain not in plist. No path after domainURIPrefix. ]; for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; XCTAssertFalse(matchesShortLinkFormat, @"Non-DDL domain URL matched short link format with URL: %@", url); } } #pragma mark - Private Helpers - (void)removeAllFIRApps { NSDictionary *apps = [FIRApp allApps]; for (FIRApp *app in apps.allValues) { [app deleteApp:^(BOOL succes) { if (!succes) { NSLog(@"Error deleting FIRApp before tests - config tests may fail."); } }]; } } #pragma clang pop @end