FBSDKLoginKit/FBSDKLoginKitTests/DeviceLoginManagerTests.swift (428 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 FBSDKLoginKit
import TestTools
import XCTest
final class DeviceLoginManagerTests: XCTestCase {
let fakeAppID = "123"
let fakeClientToken = "abc"
let permissions = ["email", "public_profile"]
let redirectURL = URL(string: "https://www.example.com")! // swiftlint:disable:this force_unwrapping
// swiftlint:disable implicitly_unwrapped_optional
var poller: TestDevicePoller!
var delegate: TestDeviceLoginManagerDelegate!
var factory: TestGraphRequestFactory!
var settings: TestSettings!
var internalUtility: TestInternalUtility!
var manager: DeviceLoginManager!
// swiftlint:enable implicitly_unwrapped_optional
override func setUp() {
super.setUp()
poller = TestDevicePoller()
delegate = TestDeviceLoginManagerDelegate()
factory = TestGraphRequestFactory()
settings = TestSettings()
internalUtility = TestInternalUtility()
manager = DeviceLoginManager(
permissions: permissions,
enableSmartLogin: false,
graphRequestFactory: factory,
devicePoller: poller,
settings: settings,
internalUtility: internalUtility
)
manager.redirectURL = redirectURL
manager.delegate = delegate
manager.setCodeInfo(sampleCodeInfo())
}
override func tearDown() {
poller = nil
delegate = nil
factory = nil
settings = nil
internalUtility = nil
manager = nil
super.tearDown()
}
// MARK: Dependencies
func testCreatingWithDependencies() {
XCTAssertIdentical(
manager.graphRequestFactory,
factory,
"A device login manager should be created with the provided graph request factory"
)
XCTAssertIdentical(
manager.devicePoller,
poller,
"A device login manager should be created with the provided device poller"
)
XCTAssertIdentical(
manager.settings,
settings,
"A device login manager should be created with the provided settings"
)
XCTAssertIdentical(
manager.internalUtility,
internalUtility,
"A device login manager should be created with the provided internal utility"
)
}
func testDefaultDependencies() {
manager = DeviceLoginManager(
permissions: permissions,
enableSmartLogin: false
)
XCTAssertTrue(
manager.graphRequestFactory is GraphRequestFactory,
"A device login manager should be created with a concrete graph request factory by default"
)
XCTAssertTrue(
manager.devicePoller is DevicePoller,
"A device login manager should be created with a concrete device poller by default"
)
XCTAssertIdentical(
manager.settings,
Settings.shared,
"A device login manager should be created with the shared settings by default"
)
XCTAssertIdentical(
manager.internalUtility,
InternalUtility.shared,
"A device login manager should be created with the shared internal utility by default"
)
}
// MARK: Start
func testStartGraphRequestCreation() throws {
manager.start()
let request = try XCTUnwrap(factory.capturedRequests.first)
XCTAssertEqual(
request.graphPath,
"device/login",
"Should create a graph request with the expected graph path"
)
XCTAssertEqual(
request.parameters["scope"] as? String,
permissions.joined(separator: ","),
"Should create a graph request with the expected scope"
)
XCTAssertEqual(
request.parameters["redirect_uri"] as? String,
redirectURL.absoluteString,
"Should create a graph request with the expected redirect URL"
)
XCTAssertNotNil(
request.parameters["device_info"],
"Should create a graph request with device info"
)
}
func testStartGraphRequestCompleteWithError() throws {
manager.start()
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, nil, NSError(domain: "foo", code: 0, userInfo: nil))
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testStartGraphRequestCompleteWithEmptyResponse() throws {
manager.start()
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, [], nil)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testStartGraphRequestCompleteWithCodeInfo() throws {
manager.start()
let expectedCodeInfo = sampleCodeInfo()
let result = [
"code": expectedCodeInfo.identifier,
"user_code": expectedCodeInfo.loginCode,
"verification_uri": expectedCodeInfo.verificationURL.absoluteString,
"expires_in": String(expectedCodeInfo.expirationDate.timeIntervalSinceNow),
"interval": expectedCodeInfo.pollingInterval,
] as [String: Any]
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, result, nil)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNil(delegate.capturedError)
XCTAssertNil(delegate.capturedResult)
let codeInfo = try XCTUnwrap(delegate.capturedCodeInfo, "Should receive code info")
XCTAssertEqual(codeInfo.identifier, expectedCodeInfo.identifier)
XCTAssertEqual(codeInfo.loginCode, expectedCodeInfo.loginCode)
XCTAssertEqual(codeInfo.verificationURL, expectedCodeInfo.verificationURL)
XCTAssertEqual(
codeInfo.expirationDate.timeIntervalSince1970,
expectedCodeInfo.expirationDate.timeIntervalSince1970,
accuracy: 1
)
XCTAssertEqual(codeInfo.pollingInterval, expectedCodeInfo.pollingInterval)
}
// MARK: _schedulePoll
func testStatusGraphRequestCreation() throws {
let tokenString = "sample-token"
internalUtility.stubbedRequiredClientAccessToken = tokenString
let codeInfo = sampleCodeInfo()
manager._schedulePoll(codeInfo.pollingInterval)
XCTAssertEqual(poller.capturedInterval, codeInfo.pollingInterval)
let request = try XCTUnwrap(factory.capturedRequests.first)
XCTAssertEqual(
request.graphPath,
"device/login_status",
"Should create a graph request with the expected graph path"
)
let parameters = request.parameters
XCTAssertEqual(
parameters["code"] as? String,
sampleCodeInfo().identifier,
"Should create a graph request with the expected code"
)
XCTAssertEqual(
request.tokenString,
tokenString
)
}
func testStatusGraphRequestCompleteWithError() throws {
manager._schedulePoll(sampleCodeInfo().pollingInterval)
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, nil, NSError(domain: "foo", code: 0, userInfo: nil))
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testStatusGraphRequestCompleteWithNoToken() throws {
manager._schedulePoll(sampleCodeInfo().pollingInterval)
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, [], nil)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testStatusGraphRequestCompleteWithAccessToken() throws {
manager._schedulePoll(sampleCodeInfo().pollingInterval)
let result: [String: String] = [
"access_token": SampleAccessTokens.validToken.tokenString,
"expires_in": String(SampleAccessTokens.validToken.expirationDate.timeIntervalSinceNow),
"data_access_expiration_time": String(
SampleAccessTokens.validToken.dataAccessExpirationDate.timeIntervalSince1970
),
]
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, result, nil)
let request = try XCTUnwrap(factory.capturedRequests.last)
XCTAssertEqual(request.tokenString, SampleAccessTokens.validToken.tokenString)
XCTAssertEqual(request.graphPath, "me")
}
func testSchedulePollAfterCancel() throws {
manager._schedulePoll(sampleCodeInfo().pollingInterval)
manager.cancel()
let result: [String: String] = [
"access_token": SampleAccessTokens.validToken.tokenString,
]
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, result, nil)
XCTAssertNil(delegate.capturedError)
XCTAssertEqual(
factory.capturedRequests.count,
1,
"Should not be making another graph request to fetch permissions"
)
}
// MARK: _notifyToken
func testNotifyTokenGraphRequestCreation() throws {
manager._notifyToken(
SampleAccessTokens.validToken.tokenString,
withExpirationDate: SampleAccessTokens.validToken.expirationDate,
withDataAccessExpirationDate: SampleAccessTokens.validToken.dataAccessExpirationDate
)
let request = try XCTUnwrap(factory.capturedRequests.last)
XCTAssertEqual(
request.graphPath,
"me",
"Should create a graph request with the expected graph path"
)
let parameters = request.parameters
XCTAssertEqual(
parameters["fields"] as? String,
"id,permissions",
"Should create a graph request with the expected fields"
)
XCTAssertEqual(
request.tokenString,
SampleAccessTokens.validToken.tokenString,
"Should create a graph request with the expected token string"
)
}
func testNotifyTokenGraphRequestCompleteWithError() throws {
manager._notifyToken(
SampleAccessTokens.validToken.tokenString,
withExpirationDate: SampleAccessTokens.validToken.expirationDate,
withDataAccessExpirationDate: SampleAccessTokens.validToken.dataAccessExpirationDate
)
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(nil, nil, NSError(domain: "foo", code: 0, userInfo: nil))
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testNotifyTokenGraphRequestCompleteWithNoUserID() throws {
let result: [String: Any] = [
"permissions": ["data": []],
]
manager._notifyToken(
SampleAccessTokens.validToken.tokenString,
withExpirationDate: SampleAccessTokens.validToken.expirationDate,
withDataAccessExpirationDate: SampleAccessTokens.validToken.dataAccessExpirationDate
)
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(
nil,
result,
nil
)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testNotifyTokenGraphRequestCompleteWithNoPermissions() throws {
manager._notifyToken(
SampleAccessTokens.validToken.tokenString,
withExpirationDate: SampleAccessTokens.validToken.expirationDate,
withDataAccessExpirationDate: SampleAccessTokens.validToken.dataAccessExpirationDate
)
let result: [String: Any] = [
"id": "123",
]
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(
nil,
result,
nil
)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNotNil(delegate.capturedError)
}
func testNotifyTokenGraphRequestCompleteWithPermissionsAndUserID() throws {
manager._notifyToken(
SampleAccessTokens.validToken.tokenString,
withExpirationDate: SampleAccessTokens.validToken.expirationDate,
withDataAccessExpirationDate: SampleAccessTokens.validToken.dataAccessExpirationDate
)
let response: [String: Any] = [
"id": "123",
"permissions": [
"marker": true,
],
]
let granted = ["public_profile", "email"]
let declined = ["user_friends"]
let expired = ["user_birthday"]
internalUtility.stubbedGrantedPermissions = granted
internalUtility.stubbedDeclinedPermissions = declined
internalUtility.stubbedExpiredPermissions = expired
let completion = try XCTUnwrap(factory.capturedRequests.first?.capturedCompletionHandler)
completion(
nil,
response,
nil
)
guard let loginResult = delegate.capturedResult else {
XCTFail("Should receive a login result")
return
}
XCTAssertFalse(loginResult.isCancelled)
guard let token = loginResult.accessToken else {
XCTFail("Should receive an AccessToken within login result")
return
}
let marker = try XCTUnwrap(internalUtility.capturedExtractPermissionsResponse?["marker"] as? Bool)
XCTAssertTrue(
marker,
"The response's permissions should be passed to the internal utility"
)
XCTAssertEqual(token.userID, "123")
XCTAssertEqual(
Set(token.permissions.map(\.name)),
Set(granted)
)
XCTAssertEqual(
Set(token.declinedPermissions.map(\.name)),
Set(declined)
)
XCTAssertEqual(
Set(token.expiredPermissions.map(\.name)),
Set(expired)
)
XCTAssertEqual(token, AccessToken.current)
}
func testNotifyTokenWithNoTokenString() {
manager._notifyToken(
nil,
withExpirationDate: nil,
withDataAccessExpirationDate: nil
)
XCTAssertEqual(factory.capturedRequests.count, 0)
guard let loginResult = delegate.capturedResult else {
XCTFail("Should receive an login result")
return
}
XCTAssert(loginResult.isCancelled)
XCTAssertNil(loginResult.accessToken)
}
// MARK: _processError
func testProcessErrorAuthorizationPending() throws {
manager._processError(
NSError(
domain: "foo",
code: 0,
userInfo: [GraphRequestErrorGraphErrorSubcodeKey: DeviceLoginError.authorizationPending.rawValue]
)
)
let request = try XCTUnwrap(factory.capturedRequests.first)
XCTAssertEqual(
request.graphPath,
"device/login_status",
"Should create a graph request with the expected graph path"
)
}
func testProcessErrorCodeExpired() {
manager._processError(
NSError(
domain: "foo",
code: 0,
userInfo: [GraphRequestErrorGraphErrorSubcodeKey: DeviceLoginError.codeExpired.rawValue]
)
)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNil(factory.capturedRequests.first)
assertCancelResult()
}
func testProcessErrorAuthorizationDeclined() {
manager._processError(
NSError(
domain: "foo",
code: 0,
userInfo: [GraphRequestErrorGraphErrorSubcodeKey: DeviceLoginError.authorizationDeclined.rawValue]
)
)
XCTAssertEqual(delegate.capturedLoginManager, manager)
XCTAssertNil(factory.capturedRequests.first)
assertCancelResult()
}
func testProcessErrorExcessivePolling() throws {
manager._processError(
NSError(
domain: "foo",
code: 0,
userInfo: [GraphRequestErrorGraphErrorSubcodeKey: DeviceLoginError.excessivePolling.rawValue]
)
)
let request = try XCTUnwrap(factory.capturedRequests.first)
XCTAssertEqual(
request.graphPath,
"device/login_status",
"Should create a graph request with the expected graph path"
)
}
// MARK: Helpers
func sampleCodeInfo() -> DeviceLoginCodeInfo {
DeviceLoginCodeInfo(
identifier: "identifier",
loginCode: "loginCode",
verificationURL: URL(string: "https://www.facebook.com")!, // swiftlint:disable:this force_unwrapping
expirationDate: Date.distantFuture,
pollingInterval: 10
)
}
func assertCancelResult() {
guard let loginResult = delegate.capturedResult else {
XCTFail("Should receive an login result")
return
}
XCTAssert(loginResult.isCancelled)
XCTAssertNil(loginResult.accessToken)
}
}