Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
584 lines (492 sloc) 23.3 KB
/*
* Copyright 2012 Facebook
*
* 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.
*/
#define SAFE_TO_USE_FBTESTSESSION
#import "FBTestSession.h"
#import "FBTestSession+Internal.h"
#import "FBSessionManualTokenCachingStrategy.h"
#import "FBError.h"
#import "FBSession+Protected.h"
#import "FBSession+Internal.h"
#import "FBRequest.h"
#import <pthread.h>
#import "FBSBJSON.h"
#import "FBGraphUser.h"
/*
Indicates whether the test user for an FBTestSession should be shared
(created only if necessary, not deleted automatically) or private (created specifically
for this session, deleted automatically upon close).
*/
typedef enum {
// Create and delete a new test user for this session.
FBTestSessionModePrivate = 0,
// Use an existing available test user with the right permissions, or create
// a new one if none are available. Not automatically deleted.
FBTestSessionModeShared = 1,
} FBTestSessionMode;
static NSString *const FBPLISTAppIDKey = @"FacebookAppID";
static NSString *const FBPLISTAppSecretKey = @"FacebookAppSecret";
static NSString *const FBPLISTUniqueUserTagKey = @"UniqueUserTag";
static NSString *const FBLoginAuthTestUserURLPath = @"oauth/access_token";
static NSString *const FBLoginAuthTestUserCreatePathFormat = @"%@/accounts/test-users";
static NSString *const FBLoginTestUserClientID = @"client_id";
static NSString *const FBLoginTestUserClientSecret = @"client_secret";
static NSString *const FBLoginTestUserGrantType = @"grant_type";
static NSString *const FBLoginTestUserGrantTypeClientCredentials = @"client_credentials";
static NSString *const FBLoginTestUserAccessToken = @"access_token";
static NSString *const FBLoginTestUserID = @"id";
static NSString *const FBLoginTestUserName = @"name";
NSString *kSecondTestUserTag = @"Second";
NSString *kThirdTestUserTag = @"Third";
NSString *const FBErrorLoginFailedReasonUnitTestResponseUnrecognized = @"com.facebook.sdk:UnitTestResponseUnrecognized";
#pragma mark Module scoped global variables
static NSMutableDictionary *testUsers = nil;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#pragma mark -
#pragma mark Private interface
@interface FBTestSession ()
{
BOOL _forceAccessTokenRefresh;
}
@property (readwrite, copy) NSString *appAccessToken;
@property (readwrite, copy) NSString *testUserID;
@property (readwrite, copy) NSString *testAppID;
@property (readwrite, copy) NSString *testAppSecret;
@property (readwrite, copy) NSString *machineUniqueUserTag;
@property (readwrite, copy) NSString *sessionUniqueUserTag;
@property (readonly, copy) NSString *permissionsString;
@property (readonly, copy) NSString *sharedTestUserIdentifier;
@property (readwrite) FBTestSessionMode mode;
- (id)initWithAppID:(NSString*)appID
appSecret:(NSString*)appSecret
machineUniqueUserTag:(NSString*)uniqueUserTag
sessionUniqueUserTag:(NSString*)sessionUniqueUserTag
mode:(FBTestSessionMode)mode
permissions:(NSArray*)permissions
tokenCachingStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy;
- (void)createNewTestUser;
- (void)retrieveTestUsersForApp;
- (void)findOrCreateSharedUser;
- (void)transitionToOpenWithToken:(NSString*)token;
- (NSString*)validNameStringFromInteger:(NSUInteger)input;
- (void)raiseException:(NSError*)innerError;
+ (void)deleteUnitTestUser:(NSString*)userID accessToken:(NSString*)accessToken;
+ (id)sessionForUnitTestingWithPermissions:(NSArray*)permissions mode:(FBTestSessionMode)mode sessionUniqueUserTag:(NSString*)sessionUniqueUserTag;
@end
#pragma mark -
@implementation FBTestSession
@synthesize appAccessToken = _appAccessToken;
@synthesize testUserID = _testUserID;
@synthesize testAppID = _testAppID;
@synthesize testAppSecret = _testAppSecret;
@synthesize mode = _mode;
@synthesize machineUniqueUserTag = _machineUniqueUserKey;
@synthesize sessionUniqueUserTag = _sessionUniqueUserTag;
#pragma mark Lifecycle
- (id)initWithAppID:(NSString*)appID
appSecret:(NSString*)appSecret
machineUniqueUserTag:(NSString*)machineUniqueUserTag
sessionUniqueUserTag:(NSString*)sessionUniqueUserTag
mode:(FBTestSessionMode)mode
permissions:(NSArray*)permissions
tokenCachingStrategy:(FBSessionTokenCachingStrategy*)tokenCachingStrategy
{
if (self = [super initWithAppID:appID
permissions:permissions
urlSchemeSuffix:nil
tokenCacheStrategy:tokenCachingStrategy]) {
self.testAppID = appID;
self.testAppSecret = appSecret;
self.machineUniqueUserTag = machineUniqueUserTag;
self.sessionUniqueUserTag = sessionUniqueUserTag;
self.appAccessToken = [NSString stringWithFormat:@"%@|%@", appID, appSecret];
self.mode = mode;
}
return self;
}
- (void)dealloc
{
[_appAccessToken release];
[_testUserID release];
[_testAppID release];
[_testAppSecret release];
[_machineUniqueUserKey release];
[_sessionUniqueUserTag release];
[super dealloc];
}
#pragma mark -
#pragma mark Private methods
- (NSString*)permissionsString {
return [self.permissions componentsJoinedByString:@","];
}
- (void)createNewTestUser
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithObjectsAndKeys:
@"true", @"installed",
[self permissionsString], @"permissions",
@"post", @"method",
self.appAccessToken, @"access_token",
nil];
// We don't get the user name back on create, so if we want it later, remember it now.
NSString *newName = nil;
if (self.mode == FBTestSessionModeShared) {
// Rename the user with a hashed representation of our permissions, so we can find it
// again later.
newName = [NSString stringWithFormat:@"Shared %@ Testuser", self.sharedTestUserIdentifier];
[parameters setObject:newName forKey:@"name"];
}
// fetch a test user and token
// note, this fetch uses a manually constructed app token using the appid|appsecret approach,
// if there is demand for support for apps for which this will not work, we may consider handling
// failure by falling back and fetching an app-token via a request; the current approach reduces
// traffic for common unit testing configuration, which seems like the right tradeoff to start with
FBRequest *request = [[[FBRequest alloc] initWithSession:nil
graphPath:[NSString stringWithFormat:FBLoginAuthTestUserCreatePathFormat, self.appID]
parameters:parameters
HTTPMethod:nil]
autorelease];
[request startWithCompletionHandler:
^(FBRequestConnection *connection, id result, NSError *error) {
id userToken;
id userID;
if (!error &&
[result isKindOfClass:[NSDictionary class]] &&
(userToken = [result objectForKey:FBLoginTestUserAccessToken]) &&
[userToken isKindOfClass:[NSString class]] &&
(userID = [result objectForKey:FBLoginTestUserID]) &&
[userID isKindOfClass:[NSString class]]) {
// capture the id for future use
self.testUserID = userID;
// Remember this user if it is going to be shared.
if (self.mode == FBTestSessionModeShared) {
NSDictionary *user = [NSDictionary dictionaryWithObjectsAndKeys:
userID, FBLoginTestUserID,
userToken, FBLoginTestUserAccessToken,
newName, FBLoginTestUserName,
nil];
pthread_mutex_lock(&mutex);
[testUsers setObject:user forKey:userID];
pthread_mutex_unlock(&mutex);
}
[self transitionToOpenWithToken:userToken];
} else {
if (error) {
NSLog(@"Error: [FBSession createNewTestUserAndRename:] failed with error: %@", error.description);
} else {
// we fetched something unexpected when requesting an app token
error = [FBSession errorLoginFailedWithReason:FBErrorLoginFailedReasonUnitTestResponseUnrecognized
errorCode:nil
innerError:nil];
}
// state transition, and call the handler if there is one
[self transitionAndCallHandlerWithState:FBSessionStateClosedLoginFailed
error:error
token:nil
expirationDate:nil
shouldCache:NO
loginType:FBSessionLoginTypeNone];
}
}];
}
- (void)transitionToOpenWithToken:(NSString*)token
{
[self transitionAndCallHandlerWithState:FBSessionStateOpen
error:nil
token:token
expirationDate:[NSDate distantFuture]
shouldCache:NO
loginType:FBSessionLoginTypeTestUser];
}
// We raise exceptions when things go wrong here, because this is intended for use only
// in unit tests and we want things to stop as soon as something bad happens.
- (void)raiseException:(NSError*)innerError
{
NSDictionary *userInfo = nil;
if (innerError) {
userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
innerError, FBErrorInnerErrorKey,
nil];
}
[[NSException exceptionWithName:FBInvalidOperationException
reason:@"FBTestSession encountered an error"
userInfo:userInfo]
raise];
}
- (void)populateTestUsers:(NSArray*)users testAccounts:(NSArray*)testAccounts
{
pthread_mutex_lock(&mutex);
// Map user IDs to test_accounts
for (NSDictionary *testAccount in testAccounts) {
id uid = [[testAccount objectForKey:FBLoginTestUserID] stringValue];
[testUsers setObject:[NSMutableDictionary dictionaryWithDictionary:testAccount]
forKey:uid];
}
// Add the user name to the test_account data.
for (NSDictionary *user in users) {
id uid = [[user objectForKey:@"uid"] stringValue];
NSMutableDictionary *testUser = [testUsers objectForKey:uid];
[testUser setObject:[user objectForKey:FBLoginTestUserName] forKey:FBLoginTestUserName];
}
pthread_mutex_unlock(&mutex);
}
- (void)retrieveTestUsersForApp
{
// We need three pieces of data: id, access_token, and name (which we use to
// encode permissions). We get access_token from the test_account FQL table and
// name from the user table; they share an id. Use FQL multiquery to get it all
// in one go.
NSString *testAccountQuery = [NSString stringWithFormat:
@"SELECT id,access_token FROM test_account WHERE app_id = %@",
self.testAppID];
NSString *userQuery = @"SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)";
NSDictionary *multiquery = [NSDictionary dictionaryWithObjectsAndKeys:
testAccountQuery, @"test_accounts",
userQuery, @"users",
nil];
FBSBJSON *writer = [[FBSBJSON alloc] init];
NSString *jsonMultiquery = [writer stringWithObject:multiquery];
[writer release];
NSDictionary *parameters = [NSDictionary dictionaryWithObjectsAndKeys:
jsonMultiquery, @"q",
self.appAccessToken, @"access_token",
nil];
FBRequest *request = [[[FBRequest alloc] initWithSession:nil
graphPath:@"fql"
parameters:parameters
HTTPMethod:nil]
autorelease];
[request startWithCompletionHandler:
^(FBRequestConnection *connection, id result, NSError *error) {
if (error ||
!result) {
[self raiseException:error];
}
id data = [result objectForKey:@"data"];
if (![data isKindOfClass:[NSArray class]] ||
[data count] != 2) {
[self raiseException:nil];
}
// We get back two sets of results. The first is from the test_accounts
// query, the second from the users query.
id testAccounts = [[data objectAtIndex:0] objectForKey:@"fql_result_set"];
id users = [[data objectAtIndex:1] objectForKey:@"fql_result_set"];
if (![testAccounts isKindOfClass:[NSArray class]] ||
![users isKindOfClass:[NSArray class]]) {
[self raiseException:nil];
}
// Use both sets of results to populate our static array of accounts.
[self populateTestUsers:users testAccounts:testAccounts];
// Now that we've populated all test users, we can continue looking for
// the matching user, which started this all off.
[self findOrCreateSharedUser];
}];
}
// Given a long string, generate its hash value, and then convert that to a string that
// we can use as part of a Facebook test user name (i.e., no digits).
- (NSString*)validNameStringFromInteger:(NSUInteger)input
{
NSString *hashAsString = [NSString stringWithFormat:@"%u", input];
NSMutableString *result = [NSMutableString stringWithString:@"Perm"];
// We know each character is a digit. Convert it into a letter starting with 'a'.
for (int i = 0; i < hashAsString.length; ++i) {
NSString *ch = [NSString stringWithFormat:@"%C",
(unsigned short)([hashAsString characterAtIndex:i] + 'a' - '0')];
[result appendString:ch];
}
return result;
}
- (NSString*)sharedTestUserIdentifier
{
NSUInteger permissionsHash = self.permissionsString.hash;
NSUInteger machineTagHash = self.machineUniqueUserTag.hash;
NSUInteger sessionTagHash = self.sessionUniqueUserTag.hash;
NSUInteger combinedHash = permissionsHash ^ machineTagHash ^ sessionTagHash;
return [self validNameStringFromInteger:combinedHash];
}
- (void)findOrCreateSharedUser
{
pthread_mutex_lock(&mutex);
NSString *userIdentifier = self.sharedTestUserIdentifier;
id matchingTestUser = nil;
for (id testUser in [testUsers allValues]) {
NSString *userName = [testUser objectForKey:FBLoginTestUserName];
// Does this user have the right permissions and is it not in use?
if ([userName rangeOfString:userIdentifier].length > 0) {
matchingTestUser = testUser;
break;
}
}
pthread_mutex_unlock(&mutex);
if (matchingTestUser) {
// We can use this user. IDs come back as numbers, make sure we return as a string.
self.testUserID = [[matchingTestUser objectForKey:FBLoginTestUserID] description];
[self transitionToOpenWithToken:[matchingTestUser objectForKey:FBLoginTestUserAccessToken]];
} else {
// Need to create a user. Do so, and rename it using our hashed permissions string.
[self createNewTestUser];
}
}
- (void)setForceAccessTokenRefresh:(BOOL)forceAccessTokenRefresh {
_forceAccessTokenRefresh = forceAccessTokenRefresh;
}
- (BOOL)forceAccessTokenRefresh {
return _forceAccessTokenRefresh;
}
#pragma mark -
#pragma mark Overrides
- (BOOL)transitionToState:(FBSessionState)state
andUpdateToken:(NSString*)token
andExpirationDate:(NSDate*)date
shouldCache:(BOOL)shouldCache
loginType:(FBSessionLoginType)loginType {
// in case we need these after the transition
NSString *userID = self.testUserID;
BOOL didTransition = [super transitionToState:state
andUpdateToken:token
andExpirationDate:date
shouldCache:shouldCache
loginType:loginType];
if (didTransition && FB_ISSESSIONSTATETERMINAL(self.state)) {
if (self.mode == FBTestSessionModePrivate) {
[FBTestSession deleteUnitTestUser:userID accessToken:self.appAccessToken];
}
}
return didTransition;
}
- (void)authorizeWithPermissions:(NSArray*)permissions
behavior:(FBSessionLoginBehavior)behavior
defaultAudience:(FBSessionDefaultAudience)audience
isReauthorize:(BOOL)isReauthorize {
// We ignore behavior, since we aren't going to present UI.
if (self.mode == FBTestSessionModePrivate) {
// If we aren't wanting a shared user, just create a user. Don't waste time renaming it since
// we will be deleting it when done.
[self createNewTestUser];
} else {
// We need to see if there are any test users that fit the bill.
// Did we already get the test users?
pthread_mutex_lock(&mutex);
if (testUsers) {
pthread_mutex_unlock(&mutex);
// Yes, look for one that we can use.
[self findOrCreateSharedUser];
} else {
// No, populate the list and then continue.
// We never release testUsers. We should only populate it once.
testUsers = [[NSMutableDictionary alloc] init];
pthread_mutex_unlock(&mutex);
[self retrieveTestUsersForApp];
}
}
}
- (BOOL)shouldExtendAccessToken {
// Note: we reset the flag each time we are queried. Tests should set it as needed for more complicated logic.
BOOL extend = self.forceAccessTokenRefresh || [super shouldExtendAccessToken];
self.forceAccessTokenRefresh = NO;
return extend;
}
#pragma mark -
#pragma mark Class methods
+ (id)sessionWithSharedUserWithPermissions:(NSArray*)permissions
uniqueUserTag:(NSString*)uniqueUserTag
{
return [self sessionForUnitTestingWithPermissions:permissions
mode:FBTestSessionModeShared
sessionUniqueUserTag:uniqueUserTag];
}
+ (id)sessionWithSharedUserWithPermissions:(NSArray*)permissions
{
return [self sessionWithSharedUserWithPermissions:permissions uniqueUserTag:nil];
}
+ (id)sessionWithPrivateUserWithPermissions:(NSArray*)permissions
{
return [self sessionForUnitTestingWithPermissions:permissions
mode:FBTestSessionModePrivate
sessionUniqueUserTag:nil];
}
+ (id)sessionForUnitTestingWithPermissions:(NSArray*)permissions
mode:(FBTestSessionMode)mode
sessionUniqueUserTag:(NSString*)sessionUniqueUserTag
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
// fetch config contents
NSString *configFilename = [documentsDirectory stringByAppendingPathComponent:@"FacebookSDK-UnitTestConfig.plist"];
NSDictionary *configSettings = [NSDictionary dictionaryWithContentsOfFile:configFilename];
NSString *appID = [configSettings objectForKey:FBPLISTAppIDKey];
NSString *appSecret = [configSettings objectForKey:FBPLISTAppSecretKey];
if (!appID || !appSecret) {
[[NSException exceptionWithName:FBInvalidOperationException
reason:
@"FBSession: Missing AppID or AppSecret; FacebookSDK-UnitTestConfig.plist is "
@"is missing or invalid; to create a Facebook AppID, "
@"visit https://developers.facebook.com/apps"
userInfo:nil]
raise];
}
NSString *machineUniqueUserTag = [configSettings objectForKey:FBPLISTUniqueUserTagKey];
FBSessionManualTokenCachingStrategy *tokenCachingStrategy =
[[FBSessionManualTokenCachingStrategy alloc] init];
if (!permissions.count) {
permissions = [NSArray arrayWithObjects:@"email", @"publish_actions", nil];
}
// call our internal designated initializer to create a unit-testing instance
FBTestSession *session = [[[FBTestSession alloc]
initWithAppID:appID
appSecret:appSecret
machineUniqueUserTag:machineUniqueUserTag
sessionUniqueUserTag:sessionUniqueUserTag
mode:mode
permissions:permissions
tokenCachingStrategy:tokenCachingStrategy]
autorelease];
[tokenCachingStrategy release];
return session;
}
+ (void)deleteUnitTestUser:(NSString*)userID
accessToken:(NSString*)accessToken
{
if (userID && accessToken) {
// use FBRequest/FBRequestConnection to create an NSURLRequest
FBRequest *temp = [[FBRequest alloc ] initWithSession:nil
graphPath:userID
parameters:[NSDictionary dictionaryWithObjectsAndKeys:
@"delete", @"method",
accessToken, @"access_token",
nil]
HTTPMethod:nil];
FBRequestConnection *connection = [[FBRequestConnection alloc] init];
[connection addRequest:temp completionHandler:nil];
NSURLRequest *request = connection.urlRequest;
[temp release];
[connection release];
// synchronously delete the user
NSURLResponse *response;
NSError *error = nil;
NSData *data;
data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
// if !data or if data == false, log
NSString *body = !data ? nil : [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding]
autorelease];
if (!data || [body isEqualToString:@"false"]) {
NSLog(@"FBSession !delete test user with id:%@ error:%@", userID, error ? error : body);
}
}
}
#pragma mark -
@end
Something went wrong with that request. Please try again.