<?php

/**
 * @file
 * Credit card payment method tests.
 */

/**
 * Tests credit card payments with the test gateway.
 *
 * This class is intended to be subclassed for use in testing other credit
 * card gateways. Subclasses which test other gateways need to:
 * - Define getInfo().
 * - Override setUp(), if necessary, to enable the other gateway and any other
 *   needed modules.
 * - Override configureGateway() to implement gateway-specific configuration.
 * - Override testGatewayFails() to test gateway-specific failure modes.
 * No other overrides are necessary, although a subclass may want to add
 * additional test functions to cover cases not included in this base class.
 */
class UbercartCreditCardTestCase extends UbercartTestHelper {

  /**
   * A selection of "test" numbers to use for testing credit card payemnts.
   * These numbers all pass the Luhn algorithm check and are reserved by
   * the card issuer for testing purposes.
   */
  protected static $test_cards = array(
    '378282246310005',  // American Express
    '371449635398431',
    '370000000000002',
    '378734493671000',  // American Express Corporate
    '5610591081018250', // Australian BankCard
    '30569309025904',   // Diners Club
    '38520000023237',
    '38000000000006',   // Carte Blanche
    '6011111111111117', // Discover
    '6011000990139424',
    '6011000000000012',
    '3530111333300000', // JCB
    '3566002020360505',
    '3088000000000017',
    '5555555555554444', // MasterCard
    '5105105105105100',
    '4111111111111111', // Visa
    '4012888888881881',
    '4007000000027',
    '4012888818888',
  );


  /**
   * Describes this test case.
   */
  public static function getInfo() {
    return array(
      'name' => 'Credit cards with Test Gateway',
      'description' => 'Uses the Test Gateway to ensure credit card processing is functioning.',
      'group' => 'Ubercart',
    );
  }

  /**
   * Implements DrupalWebTestCase::setUp().
   */
  public function setUp(array $modules = array()) {
    $modules += array('uc_payment', 'uc_credit', 'test_gateway');
    $permissions = array('administer permissions', 'administer credit cards');
    parent::setUp($modules, $permissions);

    // Need admin permissions in order to change credit card settings.
    $this->drupalLogin($this->adminUser);

    // Configure and enable Credit card module and Test gateway.
    $this->configureCreditCard();
    $this->configureGateway();
  }

  /**
   * Helper function to configure Credit Card payment method settings.
   */
  protected function configureCreditCard() {
    $this->drupalPost(
      'admin/store/settings/payment',
      array('uc_payment_method_credit_checkout' => TRUE),
      t('Save configuration')
    );
    $this->assertFieldByName(
      'uc_payment_method_credit_checkout',
      TRUE,
      t('Credit card payment method is enabled')
    );

    // Create key directory, make it readable and writeable.
    // Putting this under sites/default/files because SimpleTest needs to be
    // able to create the directory - this is NOT where you'd put the key file
    // on a live site.  On a live site, it should be outside the web root.
    drupal_mkdir('sites/default/files/simpletest.keys', 0755);

    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array(
        'uc_credit_encryption_path' => 'sites/default/files/simpletest.keys',
      ),
      t('Save configuration')
    );

    $this->assertFieldByName(
      'uc_credit_encryption_path',
      'sites/default/files/simpletest.keys',
      t('Key file path has been set.')
    );

    $this->assertTrue(
      file_exists('sites/default/files/simpletest.keys/' . UC_CREDIT_KEYFILE_NAME),
      t('Key has been generated and stored.')
    );
    $this->pass('Key = ' . uc_credit_encryption_key());

  }

  /**
   * Helper function to configure Credit Card gateway.
   */
  protected function configureGateway() {
    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array(
        'uc_payment_credit_gateway' => 'test_gateway',
        'uc_pg_test_gateway_enabled' => TRUE,
      ),
      t('Save configuration')
    );

    $this->assertFieldByName(
      'uc_pg_test_gateway_enabled',
      TRUE,
      t('Test gateway is enabled')
    );
  }

  /**
   * Implements DrupalWebTestCase::tearDown().
   */
  public function tearDown() {
    // Cleanup keys directory after test.
    drupal_unlink('sites/default/files/simpletest.keys/' . UC_CREDIT_KEYFILE_NAME);
    drupal_rmdir('sites/default/files/simpletest.keys');
    parent::tearDown();
  }

  /**
   * Helper function. Creates a new order.
   */
  protected function createOrder($fields = array()) {
    $order = uc_order_new();
    foreach ($fields as $key => $value) {
      $order->$key = $value;
    }

    if (empty($order->primary_email)) {
      $order->primary_email = $this->randomString() . '@example.org';
    }

    if (!isset($fields['products'])) {
      $item = clone $this->product;
      $item->qty = 1;
      $item->price = $item->sell_price;
      $item->data = array();
      $order->products = array($item);
    }

    $order->order_total = uc_order_get_total($order, TRUE);
    $order->line_items = uc_order_load_line_items($order, TRUE);
    uc_order_save($order);

    return $order;
  }

  /**
   * Tests security settings configuration.
   */
  public function testSecuritySettings() {
    // TODO:  Still need tests with existing key file
    // where key file is not readable or doesn't contain a valid key

    // Create key directory, make it readable and writeable.
    drupal_mkdir('sites/default/files/testkey', 0755);

    // Try to submit settings form without a key file path.
    // Save current variable, reset to its value when first installed.
    $temp_variable = variable_get('uc_credit_encryption_path', '');
    variable_set('uc_credit_encryption_path', '');

    $this->drupalGet('admin/store/settings/payment/method/credit');
    $this->assertText(t('Credit card security settings must be configured in the security settings tab.'));

    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array(),
      t('Save configuration')
    );
    $this->assertFieldByName(
      'uc_credit_encryption_path',
      t('Not configured.'),
      t('Key file has not yet been configured.')
    );
    // Restore variable setting.
    variable_set('uc_credit_encryption_path', $temp_variable);

    // Try to submit settings form with an empty key file path.
    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array('uc_credit_encryption_path' => ''),
      t('Save configuration')
    );
    $this->assertText('Key path must be specified in security settings tab.');

    // Specify non-existent directory
    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array('uc_credit_encryption_path' => 'sites/default/ljkh/asdfasfaaaaa'),
      t('Save configuration')
    );
    $this->assertText('You have specified a non-existent directory.');

    // Next, specify existing directory that's write protected.
    // Use /dev, as that should never be accessible.
    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array('uc_credit_encryption_path' => '/dev'),
      t('Save configuration')
    );
    $this->assertText('Cannot write to directory, please verify the directory permissions.');

    // Next, specify writeable directory, but with excess whitespace
    // and trailing /
    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array('uc_credit_encryption_path' => '  sites/default/files/testkey/ '),
      t('Save configuration')
    );
    // See that the directory has been properly re-written to remove
    // whitespace and trailing /
    $this->assertFieldByName(
      'uc_credit_encryption_path',
      'sites/default/files/testkey',
      t('Key file path has been set.')
    );
    $this->assertText('Credit card encryption key file generated.');

    // Check that warning about needing key file goes away.
    $this->assertNoText(t('Credit card security settings must be configured in the security settings tab.'));
    // Remove key file.
    drupal_unlink('sites/default/files/testkey/' . UC_CREDIT_KEYFILE_NAME);

    // Finally, specify good directory
    $this->drupalPost(
      'admin/store/settings/payment/method/credit',
      array('uc_credit_encryption_path' => 'sites/default/files/testkey'),
      t('Save configuration')
    );
    $this->assertText('Credit card encryption key file generated.');

    // Test contents - must contain 32-character hexadecimal string.
    $this->assertTrue(
      file_exists('sites/default/files/simpletest.keys/' . UC_CREDIT_KEYFILE_NAME),
      t('Key has been generated and stored.')
    );
    $this->assertTrue(
      preg_match("([0-9a-fA-F]{32})", uc_credit_encryption_key()),
      t('Valid key detected in key file.')
    );

    // Cleanup keys directory after test.
    drupal_unlink('sites/default/files/testkey/' . UC_CREDIT_KEYFILE_NAME);
    drupal_rmdir('sites/default/files/testkey');
  }

  /**
   * Expected-to-fail payment test cases.
   *
   * Most gateways provide test cases which will trigger payment acceptance,
   * failure, or specific error messages.  Usually, these test cases only work
   * on the gateway's testing server.
   */
  public function testGatewayFails() {

// Amex's card security code (CSC) # is 4 digits, MC and V are 3
// Expiration must be in the future

    // Fails for Test Gateway
    $fail_data = array(
      'panes[billing][billing_first_name]' => 'Fictitious',
      'panes[billing][billing_phone]' => '8675309',
      'panes[payment][details][cc_number]' => '0000000000000000',
      'panes[payment][details][cc_cvv]' => '000',
      'panes[payment][details][cc_exp_month]' => mt_rand(1, 12),
//    ($expiration_date - time()) <= 0
//    $amount == 12.34
    );
  }

  /**
   * Tests that an order can be placed using the test gateway.
   */
  public function testCheckout() {
    // Allow customer to specify username and password, and log in new users after checkout.
    $settings = array(
      'uc_cart_new_account_name' => TRUE,
      'uc_cart_new_account_password' => TRUE,
      'uc_new_customer_login' => TRUE
    );
    $this->drupalLogin($this->adminUser);
    $this->drupalPost('admin/store/settings/checkout', $settings, t('Save configuration'));
    $this->drupalLogout();

    $new_user = new stdClass();
    $new_user->name = $this->randomName(20);
    $new_user->pass_raw = $this->randomString(20);

    // Test as anonymous user.
    $this->drupalPost('node/' . $this->product->nid, array(), t('Add to cart'));
    $this->checkout(array(
      'panes[customer][new_account][name]' => $new_user->name,
      'panes[customer][new_account][pass]' => $new_user->pass_raw,
      'panes[customer][new_account][pass_confirm]' => $new_user->pass_raw,

      'panes[payment][details][cc_number]' => array_rand(array_flip(self::$test_cards)),
      'panes[payment][details][cc_cvv]' => mt_rand(100, 999),
      'panes[payment][details][cc_exp_month]' => mt_rand(1, 12),
      'panes[payment][details][cc_exp_year]' => mt_rand(2012, 2022),
    ));
    $this->assertText('Your order is complete!');
    $this->assertText('you are already logged in');

    // Confirm login.
    $this->drupalGet('<front>');
    $this->assertText('My account', 'User is logged in.');

    // Check that cart is now empty.
    $this->drupalGet('cart');
    $this->assertText('There are no products in your shopping cart.');

    // Check that the password works.
    $this->drupalLogout();
    $this->drupalLogin($new_user);

    // Test again as authenticated user.
    $this->drupalPost('node/' . $this->product->nid, array(), t('Add to cart'));
    $this->checkout(array(
      'panes[payment][details][cc_number]' => array_rand(array_flip(self::$test_cards)),
      'panes[payment][details][cc_cvv]' => mt_rand(100, 999),
      'panes[payment][details][cc_exp_month]' => mt_rand(1, 12),
      'panes[payment][details][cc_exp_year]' => mt_rand(2012, 2022),
    ));
    $this->assertText('Your order is complete!');
    $this->assertText('While logged in');

    // Test again with generated username and password.
    $this->drupalLogout();
    $this->drupalPost('node/' . $this->product->nid, array(), t('Add to cart'));
    $this->checkout(array(
      'panes[payment][details][cc_number]' => array_rand(array_flip(self::$test_cards)),
      'panes[payment][details][cc_cvv]' => mt_rand(100, 999),
      'panes[payment][details][cc_exp_month]' => mt_rand(1, 12),
      'panes[payment][details][cc_exp_year]' => mt_rand(2012, 2022),
    ));
    $this->assertText('Your order is complete!');
    $this->assertText('you are already logged in');
  }

  /**
   * Tests that a payment was received and recorded.
   */
  public function testPaymentReceived() {

    // TODO:  This is all functional, but doesn't do what the function
    // comment says.  Needs to be fixed to match the comment!

    // Payment notification is received first.
    $order_data = array('primary_email' => 'simpletest@ubercart.org');
    $order = $this->createOrder($order_data);
    uc_payment_enter($order->order_id, 'SimpleTest', $order->order_total);
    $output = uc_cart_complete_sale($order);

    // Different user, sees the checkout page first.
    $order_data = array('primary_email' => 'simpletest2@ubercart.org');
    $order = $this->createOrder($order_data);
    $output = uc_cart_complete_sale($order, TRUE);
    uc_payment_enter($order->order_id, 'SimpleTest', $order->order_total);

    // Same user, new order.
    $order = $this->createOrder($order_data);
    $output = uc_cart_complete_sale($order, TRUE);
    uc_payment_enter($order->order_id, 'SimpleTest', $order->order_total);

    // Check that no new account was created.
    $this->assertTrue(strpos($output['#message'], 'order has been attached to the account') !== FALSE, 'Checkout message mentions existing account.');
  }
}
