Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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