Skip to content

Commit

Permalink
Validate client configuration parameters (#907)
Browse files Browse the repository at this point in the history
  • Loading branch information
ob-stripe committed Apr 3, 2020
1 parent 7db27aa commit 4768ab4
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 57 deletions.
159 changes: 104 additions & 55 deletions lib/BaseStripeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,12 @@ class BaseStripeClient implements StripeClientInterface
/** @var string default base URL for Stripe's Files API */
const DEFAULT_FILES_BASE = 'https://files.stripe.com';

/** @var null|string */
private $apiKey;

/** @var null|string */
private $clientId;
/** @var array<string, mixed> */
private $config;

/** @var \Stripe\Util\RequestOptions */
private $defaultOpts;

/** @var string */
private $apiBase;

/** @var string */
private $connectBase;

/** @var string */
private $filesBase;

/**
* Initializes a new instance of the {@link BaseStripeClient} class.
*
Expand All @@ -39,18 +27,22 @@ class BaseStripeClient implements StripeClientInterface
*
* Configuration settings include the following options:
*
* - api_key (string): the Stripe API key, to be used in regular API requests.
* - client_id (string): the Stripe client ID, to be used in OAuth requests.
* - stripe_account (string): a Stripe account ID. If set, all requests sent by the client
* - api_key (null|string): the Stripe API key, to be used in regular API requests.
* - client_id (null|string): the Stripe client ID, to be used in OAuth requests.
* - stripe_account (null|string): a Stripe account ID. If set, all requests sent by the client
* will automatically use the {@code Stripe-Account} header with that account ID.
* - stripe_version (string): a Stripe API verion. If set, all requests sent by the client
* - stripe_version (null|string): a Stripe API verion. If set, all requests sent by the client
* will include the {@code Stripe-Version} header with that API version.
*
* The following configuration settings are also available, though setting these should rarely be necessary
* (only useful if you want to send requests to a mock server like stripe-mock):
*
* - api_base (string): the base URL for regular API requests. Defaults to
* {@link DEFAULT_API_BASE}. Changing this should rarely be necessary.
* {@link DEFAULT_API_BASE}.
* - connect_base (string): the base URL for OAuth requests. Defaults to
* {@link DEFAULT_CONNECT_BASE}. Changing this should rarely be necessary.
* {@link DEFAULT_CONNECT_BASE}.
* - files_base (string): the base URL for file creation requests. Defaults to
* {@link DEFAULT_FILES_BASE}. Changing this should rarely be necessary.
* {@link DEFAULT_FILES_BASE}.
*
* @param array<string, mixed>|string $config the API key as a string, or an array containing
* the client configuration settings
Expand All @@ -59,42 +51,19 @@ public function __construct($config = [])
{
if (\is_string($config)) {
$config = ['api_key' => $config];
} elseif (!\is_array($config)) {
throw new \Stripe\Exception\InvalidArgumentException('$config must be a string or an array');
}

$defaults = [
'api_key' => null,
'client_id' => null,
'stripe_account' => null,
'stripe_version' => null,
'api_base' => self::DEFAULT_API_BASE,
'connect_base' => self::DEFAULT_CONNECT_BASE,
'files_base' => self::DEFAULT_FILES_BASE,
];
$config = \array_merge($defaults, $config);

$apiKey = $config['api_key'];
$config = \array_merge($this->getDefaultConfig(), $config);
$this->validateConfig($config);

if (null !== $apiKey && ('' === $apiKey)) {
$msg = 'API key cannot be the empty string.';
$this->config = $config;

throw new \Stripe\Exception\InvalidArgumentException($msg);
}

if (null !== $apiKey && (\preg_match('/\s/', $apiKey))) {
$msg = 'API key cannot contain whitespace.';

throw new \Stripe\Exception\InvalidArgumentException($msg);
}

$this->apiKey = $apiKey;
$this->clientId = $config['client_id'];
$this->defaultOpts = \Stripe\Util\RequestOptions::parse([
'stripe_account' => $config['stripe_account'],
'stripe_version' => $config['stripe_version'],
]);
$this->apiBase = $config['api_base'];
$this->connectBase = $config['connect_base'];
$this->filesBase = $config['files_base'];
}

/**
Expand All @@ -104,7 +73,7 @@ public function __construct($config = [])
*/
public function getApiKey()
{
return $this->apiKey;
return $this->config['api_key'];
}

/**
Expand All @@ -114,7 +83,7 @@ public function getApiKey()
*/
public function getClientId()
{
return $this->clientId;
return $this->config['client_id'];
}

/**
Expand All @@ -124,7 +93,7 @@ public function getClientId()
*/
public function getApiBase()
{
return $this->apiBase;
return $this->config['api_base'];
}

/**
Expand All @@ -134,7 +103,7 @@ public function getApiBase()
*/
public function getConnectBase()
{
return $this->connectBase;
return $this->config['connect_base'];
}

/**
Expand All @@ -144,7 +113,7 @@ public function getConnectBase()
*/
public function getFilesBase()
{
return $this->filesBase;
return $this->config['files_base'];
}

/**
Expand Down Expand Up @@ -186,9 +155,89 @@ private function apiKeyForRequest($opts)
. 'StripeClient instance, or provide it on a per-request basis '
. 'using the `api_key` key in the $opts argument.';

throw new Exception\AuthenticationException($msg);
throw new \Stripe\Exception\AuthenticationException($msg);
}

return $apiKey;
}

/**
* TODO: replace this with a private constant when we drop support for PHP < 5.
*
* @return array<string, mixed>
*/
private function getDefaultConfig()
{
return [
'api_key' => null,
'client_id' => null,
'stripe_account' => null,
'stripe_version' => null,
'api_base' => self::DEFAULT_API_BASE,
'connect_base' => self::DEFAULT_CONNECT_BASE,
'files_base' => self::DEFAULT_FILES_BASE,
];
}

/**
* @param array<string, mixed> $config
*
* @throws \Stripe\Exception\InvalidArgumentException
*/
private function validateConfig($config)
{
// api_key
if (null !== $config['api_key'] && !\is_string($config['api_key'])) {
throw new \Stripe\Exception\InvalidArgumentException('api_key must be null or a string');
}

if (null !== $config['api_key'] && ('' === $config['api_key'])) {
$msg = 'api_key cannot be the empty string';

throw new \Stripe\Exception\InvalidArgumentException($msg);
}

if (null !== $config['api_key'] && (\preg_match('/\s/', $config['api_key']))) {
$msg = 'api_key cannot contain whitespace';

throw new \Stripe\Exception\InvalidArgumentException($msg);
}

// client_id
if (null !== $config['client_id'] && !\is_string($config['client_id'])) {
throw new \Stripe\Exception\InvalidArgumentException('client_id must be null or a string');
}

// stripe_account
if (null !== $config['stripe_account'] && !\is_string($config['stripe_account'])) {
throw new \Stripe\Exception\InvalidArgumentException('stripe_account must be null or a string');
}

// stripe_version
if (null !== $config['stripe_version'] && !\is_string($config['stripe_version'])) {
throw new \Stripe\Exception\InvalidArgumentException('stripe_version must be null or a string');
}

// api_base
if (!\is_string($config['api_base'])) {
throw new \Stripe\Exception\InvalidArgumentException('api_base must be a string');
}

// connect_base
if (!\is_string($config['connect_base'])) {
throw new \Stripe\Exception\InvalidArgumentException('connect_base must be a string');
}

// files_base
if (!\is_string($config['files_base'])) {
throw new \Stripe\Exception\InvalidArgumentException('files_base must be a string');
}

// check absence of extra keys
$validConfigKeys = \array_keys($this->getDefaultConfig());
$extraConfigKeys = \array_diff(\array_keys($config), \array_keys($this->getDefaultConfig()));
if (!empty($extraConfigKeys)) {
throw new \Stripe\Exception\InvalidArgumentException('Found unknown key(s) in configuration array: ' . \implode(',', $extraConfigKeys));
}
}
}
28 changes: 26 additions & 2 deletions tests/Stripe/BaseStripeClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,46 @@ public function testCtorDoesNotThrowWhenNoParams()
static::assertNull($client->getApiKey());
}

public function testCtorThrowsIfConfigIsUnexpectedType()
{
$this->expectException(\Stripe\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('$config must be a string or an array');

$client = new BaseStripeClient(234);
}

public function testCtorThrowsIfApiKeyIsEmpty()
{
$this->expectException(\Stripe\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('API key cannot be the empty string.');
$this->expectExceptionMessage('api_key cannot be the empty string');

$client = new BaseStripeClient('');
}

public function testCtorThrowsIfApiKeyContainsWhitespace()
{
$this->expectException(\Stripe\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('API key cannot contain whitespace.');
$this->expectExceptionMessage('api_key cannot contain whitespace');

$client = new BaseStripeClient("sk_test_123\n");
}

public function testCtorThrowsIfApiKeyIsUnexpectedType()
{
$this->expectException(\Stripe\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('api_key must be null or a string');

$client = new BaseStripeClient(['api_key' => 234]);
}

public function testCtorThrowsIfConfigArrayContainsUnexpectedKey()
{
$this->expectException(\Stripe\Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Found unknown key(s) in configuration array: foo');

$client = new BaseStripeClient(['foo' => 'bar']);
}

public function testRequestWithClientApiKey()
{
$client = new BaseStripeClient(['api_key' => 'sk_test_client', 'api_base' => MOCK_URL]);
Expand Down

0 comments on commit 4768ab4

Please sign in to comment.