<?php
/**
 * Copyright 2011 Facebook, Inc.
 *
 * 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.
 */

class PHPSDKTestCase extends PHPUnit_Framework_TestCase {
  const APP_ID = '117743971608120';
  const SECRET = '9c8ea2071859659bea1246d33a9207cf';

  const MIGRATED_APP_ID = '174236045938435';
  const MIGRATED_SECRET = '0073dce2d95c4a5c2922d1827ea0cca6';

  const TEST_USER   = 499834690;
  const TEST_USER_2 = 499835484;

  private static $kExpiredAccessToken = 'AAABrFmeaJjgBAIshbq5ZBqZBICsmveZCZBi6O4w9HSTkFI73VMtmkL9jLuWsZBZC9QMHvJFtSulZAqonZBRIByzGooCZC8DWr0t1M4BL9FARdQwPWPnIqCiFQ';

  private static function kValidSignedRequest($id = self::TEST_USER, $oauth_token = null) {
    $facebook = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    return $facebook->publicMakeSignedRequest(
      array(
        'user_id' => $id,
        'oauth_token' => $oauth_token
      )
    );
  }

  private static function kNonTosedSignedRequest() {
    $facebook = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    return $facebook->publicMakeSignedRequest(array());
  }

  private static function kSignedRequestWithEmptyValue() {
    return '';
  }

  private static function kSignedRequestWithBogusSignature() {
    $facebook = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => 'bogus',
    ));
    return $facebook->publicMakeSignedRequest(
      array(
        'algorithm' => 'HMAC-SHA256',
      )
    );
  }

  private static function kSignedRequestWithWrongAlgo() {
    $facebook = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $data['algorithm'] = 'foo';
    $json = json_encode($data);
    $b64 = $facebook->publicBase64UrlEncode($json);
    $raw_sig = hash_hmac('sha256', $b64, self::SECRET, $raw = true);
    $sig = $facebook->publicBase64UrlEncode($raw_sig);
    return $sig.'.'.$b64;
  }

  public function testConstructor() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $this->assertEquals($facebook->getAppId(), self::APP_ID,
                        'Expect the App ID to be set.');
    $this->assertEquals($facebook->getAppSecret(), self::SECRET,
                        'Expect the API secret to be set.');
  }

  public function testConstructorWithFileUpload() {
    $facebook = new TransientFacebook(array(
      'appId'      => self::APP_ID,
      'secret'     => self::SECRET,
      'fileUpload' => true,
    ));
    $this->assertEquals($facebook->getAppId(), self::APP_ID,
                        'Expect the App ID to be set.');
    $this->assertEquals($facebook->getAppSecret(), self::SECRET,
                        'Expect the API secret to be set.');
    $this->assertTrue($facebook->getFileUploadSupport(),
                      'Expect file upload support to be on.');
    // alias (depricated) for getFileUploadSupport -- test until removed
    $this->assertTrue($facebook->useFileUploadSupport(),
                      'Expect file upload support to be on.');
  }

  public function testSetAppId() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $facebook->setAppId('dummy');
    $this->assertEquals($facebook->getAppId(), 'dummy',
                        'Expect the App ID to be dummy.');
  }

  public function testSetAPISecret() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $facebook->setApiSecret('dummy');
    $this->assertEquals($facebook->getApiSecret(), 'dummy',
                        'Expect the API secret to be dummy.');
  }

  public function testSetAPPSecret() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $facebook->setAppSecret('dummy');
    $this->assertEquals($facebook->getAppSecret(), 'dummy',
                        'Expect the API secret to be dummy.');
  }

  public function testSetAccessToken() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $facebook->setAccessToken('saltydog');
    $this->assertEquals($facebook->getAccessToken(), 'saltydog',
                        'Expect installed access token to remain \'saltydog\'');
  }

  public function testSetFileUploadSupport() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $this->assertFalse($facebook->getFileUploadSupport(),
                       'Expect file upload support to be off.');
    // alias for getFileUploadSupport (depricated), testing until removed
    $this->assertFalse($facebook->useFileUploadSupport(),
                       'Expect file upload support to be off.');
    $facebook->setFileUploadSupport(true);
    $this->assertTrue($facebook->getFileUploadSupport(),
                      'Expect file upload support to be on.');
    // alias for getFileUploadSupport (depricated), testing until removed
    $this->assertTrue($facebook->useFileUploadSupport(),
                      'Expect file upload support to be on.');
  }

  public function testGetCurrentURL() {
    $facebook = new FBGetCurrentURLFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    // fake the HPHP $_SERVER globals
    $_SERVER['HTTP_HOST'] = 'www.test.com';
    $_SERVER['REQUEST_URI'] = '/unit-tests.php?one=one&two=two&three=three';
    $current_url = $facebook->publicGetCurrentUrl();
    $this->assertEquals(
      'http://www.test.com/unit-tests.php?one=one&two=two&three=three',
      $current_url,
      'getCurrentUrl function is changing the current URL');

    // ensure structure of valueless GET params is retained (sometimes
    // an = sign was present, and sometimes it was not)
    // first test when equal signs are present
    $_SERVER['HTTP_HOST'] = 'www.test.com';
    $_SERVER['REQUEST_URI'] = '/unit-tests.php?one=&two=&three=';
    $current_url = $facebook->publicGetCurrentUrl();
    $this->assertEquals(
      'http://www.test.com/unit-tests.php?one=&two=&three=',
      $current_url,
      'getCurrentUrl function is changing the current URL');

    // now confirm that
    $_SERVER['HTTP_HOST'] = 'www.test.com';
    $_SERVER['REQUEST_URI'] = '/unit-tests.php?one&two&three';
    $current_url = $facebook->publicGetCurrentUrl();
    $this->assertEquals(
      'http://www.test.com/unit-tests.php?one&two&three',
      $current_url,
      'getCurrentUrl function is changing the current URL');
  }

  public function testGetLoginURL() {
    $facebook = new Facebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    // fake the HPHP $_SERVER globals
    $_SERVER['HTTP_HOST'] = 'www.test.com';
    $_SERVER['REQUEST_URI'] = '/unit-tests.php';
    $login_url = parse_url($facebook->getLoginUrl());
    $this->assertEquals($login_url['scheme'], 'https');
    $this->assertEquals($login_url['host'], 'www.facebook.com');
    $this->assertEquals($login_url['path'], '/dialog/oauth');
    $expected_login_params =
      array('client_id' => self::APP_ID,
            'redirect_uri' => 'http://www.test.com/unit-tests.php');

    $query_map = array();
    parse_str($login_url['query'], $query_map);
    $this->assertIsSubset($expected_login_params, $query_map);
    // we don't know what the state is, but we know it's an md5 and should
    // be 32 characters long.
    $this->assertEquals(strlen($query_map['state']), $num_characters = 32);
  }

  public function testGetLoginURLWithExtraParams() {
    $facebook = new Facebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    // fake the HPHP $_SERVER globals
    $_SERVER['HTTP_HOST'] = 'www.test.com';
    $_SERVER['REQUEST_URI'] = '/unit-tests.php';
    $extra_params = array('scope' => 'email, sms',
                          'nonsense' => 'nonsense');
    $login_url = parse_url($facebook->getLoginUrl($extra_params));
    $this->assertEquals($login_url['scheme'], 'https');
    $this->assertEquals($login_url['host'], 'www.facebook.com');
    $this->assertEquals($login_url['path'], '/dialog/oauth');
    $expected_login_params =
      array_merge(
        array('client_id' => self::APP_ID,
              'redirect_uri' => 'http://www.test.com/unit-tests.php'),
        $extra_params);
    $query_map = array();
    parse_str($login_url['query'], $query_map);
    $this->assertIsSubset($expected_login_params, $query_map);
    // we don't know what the state is, but we know it's an md5 and should
    // be 32 characters long.
    $this->assertEquals(strlen($query_map['state']), $num_characters = 32);
  }

  public function testGetLoginURLWithScopeParamsAsArray() {
    $facebook = new Facebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    // fake the HPHP $_SERVER globals
    $_SERVER['HTTP_HOST'] = 'www.test.com';
    $_SERVER['REQUEST_URI'] = '/unit-tests.php';
    $scope_params_as_array = array('email','sms','read_stream');
    $extra_params = array('scope' => $scope_params_as_array,
                          'nonsense' => 'nonsense');
    $login_url = parse_url($facebook->getLoginUrl($extra_params));
    $this->assertEquals($login_url['scheme'], 'https');
    $this->assertEquals($login_url['host'], 'www.facebook.com');
    $this->assertEquals($login_url['path'], '/dialog/oauth');
    // expect api to flatten array params to comma separated list
    // should do the same here before asserting to make sure API is behaving
    // correctly;
    $extra_params['scope'] = implode(',', $scope_params_as_array);
    $expected_login_params =
      array_merge(
        array('client_id' => self::APP_ID,
              'redirect_uri' => 'http://www.test.com/unit-tests.php'),
        $extra_params);
    $query_map = array();
    parse_str($login_url['query'], $query_map);
    $this->assertIsSubset($expected_login_params, $query_map);
    // we don't know what the state is, but we know it's an md5 and should
    // be 32 characters long.
    $this->assertEquals(strlen($query_map['state']), $num_characters = 32);
  }

  public function testGetCodeWithValidCSRFState() {
    $facebook = new FBCode(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $facebook->setCSRFStateToken();
    $code = $_REQUEST['code'] = $this->generateMD5HashOfRandomValue();
    $_REQUEST['state'] = $facebook->getCSRFStateToken();
    $this->assertEquals($code,
                        $facebook->publicGetCode(),
                        'Expect code to be pulled from $_REQUEST[\'code\']');
  }

  public function testGetCodeWithInvalidCSRFState() {
    $facebook = new FBCode(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $facebook->setCSRFStateToken();
    $code = $_REQUEST['code'] = $this->generateMD5HashOfRandomValue();
    $_REQUEST['state'] = $facebook->getCSRFStateToken().'forgery!!!';
    $this->assertFalse($facebook->publicGetCode(),
                       'Expect getCode to fail, CSRF state should not match.');
  }

  public function testGetCodeWithMissingCSRFState() {
    $facebook = new FBCode(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $code = $_REQUEST['code'] = $this->generateMD5HashOfRandomValue();
    // intentionally don't set CSRF token at all
    $this->assertFalse($facebook->publicGetCode(),
                       'Expect getCode to fail, CSRF state not sent back.');
  }

  public function testGetUserFromSignedRequest() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $_REQUEST['signed_request'] = self::kValidSignedRequest();
    $this->assertEquals('499834690', $facebook->getUser(),
                        'Failed to get user ID from a valid signed request.');
  }

  public function testSignedRequestRewrite(){
    $facebook = new FBRewrite(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $_REQUEST['signed_request'] = self::kValidSignedRequest(self::TEST_USER, 'Hello sweetie');

    $this->assertEquals(self::TEST_USER, $facebook->getUser(),
                        'Failed to get user ID from a valid signed request.');

    $this->assertEquals('Hello sweetie', $facebook->getAccessToken(),
                        'Failed to get access token from signed request');

    $facebook->uncache();

    $_REQUEST['signed_request'] = self::kValidSignedRequest(self::TEST_USER_2, 'spoilers');

    $this->assertEquals(self::TEST_USER_2, $facebook->getUser(),
                        'Failed to get user ID from a valid signed request.');

    $_REQUEST['signed_request'] = null;
    $facebook ->uncacheSignedRequest();

    $this->assertNotEquals('Hello sweetie', $facebook->getAccessToken(),
                        'Failed to clear access token');
  }

  public function testGetSignedRequestFromCookie() {
    $facebook = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $_COOKIE[$facebook->publicGetSignedRequestCookieName()] =
      self::kValidSignedRequest();
    $this->assertNotNull($facebook->publicGetSignedRequest());
    $this->assertEquals('499834690', $facebook->getUser(),
                        'Failed to get user ID from a valid signed request.');
  }

  public function testGetSignedRequestWithIncorrectSignature() {
    $facebook = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $_COOKIE[$facebook->publicGetSignedRequestCookieName()] =
      self::kSignedRequestWithBogusSignature();
    $this->assertNull($facebook->publicGetSignedRequest());
  }

  public function testNonUserAccessToken() {
    $facebook = new FBAccessToken(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    // no cookies, and no request params, so no user or code,
    // so no user access token (even with cookie support)
    $this->assertEquals($facebook->publicGetApplicationAccessToken(),
                        $facebook->getAccessToken(),
                        'Access token should be that for logged out users.');
  }

  public function testMissingMetadataCookie() {
    $fb = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $this->assertEmpty($fb->publicGetMetadataCookie());
  }

  public function testEmptyMetadataCookie() {
    $fb = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $_COOKIE[$fb->publicGetMetadataCookieName()] = '';
    $this->assertEmpty($fb->publicGetMetadataCookie());
  }

  public function testMetadataCookie() {
    $fb = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = 'foo';
    $val = '42';
    $_COOKIE[$fb->publicGetMetadataCookieName()] = "$key=$val";
    $this->assertEquals(array($key => $val), $fb->publicGetMetadataCookie());
  }

  public function testQuotedMetadataCookie() {
    $fb = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = 'foo';
    $val = '42';
    $_COOKIE[$fb->publicGetMetadataCookieName()] = "\"$key=$val\"";
    $this->assertEquals(array($key => $val), $fb->publicGetMetadataCookie());
  }

  public function testAPIForLoggedOutUsers() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $response = $facebook->api(array(
      'method' => 'fql.query',
      'query' => 'SELECT name FROM user WHERE uid=4',
    ));
    $this->assertEquals(count($response), 1,
                        'Expect one row back.');
    $this->assertEquals($response[0]['name'], 'Mark Zuckerberg',
                        'Expect the name back.');
  }

  public function testAPIWithBogusAccessToken() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $facebook->setAccessToken('this-is-not-really-an-access-token');
    // if we don't set an access token and there's no way to
    // get one, then the FQL query below works beautifully, handing
    // over Zuck's public data.  But if you specify a bogus access
    // token as I have right here, then the FQL query should fail.
    // We could return just Zuck's public data, but that wouldn't
    // advertise the issue that the access token is at worst broken
    // and at best expired.
    try {
      $response = $facebook->api(array(
        'method' => 'fql.query',
        'query' => 'SELECT name FROM profile WHERE id=4',
      ));
      $this->fail('Should not get here.');
    } catch(FacebookApiException $e) {
      $result = $e->getResult();
      $this->assertTrue(is_array($result), 'expect a result object');
      $this->assertEquals('190', $result['error_code'], 'expect code');
    }
  }

  public function testAPIGraphPublicData() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $response = $facebook->api('/jerry');
    $this->assertEquals(
      $response['id'], '214707', 'should get expected id.');
  }

  public function testGraphAPIWithBogusAccessToken() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $facebook->setAccessToken('this-is-not-really-an-access-token');
    try {
      $response = $facebook->api('/me');
      $this->fail('Should not get here.');
    } catch(FacebookApiException $e) {
      // means the server got the access token and didn't like it
      $msg = 'OAuthException: Invalid OAuth access token.';
      $this->assertEquals($msg, (string) $e,
                          'Expect the invalid OAuth token message.');
    }
  }

  public function testGraphAPIWithExpiredAccessToken() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $facebook->setAccessToken(self::$kExpiredAccessToken);
    try {
      $response = $facebook->api('/me');
      $this->fail('Should not get here.');
    } catch(FacebookApiException $e) {
      // means the server got the access token and didn't like it
      $error_msg_start = 'OAuthException: Error validating access token:';
      $this->assertTrue(strpos((string) $e, $error_msg_start) === 0,
                        'Expect the token validation error message.');
    }
  }

  public function testGraphAPIOAuthSpecError() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::MIGRATED_APP_ID,
      'secret' => self::MIGRATED_SECRET,
    ));

    try {
      $response = $facebook->api('/me', array(
        'client_id' => self::MIGRATED_APP_ID));

      $this->fail('Should not get here.');
    } catch(FacebookApiException $e) {
      // means the server got the access token
      $msg = 'invalid_request: An active access token must be used '.
             'to query information about the current user.';
      $this->assertEquals($msg, (string) $e,
                          'Expect the invalid session message.');
    }
  }

  public function testGraphAPIMethodOAuthSpecError() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::MIGRATED_APP_ID,
      'secret' => self::MIGRATED_SECRET,
    ));

    try {
      $response = $facebook->api('/daaku.shah', 'DELETE', array(
        'client_id' => self::MIGRATED_APP_ID));
      $this->fail('Should not get here.');
    } catch(FacebookApiException $e) {
      $this->assertEquals(strpos($e, 'invalid_request'), 0);
    }
  }

  public function testCurlFailure() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    if (!defined('CURLOPT_TIMEOUT_MS')) {
      // can't test it if we don't have millisecond timeouts
      return;
    }

    $exception = null;
    try {
      // we dont expect facebook will ever return in 1ms
      Facebook::$CURL_OPTS[CURLOPT_TIMEOUT_MS] = 50;
      $facebook->api('/naitik');
    } catch(FacebookApiException $e) {
      $exception = $e;
    }
    unset(Facebook::$CURL_OPTS[CURLOPT_TIMEOUT_MS]);
    if (!$exception) {
      $this->fail('no exception was thrown on timeout.');
    }

    $code = $exception->getCode();
    if ($code != CURLE_OPERATION_TIMEOUTED && $code != CURLE_COULDNT_CONNECT) {
      $this->fail("Expected curl error code 7 or 28 but got: $code");
    }
    $this->assertEquals('CurlException', $exception->getType(), 'expect type');
  }

  public function testGraphAPIWithOnlyParams() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));

    $response = $facebook->api('/jerry');
    $this->assertTrue(isset($response['id']),
                      'User ID should be public.');
    $this->assertTrue(isset($response['name']),
                      'User\'s name should be public.');
    $this->assertTrue(isset($response['first_name']),
                      'User\'s first name should be public.');
    $this->assertTrue(isset($response['last_name']),
                      'User\'s last name should be public.');
    $this->assertFalse(isset($response['work']),
                       'User\'s work history should only be available with '.
                       'a valid access token.');
    $this->assertFalse(isset($response['education']),
                       'User\'s education history should only be '.
                       'available with a valid access token.');
    $this->assertFalse(isset($response['verified']),
                       'User\'s verification status should only be '.
                       'available with a valid access token.');
  }

  public function testLoginURLDefaults() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl = rawurlencode('http://fbrell.com/examples');
    $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl),
                         'Expect the current url to exist.');
  }

  public function testLoginURLDefaultsDropStateQueryParam() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples?state=xx42xx';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $expectEncodedUrl = rawurlencode('http://fbrell.com/examples');
    $this->assertTrue(strpos($facebook->getLoginUrl(), $expectEncodedUrl) > -1,
                      'Expect the current url to exist.');
    $this->assertFalse(strpos($facebook->getLoginUrl(), 'xx42xx'),
                       'Expect the session param to be dropped.');
  }

  public function testLoginURLDefaultsDropCodeQueryParam() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples?code=xx42xx';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $expectEncodedUrl = rawurlencode('http://fbrell.com/examples');
    $this->assertTrue(strpos($facebook->getLoginUrl(), $expectEncodedUrl) > -1,
                      'Expect the current url to exist.');
    $this->assertFalse(strpos($facebook->getLoginUrl(), 'xx42xx'),
                       'Expect the session param to be dropped.');
  }

  public function testLoginURLDefaultsDropSignedRequestParamButNotOthers() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] =
      '/examples?signed_request=xx42xx&do_not_drop=xx43xx';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $expectEncodedUrl = rawurlencode('http://fbrell.com/examples');
    $this->assertFalse(strpos($facebook->getLoginUrl(), 'xx42xx'),
                       'Expect the session param to be dropped.');
    $this->assertTrue(strpos($facebook->getLoginUrl(), 'xx43xx') > -1,
                      'Expect the do_not_drop param to exist.');
  }

  public function testLoginURLCustomNext() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $next = 'http://fbrell.com/custom';
    $loginUrl = $facebook->getLoginUrl(array(
      'redirect_uri' => $next,
      'cancel_url' => $next
    ));
    $currentEncodedUrl = rawurlencode('http://fbrell.com/examples');
    $expectedEncodedUrl = rawurlencode($next);
    $this->assertNotNull(strpos($loginUrl, $expectedEncodedUrl),
                         'Expect the custom url to exist.');
    $this->assertFalse(strpos($loginUrl, $currentEncodedUrl),
                      'Expect the current url to not exist.');
  }

  public function testLogoutURLDefaults() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl = rawurlencode('http://fbrell.com/examples');
    $this->assertNotNull(strpos($facebook->getLogoutUrl(), $encodedUrl),
                         'Expect the current url to exist.');
    $this->assertFalse(strpos($facebook->getLogoutUrl(), self::SECRET));
  }

  public function testLoginStatusURLDefaults() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl = rawurlencode('http://fbrell.com/examples');
    $this->assertNotNull(strpos($facebook->getLoginStatusUrl(), $encodedUrl),
                         'Expect the current url to exist.');
  }

  public function testLoginStatusURLCustom() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl1 = rawurlencode('http://fbrell.com/examples');
    $okUrl = 'http://fbrell.com/here1';
    $encodedUrl2 = rawurlencode($okUrl);
    $loginStatusUrl = $facebook->getLoginStatusUrl(array(
      'ok_session' => $okUrl,
    ));
    $this->assertNotNull(strpos($loginStatusUrl, $encodedUrl1),
                         'Expect the current url to exist.');
    $this->assertNotNull(strpos($loginStatusUrl, $encodedUrl2),
                         'Expect the custom url to exist.');
  }

  public function testNonDefaultPort() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com:8080';
    $_SERVER['REQUEST_URI'] = '/examples';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl = rawurlencode('http://fbrell.com:8080/examples');
    $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl),
                         'Expect the current url to exist.');
  }

  public function testSecureCurrentUrl() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $_SERVER['REQUEST_URI'] = '/examples';
    $_SERVER['HTTPS'] = 'on';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl = rawurlencode('https://fbrell.com/examples');
    $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl),
                         'Expect the current url to exist.');
  }

  public function testSecureCurrentUrlWithNonDefaultPort() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com:8080';
    $_SERVER['REQUEST_URI'] = '/examples';
    $_SERVER['HTTPS'] = 'on';
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $encodedUrl = rawurlencode('https://fbrell.com:8080/examples');
    $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl),
                         'Expect the current url to exist.');
  }

  public function testBase64UrlEncode() {
    $input = 'Facebook rocks';
    $output = 'RmFjZWJvb2sgcm9ja3M';

    $this->assertEquals(FBPublic::publicBase64UrlDecode($output), $input);
  }

  public function testSignedToken() {
    $facebook = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $payload = $facebook->publicParseSignedRequest(self::kValidSignedRequest());
    $this->assertNotNull($payload, 'Expected token to parse');
    $this->assertEquals($facebook->getSignedRequest(), null);
    $_REQUEST['signed_request'] = self::kValidSignedRequest();
    $this->assertEquals($facebook->getSignedRequest(), $payload);
  }

  public function testNonTossedSignedtoken() {
    $facebook = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $payload = $facebook->publicParseSignedRequest(
      self::kNonTosedSignedRequest());
    $this->assertNotNull($payload, 'Expected token to parse');
    $this->assertNull($facebook->getSignedRequest());
    $_REQUEST['signed_request'] = self::kNonTosedSignedRequest();
    $sr = $facebook->getSignedRequest();
    $this->assertTrue(isset($sr['algorithm']));
  }

  public function testSignedRequestWithEmptyValue() {
    $fb = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $_REQUEST['signed_request'] = self::kSignedRequestWithEmptyValue();
    $this->assertNull($fb->getSignedRequest());
    $_COOKIE[$fb->publicGetSignedRequestCookieName()] =
      self::kSignedRequestWithEmptyValue();
    $this->assertNull($fb->getSignedRequest());
  }

  public function testSignedRequestWithWrongAlgo() {
    $fb = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $payload = $fb->publicParseSignedRequest(
      self::kSignedRequestWithWrongAlgo());
    $this->assertNull($payload, 'Expected nothing back.');
  }

  public function testMakeAndParse() {
    $fb = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $data = array('foo' => 42);
    $sr = $fb->publicMakeSignedRequest($data);
    $decoded = $fb->publicParseSignedRequest($sr);
    $this->assertEquals($data['foo'], $decoded['foo']);
  }

  /**
   * @expectedException InvalidArgumentException
   */
  public function testMakeSignedRequestExpectsArray() {
    $fb = new FBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $sr = $fb->publicMakeSignedRequest('');
  }

  public function testBundledCACert() {
    $facebook = new TransientFacebook(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));

      // use the bundled cert from the start
    Facebook::$CURL_OPTS[CURLOPT_CAINFO] =
      dirname(__FILE__) . '/../src/fb_ca_chain_bundle.crt';
    $response = $facebook->api('/naitik');

    unset(Facebook::$CURL_OPTS[CURLOPT_CAINFO]);
    $this->assertEquals(
      $response['id'], '5526183', 'should get expected id.');
  }

  public function testVideoUpload() {
    $facebook = new FBRecordURL(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));

    $facebook->api(array('method' => 'video.upload'));
    $this->assertContains('//api-video.', $facebook->getRequestedURL(),
                          'video.upload should go against api-video');
  }

  public function testVideoUploadGraph() {
    $facebook = new FBRecordURL(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));

    $facebook->api('/me/videos', 'POST');
    $this->assertContains('//graph-video.', $facebook->getRequestedURL(),
                          '/me/videos should go against graph-video');
  }

  public function testGetUserAndAccessTokenFromSession() {
    $facebook = new PersistentFBPublic(array(
                                         'appId'  => self::APP_ID,
                                         'secret' => self::SECRET
                                       ));

    $facebook->publicSetPersistentData('access_token',
                                       self::$kExpiredAccessToken);
    $facebook->publicSetPersistentData('user_id', 12345);
    $this->assertEquals(self::$kExpiredAccessToken,
                        $facebook->getAccessToken(),
                        'Get access token from persistent store.');
    $this->assertEquals('12345',
                        $facebook->getUser(),
                        'Get user id from persistent store.');
  }

  public function testGetUserAndAccessTokenFromSignedRequestNotSession() {
    $facebook = new PersistentFBPublic(array(
                                         'appId'  => self::APP_ID,
                                         'secret' => self::SECRET
                                       ));

    $_REQUEST['signed_request'] = self::kValidSignedRequest();
    $facebook->publicSetPersistentData('user_id', 41572);
    $facebook->publicSetPersistentData('access_token',
                                       self::$kExpiredAccessToken);
    $this->assertNotEquals('41572', $facebook->getUser(),
                           'Got user from session instead of signed request.');
    $this->assertEquals('499834690', $facebook->getUser(),
                        'Failed to get correct user ID from signed request.');
    $this->assertNotEquals(
      self::$kExpiredAccessToken,
      $facebook->getAccessToken(),
      'Got access token from session instead of signed request.');
    $this->assertNotEmpty(
      $facebook->getAccessToken(),
      'Failed to extract an access token from the signed request.');
  }

  public function testGetUserWithoutCodeOrSignedRequestOrSession() {
    $facebook = new PersistentFBPublic(array(
                                         'appId'  => self::APP_ID,
                                         'secret' => self::SECRET
                                       ));

    // deliberately leave $_REQUEST and _$SESSION empty
    $this->assertEmpty($_REQUEST,
                       'GET, POST, and COOKIE params exist even though '.
                       'they should.  Test cannot succeed unless all of '.
                       '$_REQUEST is empty.');
    $this->assertEmpty($_SESSION,
                       'Session is carrying state and should not be.');
    $this->assertEmpty($facebook->getUser(),
                       'Got a user id, even without a signed request, '.
                       'access token, or session variable.');
    $this->assertEmpty($_SESSION,
                       'Session superglobal incorrectly populated by getUser.');
  }

  public function testGetAccessTokenUsingCodeInJsSdkCookie() {
    $code = 'code1';
    $access_token = 'at1';
    $methods_to_stub = array('getSignedRequest', 'getAccessTokenFromCode');
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getSignedRequest')
      ->will($this->returnValue(array('code' => $code)));
    $stub
      ->expects($this->once())
      ->method('getAccessTokenFromCode')
      ->will($this->returnValueMap(array(array($code, '', $access_token))));
    $this->assertEquals($stub->getAccessToken(), $access_token);
  }

  public function testSignedRequestWithoutAuthClearsData() {
    $methods_to_stub = array('getSignedRequest', 'clearAllPersistentData');
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getSignedRequest')
      ->will($this->returnValue(array('foo' => 1)));
    $stub
      ->expects($this->once())
      ->method('clearAllPersistentData');
    $this->assertEquals(self::APP_ID.'|'.self::SECRET, $stub->getAccessToken());
  }

  public function testInvalidCodeInSignedRequestWillClearData() {
    $code = 'code1';
    $methods_to_stub = array(
      'getSignedRequest',
      'getAccessTokenFromCode',
      'clearAllPersistentData',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getSignedRequest')
      ->will($this->returnValue(array('code' => $code)));
    $stub
      ->expects($this->once())
      ->method('getAccessTokenFromCode')
      ->will($this->returnValue(null));
    $stub
      ->expects($this->once())
      ->method('clearAllPersistentData');
    $this->assertEquals(self::APP_ID.'|'.self::SECRET, $stub->getAccessToken());
  }

  public function testInvalidCodeWillClearData() {
    $code = 'code1';
    $methods_to_stub = array(
      'getCode',
      'getAccessTokenFromCode',
      'clearAllPersistentData',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getCode')
      ->will($this->returnValue($code));
    $stub
      ->expects($this->once())
      ->method('getAccessTokenFromCode')
      ->will($this->returnValue(null));
    $stub
      ->expects($this->once())
      ->method('clearAllPersistentData');
    $this->assertEquals(self::APP_ID.'|'.self::SECRET, $stub->getAccessToken());
  }

  public function testValidCodeToToken() {
    $code = 'code1';
    $access_token = 'at1';
    $methods_to_stub = array(
      'getSignedRequest',
      'getCode',
      'getAccessTokenFromCode',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getCode')
      ->will($this->returnValue($code));
    $stub
      ->expects($this->once())
      ->method('getAccessTokenFromCode')
      ->will($this->returnValueMap(array(array($code, null, $access_token))));
    $this->assertEquals($stub->getAccessToken(), $access_token);
  }

  public function testSignedRequestWithoutAuthClearsDataInAvailData() {
    $methods_to_stub = array('getSignedRequest', 'clearAllPersistentData');
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getSignedRequest')
      ->will($this->returnValue(array('foo' => 1)));
    $stub
      ->expects($this->once())
      ->method('clearAllPersistentData');
    $this->assertEquals(0, $stub->getUser());
  }

  public function testFailedToGetUserFromAccessTokenClearsData() {
    $methods_to_stub = array(
      'getAccessToken',
      'getUserFromAccessToken',
      'clearAllPersistentData',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getAccessToken')
      ->will($this->returnValue('at1'));
    $stub
      ->expects($this->once())
      ->method('getUserFromAccessToken');
    $stub
      ->expects($this->once())
      ->method('clearAllPersistentData');
    $this->assertEquals(0, $stub->getUser());
  }

  public function testUserFromAccessTokenIsStored() {
    $methods_to_stub = array(
      'getAccessToken',
      'getUserFromAccessToken',
      'setPersistentData',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $user = 42;
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getAccessToken')
      ->will($this->returnValue('at1'));
    $stub
      ->expects($this->once())
      ->method('getUserFromAccessToken')
      ->will($this->returnValue($user));
    $stub
      ->expects($this->once())
      ->method('setPersistentData');
    $this->assertEquals($user, $stub->getUser());
  }

  public function testUserFromAccessTokenPullsID() {
    $methods_to_stub = array(
      'getAccessToken',
      'api',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $user = 42;
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getAccessToken')
      ->will($this->returnValue('at1'));
    $stub
      ->expects($this->once())
      ->method('api')
      ->will($this->returnValue(array('id' => $user)));
    $this->assertEquals($user, $stub->getUser());
  }

  public function testUserFromAccessTokenResetsOnApiException() {
    $methods_to_stub = array(
      'getAccessToken',
      'clearAllPersistentData',
      'api',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('getAccessToken')
      ->will($this->returnValue('at1'));
    $stub
      ->expects($this->once())
      ->method('api')
      ->will($this->throwException(new FacebookApiException(false)));
    $stub
      ->expects($this->once())
      ->method('clearAllPersistentData');
    $this->assertEquals(0, $stub->getUser());
  }

  public function testEmptyCodeReturnsFalse() {
    $fb = new FBPublicGetAccessTokenFromCode(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $this->assertFalse($fb->publicGetAccessTokenFromCode(''));
    $this->assertFalse($fb->publicGetAccessTokenFromCode(null));
    $this->assertFalse($fb->publicGetAccessTokenFromCode(false));
  }

  public function testNullRedirectURIUsesCurrentURL() {
    $methods_to_stub = array(
      '_oauthRequest',
      'getCurrentUrl',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $access_token = 'at1';
    $stub = $this->getMock(
      'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue("access_token=$access_token"));
    $stub
      ->expects($this->once())
      ->method('getCurrentUrl');
    $this->assertEquals(
      $access_token, $stub->publicGetAccessTokenFromCode('c'));
  }

  public function testNullRedirectURIAllowsEmptyStringForCookie() {
    $methods_to_stub = array(
      '_oauthRequest',
      'getCurrentUrl',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $access_token = 'at1';
    $stub = $this->getMock(
      'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue("access_token=$access_token"));
    $stub
      ->expects($this->never())
      ->method('getCurrentUrl');
    $this->assertEquals(
      $access_token, $stub->publicGetAccessTokenFromCode('c', ''));
  }

  public function testAPIExceptionDuringCodeExchangeIsIgnored() {
    $methods_to_stub = array(
      '_oauthRequest',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->throwException(new FacebookApiException(false)));
    $this->assertFalse($stub->publicGetAccessTokenFromCode('c', ''));
  }

  public function testEmptyResponseInCodeExchangeIsIgnored() {
    $methods_to_stub = array(
      '_oauthRequest',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue(''));
    $this->assertFalse($stub->publicGetAccessTokenFromCode('c', ''));
  }

  public function testExistingStateRestoredInConstructor() {
    $fb = new FBPublicState(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $this->assertEquals(FBPublicState::STATE, $fb->publicGetState());
  }

  public function testMissingAccessTokenInCodeExchangeIsIgnored() {
    $methods_to_stub = array(
      '_oauthRequest',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue('foo=1'));
    $this->assertFalse($stub->publicGetAccessTokenFromCode('c', ''));
  }

  public function testExceptionConstructorWithErrorCode() {
    $code = 404;
    $e = new FacebookApiException(array('error_code' => $code));
    $this->assertEquals($code, $e->getCode());
  }

  // this happens often despite the fact that it is useless
  public function testExceptionTypeFalse() {
    $e = new FacebookApiException(false);
    $this->assertEquals('Exception', $e->getType());
  }

  public function testExceptionTypeMixedDraft00() {
    $e = new FacebookApiException(array('error' => array('message' => 'foo')));
    $this->assertEquals('Exception', $e->getType());
  }

  public function testExceptionTypeDraft00() {
    $error = 'foo';
    $e = new FacebookApiException(
      array('error' => array('type' => $error, 'message' => 'hello world')));
    $this->assertEquals($error, $e->getType());
  }

  public function testExceptionTypeDraft10() {
    $error = 'foo';
    $e = new FacebookApiException(array('error' => $error));
    $this->assertEquals($error, $e->getType());
  }

  public function testExceptionTypeDefault() {
    $e = new FacebookApiException(array('error' => false));
    $this->assertEquals('Exception', $e->getType());
  }

  public function testExceptionToString() {
    $e = new FacebookApiException(array(
      'error_code' => 1,
      'error_description' => 'foo',
    ));
    $this->assertEquals('Exception: 1: foo', (string) $e);
  }

  public function testDestroyClearsCookie() {
    $fb = new FBPublicCookie(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $_COOKIE[$fb->publicGetSignedRequestCookieName()] = 'foo';
    $_COOKIE[$fb->publicGetMetadataCookieName()] = 'base_domain=fbrell.com';
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb->destroySession();
    $this->assertFalse(
      array_key_exists($fb->publicGetSignedRequestCookieName(), $_COOKIE));
  }

  public function testAuthExpireSessionDestroysSession() {
    $methods_to_stub = array(
      '_oauthRequest',
      'destroySession',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $key = 'foo';
    $val = 42;
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue("{\"$key\":$val}"));
    $stub
      ->expects($this->once())
      ->method('destroySession');
    $this->assertEquals(
      array($key => $val),
      $stub->api(array('method' => 'auth.expireSession'))
    );
  }

  public function testLowercaseAuthRevokeAuthDestroysSession() {
    $methods_to_stub = array(
      '_oauthRequest',
      'destroySession',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $key = 'foo';
    $val = 42;
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue("{\"$key\":$val}"));
    $stub
      ->expects($this->once())
      ->method('destroySession');
    $this->assertEquals(
      array($key => $val),
      $stub->api(array('method' => 'auth.revokeauthorization'))
    );
  }

  /**
   * @expectedException FacebookAPIException
   */
  public function testErrorCodeFromRestAPIThrowsException() {
    $methods_to_stub = array(
      '_oauthRequest',
    );
    $constructor_args = array(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET
    ));
    $stub = $this->getMock(
      'TransientFacebook', $methods_to_stub, $constructor_args);
    $stub
      ->expects($this->once())
      ->method('_oauthRequest')
      ->will($this->returnValue('{"error_code": 500}'));
    $stub->api(array('method' => 'foo'));
  }

  public function testJsonEncodeOfNonStringParams() {
    $foo = array(1, 2);
    $params = array(
      'method' => 'get',
      'foo' => $foo,
    );
    $fb = new FBRecordMakeRequest(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $fb->api('/naitik', $params);
    $requests = $fb->publicGetRequests();
    $this->assertEquals(json_encode($foo), $requests[0]['params']['foo']);
  }

  public function testSessionBackedFacebook() {
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = 'state';
    $val = 'foo';
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals(
      $val,
      $_SESSION[sprintf('fb_%s_%s', self::APP_ID, $key)]
    );
    $this->assertEquals(
      $val,
      $fb->publicGetPersistentData($key)
    );
  }

  public function testSessionBackedFacebookIgnoresUnsupportedKey() {
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = '--invalid--';
    $val = 'foo';
    $fb->publicSetPersistentData($key, $val);
    $this->assertFalse(
      array_key_exists(
        sprintf('fb_%s_%s', self::APP_ID, $key),
        $_SESSION
      )
    );
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testClearSessionBackedFacebook() {
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = 'state';
    $val = 'foo';
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals(
      $val,
      $_SESSION[sprintf('fb_%s_%s', self::APP_ID, $key)]
    );
    $this->assertEquals(
      $val,
      $fb->publicGetPersistentData($key)
    );
    $fb->publicClearPersistentData($key);
    $this->assertFalse(
      array_key_exists(
        sprintf('fb_%s_%s', self::APP_ID, $key),
        $_SESSION
      )
    );
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testSessionBackedFacebookIgnoresUnsupportedKeyInClear() {
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = '--invalid--';
    $val = 'foo';
    $session_var_name = sprintf('fb_%s_%s', self::APP_ID, $key);
    $_SESSION[$session_var_name] = $val;
    $fb->publicClearPersistentData($key);
    $this->assertTrue(array_key_exists($session_var_name, $_SESSION));
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testClearAllSessionBackedFacebook() {
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $key = 'state';
    $val = 'foo';
    $session_var_name = sprintf('fb_%s_%s', self::APP_ID, $key);
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals($val, $_SESSION[$session_var_name]);
    $this->assertEquals($val, $fb->publicGetPersistentData($key));
    $fb->publicClearAllPersistentData();
    $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testSharedSessionBackedFacebook() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = 'state';
    $val = 'foo';
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $fb->publicGetSharedSessionID(),
      self::APP_ID,
      $key
    );
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals($val, $_SESSION[$session_var_name]);
    $this->assertEquals($val, $fb->publicGetPersistentData($key));
  }

  public function testSharedSessionBackedFacebookIgnoresUnsupportedKey() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = '--invalid--';
    $val = 'foo';
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $fb->publicGetSharedSessionID(),
      self::APP_ID,
      $key
    );
    $fb->publicSetPersistentData($key, $val);
    $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testSharedClearSessionBackedFacebook() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = 'state';
    $val = 'foo';
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $fb->publicGetSharedSessionID(),
      self::APP_ID,
      $key
    );
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals($val, $_SESSION[$session_var_name]);
    $this->assertEquals($val, $fb->publicGetPersistentData($key));
    $fb->publicClearPersistentData($key);
    $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testSharedSessionBackedFacebookIgnoresUnsupportedKeyInClear() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = '--invalid--';
    $val = 'foo';
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $fb->publicGetSharedSessionID(),
      self::APP_ID,
      $key
    );
    $_SESSION[$session_var_name] = $val;
    $fb->publicClearPersistentData($key);
    $this->assertTrue(array_key_exists($session_var_name, $_SESSION));
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testSharedClearAllSessionBackedFacebook() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = 'state';
    $val = 'foo';
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $fb->publicGetSharedSessionID(),
      self::APP_ID,
      $key
    );
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals($val, $_SESSION[$session_var_name]);
    $this->assertEquals($val, $fb->publicGetPersistentData($key));
    $fb->publicClearAllPersistentData();
    $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
    $this->assertFalse($fb->publicGetPersistentData($key));
  }

  public function testSharedSessionBackedFacebookIsRestored() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = 'state';
    $val = 'foo';
    $shared_session_id = $fb->publicGetSharedSessionID();
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $shared_session_id,
      self::APP_ID,
      $key
    );
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals($val, $_SESSION[$session_var_name]);
    $this->assertEquals($val, $fb->publicGetPersistentData($key));

    // check the new instance has the same data
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $this->assertEquals(
      $shared_session_id,
      $fb->publicGetSharedSessionID()
    );
    $this->assertEquals($val, $fb->publicGetPersistentData($key));
  }

  public function testSharedSessionBackedFacebookIsNotRestoredWhenCorrupt() {
    $_SERVER['HTTP_HOST'] = 'fbrell.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $key = 'state';
    $val = 'foo';
    $shared_session_id = $fb->publicGetSharedSessionID();
    $session_var_name = sprintf(
      '%s_fb_%s_%s',
      $shared_session_id,
      self::APP_ID,
      $key
    );
    $fb->publicSetPersistentData($key, $val);
    $this->assertEquals($val, $_SESSION[$session_var_name]);
    $this->assertEquals($val, $fb->publicGetPersistentData($key));

    // break the cookie
    $cookie_name = $fb->publicGetSharedSessionCookieName();
    $_COOKIE[$cookie_name] = substr($_COOKIE[$cookie_name], 1);

    // check the new instance does not have the data
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'sharedSession' => true,
    ));
    $this->assertFalse($fb->publicGetPersistentData($key));
    $this->assertNotEquals(
      $shared_session_id,
      $fb->publicGetSharedSessionID()
    );
  }

  public function testHttpHost() {
    $real = 'foo.com';
    $_SERVER['HTTP_HOST'] = $real;
    $_SERVER['HTTP_X_FORWARDED_HOST'] = 'evil.com';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $this->assertEquals($real, $fb->publicGetHttpHost());
  }

  public function testHttpProtocol() {
    $_SERVER['HTTPS'] = 'on';
    $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
    ));
    $this->assertEquals('https', $fb->publicGetHttpProtocol());
  }

  public function testHttpHostForwarded() {
    $real = 'foo.com';
    $_SERVER['HTTP_HOST'] = 'localhost';
    $_SERVER['HTTP_X_FORWARDED_HOST'] = $real;
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'trustForwarded' => true,
    ));
    $this->assertEquals($real, $fb->publicGetHttpHost());
  }

  public function testHttpProtocolForwarded() {
    $_SERVER['HTTPS'] = 'on';
    $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'trustForwarded' => true,
    ));
    $this->assertEquals('http', $fb->publicGetHttpProtocol());
  }

  public function testHttpProtocolForwardedSecure() {
    $_SERVER['HTTPS'] = 'on';
    $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https';
    $fb = new PersistentFBPublic(array(
      'appId'  => self::APP_ID,
      'secret' => self::SECRET,
      'trustForwarded' => true,
    ));
    $this->assertEquals('https', $fb->publicGetHttpProtocol());
  }

  /**
   * @dataProvider provideEndsWith
   */
  public function testEndsWith($big, $small, $result) {
    $this->assertEquals(
      $result,
      PersistentFBPublic::publicEndsWith($big, $small)
    );
  }

  public function provideEndsWith() {
    return array(
      array('', '', true),
      array('', 'a', false),
      array('a', '', true),
      array('a', 'b', false),
      array('a', 'a', true),
      array('aa', 'a', true),
      array('ab', 'a', false),
      array('ba', 'a', true),
    );
  }

  /**
   * @dataProvider provideIsAllowedDomain
   */
  public function testIsAllowedDomain($big, $small, $result) {
    $this->assertEquals(
      $result,
      PersistentFBPublic::publicIsAllowedDomain($big, $small)
    );
  }

  public function provideIsAllowedDomain() {
    return array(
      array('fbrell.com', 'fbrell.com', true),
      array('foo.fbrell.com', 'fbrell.com', true),
      array('foofbrell.com', 'fbrell.com', false),
      array('evil.com', 'fbrell.com', false),
      array('foo.fbrell.com', 'bar.fbrell.com', false),
    );
  }

  protected function generateMD5HashOfRandomValue() {
    return md5(uniqid(mt_rand(), true));
  }

  protected function setUp() {
    parent::setUp();
  }

  protected function tearDown() {
    $this->clearSuperGlobals();
    parent::tearDown();
  }

  protected function clearSuperGlobals() {
    unset($_SERVER['HTTPS']);
    unset($_SERVER['HTTP_HOST']);
    unset($_SERVER['REQUEST_URI']);
    $_SESSION = array();
    $_COOKIE = array();
    $_REQUEST = array();
    $_POST = array();
    $_GET = array();
    if (session_id()) {
      session_destroy();
    }
  }

  /**
   * Checks that the correct args are a subset of the returned obj
   * @param  array $correct The correct array values
   * @param  array $actual  The values in practice
   * @param  string $message to be shown on failure
   */
  protected function assertIsSubset($correct, $actual, $msg='') {
    foreach ($correct as $key => $value) {
      $actual_value = $actual[$key];
      $newMsg = (strlen($msg) ? ($msg.' ') : '').'Key: '.$key;
      $this->assertEquals($value, $actual_value, $newMsg);
    }
  }
}

class TransientFacebook extends BaseFacebook {
  protected function setPersistentData($key, $value) {}
  protected function getPersistentData($key, $default = false) {
    return $default;
  }
  protected function clearPersistentData($key) {}
  protected function clearAllPersistentData() {}
}

class FBRecordURL extends TransientFacebook {
  private $url;

  protected function _oauthRequest($url, $params) {
    $this->url = $url;
  }

  public function getRequestedURL() {
    return $this->url;
  }
}

class FBRecordMakeRequest extends TransientFacebook {
  private $requests = array();

  protected function makeRequest($url, $params, $ch=null) {
    $this->requests[] = array(
      'url' => $url,
      'params' => $params,
    );
    return parent::makeRequest($url, $params, $ch);
  }

  public function publicGetRequests() {
    return $this->requests;
  }
}

class FBPublic extends TransientFacebook {
  public static function publicBase64UrlDecode($input) {
    return self::base64UrlDecode($input);
  }
  public static function publicBase64UrlEncode($input) {
    return self::base64UrlEncode($input);
  }
  public function publicParseSignedRequest($input) {
    return $this->parseSignedRequest($input);
  }
  public function publicMakeSignedRequest($data) {
    return $this->makeSignedRequest($data);
  }
}

class PersistentFBPublic extends Facebook {
  public function publicParseSignedRequest($input) {
    return $this->parseSignedRequest($input);
  }

  public function publicSetPersistentData($key, $value) {
    $this->setPersistentData($key, $value);
  }

  public function publicGetPersistentData($key, $default = false) {
    return $this->getPersistentData($key, $default);
  }

  public function publicClearPersistentData($key) {
    return $this->clearPersistentData($key);
  }

  public function publicClearAllPersistentData() {
    return $this->clearAllPersistentData();
  }

  public function publicGetSharedSessionID() {
    return $this->sharedSessionID;
  }

  public static function publicIsAllowedDomain($big, $small) {
    return self::isAllowedDomain($big, $small);
  }

  public static function publicEndsWith($big, $small) {
    return self::endsWith($big, $small);
  }

  public function publicGetSharedSessionCookieName() {
    return $this->getSharedSessionCookieName();
  }

  public function publicGetHttpHost() {
    return $this->getHttpHost();
  }

  public function publicGetHttpProtocol() {
    return $this->getHttpProtocol();
  }
}

class FBCode extends Facebook {
  public function publicGetCode() {
    return $this->getCode();
  }

  public function setCSRFStateToken() {
    $this->establishCSRFTokenState();
  }

  public function getCSRFStateToken() {
    return $this->getPersistentData('state');
  }
}

class FBAccessToken extends TransientFacebook {
  public function publicGetApplicationAccessToken() {
    return $this->getApplicationAccessToken();
  }
}

class FBGetCurrentURLFacebook extends TransientFacebook {
  public function publicGetCurrentUrl() {
    return $this->getCurrentUrl();
  }
}

class FBPublicCookie extends TransientFacebook {
  public function publicGetSignedRequest() {
    return $this->getSignedRequest();
  }

  public function publicGetSignedRequestCookieName() {
    return $this->getSignedRequestCookieName();
  }

  public function publicGetMetadataCookie() {
    return $this->getMetadataCookie();
  }

  public function publicGetMetadataCookieName() {
    return $this->getMetadataCookieName();
  }
}

class FBRewrite extends Facebook{

  public function uncacheSignedRequest(){
    $this->signedRequest = null;
  }

  public function uncache()
  {
    $this->user = null;
    $this->signedRequest = null;
    $this->accessToken = null;
  }
}


class FBPublicGetAccessTokenFromCode extends TransientFacebook {
  public function publicGetAccessTokenFromCode($code, $redirect_uri = null) {
    return $this->getAccessTokenFromCode($code, $redirect_uri);
  }
}

class FBPublicState extends TransientFacebook {
  const STATE = 'foo';
  protected function getPersistentData($key, $default = false) {
    if ($key === 'state') {
      return self::STATE;
    }
    return parent::getPersistentData($key, $default);
  }

  public function publicGetState() {
    return $this->state;
  }
}
