// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/app/application_delegate/url_opener.h"
#import <Foundation/Foundation.h>
#import "base/check_op.h"
#import "base/test/with_feature_override.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/app_state_observer.h"
#import "ios/chrome/app/application_delegate/mock_tab_opener.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/application_delegate/url_opener_params.h"
#import "ios/chrome/app/startup/chrome_app_startup_parameters.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/fake_connection_information.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/stub_browser_provider.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/stub_browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/testing/open_url_context.h"
#import "ios/web/public/test/web_task_environment.h"
#import "net/base/apple/url_conversions.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
// URLOpenerTest is parameterized on this enum to test with
// enabled and disabled kExternalFilesLoadedInWebState feature flag.
enum class ExternalFilesLoadedInWebStateFeature {
Disabled = 0,
Enabled,
};
#pragma mark - stubs and test fakes
@interface StubStartupInformation : NSObject <StartupInformation>
@end
@implementation StubStartupInformation
@synthesize isFirstRun = _isFirstRun;
@synthesize isColdStart = _isColdStart;
@synthesize appLaunchTime = _appLaunchTime;
@synthesize didFinishLaunchingTime = _didFinishLaunchingTime;
@synthesize firstSceneConnectionTime = _firstSceneConnectionTime;
- (FirstUserActionRecorder*)firstUserActionRecorder {
return nil;
}
- (void)resetFirstUserActionRecorder {
}
- (void)expireFirstUserActionRecorder {
}
- (void)expireFirstUserActionRecorderAfterDelay:(NSTimeInterval)delay {
}
- (void)activateFirstUserActionRecorderWithBackgroundTime:
(NSTimeInterval)backgroundTime {
}
- (void)stopChromeMain {
}
- (NSDictionary*)launchOptions {
return @{};
}
@end
#pragma mark -
class URLOpenerTest : public PlatformTest {
protected:
URLOpenerTest() {}
private:
web::WebTaskEnvironment task_environment_;
};
TEST_F(URLOpenerTest, HandleOpenURL) {
// A set of tests for robustness of
// application:openURL:options:tabOpener:startupInformation:
// It verifies that the function handles correctly different URLs parsed by
// ChromeAppStartupParameters.
id<StartupInformation> startupInformation =
[[StubStartupInformation alloc] init];
id<ConnectionInformation> connectionInformation =
[[FakeConnectionInformation alloc] init];
// The array with the different states to tests (active, not active).
NSArray* applicationStatesToTest = @[ @YES, @NO ];
// Mock of TabOpening, preventing the creation of a new tab.
MockTabOpener* tabOpener = [[MockTabOpener alloc] init];
// The keys for this dictionary is the URL to call openURL:. The value
// from the key is either YES or NO to indicate if this is a valid URL
// or not.
NSDictionary* urlsToTest = @{
[NSNull null] : @NO,
@"" : @NO,
// Tests for http, googlechrome, and chromium scheme URLs.
@"http://www.google.com/" : @YES,
@"https://www.google.com/settings/account/" : @YES,
@"googlechrome://www.google.com/" : @YES,
@"googlechromes://www.google.com/settings/account/" : @YES,
@"chromium://www.google.com/" : @YES,
@"chromiums://www.google.com/settings/account/" : @YES,
// Google search results page URLs.
@"https://www.google.com/search?q=pony&"
"sugexp=chrome,mod=7&sourceid=chrome&ie=UTF-8" : @YES,
@"googlechromes://www.google.com/search?q=pony&"
"sugexp=chrome,mod=7&sourceid=chrome&ie=UTF-8" : @YES,
// Other protocols.
@"chromium-x-callback://x-callback-url/open?url=https://"
"www.google.com&x-success=http://success" : @YES,
@"file://localhost/path/to/file.pdf" : @YES,
// Invalid format input URL will be ignored.
@"this.is.not.a.valid.url" : @NO,
// Valid format but invalid data.
@"this://is/garbage/but/valid" : @YES
};
NSArray* sourcesToTest = @[
@"", @"com.google.GoogleMobile", @"com.google.GooglePlus",
@"com.google.SomeOtherProduct", @"com.apple.mobilesafari",
@"com.othercompany.otherproduct"
];
// See documentation for `annotation` property in
// UIDocumentInteractionstartupInformation Class Reference. The following
// values are mostly to detect garbage-in situations and ensure that the app
// won't crash or garbage out.
NSArray* annotationsToTest = @[
[NSNull null], [NSArray arrayWithObjects:@"foo", @"bar", nil],
[NSDictionary dictionaryWithObject:@"bar" forKey:@"foo"],
@"a string annotation object"
];
for (id urlString in [urlsToTest allKeys]) {
for (id source in sourcesToTest) {
for (id annotation in annotationsToTest) {
for (NSNumber* applicationActive in applicationStatesToTest) {
BOOL applicationIsActive = [applicationActive boolValue];
connectionInformation.startupParameters = nil;
[tabOpener resetURL];
NSURL* testUrl = urlString == [NSNull null]
? nil
: [NSURL URLWithString:urlString];
BOOL isValid = [[urlsToTest objectForKey:urlString] boolValue];
TestSceneOpenURLOptions* options =
[[TestSceneOpenURLOptions alloc] init];
options.sourceApplication = source;
options.annotation = annotation;
TestOpenURLContext* context = [[TestOpenURLContext alloc] init];
context.URL = testUrl;
context.options = (id)options; //< Unsafe cast intended.
URLOpenerParams* urlOpenerParams = [[URLOpenerParams alloc]
initWithUIOpenURLContext:(id)context]; //< Unsafe cast intended.
ChromeAppStartupParameters* params =
[ChromeAppStartupParameters startupParametersWithURL:testUrl
sourceApplication:nil];
// Action.
BOOL result = [URLOpener openURL:urlOpenerParams
applicationActive:applicationIsActive
tabOpener:tabOpener
connectionInformation:connectionInformation
startupInformation:startupInformation
prefService:nil
initStage:InitStageFinal];
// Tests.
EXPECT_EQ(isValid, result);
if (!applicationIsActive) {
if (result)
EXPECT_EQ([params externalURL],
connectionInformation.startupParameters.externalURL);
else
EXPECT_EQ(nil, connectionInformation.startupParameters);
} else if (result) {
if ([params completeURL].SchemeIsFile()) {
// External file:// URL will be loaded by WebState, which expects
// complete // file:// URL. chrome:// URL is expected to be
// displayed in the omnibox, and omnibox shows virtual URL.
EXPECT_EQ([params completeURL],
tabOpener.urlLoadParams.web_params.url);
EXPECT_EQ([params externalURL],
tabOpener.urlLoadParams.web_params.virtual_url);
} else {
// External chromium-x-callback:// URL will be loaded by
// WebState, which expects externalURL URL.
EXPECT_EQ([params externalURL],
tabOpener.urlLoadParams.web_params.url);
}
tabOpener.completionBlock();
EXPECT_EQ(nil, connectionInformation.startupParameters);
}
}
}
}
}
}
// Tests that -handleApplication set startup parameters as expected.
TEST_F(URLOpenerTest, VerifyLaunchOptions) {
// Setup.
NSURL* url = [NSURL URLWithString:@"chromium://www.google.com"];
URLOpenerParams* urlOpenerParams =
[[URLOpenerParams alloc] initWithURL:url
sourceApplication:@"com.apple.mobilesafari"];
id tabOpenerMock = [OCMockObject mockForProtocol:@protocol(TabOpening)];
id startupInformationMock =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
[[startupInformationMock expect] resetFirstUserActionRecorder];
id connectionInformationMock =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
__block ChromeAppStartupParameters* params = nil;
[[connectionInformationMock expect]
setStartupParameters:[OCMArg checkWithBlock:^(
ChromeAppStartupParameters* p) {
params = p;
EXPECT_NSEQ(net::NSURLWithGURL(p.completeURL), url);
EXPECT_EQ(p.callerApp, CALLER_APP_APPLE_MOBILESAFARI);
return YES;
}]];
[[[connectionInformationMock expect] andReturn:params] startupParameters];
id appStateMock = [OCMockObject mockForClass:[AppState class]];
[[[appStateMock stub] andReturnValue:@(InitStageFinal)] initStage];
// Action.
[URLOpener handleLaunchOptions:urlOpenerParams
tabOpener:tabOpenerMock
connectionInformation:connectionInformationMock
startupInformation:startupInformationMock
appState:appStateMock
prefService:nil];
// Test.
EXPECT_OCMOCK_VERIFY(startupInformationMock);
}
// Tests that -handleApplication set startup parameters as expected with options
// as nil.
TEST_F(URLOpenerTest, VerifyLaunchOptionsNil) {
// Creates a mock with no stub. This test will pass only if we don't use these
// objects.
id startupInformationMock =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
id connectionInformationMock =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
id appStateMock = [OCMockObject mockForClass:[AppState class]];
// Action.
[URLOpener handleLaunchOptions:nil
tabOpener:nil
connectionInformation:connectionInformationMock
startupInformation:startupInformationMock
appState:appStateMock
prefService:nil];
}
// Tests that -handleApplication set startup parameters as expected with no
// source application.
TEST_F(URLOpenerTest, VerifyLaunchOptionsWithNoSourceApplication) {
// Setup.
NSURL* url = [NSURL URLWithString:@"chromium://www.google.com"];
URLOpenerParams* urlOpenerParams = [[URLOpenerParams alloc] initWithURL:url
sourceApplication:nil];
MockTabOpener* tabOpenerMock = [[MockTabOpener alloc] init];
id startupInformationMock =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
[[startupInformationMock expect] resetFirstUserActionRecorder];
id connectionInformationMock =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
__block ChromeAppStartupParameters* params = nil;
[[connectionInformationMock expect]
setStartupParameters:[OCMArg checkWithBlock:^(
ChromeAppStartupParameters* p) {
params = p;
EXPECT_NSEQ(net::NSURLWithGURL(p.completeURL), url);
EXPECT_EQ(p.callerApp, CALLER_APP_NOT_AVAILABLE);
return YES;
}]];
[[[connectionInformationMock expect] andReturn:params] startupParameters];
id appStateMock = [OCMockObject mockForClass:[AppState class]];
[[[appStateMock stub] andReturnValue:@(InitStageFinal)] initStage];
// Action.
[URLOpener handleLaunchOptions:urlOpenerParams
tabOpener:tabOpenerMock
connectionInformation:connectionInformationMock
startupInformation:startupInformationMock
appState:appStateMock
prefService:nil];
// Test.
EXPECT_OCMOCK_VERIFY(startupInformationMock);
}
// Tests that -handleApplication set startup parameters as expected with no url.
TEST_F(URLOpenerTest, VerifyLaunchOptionsWithNoURL) {
// Setup.
URLOpenerParams* urlOpenerParams =
[[URLOpenerParams alloc] initWithURL:nil
sourceApplication:@"com.apple.mobilesafari"];
// Creates a mock with no stub. This test will pass only if we don't use these
// objects.
id startupInformationMock =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
id connectionInformationMock =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
id appStateMock = [OCMockObject mockForClass:[AppState class]];
// Action.
[URLOpener handleLaunchOptions:urlOpenerParams
tabOpener:nil
connectionInformation:connectionInformationMock
startupInformation:startupInformationMock
appState:appStateMock
prefService:nil];
}
// Tests that -handleApplication set startup parameters as expected with a bad
// url.
TEST_F(URLOpenerTest, VerifyLaunchOptionsWithBadURL) {
// Setup.
NSURL* url = [NSURL URLWithString:@"chromium.www.google.com"];
URLOpenerParams* urlOpenerParams =
[[URLOpenerParams alloc] initWithURL:url
sourceApplication:@"com.apple.mobilesafari"];
id tabOpenerMock = [OCMockObject mockForProtocol:@protocol(TabOpening)];
id startupInformationMock =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
[[startupInformationMock expect] resetFirstUserActionRecorder];
id connectionInformationMock =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
[[connectionInformationMock expect] setStartupParameters:[OCMArg isNil]];
[[[connectionInformationMock expect] andReturn:nil] startupParameters];
id appStateMock = [OCMockObject mockForClass:[AppState class]];
[[[appStateMock stub] andReturnValue:@(InitStageFinal)] initStage];
// Action.
[URLOpener handleLaunchOptions:urlOpenerParams
tabOpener:tabOpenerMock
connectionInformation:connectionInformationMock
startupInformation:startupInformationMock
appState:appStateMock
prefService:nil];
// Test.
EXPECT_OCMOCK_VERIFY(startupInformationMock);
}
// Tests URL is not opened if the FRE is presented.
TEST_F(URLOpenerTest, PresentingFirstRunUI) {
// Setup.
NSURL* url = [NSURL URLWithString:@"chromium://www.google.com"];
URLOpenerParams* urlOpenerParams =
[[URLOpenerParams alloc] initWithURL:url
sourceApplication:@"com.apple.mobilesafari"];
id tabOpenerMock = [OCMockObject mockForProtocol:@protocol(TabOpening)];
id startupInformationMock =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
id connectionInformationMock =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
__block ChromeAppStartupParameters* params = nil;
[[connectionInformationMock expect]
setStartupParameters:[OCMArg checkWithBlock:^(
ChromeAppStartupParameters* p) {
params = p;
EXPECT_NSEQ(net::NSURLWithGURL(p.completeURL), url);
EXPECT_EQ(p.callerApp, CALLER_APP_APPLE_MOBILESAFARI);
return YES;
}]];
[[[connectionInformationMock expect] andReturn:params] startupParameters];
id appStateMock = [OCMockObject mockForClass:[AppState class]];
[[[appStateMock stub] andReturnValue:@(InitStageFirstRun)] initStage];
// Action.
[URLOpener handleLaunchOptions:urlOpenerParams
tabOpener:tabOpenerMock
connectionInformation:connectionInformationMock
startupInformation:startupInformationMock
appState:appStateMock
prefService:nil];
// Test.
EXPECT_OCMOCK_VERIFY(tabOpenerMock);
EXPECT_OCMOCK_VERIFY(startupInformationMock);
EXPECT_OCMOCK_VERIFY(appStateMock);
}