blob: 676bb0e6940452bd8f4f1fe171255717dab2d314 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/activity_services/activity_service_controller.h"
#import <MobileCoreServices/MobileCoreServices.h>
#import "base/test/ios/wait_util.h"
#include "components/reading_list/core/reading_list_switches.h"
#import "ios/chrome/browser/ui/activity_services/activity_type_util.h"
#import "ios/chrome/browser/ui/activity_services/appex_constants.h"
#import "ios/chrome/browser/ui/activity_services/chrome_activity_item_source.h"
#import "ios/chrome/browser/ui/activity_services/print_activity.h"
#import "ios/chrome/browser/ui/activity_services/share_to_data.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#include "testing/gtest_mac.h"
#include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface ActivityServiceController (CrVisibleForTesting)
- (NSArray*)activityItemsForData:(ShareToData*)data;
- (NSArray*)applicationActivitiesForData:(ShareToData*)data
controller:(UIViewController*)controller;
- (BOOL)processItemsReturnedFromActivity:(NSString*)activityType
status:(ShareTo::ShareResult)result
items:(NSArray*)extensionItems;
// Setter function for mocking during testing
- (void)setShareToDelegateForTesting:(id<ShareToDelegate>)delegate;
@end
namespace {
class ActivityServiceControllerTest : public PlatformTest {
protected:
void SetUp() override {
PlatformTest::SetUp();
parentController_ =
[[UIViewController alloc] initWithNibName:nil bundle:nil];
[[UIApplication sharedApplication] keyWindow].rootViewController =
parentController_;
shareToDelegate_ =
[OCMockObject mockForProtocol:@protocol(ShareToDelegate)];
shareData_ =
[[ShareToData alloc] initWithURL:GURL("https://chromium.org")
title:@""
isOriginalTitle:YES
isPagePrintable:YES
thumbnailGenerator:DummyThumbnailGeneratorBlock()];
}
void TearDown() override {
[[UIApplication sharedApplication] keyWindow].rootViewController = nil;
PlatformTest::TearDown();
}
ThumbnailGeneratorBlock DummyThumbnailGeneratorBlock() {
return ^UIImage*(CGSize const& size) { return nil; };
}
id<ShareToDelegate> GetShareToDelegate() {
return static_cast<id<ShareToDelegate>>(shareToDelegate_);
}
CGRect AnchorRect() {
// On iPad, UIPopovers must be anchored to rectangles that have a non zero
// size.
return CGRectMake(0, 0, 1, 1);
}
UIView* AnchorView() {
// On iPad, UIPopovers must be anchored to non nil views.
return [parentController_ view];
}
BOOL ArrayContainsImageSource(NSArray* array) {
for (NSObject* item in array) {
if ([item class] == [UIActivityImageSource class]) {
return YES;
}
}
return NO;
}
// Search |array| for id<UIActivityItemSource> objects. Returns an array of
// matching NSExtensionItem objects returned by calling them.
NSArray* FindItemsForActivityType(NSArray* array, NSString* activityType) {
id mockActivityViewController =
[OCMockObject niceMockForClass:[UIActivityViewController class]];
NSMutableArray* result = [NSMutableArray array];
for (id item in array) {
if ([item respondsToSelector:@selector(activityViewController:
itemForActivityType:)]) {
id resultItem = [item activityViewController:mockActivityViewController
itemForActivityType:activityType];
if ([resultItem isKindOfClass:[NSExtensionItem class]])
[result addObject:resultItem];
}
}
return result;
}
// Searches |array| for objects of class |klass| and returns them in an
// autoreleased array.
NSArray* FindItemsOfClass(NSArray* array, Class klass) {
NSMutableArray* result = [NSMutableArray array];
for (id item in array) {
if ([item isKindOfClass:klass])
[result addObject:item];
}
return result;
}
// Searches |array| for objects returning |inUTType| conforming objects.
// Returns an autoreleased array of conforming objects.
NSArray* FindItemsEqualsToUTType(NSArray* array,
NSString* activityType,
NSString* inUTType) {
id mockActivityViewController =
[OCMockObject niceMockForClass:[UIActivityViewController class]];
NSMutableArray* result = [NSMutableArray array];
for (id item in array) {
if (![item conformsToProtocol:@protocol(UIActivityItemSource)])
continue;
SEL dataTypeSelector =
@selector(activityViewController:dataTypeIdentifierForActivityType:);
if (![item respondsToSelector:dataTypeSelector])
continue;
NSString* itemDataType =
[item activityViewController:mockActivityViewController
dataTypeIdentifierForActivityType:activityType];
if ([itemDataType isEqualToString:inUTType]) {
[result addObject:item];
}
}
return result;
}
// Calls -processItemsReturnedFromActivity:status:items: with the provided
// |extensionItem| and expects failure.
void ProcessItemsReturnedFromActivityFailure(NSArray* extensionItems,
BOOL expectedResetUI) {
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
// Sets up a Mock ShareToDelegate object to check that the ShareToDelegate
// callback function is not called.
OCMockObject* shareToDelegateMock =
[OCMockObject mockForProtocol:@protocol(ShareToDelegate)];
__block bool blockCalled = false;
void (^validationBlock)(NSInvocation*) = ^(NSInvocation* invocation) {
blockCalled = true;
};
// OCMock does not allow "any" specification for non-object parameters.
// To implement something that accept any non-SHARE_SUCCESS parameter
// to calling this method, all the non-success values have to be
// enumerated.
[[[shareToDelegateMock stub] andDo:validationBlock]
passwordAppExDidFinish:ShareTo::ShareResult::SHARE_CANCEL
username:OCMOCK_ANY
password:OCMOCK_ANY
successMessage:OCMOCK_ANY];
[[[shareToDelegateMock stub] andDo:validationBlock]
passwordAppExDidFinish:ShareTo::ShareResult::SHARE_NETWORK_FAILURE
username:OCMOCK_ANY
password:OCMOCK_ANY
successMessage:OCMOCK_ANY];
[[[shareToDelegateMock stub] andDo:validationBlock]
passwordAppExDidFinish:ShareTo::ShareResult::SHARE_SIGN_IN_FAILURE
username:OCMOCK_ANY
password:OCMOCK_ANY
successMessage:OCMOCK_ANY];
[[[shareToDelegateMock stub] andDo:validationBlock]
passwordAppExDidFinish:ShareTo::ShareResult::SHARE_ERROR
username:OCMOCK_ANY
password:OCMOCK_ANY
successMessage:OCMOCK_ANY];
[[[shareToDelegateMock stub] andDo:validationBlock]
passwordAppExDidFinish:ShareTo::ShareResult::SHARE_UNKNOWN_RESULT
username:OCMOCK_ANY
password:OCMOCK_ANY
successMessage:OCMOCK_ANY];
[activityController setShareToDelegateForTesting:(id)shareToDelegateMock];
// Sets up the returned item from a Password Management App Extension.
NSString* activityType = activity_services::kAppExtensionLastPass;
ShareTo::ShareResult result = ShareTo::ShareResult::SHARE_SUCCESS;
BOOL resetUI =
[activityController processItemsReturnedFromActivity:activityType
status:result
items:extensionItems];
ASSERT_EQ(expectedResetUI, resetUI);
base::test::ios::WaitUntilCondition(^{
return blockCalled;
});
EXPECT_OCMOCK_VERIFY(shareToDelegateMock);
}
web::TestWebThreadBundle thread_bundle_;
UIViewController* parentController_;
OCMockObject* shareToDelegate_;
ShareToData* shareData_;
};
TEST_F(ActivityServiceControllerTest, PresentAndDismissController) {
[[shareToDelegate_ expect] shareDidComplete:ShareTo::ShareResult::SHARE_CANCEL
successMessage:[OCMArg isNil]];
UIViewController* parentController =
static_cast<UIViewController*>(parentController_);
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
EXPECT_FALSE([activityController isActive]);
// Test sharing.
[activityController shareWithData:shareData_
controller:parentController
browserState:nullptr
shareToDelegate:GetShareToDelegate()
fromRect:AnchorRect()
inView:AnchorView()];
EXPECT_TRUE([activityController isActive]);
// Cancels sharing and isActive flag should be turned off.
[activityController cancelShareAnimated:NO];
base::test::ios::WaitUntilCondition(^bool() {
return ![activityController isActive];
});
EXPECT_OCMOCK_VERIFY(shareToDelegate_);
}
// Verifies that an UIActivityImageSource is sent to the
// UIActivityViewController if and only if the ShareToData contains an image.
TEST_F(ActivityServiceControllerTest, ActivityItemsForData) {
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
// ShareToData does not contain an image, so the result items array will not
// contain an image source.
ShareToData* data =
[[ShareToData alloc] initWithURL:GURL("https://chromium.org")
title:@"foo"
isOriginalTitle:YES
isPagePrintable:YES
thumbnailGenerator:DummyThumbnailGeneratorBlock()];
NSArray* items = [activityController activityItemsForData:data];
EXPECT_FALSE(ArrayContainsImageSource(items));
// Adds an image to the ShareToData object and call -activityItemsForData:
// again. Verifies that the result items array contains an image source.
[data setImage:[UIImage imageNamed:@"activity_services_print"]];
items = [activityController activityItemsForData:data];
EXPECT_TRUE(ArrayContainsImageSource(items));
}
// Verifies that when App Extension support is enabled, the URL string is
// passed in a dictionary as part of the Activity Items to the App Extension.
TEST_F(ActivityServiceControllerTest, ActivityItemsForDataWithPasswordAppEx) {
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
ShareToData* data =
[[ShareToData alloc] initWithURL:GURL("https://chromium.org/login.html")
title:@"kung fu fighting"
isOriginalTitle:YES
isPagePrintable:YES
thumbnailGenerator:DummyThumbnailGeneratorBlock()];
NSArray* items = [activityController activityItemsForData:data];
NSString* findLoginAction =
(NSString*)activity_services::kUTTypeAppExtensionFindLoginAction;
// Gets the list of NSExtensionItem objects returned by the array of
// id<UIActivityItemSource> objects returned by -activityItemsForData:.
NSArray* extensionItems = FindItemsForActivityType(
items, activity_services::kAppExtensionOnePassword);
ASSERT_EQ(1U, [extensionItems count]);
NSExtensionItem* item = extensionItems[0];
EXPECT_EQ(1U, item.attachments.count);
NSItemProvider* itemProvider = item.attachments[0];
// Extracts the dictionary back from the ItemProvider and then check that
// it has the expected version and the page's URL.
__block NSDictionary* result;
[itemProvider
loadItemForTypeIdentifier:findLoginAction
options:nil
completionHandler:^(id item, NSError* error) {
if (error || ![item isKindOfClass:[NSDictionary class]]) {
result = @{};
} else {
result = item;
}
}];
base::test::ios::WaitUntilCondition(^{
return result != nil;
});
EXPECT_EQ(2U, [result count]);
// Checks version.
NSNumber* version =
[result objectForKey:activity_services::kPasswordAppExVersionNumberKey];
EXPECT_NSEQ(activity_services::kPasswordAppExVersionNumber, version);
// Checks URL.
NSString* appExUrlString =
[result objectForKey:activity_services::kPasswordAppExURLStringKey];
EXPECT_NSEQ(@"https://chromium.org/login.html", appExUrlString);
// Checks that the list includes the page's title.
NSArray* sources =
FindItemsOfClass(items, [UIActivityFindLoginActionSource class]);
EXPECT_EQ(1U, [sources count]);
UIActivityFindLoginActionSource* actionSource = sources[0];
id mockActivityViewController =
[OCMockObject niceMockForClass:[UIActivityViewController class]];
NSString* title = [actionSource
activityViewController:mockActivityViewController
subjectForActivityType:activity_services::kAppExtensionOnePassword];
EXPECT_NSEQ(@"kung fu fighting", title);
}
// Verifies that a Share extension can fetch a URL when Password App Extension
// is enabled.
TEST_F(ActivityServiceControllerTest,
ActivityItemsForDataWithPasswordAppExReturnsURL) {
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
ShareToData* data =
[[ShareToData alloc] initWithURL:GURL("https://chromium.org/login.html")
title:@"kung fu fighting"
isOriginalTitle:YES
isPagePrintable:YES
thumbnailGenerator:DummyThumbnailGeneratorBlock()];
NSArray* items = [activityController activityItemsForData:data];
NSString* shareAction = @"com.apple.UIKit.activity.PostToFacebook";
NSArray* urlItems =
FindItemsEqualsToUTType(items, shareAction, @"public.url");
ASSERT_EQ(1U, [urlItems count]);
id<UIActivityItemSource> itemSource = urlItems[0];
id mockActivityViewController =
[OCMockObject niceMockForClass:[UIActivityViewController class]];
id item = [itemSource activityViewController:mockActivityViewController
itemForActivityType:shareAction];
ASSERT_TRUE([item isKindOfClass:[NSURL class]]);
EXPECT_NSEQ(@"https://chromium.org/login.html", [item absoluteString]);
}
// Verifies that -processItemsReturnedFromActivity:status:item: contains
// the username and password.
TEST_F(ActivityServiceControllerTest, ProcessItemsReturnedSuccessfully) {
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
// Sets up a Mock ShareToDelegate object to check that the callback function
// -passwordAppExDidFinish:username:password:successMessage:
// is correct with the correct username and password.
OCMockObject* shareToDelegateMock =
[OCMockObject mockForProtocol:@protocol(ShareToDelegate)];
NSString* const kSecretUsername = @"john.doe";
NSString* const kSecretPassword = @"super!secret";
__block bool blockCalled = false;
void (^validationBlock)(NSInvocation*) = ^(NSInvocation* invocation) {
__unsafe_unretained NSString* username;
__unsafe_unretained NSString* password;
// Skips 0 and 1 index because they are |self| and |cmd|.
[invocation getArgument:&username atIndex:3];
[invocation getArgument:&password atIndex:4];
EXPECT_NSEQ(kSecretUsername, username);
EXPECT_NSEQ(kSecretPassword, password);
blockCalled = true;
};
[[[shareToDelegateMock stub] andDo:validationBlock]
passwordAppExDidFinish:ShareTo::ShareResult::SHARE_SUCCESS
username:OCMOCK_ANY
password:OCMOCK_ANY
successMessage:OCMOCK_ANY];
[activityController setShareToDelegateForTesting:(id)shareToDelegateMock];
// Sets up the returned item from a Password Management App Extension.
NSString* activityType = @"com.software.find-login-action.extension";
ShareTo::ShareResult result = ShareTo::ShareResult::SHARE_SUCCESS;
NSDictionary* dictionaryFromAppEx =
@{ @"username" : kSecretUsername,
@"password" : kSecretPassword };
NSItemProvider* itemProvider =
[[NSItemProvider alloc] initWithItem:dictionaryFromAppEx
typeIdentifier:(NSString*)kUTTypePropertyList];
NSExtensionItem* extensionItem = [[NSExtensionItem alloc] init];
[extensionItem setAttachments:@[ itemProvider ]];
BOOL resetUI =
[activityController processItemsReturnedFromActivity:activityType
status:result
items:@[ extensionItem ]];
ASSERT_FALSE(resetUI);
// Wait for -passwordAppExDidFinish:username:password:successMessage:
// to be called.
base::test::ios::WaitUntilCondition(^{
return blockCalled;
});
EXPECT_OCMOCK_VERIFY(shareToDelegateMock);
}
// Verifies that -processItemsReturnedFromActivity:status:item: fails when
// called with invalid NSExtensionItem.
TEST_F(ActivityServiceControllerTest, ProcessItemsReturnedFailures) {
ProcessItemsReturnedFromActivityFailure(@[], YES);
// Extension Item is empty.
NSExtensionItem* extensionItem = [[NSExtensionItem alloc] init];
[extensionItem setAttachments:@[]];
ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], YES);
// Extension Item does not have a property list provider as the first
// attachment.
NSItemProvider* itemProvider =
[[NSItemProvider alloc] initWithItem:@"some arbitrary garbage"
typeIdentifier:(NSString*)kUTTypeText];
[extensionItem setAttachments:@[ itemProvider ]];
ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], YES);
// Property list provider did not return a dictionary object.
itemProvider =
[[NSItemProvider alloc] initWithItem:@[ @"foo", @"bar" ]
typeIdentifier:(NSString*)kUTTypePropertyList];
[extensionItem setAttachments:@[ itemProvider ]];
ProcessItemsReturnedFromActivityFailure(@[ extensionItem ], NO);
}
// Verifies that the PrintActivity is sent to the UIActivityViewController if
// and only if the activity is "printable".
TEST_F(ActivityServiceControllerTest, ApplicationActivitiesForData) {
ActivityServiceController* activityController =
[[ActivityServiceController alloc] init];
// Verify printable data.
ShareToData* data =
[[ShareToData alloc] initWithURL:GURL("https://chromium.org/printable")
title:@"bar"
isOriginalTitle:YES
isPagePrintable:YES
thumbnailGenerator:DummyThumbnailGeneratorBlock()];
NSArray* items =
[activityController applicationActivitiesForData:data controller:nil];
NSUInteger expected_items_count =
reading_list::switches::IsReadingListEnabled() ? 2U : 1U;
ASSERT_EQ(expected_items_count, [items count]);
EXPECT_EQ([PrintActivity class], [[items objectAtIndex:0] class]);
// Verify non-printable data.
data =
[[ShareToData alloc] initWithURL:GURL("https://chromium.org/unprintable")
title:@"baz"
isOriginalTitle:YES
isPagePrintable:NO
thumbnailGenerator:DummyThumbnailGeneratorBlock()];
items = [activityController applicationActivitiesForData:data controller:nil];
EXPECT_EQ(expected_items_count - 1, [items count]);
}
TEST_F(ActivityServiceControllerTest, FindLoginActionTypeConformsToPublicURL) {
// If this test fails, it is probably due to missing or incorrect
// UTImportedTypeDeclarations in Info.plist. Note that there are
// two Info.plist,
// - ios/chrome/app/resources/Info.plist for Chrome app
// - testing/gtest_ios/unittest-Info.plist for ios_chrome_unittests
// Both of them must be changed.
// 1Password defined the type @"org.appextension.find-login-action" so
// any app can launch the 1Password app extension to fill in username and
// password. This is being used by iOS native apps to launch 1Password app
// extension and show *only* 1Password app extension as an option.
// Therefore, this data type should *not* conform to public.url.
// During the transition period, this test:
// EXPECT_FALSE(UTTypeConformsTo(onePasswordFindLoginAction, kUTTypeURL));
// is not possible due to backward compatibility configurations.
CFStringRef onePasswordFindLoginAction =
reinterpret_cast<CFStringRef>(@"org.appextension.find-login-action");
// Chrome defines kUTTypeAppExtensionFindLoginAction which conforms to
// public.url UTType in order to allow Share actions (e.g. Facebook, Twitter,
// etc) to appear on UIActivityViewController opened by Chrome).
CFStringRef chromeFindLoginAction = reinterpret_cast<CFStringRef>(
activity_services::kUTTypeAppExtensionFindLoginAction);
EXPECT_TRUE(UTTypeConformsTo(chromeFindLoginAction, kUTTypeURL));
EXPECT_TRUE(
UTTypeConformsTo(chromeFindLoginAction, onePasswordFindLoginAction));
}
} // namespace