PageRenderTime 50ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/facebook/src/com/facebook/TestSession.java

https://bitbucket.org/mobileup/facebook-android-sdk
Java | 516 lines | 294 code | 88 blank | 134 comment | 51 complexity | c731f01a46c4e58bfdec3c86b33a8203 MD5 | raw file
  1. /**
  2. * Copyright 2010-present Facebook.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.facebook;
  17. import android.app.Activity;
  18. import android.os.Bundle;
  19. import android.text.TextUtils;
  20. import android.util.Log;
  21. import com.facebook.model.GraphObject;
  22. import com.facebook.model.GraphObjectList;
  23. import com.facebook.internal.Logger;
  24. import com.facebook.internal.Utility;
  25. import com.facebook.internal.Validate;
  26. import org.json.JSONException;
  27. import org.json.JSONObject;
  28. import java.util.*;
  29. /**
  30. * Implements an subclass of Session that knows about test users for a particular
  31. * application. This should never be used from a real application, but may be useful
  32. * for writing unit tests, etc.
  33. * <p/>
  34. * Facebook allows developers to create test accounts for testing their applications'
  35. * Facebook integration (see https://developers.facebook.com/docs/test_users/). This class
  36. * simplifies use of these accounts for writing unit tests. It is not designed for use in
  37. * production application code.
  38. * <p/>
  39. * The main use case for this class is using {@link #createSessionWithPrivateUser(android.app.Activity, java.util.List)}
  40. * or {@link #createSessionWithSharedUser(android.app.Activity, java.util.List)}
  41. * to create a session for a test user. Two modes are supported. In "shared" mode, an attempt
  42. * is made to find an existing test user that has the required permissions. If no such user is available,
  43. * a new one is created with the required permissions. In "private" mode, designed for
  44. * scenarios which require a new user in a known clean state, a new test user will always be
  45. * created, and it will be automatically deleted when the TestSession is closed. The session
  46. * obeys the same lifecycle as a regular Session, meaning it must be opened after creation before
  47. * it can be used to make calls to the Facebook API.
  48. * <p/>
  49. * Prior to creating a TestSession, two static methods must be called to initialize the
  50. * application ID and application Secret to be used for managing test users. These methods are
  51. * {@link #setTestApplicationId(String)} and {@link #setTestApplicationSecret(String)}.
  52. * <p/>
  53. * Note that the shared test user functionality depends on a naming convention for the test users.
  54. * It is important that any testing of functionality which will mutate the permissions for a
  55. * test user NOT use a shared test user, or this scheme will break down. If a shared test user
  56. * seems to be in an invalid state, it can be deleted manually via the Web interface at
  57. * https://developers.facebook.com/apps/APP_ID/permissions?role=test+users.
  58. */
  59. public class TestSession extends Session {
  60. private static final long serialVersionUID = 1L;
  61. private enum Mode {
  62. PRIVATE, SHARED
  63. }
  64. private static final String LOG_TAG = Logger.LOG_TAG_BASE + "TestSession";
  65. private static Map<String, TestAccount> appTestAccounts;
  66. private static String testApplicationSecret;
  67. private static String testApplicationId;
  68. private final String sessionUniqueUserTag;
  69. private final List<String> requestedPermissions;
  70. private final Mode mode;
  71. private String testAccountId;
  72. private boolean wasAskedToExtendAccessToken;
  73. TestSession(Activity activity, List<String> permissions, TokenCachingStrategy tokenCachingStrategy,
  74. String sessionUniqueUserTag, Mode mode) {
  75. super(activity, TestSession.testApplicationId, tokenCachingStrategy);
  76. Validate.notNull(permissions, "permissions");
  77. // Validate these as if they were arguments even though they are statics.
  78. Validate.notNullOrEmpty(testApplicationId, "testApplicationId");
  79. Validate.notNullOrEmpty(testApplicationSecret, "testApplicationSecret");
  80. this.sessionUniqueUserTag = sessionUniqueUserTag;
  81. this.mode = mode;
  82. this.requestedPermissions = permissions;
  83. }
  84. /**
  85. * Constructs a TestSession which creates a test user on open, and destroys the user on
  86. * close; This method should not be used in application code -- but is useful for creating unit tests
  87. * that use the Facebook SDK.
  88. *
  89. * @param activity the Activity to use for opening the session
  90. * @param permissions list of strings containing permissions to request; nil will result in
  91. * a common set of permissions (email, publish_actions) being requested
  92. * @return a new TestSession that is in the CREATED state, ready to be opened
  93. */
  94. public static TestSession createSessionWithPrivateUser(Activity activity, List<String> permissions) {
  95. return createTestSession(activity, permissions, Mode.PRIVATE, null);
  96. }
  97. /**
  98. * Constructs a TestSession which uses a shared test user with the right permissions,
  99. * creating one if necessary on open (but not deleting it on close, so it can be re-used in later
  100. * tests).
  101. * <p/>
  102. * This method should not be used in application code -- but is useful for creating unit tests
  103. * that use the Facebook SDK.
  104. *
  105. * @param activity the Activity to use for opening the session
  106. * @param permissions list of strings containing permissions to request; nil will result in
  107. * a common set of permissions (email, publish_actions) being requested
  108. * @return a new TestSession that is in the CREATED state, ready to be opened
  109. */
  110. public static TestSession createSessionWithSharedUser(Activity activity, List<String> permissions) {
  111. return createSessionWithSharedUser(activity, permissions, null);
  112. }
  113. /**
  114. * Constructs a TestSession which uses a shared test user with the right permissions,
  115. * creating one if necessary on open (but not deleting it on close, so it can be re-used in later
  116. * tests).
  117. * <p/>
  118. * This method should not be used in application code -- but is useful for creating unit tests
  119. * that use the Facebook SDK.
  120. *
  121. * @param activity the Activity to use for opening the session
  122. * @param permissions list of strings containing permissions to request; nil will result in
  123. * a common set of permissions (email, publish_actions) being requested
  124. * @param sessionUniqueUserTag a string which will be used to make this user unique among other
  125. * users with the same permissions. Useful for tests which require two or more users to interact
  126. * with each other, and which therefore must have sessions associated with different users.
  127. * @return a new TestSession that is in the CREATED state, ready to be opened
  128. */
  129. public static TestSession createSessionWithSharedUser(Activity activity, List<String> permissions,
  130. String sessionUniqueUserTag) {
  131. return createTestSession(activity, permissions, Mode.SHARED, sessionUniqueUserTag);
  132. }
  133. /**
  134. * Gets the Facebook Application ID for the application under test.
  135. *
  136. * @return the application ID
  137. */
  138. public static synchronized String getTestApplicationId() {
  139. return testApplicationId;
  140. }
  141. /**
  142. * Sets the Facebook Application ID for the application under test. This must be specified
  143. * prior to creating a TestSession.
  144. *
  145. * @param applicationId the application ID
  146. */
  147. public static synchronized void setTestApplicationId(String applicationId) {
  148. if (testApplicationId != null && !testApplicationId.equals(applicationId)) {
  149. throw new FacebookException("Can't have more than one test application ID");
  150. }
  151. testApplicationId = applicationId;
  152. }
  153. /**
  154. * Gets the Facebook Application Secret for the application under test.
  155. *
  156. * @return the application secret
  157. */
  158. public static synchronized String getTestApplicationSecret() {
  159. return testApplicationSecret;
  160. }
  161. /**
  162. * Sets the Facebook Application Secret for the application under test. This must be specified
  163. * prior to creating a TestSession.
  164. *
  165. * @param applicationSecret the application secret
  166. */
  167. public static synchronized void setTestApplicationSecret(String applicationSecret) {
  168. if (testApplicationSecret != null && !testApplicationSecret.equals(applicationSecret)) {
  169. throw new FacebookException("Can't have more than one test application secret");
  170. }
  171. testApplicationSecret = applicationSecret;
  172. }
  173. /**
  174. * Gets the ID of the test user that this TestSession is authenticated as.
  175. *
  176. * @return the Facebook user ID of the test user
  177. */
  178. public final String getTestUserId() {
  179. return testAccountId;
  180. }
  181. private static synchronized TestSession createTestSession(Activity activity, List<String> permissions, Mode mode,
  182. String sessionUniqueUserTag) {
  183. if (Utility.isNullOrEmpty(testApplicationId) || Utility.isNullOrEmpty(testApplicationSecret)) {
  184. throw new FacebookException("Must provide app ID and secret");
  185. }
  186. if (Utility.isNullOrEmpty(permissions)) {
  187. permissions = Arrays.asList("email", "publish_actions");
  188. }
  189. return new TestSession(activity, permissions, new TestTokenCachingStrategy(), sessionUniqueUserTag,
  190. mode);
  191. }
  192. private static synchronized void retrieveTestAccountsForAppIfNeeded() {
  193. if (appTestAccounts != null) {
  194. return;
  195. }
  196. appTestAccounts = new HashMap<String, TestAccount>();
  197. // The data we need is split across two different FQL tables. We construct two queries, submit them
  198. // together (the second one refers to the first one), then cross-reference the results.
  199. // Get the test accounts for this app.
  200. String testAccountQuery = String.format("SELECT id,access_token FROM test_account WHERE app_id = %s",
  201. testApplicationId);
  202. // Get the user names for those accounts.
  203. String userQuery = "SELECT uid,name FROM user WHERE uid IN (SELECT id FROM #test_accounts)";
  204. Bundle parameters = new Bundle();
  205. // Build a JSON string that contains our queries and pass it as the 'q' parameter of the query.
  206. JSONObject multiquery;
  207. try {
  208. multiquery = new JSONObject();
  209. multiquery.put("test_accounts", testAccountQuery);
  210. multiquery.put("users", userQuery);
  211. } catch (JSONException exception) {
  212. throw new FacebookException(exception);
  213. }
  214. parameters.putString("q", multiquery.toString());
  215. // We need to authenticate as this app.
  216. parameters.putString("access_token", getAppAccessToken());
  217. Request request = new Request(null, "fql", parameters, null);
  218. Response response = request.executeAndWait();
  219. if (response.getError() != null) {
  220. throw response.getError().getException();
  221. }
  222. FqlResponse fqlResponse = response.getGraphObjectAs(FqlResponse.class);
  223. GraphObjectList<FqlResult> fqlResults = fqlResponse.getData();
  224. if (fqlResults == null || fqlResults.size() != 2) {
  225. throw new FacebookException("Unexpected number of results from FQL query");
  226. }
  227. // We get back two sets of results. The first is from the test_accounts query, the second from the users query.
  228. Collection<TestAccount> testAccounts = fqlResults.get(0).getFqlResultSet().castToListOf(TestAccount.class);
  229. Collection<UserAccount> userAccounts = fqlResults.get(1).getFqlResultSet().castToListOf(UserAccount.class);
  230. // Use both sets of results to populate our static array of accounts.
  231. populateTestAccounts(testAccounts, userAccounts);
  232. return;
  233. }
  234. private static synchronized void populateTestAccounts(Collection<TestAccount> testAccounts,
  235. Collection<UserAccount> userAccounts) {
  236. // We get different sets of data from each of these queries. We want to combine them into a single data
  237. // structure. We have added a Name property to the TestAccount interface, even though we don't really get
  238. // a name back from the service from that query. We stick the Name from the corresponding UserAccount in it.
  239. for (TestAccount testAccount : testAccounts) {
  240. storeTestAccount(testAccount);
  241. }
  242. for (UserAccount userAccount : userAccounts) {
  243. TestAccount testAccount = appTestAccounts.get(userAccount.getUid());
  244. if (testAccount != null) {
  245. testAccount.setName(userAccount.getName());
  246. }
  247. }
  248. }
  249. private static synchronized void storeTestAccount(TestAccount testAccount) {
  250. appTestAccounts.put(testAccount.getId(), testAccount);
  251. }
  252. private static synchronized TestAccount findTestAccountMatchingIdentifier(String identifier) {
  253. retrieveTestAccountsForAppIfNeeded();
  254. for (TestAccount testAccount : appTestAccounts.values()) {
  255. if (testAccount.getName().contains(identifier)) {
  256. return testAccount;
  257. }
  258. }
  259. return null;
  260. }
  261. @Override
  262. public final String toString() {
  263. String superString = super.toString();
  264. return new StringBuilder().append("{TestSession").append(" testUserId:").append(testAccountId)
  265. .append(" ").append(superString).append("}").toString();
  266. }
  267. @Override
  268. void authorize(AuthorizationRequest request) {
  269. if (mode == Mode.PRIVATE) {
  270. createTestAccountAndFinishAuth();
  271. } else {
  272. findOrCreateSharedTestAccount();
  273. }
  274. }
  275. @Override
  276. void postStateChange(final SessionState oldState, final SessionState newState, final Exception error) {
  277. // Make sure this doesn't get overwritten.
  278. String id = testAccountId;
  279. super.postStateChange(oldState, newState, error);
  280. if (newState.isClosed() && id != null && mode == Mode.PRIVATE) {
  281. deleteTestAccount(id, getAppAccessToken());
  282. }
  283. }
  284. boolean getWasAskedToExtendAccessToken() {
  285. return wasAskedToExtendAccessToken;
  286. }
  287. void forceExtendAccessToken(boolean forceExtendAccessToken) {
  288. AccessToken currentToken = getTokenInfo();
  289. setTokenInfo(
  290. new AccessToken(currentToken.getToken(), new Date(), currentToken.getPermissions(),
  291. AccessTokenSource.TEST_USER, new Date(0)));
  292. setLastAttemptedTokenExtendDate(new Date(0));
  293. }
  294. @Override
  295. boolean shouldExtendAccessToken() {
  296. boolean result = super.shouldExtendAccessToken();
  297. wasAskedToExtendAccessToken = false;
  298. return result;
  299. }
  300. @Override
  301. void extendAccessToken() {
  302. wasAskedToExtendAccessToken = true;
  303. super.extendAccessToken();
  304. }
  305. void fakeTokenRefreshAttempt() {
  306. setCurrentTokenRefreshRequest(new TokenRefreshRequest());
  307. }
  308. static final String getAppAccessToken() {
  309. return testApplicationId + "|" + testApplicationSecret;
  310. }
  311. private void findOrCreateSharedTestAccount() {
  312. TestAccount testAccount = findTestAccountMatchingIdentifier(getSharedTestAccountIdentifier());
  313. if (testAccount != null) {
  314. finishAuthWithTestAccount(testAccount);
  315. } else {
  316. createTestAccountAndFinishAuth();
  317. }
  318. }
  319. private void finishAuthWithTestAccount(TestAccount testAccount) {
  320. testAccountId = testAccount.getId();
  321. AccessToken accessToken = AccessToken.createFromString(testAccount.getAccessToken(), requestedPermissions,
  322. AccessTokenSource.TEST_USER);
  323. finishAuthOrReauth(accessToken, null);
  324. }
  325. private TestAccount createTestAccountAndFinishAuth() {
  326. Bundle parameters = new Bundle();
  327. parameters.putString("installed", "true");
  328. parameters.putString("permissions", getPermissionsString());
  329. parameters.putString("access_token", getAppAccessToken());
  330. // If we're in shared mode, we want to rename this user to encode its permissions, so we can find it later
  331. // in another shared session. If we're in private mode, don't bother renaming it since we're just going to
  332. // delete it at the end of the session.
  333. if (mode == Mode.SHARED) {
  334. parameters.putString("name", String.format("Shared %s Testuser", getSharedTestAccountIdentifier()));
  335. }
  336. String graphPath = String.format("%s/accounts/test-users", testApplicationId);
  337. Request createUserRequest = new Request(null, graphPath, parameters, HttpMethod.POST);
  338. Response response = createUserRequest.executeAndWait();
  339. FacebookRequestError error = response.getError();
  340. TestAccount testAccount = response.getGraphObjectAs(TestAccount.class);
  341. if (error != null) {
  342. finishAuthOrReauth(null, error.getException());
  343. return null;
  344. } else {
  345. assert testAccount != null;
  346. // If we are in shared mode, store this new account in the dictionary so we can re-use it later.
  347. if (mode == Mode.SHARED) {
  348. // Remember the new name we gave it, since we didn't get it back in the results of the create request.
  349. testAccount.setName(parameters.getString("name"));
  350. storeTestAccount(testAccount);
  351. }
  352. finishAuthWithTestAccount(testAccount);
  353. return testAccount;
  354. }
  355. }
  356. private void deleteTestAccount(String testAccountId, String appAccessToken) {
  357. Bundle parameters = new Bundle();
  358. parameters.putString("access_token", appAccessToken);
  359. Request request = new Request(null, testAccountId, parameters, HttpMethod.DELETE);
  360. Response response = request.executeAndWait();
  361. FacebookRequestError error = response.getError();
  362. GraphObject graphObject = response.getGraphObject();
  363. if (error != null) {
  364. Log.w(LOG_TAG, String.format("Could not delete test account %s: %s", testAccountId, error.getException().toString()));
  365. } else if (graphObject.getProperty(Response.NON_JSON_RESPONSE_PROPERTY) == (Boolean) false) {
  366. Log.w(LOG_TAG, String.format("Could not delete test account %s: unknown reason", testAccountId));
  367. }
  368. }
  369. private String getPermissionsString() {
  370. return TextUtils.join(",", requestedPermissions);
  371. }
  372. private String getSharedTestAccountIdentifier() {
  373. // We use long even though hashes are ints to avoid sign issues.
  374. long permissionsHash = getPermissionsString().hashCode() & 0xffffffffL;
  375. long sessionTagHash = (sessionUniqueUserTag != null) ? sessionUniqueUserTag.hashCode() & 0xffffffffL : 0;
  376. long combinedHash = permissionsHash ^ sessionTagHash;
  377. return validNameStringFromInteger(combinedHash);
  378. }
  379. private String validNameStringFromInteger(long i) {
  380. String s = Long.toString(i);
  381. StringBuilder result = new StringBuilder("Perm");
  382. // We know each character is a digit. Convert it into a letter 'a'-'j'. Avoid repeated characters
  383. // that might make Facebook reject the name by converting every other repeated character into one
  384. // 10 higher ('k'-'t').
  385. char lastChar = 0;
  386. for (char c : s.toCharArray()) {
  387. if (c == lastChar) {
  388. c += 10;
  389. }
  390. result.append((char) (c + 'a' - '0'));
  391. lastChar = c;
  392. }
  393. return result.toString();
  394. }
  395. private interface TestAccount extends GraphObject {
  396. String getId();
  397. String getAccessToken();
  398. // Note: We don't actually get Name from our FQL query. We fill it in by correlating with UserAccounts.
  399. String getName();
  400. void setName(String name);
  401. }
  402. private interface UserAccount extends GraphObject {
  403. String getUid();
  404. String getName();
  405. void setName(String name);
  406. }
  407. private interface FqlResult extends GraphObject {
  408. GraphObjectList<GraphObject> getFqlResultSet();
  409. }
  410. private interface FqlResponse extends GraphObject {
  411. GraphObjectList<FqlResult> getData();
  412. }
  413. private static final class TestTokenCachingStrategy extends TokenCachingStrategy {
  414. private Bundle bundle;
  415. @Override
  416. public Bundle load() {
  417. return bundle;
  418. }
  419. @Override
  420. public void save(Bundle value) {
  421. bundle = value;
  422. }
  423. @Override
  424. public void clear() {
  425. bundle = null;
  426. }
  427. }
  428. }