FBSDKShareKit/FBSDKShareKitTests/UserInterface/ShareDialogTests.swift (678 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ @testable import FBSDKShareKit import Photos import TestTools import UIKit import XCTest final class ShareDialogTests: XCTestCase { // swiftlint:disable implicitly_unwrapped_optional var internalURLOpener: TestInternalURLOpener! var internalUtility: TestInternalUtility! var settings: TestSettings! var bridgeAPIRequestFactory: TestBridgeAPIRequestFactory! var bridgeAPIRequestOpener: TestBridgeAPIRequestOpener! var socialComposeViewController: TestSocialComposeViewController! var socialComposeViewControllerFactory: TestSocialComposeViewControllerFactory! var windowFinder: TestWindowFinder! var errorFactory: TestErrorFactory! var eventLogger: TestShareEventLogger! var mediaLibrarySearcher: TestMediaLibrarySearcher! // swiftlint:enable implicitly_unwrapped_optional override func setUp() { super.setUp() AccessToken.current = nil internalURLOpener = TestInternalURLOpener() internalUtility = TestInternalUtility() settings = TestSettings() bridgeAPIRequestFactory = TestBridgeAPIRequestFactory() bridgeAPIRequestOpener = TestBridgeAPIRequestOpener() socialComposeViewController = TestSocialComposeViewController() socialComposeViewControllerFactory = TestSocialComposeViewControllerFactory() socialComposeViewControllerFactory.stubbedSocialComposeViewController = socialComposeViewController windowFinder = TestWindowFinder() errorFactory = TestErrorFactory() eventLogger = TestShareEventLogger() mediaLibrarySearcher = TestMediaLibrarySearcher() ShareDialog.setDependencies( .init( internalURLOpener: internalURLOpener, internalUtility: internalUtility, settings: settings, shareUtility: TestShareUtility.self, bridgeAPIRequestFactory: bridgeAPIRequestFactory, bridgeAPIRequestOpener: bridgeAPIRequestOpener, socialComposeViewControllerFactory: socialComposeViewControllerFactory, windowFinder: windowFinder, errorFactory: errorFactory, eventLogger: eventLogger, mediaLibrarySearcher: mediaLibrarySearcher ) ) ShareCameraEffectContent.setDependencies( .init( internalUtility: internalUtility, errorFactory: errorFactory ) ) } override func tearDown() { internalURLOpener = nil internalUtility = nil settings = nil bridgeAPIRequestFactory = nil bridgeAPIRequestOpener = nil socialComposeViewController = nil socialComposeViewControllerFactory = nil windowFinder = nil errorFactory = nil eventLogger = nil mediaLibrarySearcher = nil ShareDialog.resetDependencies() TestShareUtility.reset() ShareCameraEffectContent.resetDependencies() AccessToken.current = nil super.tearDown() } func testDefaultDependencies() throws { ShareDialog.resetDependencies() let dependencies = try ShareDialog.getDependencies() XCTAssertIdentical(dependencies.internalURLOpener, ShareUIApplication.shared, .usesInternalURLOpenerByDefault) XCTAssertIdentical(dependencies.internalUtility, InternalUtility.shared, .usesInternalUtilityByDefault) XCTAssertIdentical(dependencies.settings, Settings.shared, .usesSettingsByDefault) XCTAssertTrue(dependencies.shareUtility is _ShareUtility.Type, .usesShareUtilityByDefault) XCTAssertTrue( dependencies.bridgeAPIRequestFactory is ShareBridgeAPIRequestFactory, .usesShareBridgeAPIRequestFactoryByDefault ) XCTAssertIdentical(dependencies.bridgeAPIRequestOpener, BridgeAPI.shared, .usesBridgeAPIByDefault) XCTAssertTrue( dependencies.socialComposeViewControllerFactory is SocialComposeViewControllerFactory, .usesSocialComposeViewControllerFactoryByDefault ) XCTAssertIdentical(dependencies.windowFinder, InternalUtility.shared, .usesInternalUtilityAsWindowFinderByDefault) XCTAssertTrue(dependencies.errorFactory is ErrorFactory, .usesErrorFactoryByDefault) XCTAssertIdentical(dependencies.eventLogger as AnyObject, AppEvents.shared, .usesAppEventsByDefault) XCTAssertIdentical( dependencies.mediaLibrarySearcher as AnyObject, PHImageManager.default(), .usesPHImageManagerAsMediaLibrarySearcherByDefault ) } func testCustomDependencies() throws { let dependencies = try ShareDialog.getDependencies() XCTAssertIdentical(dependencies.internalURLOpener, internalURLOpener, .usesCustomInternalURLOpener) XCTAssertIdentical(dependencies.internalUtility, internalUtility, .usesCustomInternalUtility) XCTAssertIdentical(dependencies.settings, settings, .usesCustomSettings) XCTAssertTrue(dependencies.shareUtility is TestShareUtility.Type, .usesCustomShareUtility) XCTAssertIdentical( dependencies.bridgeAPIRequestFactory, bridgeAPIRequestFactory, .usesCustomShareBridgeAPIRequestFactory ) XCTAssertIdentical(dependencies.bridgeAPIRequestOpener, bridgeAPIRequestOpener, .usesCustomBridgeAPIRequestOpener) XCTAssertIdentical( dependencies.socialComposeViewControllerFactory as AnyObject, socialComposeViewControllerFactory, .usesCustomSocialComposeViewControllerFactory ) XCTAssertIdentical(dependencies.windowFinder, windowFinder, .usesCustomWindowFinder) XCTAssertIdentical(dependencies.errorFactory, errorFactory, .usesCustomErrorFactory) XCTAssertIdentical(dependencies.eventLogger as AnyObject, eventLogger, .usesCustomEventLogger) XCTAssertIdentical( dependencies.mediaLibrarySearcher as AnyObject, mediaLibrarySearcher, .usesCustomMediaLibrarySearcher ) } func testCanShowNativeDialogWithoutShareContent() { let dialog = createEmptyDialog() dialog.mode = .native internalURLOpener.canOpenURL = true internalUtility.isFacebookAppInstalled = true XCTAssertTrue( dialog.canShow, "A dialog without share content should be showable on a native dialog" ) } func testCanShowNativeLinkContent() { let dialog = createEmptyDialog() dialog.mode = .native dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with valid link content should be showable on a native dialog" ) } func testCanShowNativePhotoContent() { let dialog = createEmptyDialog() dialog.mode = .native dialog.shareContent = ShareModelTestUtility.photoContent TestShareUtility.validateShareContentShouldThrow = true XCTAssertFalse( dialog.canShow, "Photo content with photos that have web urls should not be showable on a native dialog" ) } func testCanShowNativePhotoContentWithFileURL() { let dialog = createEmptyDialog() dialog.mode = .native dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs XCTAssertTrue( dialog.canShow, "Photo content with photos that have file urls should be showable on a native dialog" ) } func testCanShowNativeVideoContentWithoutPreviewPhoto() { let dialog = createEmptyDialog() dialog.mode = .native internalURLOpener.canOpenURL = true dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertTrue( dialog.canShow, "Video content without a preview photo should be showable on a native dialog" ) } func testCanShowNative() { let dialog = createEmptyDialog() dialog.mode = .native XCTAssertFalse( dialog.canShow, "A native dialog should not be showable if the application is unable to open a url, this can also occur if the api scheme is not whitelisted in the third party app or if the application cannot handle the share API scheme" // swiftlint:disable:this line_length ) } func testShowNativeDoesValidate() { let dialog = createEmptyDialog() dialog.mode = .native dialog.shareContent = ShareModelTestUtility.photoContent internalURLOpener.canOpenURL = true XCTAssertFalse(dialog.show()) } func testValidateShareSheet() throws { let dialog = createEmptyDialog() dialog.mode = .shareSheet dialog.shareContent = ShareModelTestUtility.linkContentWithoutQuote XCTAssertNoThrow( try dialog.validate(), "Should not throw an error when validating link content without quotes" ) dialog.shareContent = ShareModelTestUtility.photoContentWithImages XCTAssertNoThrow( try dialog.validate(), "Should not throw an error when validating photo content with images" ) dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertThrowsError( try dialog.validate(), "Should throw an error when validating photo content on a share sheet" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError( try dialog.validate(), "Should throw an error when validating video content without a preview photo on a share sheet" ) } func testCanShowBrowser() { let dialog = createEmptyDialog() dialog.mode = .browser XCTAssertTrue( dialog.canShow, "A dialog without share content should be showable in a browser" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable in a browser" ) AccessToken.current = SampleAccessTokens.validToken dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs XCTAssertTrue( dialog.canShow, "A dialog with photo content with file urls should be showable in a browser when there is a current access token" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertTrue( dialog.canShow, "A dialog with video content without a preview photo should be showable in a browser when there is a current access token" // swiftlint:disable:this line_length ) } func testValidateBrowser() throws { let dialog = createEmptyDialog() dialog.mode = .browser dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.photoContentWithImages AccessToken.current = SampleAccessTokens.validToken XCTAssertNoThrow(try dialog.validate()) AccessToken.current = nil TestShareUtility.stubbedTestShareContainsPhotos = true AccessToken.current = nil XCTAssertThrowsError(try dialog.validate()) TestShareUtility.reset() TestShareUtility.stubbedTestShareContainsVideos = true dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError(try dialog.validate()) } func testCanShowWeb() { let dialog = createEmptyDialog() dialog.mode = .web XCTAssertTrue( dialog.canShow, "A dialog without share content should be showable on web" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable on web" ) AccessToken.current = SampleAccessTokens.validToken TestShareUtility.stubbedTestShareContainsPhotos = true TestShareUtility.validateShareContentShouldThrow = true dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertFalse( dialog.canShow, "A dialog with photos should not be showable on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto TestShareUtility.stubbedTestShareContainsMedia = true XCTAssertFalse( dialog.canShow, "A dialog with content that contains local media should not be showable on web" ) } func testValidateWeb() throws { let dialog = createEmptyDialog() dialog.mode = .web dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) AccessToken.current = SampleAccessTokens.validToken dialog.shareContent = ShareModelTestUtility.photoContent TestShareUtility.validateShareContentShouldThrow = true XCTAssertThrowsError( try dialog.validate(), "A dialog with photo content that points to remote urls should not be considered valid on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.photoContentWithImages TestShareUtility.stubbedTestShareContainsPhotos = true XCTAssertThrowsError( try dialog.validate(), "A dialog with photo content that is already loaded should not be considered valid on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs TestShareUtility.stubbedTestShareContainsPhotos = true XCTAssertThrowsError( try dialog.validate(), "A dialog with photo content that points to file urls should not be considered valid on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto TestShareUtility.stubbedTestShareContainsMedia = true XCTAssertThrowsError( try dialog.validate(), "A dialog that includes local media should not be considered valid on web" ) TestShareUtility.reset() AccessToken.current = nil TestShareUtility.stubbedTestShareContainsVideos = true XCTAssertThrowsError( try dialog.validate(), "A dialog with content but no access token should not be considered valid on web" ) } func testCanShowFeedBrowser() { let dialog = createEmptyDialog() dialog.mode = .feedBrowser XCTAssertTrue( dialog.canShow, "A dialog without content should be showable in a browser feed" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable in a browser feed" ) dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertFalse( dialog.canShow, "A dialog with photo content should not be showable in a browser feed" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertFalse( dialog.canShow, "A dialog with video content that has no preview photo should not be showable in a browser feed" ) } func testValidateFeedBrowser() throws { let dialog = createEmptyDialog() dialog.mode = .feedBrowser dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.photoContentWithImages XCTAssertThrowsError(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError(try dialog.validate()) } func testCanShowFeedWeb() { let dialog = createEmptyDialog() dialog.mode = .feedWeb XCTAssertTrue( dialog.canShow, "A dialog without content should be showable in a web feed" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable in a web feed" ) dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertFalse( dialog.canShow, "A dialog with photo content should not be showable in a web feed" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertFalse( dialog.canShow, "A dialog with video content and no preview photo should not be showable in a web feed" ) } func testValidateFeedWeb() throws { let dialog = createEmptyDialog() dialog.mode = .feedWeb dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.photoContentWithImages XCTAssertThrowsError(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError(try dialog.validate()) } func testThatInitialTextIsSetCorrectlyWhenShareExtensionIsAvailable() throws { let dialog = createEmptyDialog() let content = ShareModelTestUtility.linkContent content.hashtag = Hashtag("#hashtag") TestShareUtility.stubbedHashtagString = "#hashtag" content.quote = "a quote" dialog.shareContent = content internalUtility.isFacebookAppInstalled = true internalURLOpener.canOpenURL = true settings.appID = "appID" let viewController = UIViewController() dialog.fromViewController = viewController dialog.mode = .shareSheet XCTAssertTrue(dialog.show()) try validateInitialText( capturedText: socialComposeViewController.capturedInitialText, expectedAppID: "appID", expectedHashtag: "#hashtag", expectedQuotes: ["a quote"] ) } func testCameraShareModesWhenNativeAvailable() throws { let dialog = createEmptyDialog() dialog.shareContent = ShareModelTestUtility.cameraEffectContent internalURLOpener.canOpenURL = true internalUtility.isFacebookAppInstalled = true // Check supported modes dialog.mode = .automatic XCTAssertNoThrow(try dialog.validate()) dialog.mode = .native XCTAssertNoThrow(try dialog.validate()) // Check unsupported modes dialog.mode = .web XCTAssertThrowsError(try dialog.validate()) dialog.mode = .browser XCTAssertThrowsError(try dialog.validate()) dialog.mode = .shareSheet XCTAssertThrowsError(try dialog.validate()) dialog.mode = .feedWeb XCTAssertThrowsError(try dialog.validate()) dialog.mode = .feedBrowser XCTAssertThrowsError(try dialog.validate()) } func testCameraShareModesWhenNativeUnavailable() { let dialog = createEmptyDialog() dialog.shareContent = ShareModelTestUtility.cameraEffectContent dialog.mode = .automatic XCTAssertThrowsError(try dialog.validate()) } func testPassingValidationForLinkQuoteWithValidShareExtensionVersion() { internalUtility.isFacebookAppInstalled = true validate( shareContent: ShareModelTestUtility.linkContent, expectValid: true, expectShow: true, mode: .shareSheet, nonSupportedScheme: nil ) } func testValidateWithErrorReturnsFalseForMMPIfAValidShareExtensionVersionIsNotAvailable() { TestShareUtility.validateShareContentShouldThrow = true validate( shareContent: ShareModelTestUtility.mediaContent, expectValid: false, expectShow: false, mode: .shareSheet, nonSupportedScheme: "fbapi20160328:/" ) } func testThatValidateWithErrorReturnsTrueForMMPIfAValidShareExtensionVersionIsAvailable() { internalUtility.isFacebookAppInstalled = true validate( shareContent: ShareModelTestUtility.mediaContent, expectValid: true, expectShow: true, mode: .shareSheet, nonSupportedScheme: nil ) } func testThatValidateWithErrorReturnsFalseForMMPWithMoreThan1Video() { validate( shareContent: ShareModelTestUtility.multiVideoMediaContent, expectValid: false, expectShow: false, mode: .shareSheet, nonSupportedScheme: nil ) } // MARK: - Helpers func createEmptyDialog() -> ShareDialog { ShareDialog(viewController: nil, content: nil, delegate: nil) } func showAndValidate( nativeDialog dialog: ShareDialog, nonSupportedScheme: String?, expectRequestScheme scheme: String?, methodName: String, _ file: StaticString = #file, _ line: UInt = #line ) { internalURLOpener.computeCanOpenURL = { url in url.absoluteString != nonSupportedScheme } settings.appID = "AppID" let stubbedRequest = TestBridgeAPIRequest( url: nil, protocolType: .native, scheme: "1" ) bridgeAPIRequestFactory.stubbedBridgeAPIRequest = stubbedRequest let viewController = UIViewController() dialog.fromViewController = viewController XCTAssertTrue( dialog.show(), "Should be able to show the dialog", file: file, line: line ) XCTAssertEqual( bridgeAPIRequestFactory.capturedMethodName, methodName, "Should create the request with the expected method name", file: file, line: line ) if let expectedScheme = scheme { XCTAssertEqual( bridgeAPIRequestFactory.capturedScheme, expectedScheme, "Should create the request with the expected scheme", file: file, line: line ) } else { XCTAssertNil( bridgeAPIRequestFactory.capturedScheme, "Should not create the request with a scheme", file: file, line: line ) } XCTAssertTrue( bridgeAPIRequestOpener.capturedRequest === stubbedRequest, "Should pass the created request to the opener", file: file, line: line ) } func validate( shareContent: SharingContent, expectValid: Bool, expectShow: Bool, mode: ShareDialog.Mode, nonSupportedScheme: String?, file: StaticString = #file, line: UInt = #line ) { internalURLOpener.computeCanOpenURL = { url in url.absoluteString != nonSupportedScheme } let viewController = UIViewController() let dialog = createEmptyDialog() dialog.shareContent = shareContent dialog.mode = mode dialog.fromViewController = viewController if expectValid { XCTAssertNoThrow( try dialog.validate(), "Should not throw an error when validating the dialog", file: file, line: line ) } else { XCTAssertThrowsError( try dialog.validate(), "Should not throw an error when validating the dialog" ) } XCTAssertEqual( expectShow, dialog.show(), "Showing the dialog should return \(expectShow)", file: file, line: line ) } private func validateInitialText( capturedText: String?, expectedAppID: String, expectedHashtag: String, expectedQuotes: [String], file: StaticString = #file, line: UInt = #line ) throws { let capturedComponents = try XCTUnwrap(initialTextComponents(for: capturedText)) XCTAssertEqual( capturedComponents.appID, "fb-app-id:\(expectedAppID)", file: file, line: line ) XCTAssertEqual( capturedComponents.hashtag, expectedHashtag, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject.count, 3, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject["quotes"] as? [String], expectedQuotes, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject["app_id"] as? String, expectedAppID, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject["hashtags"] as? [String], [expectedHashtag], file: file, line: line ) } private typealias InitialTextComponents = (appID: String, hashtag: String, jsonObject: [String: Any]) private func initialTextComponents(for potentialText: String?) -> InitialTextComponents? { guard let splitAtVerticalBar = potentialText?.split(separator: "|"), splitAtVerticalBar.count == 2, let beforeJSON = splitAtVerticalBar.first?.split(separator: " "), beforeJSON.count == 2, let appID = beforeJSON.first, let hashtag = beforeJSON.last, let json = splitAtVerticalBar.last, let data = String(json).data(using: .utf8), let jsonObject = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { return nil } return ( appID: String(appID), hashtag: String(hashtag), jsonObject: jsonObject ) } } // MARK: - Assumptions fileprivate extension String { static let usesInternalURLOpenerByDefault = """ The default internal URL opening dependency should be the shared UIApplication """ static let usesInternalUtilityByDefault = """ The default internal utility dependency should be the shared InternalUtility """ static let usesSettingsByDefault = "The default settings dependency should be the shared Settings" static let usesShareUtilityByDefault = "The default share utility dependency should be the _ShareUtility class" static let usesShareBridgeAPIRequestFactoryByDefault = """ The default bridge API request factory dependency should be a concrete ShareBridgeAPIRequestFactory """ static let usesBridgeAPIByDefault = """ The default bridge API request opening dependency should be the shared BridgeAPI for its default """ static let usesSocialComposeViewControllerFactoryByDefault = """ The default social compose view controller factory dependency should be a concrete \ SocialComposeViewControllerFactory """ static let usesInternalUtilityAsWindowFinderByDefault = """ The default window finding dependency should be the shared InternalUtility """ static let usesErrorFactoryByDefault = "The default error factory dependency should be a concrete ErrorFactory" static let usesAppEventsByDefault = "The default event logging dependency should be the shared AppEvents" static let usesPHImageManagerAsMediaLibrarySearcherByDefault = """ The default media library searching dependency should be the default PHImageManager """ static let usesCustomInternalURLOpener = "The internal URL opening dependency should be configurable" static let usesCustomInternalUtility = "The internal utility dependency should be configurable" static let usesCustomSettings = "The settings dependency should be configurable" static let usesCustomShareUtility = "The share utility dependency should be configurable" static let usesCustomShareBridgeAPIRequestFactory = "The bridge API request factory dependency should be configurable" static let usesCustomBridgeAPIRequestOpener = "The bridge API request opening dependency should be configurable" static let usesCustomSocialComposeViewControllerFactory = """ The social compose view controller factory dependency should be configurable """ static let usesCustomWindowFinder = "The window finding dependency should be configurable" static let usesCustomErrorFactory = "The error factory dependency should be configurable" static let usesCustomEventLogger = "The event logging dependency should be configurable" static let usesCustomMediaLibrarySearcher = "The media library searching dependency should be configurable" }