From a355cfa5adac4308471a579a2e3a2e8912cae299 Mon Sep 17 00:00:00 2001 From: Zachary Schneider Date: Tue, 6 Jul 2021 16:22:31 -0700 Subject: [PATCH] Refactor, improve exception handling, tests --- src/Client.php | 16 ++- src/Config.php | 7 +- src/Exceptions/B2APIException.php | 21 ---- src/Exceptions/BadBucketIdException.php | 5 - src/Exceptions/BucketNotEmptyException.php | 5 - src/Exceptions/HttpException.php | 5 - src/Exceptions/NotFoundException.php | 4 +- .../RangeNotSatisfiableException.php | 5 - .../Request/AccessDeniedException.php | 5 + src/Exceptions/Request/B2APIException.php | 44 +++++++ .../{ => Request}/BadAuthTokenException.php | 2 +- .../{ => Request}/BadRequestException.php | 2 +- .../{ => Request}/CapExceededException.php | 2 +- src/Exceptions/Request/ConflictException.php | 5 + .../Request/DownloadCapExceededException.php | 5 + .../DuplicateBucketNameException.php | 2 +- .../Request/ExpiredAuthTokenException.php | 5 + .../{ => Request}/FileNotPresentException.php | 2 +- .../Request/InvalidBucketIdException.php | 5 + .../Request/InvalidFileIdException.php | 5 + .../Request/MethodNotAllowedException.php | 5 + src/Exceptions/Request/NotFoundException.php | 5 + .../Request/OutOfRangeException.php | 5 + .../Request/RangeNotSatisfiableException.php | 5 + .../Request/RequestTimeoutException.php | 5 + .../Request/ServiceUnavailableException.php | 5 + .../StorageCapExceededException.php | 2 +- .../{ => Request}/TooManyBucketsException.php | 2 +- .../TransactionCapExceededException.php | 2 +- .../Request/UnauthorizedException.php | 5 + .../{ => Request}/UnsupportedException.php | 2 +- src/Exceptions/UnauthorizedException.php | 5 - src/Exceptions/ValidationException.php | 5 - src/Http/ErrorHandler.php | 107 ++++++++++++++---- .../Exceptions/TooManyRequestsException.php | 7 ++ .../ApplyAuthorizationMiddleware.php | 7 +- src/Http/Middleware/ExceptionMiddleware.php | 39 +++---- src/Http/Middleware/RetryMiddleware.php | 41 ++++--- src/Object/AccountAuthorization.php | 11 +- .../ApplicationKeyOperationsTrait.php | 6 +- src/Operations/BucketOperationsTrait.php | 20 ++-- src/Operations/FileOperationsTrait.php | 99 ++++++++-------- tests/ClientBucketOperationsTest.php | 13 ++- tests/ClientDownloadTest.php | 11 +- tests/ClientExceptionsTest.php | 99 ++++++++++++++++ tests/ClientFileOperationsTest.php | 20 ++-- tests/ClientTest.php | 31 ++++- tests/ClientTestBase.php | 35 +++++- tests/ClientUploadTest.php | 14 +-- tests/Endpoint.php | 58 +++++----- tests/MockResponse.php | 8 ++ tests/Traits/EndpointHelpersTrait.php | 20 ++++ tests/Utils.php | 13 +++ tests/responses/download_by_incorrect_id.json | 2 +- 54 files changed, 596 insertions(+), 270 deletions(-) delete mode 100644 src/Exceptions/B2APIException.php delete mode 100644 src/Exceptions/BadBucketIdException.php delete mode 100644 src/Exceptions/BucketNotEmptyException.php delete mode 100644 src/Exceptions/HttpException.php delete mode 100644 src/Exceptions/RangeNotSatisfiableException.php create mode 100644 src/Exceptions/Request/AccessDeniedException.php create mode 100644 src/Exceptions/Request/B2APIException.php rename src/Exceptions/{ => Request}/BadAuthTokenException.php (55%) rename src/Exceptions/{ => Request}/BadRequestException.php (55%) rename src/Exceptions/{ => Request}/CapExceededException.php (55%) create mode 100644 src/Exceptions/Request/ConflictException.php create mode 100644 src/Exceptions/Request/DownloadCapExceededException.php rename src/Exceptions/{ => Request}/DuplicateBucketNameException.php (58%) create mode 100644 src/Exceptions/Request/ExpiredAuthTokenException.php rename src/Exceptions/{ => Request}/FileNotPresentException.php (56%) create mode 100644 src/Exceptions/Request/InvalidBucketIdException.php create mode 100644 src/Exceptions/Request/InvalidFileIdException.php create mode 100644 src/Exceptions/Request/MethodNotAllowedException.php create mode 100644 src/Exceptions/Request/NotFoundException.php create mode 100644 src/Exceptions/Request/OutOfRangeException.php create mode 100644 src/Exceptions/Request/RangeNotSatisfiableException.php create mode 100644 src/Exceptions/Request/RequestTimeoutException.php create mode 100644 src/Exceptions/Request/ServiceUnavailableException.php rename src/Exceptions/{ => Request}/StorageCapExceededException.php (60%) rename src/Exceptions/{ => Request}/TooManyBucketsException.php (56%) rename src/Exceptions/{ => Request}/TransactionCapExceededException.php (61%) create mode 100644 src/Exceptions/Request/UnauthorizedException.php rename src/Exceptions/{ => Request}/UnsupportedException.php (55%) delete mode 100644 src/Exceptions/UnauthorizedException.php delete mode 100644 src/Exceptions/ValidationException.php create mode 100644 src/Http/Exceptions/TooManyRequestsException.php create mode 100644 tests/ClientExceptionsTest.php create mode 100644 tests/Traits/EndpointHelpersTrait.php diff --git a/src/Client.php b/src/Client.php index b280fe8..97714cf 100644 --- a/src/Client.php +++ b/src/Client.php @@ -26,8 +26,8 @@ class Client { public const VERSION = '2.0.0'; public const USER_AGENT_PREFIX = 'backblaze-b2-php/'; - public const BASE_URI = 'https://api.backblazeb2.com'; - public const B2_API_VERSION = '/b2api/v2'; + public const BASE_URI = 'https://api.backblazeb2.com/'; + public const B2_API_VERSION = 'b2api/v2/'; use FileOperationsTrait; use BucketOperationsTrait; @@ -92,7 +92,7 @@ public function __construct($config) * @param ClientInterface $client */ public function authorizeAccount(): AccountAuthorization { - $response = $this->http->request('GET', Client::BASE_URI . Client::B2_API_VERSION . '/b2_authorize_account', [ + $response = $this->http->request('GET', Client::BASE_URI . Client::B2_API_VERSION . 'b2_authorize_account', [ 'headers' => [ 'Authorization' => Utils::basicAuthorization($this->config->applicationKeyId(), $this->config->applicationKey()), ], @@ -151,6 +151,16 @@ protected function createDefaultHttpClient(): ClientInterface { return $client; } + public function getAllowedBucketId(): ?string + { + return $this->accountAuthorization->getAllowed('bucketId') ?? null; + } + + public function getAllowedBucketName(): ?string + { + return $this->accountAuthorization->getAllowed('bucketName') ?? null; + } + /** * @see __construct() */ diff --git a/src/Config.php b/src/Config.php index b7ce2b6..12ca625 100644 --- a/src/Config.php +++ b/src/Config.php @@ -2,13 +2,9 @@ namespace Zaxbux\BackblazeB2; -use Exception; -use GuzzleHttp\ClientInterface; use GuzzleHttp\HandlerStack; use Zaxbux\BackblazeB2\Classes\BuiltinAuthorizationCache; -use Zaxbux\BackblazeB2\Http\ClientFactory; use Zaxbux\BackblazeB2\Interfaces\AuthorizationCacheInterface; -use Zaxbux\BackblazeB2\Object\AccountAuthorization; /** * The main configuration object for the client. @@ -66,7 +62,7 @@ class Config /** * Maximum amount of time, in seconds, to wait before retrying a failed request. * Ignored for HTTP status codes: 429, 503. - * Should be a power of 3. + * Should be a power of 2. * * @var int */ @@ -184,6 +180,7 @@ public static function fromArray($data): Config private function setOptions(array $options) { $this->handler = $options['handler'] ?? null; + $this->maxRetries = $options['maxRetries'] ?? 4; $this->authorizationCache = $options['authorizationCache'] ?? new BuiltinAuthorizationCache(); } } \ No newline at end of file diff --git a/src/Exceptions/B2APIException.php b/src/Exceptions/B2APIException.php deleted file mode 100644 index fb7b949..0000000 --- a/src/Exceptions/B2APIException.php +++ /dev/null @@ -1,21 +0,0 @@ -code = $code; - - parent::__construct($message); - } - - /** - * Get the error code - */ - public function getAPIErrorCode() { - return $this->code; - } -} diff --git a/src/Exceptions/BadBucketIdException.php b/src/Exceptions/BadBucketIdException.php deleted file mode 100644 index 7447581..0000000 --- a/src/Exceptions/BadBucketIdException.php +++ /dev/null @@ -1,5 +0,0 @@ -getReasonPhrase(); + $code = $response->getStatusCode(); + + try { + $responseJson = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR); + + $this->statusCode = $responseJson['status'] ?? null; + $message = $responseJson['message'] ?? null; + $code = $responseJson['code'] ?? null; + } catch (JsonException $e) { + // Ignore JSON exceptions, response object is available instead. + } + + parent::__construct($message, $request, $response, $previous); + $this->code = $code; + } + + /** + * Get the status code as returned by the B2 API. + */ + public function getStatus(): ?string + { + return $this->statusCode; + } +} diff --git a/src/Exceptions/BadAuthTokenException.php b/src/Exceptions/Request/BadAuthTokenException.php similarity index 55% rename from src/Exceptions/BadAuthTokenException.php rename to src/Exceptions/Request/BadAuthTokenException.php index 90ad705..40fc3fa 100644 --- a/src/Exceptions/BadAuthTokenException.php +++ b/src/Exceptions/Request/BadAuthTokenException.php @@ -1,5 +1,5 @@ Exceptions\BadRequestException::class, - 'bad_auth_token' => Exceptions\BadAuthTokenException::class, - 'bad_bucket_id' => Exceptions\BadBucketIdException::class, - 'expired_auth_token' => Exceptions\ExpiredAuthException::class, - 'not_found' => Exceptions\NotFoundException::class, - 'file_not_present' => Exceptions\FileNotPresentException::class, - 'transaction_cap_exceeded' => Exceptions\TransactionCapExceededException::class, - 'cap_exceeded' => Exceptions\CapExceededException::class, - 'unauthorized' => Exceptions\UnauthorizedException::class, - 'unsupported' => Exceptions\UnsupportedException::class, - 'range_not_satisfiable' => Exceptions\RangeNotSatisfiableException::class, - 'too_many_buckets' => Exceptions\TooManyBucketsException::class, - 'duplicate_bucket_name' => Exceptions\DuplicateBucketNameException::class, +use Zaxbux\BackblazeB2\Exceptions\Request\{ + B2APIException, + BadRequestException, + TooManyBucketsException, + DuplicateBucketNameException, + FileNotPresentException, + OutOfRangeException, + InvalidBucketIdException, + InvalidFileIdException, + UnsupportedException, + UnauthorizedException, + BadAuthTokenException, + ExpiredAuthTokenException, + AccessDeniedException, + CapExceededException, + StorageCapExceededException, + TransactionCapExceededException, + DownloadCapExceededException, + NotFoundException, + MethodNotAllowedException, + RequestTimeoutException, + ConflictException, + RangeNotSatisfiableException, + ServiceUnavailableException, +}; + +class ErrorHandler +{ + private const ERROR_CODES = [ + Response::HTTP_BAD_REQUEST => [ + 'bad_request' => BadRequestException::class, + 'too_many_buckets' => TooManyBucketsException::class, + 'duplicate_bucket_name' => DuplicateBucketNameException::class, + 'file_not_present' => FileNotPresentException::class, + 'out_of_range' => OutOfRangeException::class, + 'invalid_bucket_id' => InvalidBucketIdException::class, + 'bad_bucket_id' => InvalidBucketIdException::class, + 'invalid_file_id' => InvalidFileIdException::class, + ], + Response::HTTP_UNAUTHORIZED => [ + 'unsupported' => UnsupportedException::class, + 'unauthorized' => UnauthorizedException::class, + 'bad_auth_token' => BadAuthTokenException::class, + 'expired_auth_token' => ExpiredAuthTokenException::class, + 'access_denied' => AccessDeniedException::class, + ], + Response::HTTP_FORBIDDEN => [ + 'cap_exceeded' => CapExceededException::class, + 'storage_cap_exceeded' => StorageCapExceededException::class, + 'transaction_cap_exceeded' => TransactionCapExceededException::class, + 'access_denied' => AccessDeniedException::class, + 'download_cap_exceeded' => DownloadCapExceededException::class, + ], + Response::HTTP_NOT_FOUND => ['not_found' => NotFoundException::class,], + Response::HTTP_METHOD_NOT_ALLOWED => ['method_not_allowed' => MethodNotAllowedException::class,], + Response::HTTP_REQUEST_TIMEOUT => ['request_timeout' => RequestTimeoutException::class,], + Response::HTTP_CONFLICT => ['conflict' => ConflictException::class,], + Response::HTTP_RANGE_NOT_SATISFIABLE => ['range_not_satisfiable' => RangeNotSatisfiableException::class,], + Response::HTTP_SERVICE_UNAVAILABLE => [ + 'service_unavailable' => ServiceUnavailableException::class, + 'bad_request' => BadRequestException::class, + ], ]; - public static function getException(ResponseInterface $response) { - $responseJson = json_decode($response->getBody(), true); + public static function getException(RequestInterface $request, ResponseInterface $response): B2APIException + { + $exceptionClass = B2APIException::class; + + try { + $responseJson = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR); + + $statusCode = $response->getStatusCode(); + $errorCode = $responseJson['code']; - if (isset(self::$errorCodes[$responseJson['code']])) { - $exceptionClass = self::$errorCodes[$responseJson['code']]; - } else { - // We don't have an exception mapped to this response error, throw generic exception - $exceptionClass = Exceptions\B2APIException::class; + if (array_key_exists($statusCode, static::ERROR_CODES)) { + /** @var string */ + $exceptionClass = static::ERROR_CODES[$statusCode][$errorCode] ?? B2APIException::class; + } + } catch (JsonException $ex) { + // Ignore JSON exceptions, response object is available instead } - return new $exceptionClass('B2 API error: '.$responseJson['message'], $responseJson['code']); + return new $exceptionClass($request, $response); } } diff --git a/src/Http/Exceptions/TooManyRequestsException.php b/src/Http/Exceptions/TooManyRequestsException.php new file mode 100644 index 0000000..4b1f482 --- /dev/null +++ b/src/Http/Exceptions/TooManyRequestsException.php @@ -0,0 +1,7 @@ + new Uri($this->client->accountAuthorization()->getApiUrl() . $request->getUri()), + $request = Psr7Utils::modifyRequest($request, [ + 'uri' => new Uri(Utils::joinPaths($this->client->accountAuthorization()->getApiUrl(), (string)$request->getUri())), 'set_headers' => [ 'Authorization' => $this->client->accountAuthorization()->getAuthorizationToken(), ], diff --git a/src/Http/Middleware/ExceptionMiddleware.php b/src/Http/Middleware/ExceptionMiddleware.php index 3c620a3..4a89482 100644 --- a/src/Http/Middleware/ExceptionMiddleware.php +++ b/src/Http/Middleware/ExceptionMiddleware.php @@ -5,45 +5,32 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Zaxbux\BackblazeB2\Http\ErrorHandler; +use Zaxbux\BackblazeB2\Http\Exceptions\TooManyRequestsException; +use Zaxbux\BackblazeB2\Http\Response; class ExceptionMiddleware { public function __invoke(callable $handler) { - return function (RequestInterface $request, array $options = []) use ($handler) { + return static function (RequestInterface $request, array $options = []) use ($handler) { $promise = $handler($request, $options); - return $promise->then(function (ResponseInterface $response) { - if ($this->isSuccessful($response)) { + return $promise->then(function (ResponseInterface $response) use($request) { + if (static::isSuccessful($response)) { return $response; } - $this->handleErrorResponse($response); + + if ($response->getStatusCode() === Response::HTTP_TOO_MANY_REQUESTS) { + throw new TooManyRequestsException('', $request, $response); + } + + throw ErrorHandler::getException($request, $response); }); }; } - public function isSuccessful(ResponseInterface $response) - { - return $response->getStatusCode() <= 299; - //return $response->getStatusCode() == Response::HTTP_OK || Response::HTTP_NO_CONTENT || Response::HTTP_PARTIAL_CONTENT; - //return $response->getStatusCode() < Response::HTTP_BAD_REQUEST; - } - - public function handleErrorResponse(ResponseInterface $response) + public static function isSuccessful(ResponseInterface $response) { - throw ErrorHandler::getException($response); - /* - switch ($response->getStatusCode()) { - //case Response::HTTP_UNPROCESSABLE_ENTITY: - // throw new ValidationException(Utils::jsonDecode($response->getBody(), true)); - case Response::HTTP_NOT_FOUND: - throw new NotFoundException; - case Response::HTTP_UNAUTHORIZED: - throw new UnauthorizedException; - default: - throw ErrorHandler::getException($response); - //throw new B2APIException((string) $response->getBody()); - } - */ + return $response->getStatusCode() >= Response::HTTP_OK && $response->getStatusCode() <= Response::HTTP_PARTIAL_CONTENT; } } diff --git a/src/Http/Middleware/RetryMiddleware.php b/src/Http/Middleware/RetryMiddleware.php index 6c6e2ff..ee9c629 100644 --- a/src/Http/Middleware/RetryMiddleware.php +++ b/src/Http/Middleware/RetryMiddleware.php @@ -11,7 +11,10 @@ class RetryMiddleware { - public const RETRY_POWER = 2; + protected const RETRY_STATUS_CODES = [ + Response::HTTP_TOO_MANY_REQUESTS, + Response::HTTP_SERVICE_UNAVAILABLE + ]; /** @var \Zaxbux\BackblazeB2\Config */ protected $config; @@ -39,18 +42,22 @@ private static function retryDecider(Config $config): callable { ResponseInterface $response = null, $exception = null ) use ($config): bool { - // Do not retry `b2_authorize_account` requests. - /*if (preg_match('/b2_authorize_account$/', rtrim($request->getUri()->getPath(), '/'))) { + // Only retry allowed status codes. + if (!in_array($response->getStatusCode(), static::RETRY_STATUS_CODES)) { return false; - }*/ + } - $overRetryLimit = ($retries > $config->maxRetries()); - $statusCode = $response->getStatusCode() ?? null; + // Do not retry more than the configured maximum. + if ($retries >= $config->maxRetries()) { + return false; + } - return !$overRetryLimit && ( - $statusCode === Response::HTTP_TOO_MANY_REQUESTS || - $statusCode === Response::HTTP_SERVICE_UNAVAILABLE - ); + // Do not retry `b2_authorize_account` requests. + if (preg_match('/b2_authorize_account$/', rtrim($request->getUri()->getPath(), '/'))) { + return false; + } + + return true; }; } @@ -63,21 +70,13 @@ private static function retryDecider(Config $config): callable { */ private static function retryDelay(Config $config): callable { return static function(int $retries, ResponseInterface $response) use ($config): int { - // Set a default delay - $delay = $config->maxRetryDelay(); //$config->maxRetries() ** static::RETRY_POWER; - // Use the value of the `Retry-After` header if ($response->getStatusCode() === Response::HTTP_TOO_MANY_REQUESTS) { - $delay = (int) $response->getHeader('Retry-After')[0] ?? $delay; - } - - // Exponential back-off - if ($response->getStatusCode() === Response::HTTP_SERVICE_UNAVAILABLE) { - $delay = $retries ** static::RETRY_POWER; + $delay = (int) $response->getHeader('Retry-After')[0] ?? $config->maxRetryDelay(); + return $delay * 1000; } - // Convert to milliseconds - return $delay * 1000; + return \GuzzleHttp\RetryMiddleware::exponentialDelay($retries); }; } } diff --git a/src/Object/AccountAuthorization.php b/src/Object/AccountAuthorization.php index 1c79eef..6655e80 100644 --- a/src/Object/AccountAuthorization.php +++ b/src/Object/AccountAuthorization.php @@ -104,8 +104,12 @@ public function getAuthorizationToken(): ?string /** * Get the capabilities, bucket restrictions, and prefix restrictions. */ - public function getAllowed(): ?array + public function getAllowed(?string $key = null): ?array { + if ($key) { + return $this->allowed[$key] ?? null; + } + return $this->allowed; } @@ -166,6 +170,11 @@ public function expired(): bool return time() - AuthorizationCacheInterface::EXPIRES >= $this->created; } + public function hasCapability(string $capability): bool + { + return in_array($capability, $this->allowed['capabilities'] ?? []); + } + public static function fromResponse(ResponseInterface $response): AccountAuthorization { return static::fromArray(Utils::jsonDecode((string) $response->getBody(), true)); diff --git a/src/Operations/ApplicationKeyOperationsTrait.php b/src/Operations/ApplicationKeyOperationsTrait.php index 44e54c1..666a660 100644 --- a/src/Operations/ApplicationKeyOperationsTrait.php +++ b/src/Operations/ApplicationKeyOperationsTrait.php @@ -39,7 +39,7 @@ public function createKey( ?string $bucketId = null, ?string $namePrefix = null ): Key { - $response = $this->http->request('POST', '/b2_create_key', [ + $response = $this->http->request('POST', 'b2_create_key', [ 'json' => Utils::filterRequestOptions([ Key::ATTRIBUTE_ACCOUNT_ID => $this->accountAuthorization()->getAccountId(), Key::ATTRIBUTE_CAPABILITIES => $capabilities, @@ -63,7 +63,7 @@ public function createKey( */ public function deleteKey(string $applicationKeyId): Key { - $response = $this->http->request('POST', '/b2_delete_key', [ + $response = $this->http->request('POST', 'b2_delete_key', [ 'json' => [ Key::ATTRIBUTE_APPLICATION_KEY_ID => $applicationKeyId, ] @@ -86,7 +86,7 @@ public function listKeys( ?string $startApplicationKeyId = null, ?int $maxKeyCount = 1000 ): KeyList { - $response = $this->http->request('POST', '/b2_list_keys', [ + $response = $this->http->request('POST', 'b2_list_keys', [ 'json' => Utils::filterRequestOptions([ Key::ATTRIBUTE_ACCOUNT_ID => $this->accountAuthorization()->getAccountId(), ], [ diff --git a/src/Operations/BucketOperationsTrait.php b/src/Operations/BucketOperationsTrait.php index be2f9c4..a02b907 100644 --- a/src/Operations/BucketOperationsTrait.php +++ b/src/Operations/BucketOperationsTrait.php @@ -40,7 +40,7 @@ public function createBucket( ?array $corsRules = null, ?array $lifecycleRules = null ): Bucket { - $response = $this->http->request('POST', '/b2_create_bucket', [ + $response = $this->http->request('POST', 'b2_create_bucket', [ 'json' => Utils::filterRequestOptions([ Bucket::ATTRIBUTE_ACCOUNT_ID => $this->accountAuthorization()->getAccountId(), Bucket::ATTRIBUTE_BUCKET_NAME => $bucketName, @@ -70,7 +70,7 @@ public function deleteBucket(string $bucketId, ?bool $withFiles = false): Bucket $this->deleteAllFileVersions($bucketId); } - $response = $this->http->request('POST', '/b2_delete_bucket', [ + $response = $this->http->request('POST', 'b2_delete_bucket', [ 'json' => [ Bucket::ATTRIBUTE_ACCOUNT_ID => $this->accountAuthorization()->getAccountId(), Bucket::ATTRIBUTE_BUCKET_ID => $bucketId @@ -99,7 +99,7 @@ public function listBuckets( ?string $bucketName = null, ?array $bucketTypes = null ): BucketList { - $response = $this->http->request('POST', '/b2_list_buckets', [ + $response = $this->http->request('POST', 'b2_list_buckets', [ 'json' => Utils::filterRequestOptions([ Bucket::ATTRIBUTE_ACCOUNT_ID => $this->accountAuthorization()->getAccountId(), ], [ @@ -128,17 +128,17 @@ public function listBuckets( * the B2 service matches the one passed in. */ public function updateBucket( - string $bucketId, + ?string $bucketId = null, ?string $bucketType = null, ?array $bucketInfo = null, ?array $corsRules = null, ?array $lifecycleRules = null, ?int $ifRevisionIs = null ): Bucket { - $response = $this->http->request('POST', '/b2_update_bucket', [ + $response = $this->http->request('POST', 'b2_update_bucket', [ 'json' => Utils::filterRequestOptions([ Bucket::ATTRIBUTE_ACCOUNT_ID => $this->accountAuthorization()->getAccountId(), - Bucket::ATTRIBUTE_BUCKET_ID => $bucketId, + Bucket::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), ], [ Bucket::ATTRIBUTE_BUCKET_TYPE => $bucketType, Bucket::ATTRIBUTE_BUCKET_INFO => $bucketInfo, @@ -154,13 +154,15 @@ public function updateBucket( /** * Get a bucket by ID. * - * @param string $bucketId The ID of the bucket to fetch. + * @param string $bucketId The ID of the bucket to fetch. Defaults to the authorized bucket, if any. * @param array|null $bucketTypes Filter for bucket types returned in the list buckets response. * * @throws NotFoundException */ - public function getBucketById(string $bucketId, array $bucketTypes = null): Bucket - { + public function getBucketById( + ?string $bucketId = null, + ?array $bucketTypes = null + ): Bucket { $response = $this->listBuckets($bucketId, null, $bucketTypes); if (iterator_count($response->getBuckets()) !== 1) { diff --git a/src/Operations/FileOperationsTrait.php b/src/Operations/FileOperationsTrait.php index afc3610..c2fc06b 100644 --- a/src/Operations/FileOperationsTrait.php +++ b/src/Operations/FileOperationsTrait.php @@ -6,7 +6,6 @@ use AppendIterator; use ArrayIterator; -use GuzzleHttp\Psr7\Stream; use Iterator; use NoRewindIterator; use Zaxbux\BackblazeB2\Client; @@ -43,7 +42,7 @@ abstract protected function accountAuthorization(): AccountAuthorization; */ public function cancelLargeFile(string $fileId) { - $response = $this->http->request('POST', '/b2_cancel_large_file', [ + $response = $this->http->request('POST', 'b2_cancel_large_file', [ 'json' => [ File::ATTRIBUTE_FILE_ID => $fileId, ], @@ -112,7 +111,7 @@ public function copyFile( } */ - $response = $this->http->request('POST', '/b2_copy_file', [ + $response = $this->http->request('POST', 'b2_copy_file', [ 'json' => Utils::filterRequestOptions([ File::ATTRIBUTE_SOURCE_FILE_ID => $sourceFileId, File::ATTRIBUTE_FILE_NAME => $fileName, @@ -157,7 +156,7 @@ public function copyPart( ?ServerSideEncryption $sourceSSE = null, ?ServerSideEncryption $destinationSSE = null ): File { - $response = $this->http->request('POST', '/b2_copy_part', [ + $response = $this->http->request('POST', 'b2_copy_part', [ 'json' => Utils::filterRequestOptions([ File::ATTRIBUTE_SOURCE_FILE_ID => $sourceFileId, File::ATTRIBUTE_LARGE_FILE_ID => $largeFileId, @@ -182,9 +181,9 @@ public function copyPart( * @param bool $bypassGovernance Must be specified and set to true if deleting a file version protected by * File Lock governance mode retention settings. */ - public function deleteFileVersion(string $fileName, string $fileId, ?bool $bypassGovernance = false): File + public function deleteFileVersion(string $fileId, string $fileName, ?bool $bypassGovernance = false): File { - $response = $this->http->request('POST', '/b2_delete_file_version', [ + $response = $this->http->request('POST', 'b2_delete_file_version', [ 'json' => Utils::filterRequestOptions([ File::ATTRIBUTE_FILE_NAME => $fileName, File::ATTRIBUTE_FILE_ID => $fileId, @@ -242,7 +241,7 @@ public function downloadFileById( */ public function downloadFileByName( string $fileName, - string $bucketName, + ?string $bucketName = null, $options = null, $sink = null, ?bool $headersOnly = false @@ -251,7 +250,7 @@ public function downloadFileByName( Utils::joinPaths( $this->accountAuthorization()->getApiUrl(), 'file', - $bucketName, + $bucketName ?? $this->getAllowedBucketName(), $fileName ), null, @@ -273,7 +272,7 @@ public function downloadFileByName( */ public function finishLargeFile(string $fileId, array $hashes) { - $response = $this->http->request('POST', '/b2_finish_large_file', [ + $response = $this->http->request('POST', 'b2_finish_large_file', [ 'json' => [ File::ATTRIBUTE_FILE_ID => $fileId, File::ATTRIBUTE_PART_SHA1_ARRAY => $hashes, @@ -296,8 +295,8 @@ public function finishLargeFile(string $fileId, array $hashes) * @param DownloadOptions|array $options Additional options to pass to the API. */ public function getDownloadAuthorization( - string $bucketId, string $fileNamePrefix, + ?string $bucketId = null, ?int $validDuration = DownloadAuthorization::VALID_DURATION_MAX, $options = null ): DownloadAuthorization { @@ -305,9 +304,9 @@ public function getDownloadAuthorization( $options = DownloadOptions::fromArray($options ?? []); } - $response = $this->http->request('POST', '/b2_get_download_authorization', [ + $response = $this->http->request('POST', 'b2_get_download_authorization', [ 'json' => Utils::filterRequestOptions([ - File::ATTRIBUTE_BUCKET_ID => $bucketId, + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), File::ATTRIBUTE_FILE_NAME_PREFIX => $fileNamePrefix, File::ATTRIBUTE_VALID_DURATION => $validDuration, ], $options->getAuthorizationOptions()), @@ -325,7 +324,7 @@ public function getDownloadAuthorization( */ public function getFileInfo(string $fileId): File { - $response = $this->http->request('POST', '/b2_get_file_info', [ + $response = $this->http->request('POST', 'b2_get_file_info', [ 'json' => [ File::ATTRIBUTE_FILE_ID => $fileId ] @@ -343,7 +342,7 @@ public function getFileInfo(string $fileId): File */ public function getUploadPartUrl(string $fileId): UploadPartUrl { - $response = $this->http->request('POST', '/b2_get_upload_part_url', [ + $response = $this->http->request('POST', 'b2_get_upload_part_url', [ 'json' => [ File::ATTRIBUTE_FILE_ID => $fileId ] @@ -361,11 +360,11 @@ public function getUploadPartUrl(string $fileId): UploadPartUrl * * @return UploadUrl */ - public function getUploadUrl(string $bucketId): UploadUrl + public function getUploadUrl(?string $bucketId): UploadUrl { - $response = $this->http->request('POST', '/b2_get_upload_url', [ + $response = $this->http->request('POST', 'b2_get_upload_url', [ 'json' => [ - File::ATTRIBUTE_BUCKET_ID => $bucketId + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId() ] ]); @@ -381,12 +380,12 @@ public function getUploadUrl(string $bucketId): UploadUrl * @param string $bucketId * @param string $fileName */ - public function hideFile(string $bucketId, string $fileName): File + public function hideFile(string $fileName, ?string $bucketId = null): File { - $response = $this->http->request('POST', '/b2_hide_file', [ + $response = $this->http->request('POST', 'b2_hide_file', [ 'json' => [ - File::ATTRIBUTE_BUCKET_ID => $bucketId, + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), File::ATTRIBUTE_FILE_NAME => $fileName, ] ]); @@ -412,15 +411,15 @@ public function hideFile(string $bucketId, string $fileName): File * the maximum is 10000. */ public function listFileNames( - string $bucketId, + ?string $bucketId = null, ?string $prefix = null, ?string $delimiter = null, ?string $startFileName = null, ?int $maxFileCount = 1000 ): FileList { - $response = $this->http->request('POST', '/b2_list_file_names', [ + $response = $this->http->request('POST', 'b2_list_file_names', [ 'json' => Utils::filterRequestOptions([ - File::ATTRIBUTE_BUCKET_ID => $bucketId, + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), File::ATTRIBUTE_MAX_FILE_COUNT => $maxFileCount, ], [ File::ATTRIBUTE_PREFIX => $prefix, @@ -453,16 +452,16 @@ public function listFileNames( * If more than 1000 are returned, the call will be billed as multiple transactions. */ public function listFileVersions( - string $bucketId, + ?string $bucketId = null, ?string $prefix = '', ?string $delimiter = null, ?string $startFileName = null, ?string $startFileId = null, ?int $maxFileCount = 1000 ): FileList { - $response = $this->http->request('POST', '/b2_list_file_versions', [ + $response = $this->http->request('POST', 'b2_list_file_versions', [ 'json' => Utils::filterRequestOptions([ - File::ATTRIBUTE_BUCKET_ID => $bucketId, + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), ], [ File::ATTRIBUTE_START_FILE_NAME => $startFileName, File::ATTRIBUTE_START_FILE_ID => $startFileId, @@ -495,7 +494,7 @@ public function listParts( ?int $startPartNumber = null, ?int $maxPartCount = 1000 ): FilePartList { - $response = $this->http->request('POST', '/b2_list_parts', [ + $response = $this->http->request('POST', 'b2_list_parts', [ 'json' => Utils::filterRequestOptions([ File::ATTRIBUTE_FILE_ID => $fileId ], [ @@ -520,18 +519,18 @@ public function listParts( * the maximum allowed is 100. */ public function listUnfinishedLargeFiles( - string $bucketId, + ?string $bucketId = null, ?string $namePrefix = null, ?string $startFileId = null, ?int $maxFileCount = 100 ): FileList { - $response = $this->http->request('POST', '/b2_list_unfinished_large_files', [ + $response = $this->http->request('POST', 'b2_list_unfinished_large_files', [ 'json' => Utils::filterRequestOptions([ - File::ATTRIBUTE_BUCKET_ID => $bucketId, - File::ATTRIBUTE_MAX_FILE_COUNT => $maxFileCount, + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), ], [ File::ATTRIBUTE_NAME_PREFIX => $namePrefix, File::ATTRIBUTE_START_FILE_ID => $startFileId, + File::ATTRIBUTE_MAX_FILE_COUNT => $maxFileCount, ]), ]); @@ -549,8 +548,8 @@ public function listUnfinishedLargeFiles( * @param FileInfo|array $fileInfo A JSON object holding the name/value pairs for the custom file info. */ public function startLargeFile( - string $bucketId, string $fileName, + ?string $bucketId = null, ?string $contentType = null, $fileInfo = null, ?array $fileRetention = null, @@ -561,9 +560,9 @@ public function startLargeFile( $fileInfo = FileInfo::fromArray($fileInfo); } - $response = $this->http->request('POST', '/b2_start_large_file', [ + $response = $this->http->request('POST', 'b2_start_large_file', [ 'json' => Utils::filterRequestOptions([ - File::ATTRIBUTE_BUCKET_ID => $bucketId, + File::ATTRIBUTE_BUCKET_ID => $bucketId ?? $this->getAllowedBucketId(), File::ATTRIBUTE_FILE_NAME => $fileName, File::ATTRIBUTE_CONTENT_TYPE => $contentType ?? File::CONTENT_TYPE_AUTO, ], [ @@ -591,7 +590,7 @@ public function updateFileLegalHold( string $fileId, string $legalHold ): File { - $response = $this->http->request('POST', '/b2_update_file_legal_hold', [ + $response = $this->http->request('POST', 'b2_update_file_legal_hold', [ 'json' => Utils::filterRequestOptions([ File::ATTRIBUTE_FILE_NAME => $fileName, File::ATTRIBUTE_FILE_ID => $fileId, @@ -619,7 +618,7 @@ public function updateFileRetention( string $fileRetention, ?bool $bypassGovernance = false ): File { - $response = $this->http->request('POST', '/b2_update_file_retention', [ + $response = $this->http->request('POST', 'b2_update_file_retention', [ 'json' => Utils::filterRequestOptions([ File::ATTRIBUTE_FILE_NAME => $fileName, File::ATTRIBUTE_FILE_ID => $fileId, @@ -645,8 +644,8 @@ public function updateFileRetention( * @param UploadUrl $uploadUrl The upload authorization data. */ public function uploadFile( - string $bucketId, string $fileName, + ?string $bucketId = null, $body, ?string $contentType = null, $fileInfo = null, @@ -737,7 +736,7 @@ public function uploadPart( * @return iterable */ public function listAllFileNames( - string $bucketId, + ?string $bucketId = null, string $prefix = '', string $delimiter = null, string $startFileName = null, @@ -756,10 +755,10 @@ public function listAllFileNames( return $allFiles; } - public function getFileByName(string $bucketId, string $fileName): File + public function getFileByName(string $fileName, ?string $bucketId = null): File { if (!$file = $this->listFileNames($bucketId, '', null, $fileName, 1)->first()) { - throw new NotFoundException(sprintf('No results returned for file name "%s"')); + throw new NotFoundException(sprintf('No results returned for file name "%s"', $fileName)); } return $file; @@ -773,7 +772,7 @@ public function getFileByName(string $bucketId, string $fileName): File * @return iterable */ public function listAllFileVersions( - string $bucketId, + ?string $bucketId = null, ?string $prefix = '', ?string $delimiter = null, ?string $startFileName = null, @@ -794,10 +793,10 @@ public function listAllFileVersions( return $allFiles; } - public function getFileById(string $bucketId, string $fileId): File + public function getFileById(string $fileId, ?string $bucketId = null): File { if (!$file = $this->listFileVersions($bucketId, '', null, null, $fileId, 1)->first()) { - throw new NotFoundException(sprintf('No results returned for file id "%s"')); + throw new NotFoundException(sprintf('No results returned for file id "%s"', $fileId)); } return $file; @@ -836,10 +835,10 @@ public function listAllParts( * @return iterable */ public function listAllUnfinishedLargeFiles( - string $bucketId, - string $namePrefix = null, - string $startFileId = null, - int $maxFileCount = 100 + ?string $bucketId = null, + ?string $namePrefix = null, + ?string $startFileId = null, + ?int $maxFileCount = 100 ): iterable { $allFiles = new AppendIterator(); @@ -868,11 +867,11 @@ public function listAllUnfinishedLargeFiles( * @param null|bool $bypassGovernance */ public function deleteAllFileVersions( - string $bucketId, + ?string $startFileId = null, + ?string $startFileName = null, ?string $prefix = '', ?string $delimiter = null, - ?string $startFileName = null, - ?string $startFileId = null, + ?string $bucketId = null, ?bool $bypassGovernance = false ): FileList { $fileVersions = $this->listAllFileVersions($bucketId, $prefix, $delimiter, $startFileName, $startFileId); diff --git a/tests/ClientBucketOperationsTest.php b/tests/ClientBucketOperationsTest.php index 1fbae8f..6963b28 100644 --- a/tests/ClientBucketOperationsTest.php +++ b/tests/ClientBucketOperationsTest.php @@ -2,11 +2,12 @@ namespace tests; +use Zaxbux\BackblazeB2\Client; use Zaxbux\BackblazeB2\Object\Bucket; use Zaxbux\BackblazeB2\Object\Bucket\BucketType; -use Zaxbux\BackblazeB2\Exceptions\B2APIException; -use Zaxbux\BackblazeB2\Exceptions\BadRequestException; -use Zaxbux\BackblazeB2\Exceptions\DuplicateBucketNameException; +use Zaxbux\BackblazeB2\Exceptions\Request\B2APIException; +use Zaxbux\BackblazeB2\Exceptions\Request\BadRequestException; +use Zaxbux\BackblazeB2\Exceptions\Request\DuplicateBucketNameException; class ClientBucketOperationsTest extends ClientTestBase { @@ -17,7 +18,7 @@ public function testCreateBucket() ); $this->guzzler->expects($this->once()) - ->post(Endpoint::CREATE_BUCKET); + ->post(static::getEndpointUri(Endpoint::CREATE_BUCKET)); $bucket = $this->client->createBucket( 'Test bucket', @@ -36,7 +37,7 @@ public function testCreatePublicBucket() ); $this->guzzler->expects($this->once()) - ->post(Endpoint::CREATE_BUCKET); + ->post(static::getEndpointUri(Endpoint::CREATE_BUCKET)); // Test that we get a public bucket back after creation $bucket = $this->client->createBucket( @@ -137,7 +138,7 @@ public function testDeleteBucket() ); $this->guzzler->expects($this->once()) - ->post('/b2_delete_bucket'); + ->post(static::getEndpointUri(Endpoint::DELETE_BUCKET)); $this->assertInstanceOf(Bucket::class, $this->client->deleteBucket( 'bucketId' diff --git a/tests/ClientDownloadTest.php b/tests/ClientDownloadTest.php index 6af2379..40897fa 100644 --- a/tests/ClientDownloadTest.php +++ b/tests/ClientDownloadTest.php @@ -2,8 +2,9 @@ namespace tests; -use Zaxbux\BackblazeB2\Exceptions\B2APIException; -use Zaxbux\BackblazeB2\Exceptions\NotFoundException; +use Zaxbux\BackblazeB2\Exceptions\Request\B2APIException; +use Zaxbux\BackblazeB2\Exceptions\Request\BadRequestException; +use Zaxbux\BackblazeB2\Exceptions\Request\NotFoundException; class ClientDownloadTest extends ClientTestBase { @@ -14,8 +15,8 @@ public function testGetDownloadAuthorization() ); $authorization = $this->client->getDownloadAuthorization( - 'bucketId', 'public', + 'bucketId', 60 ); @@ -49,7 +50,7 @@ public function testDownloadByIdWithSavePath() public function testDownloadingByIncorrectIdThrowsException() { - $this->expectException(B2APIException::class); + $this->expectException(BadRequestException::class); $this->guzzler->queueResponse( MockResponse::fromFile('download_by_incorrect_id.json', 400), @@ -88,7 +89,7 @@ public function testDownloadingByIncorrectPathThrowsException() $this->expectException(NotFoundException::class); $this->guzzler->queueResponse( - MockResponse::fromFile('download_by_incorrect_path.json', 400), + MockResponse::fromFile('download_by_incorrect_path.json', 404), ); $this->client->downloadFileByName('path/to/incorrect/file.txt', 'test-bucket'); diff --git a/tests/ClientExceptionsTest.php b/tests/ClientExceptionsTest.php new file mode 100644 index 0000000..390553b --- /dev/null +++ b/tests/ClientExceptionsTest.php @@ -0,0 +1,99 @@ + 'testId', + 'applicationKey' => 'testKey', + 'handler' => $this->guzzler->getHandlerStack(), + 'maxRetries' => 1, // 503 errors cause the client to retry with exponential delay + ]; + } + + /** + * @dataProvider exceptionDataProvider + */ + public function testThrowsException($errorCode, $statusCode, $exceptionClass) + { + $this->expectException($exceptionClass); + $this->expectExceptionCode($errorCode); + $this->expectExceptionMessage($errorCode); + + $this->guzzler->queueResponse( + MockResponse::json(['code' => $errorCode, 'message' => $errorCode], $statusCode) + ); + + if ($statusCode === 503) { + $this->guzzler->queueResponse( + MockResponse::json(['code' => $errorCode, 'message' => $errorCode], $statusCode) + ); + } + + $this->client->getHttpClient()->request('POST', '__any__'); + } + + public function exceptionDataProvider(): array + { + return [ + ['bad_request', 400, BadRequestException::class], + ['too_many_buckets', 400, TooManyBucketsException::class], + ['duplicate_bucket_name', 400, DuplicateBucketNameException::class], + ['file_not_present', 400, FileNotPresentException::class], + ['out_of_range', 400, OutOfRangeException::class], + ['invalid_bucket_id', 400, InvalidBucketIdException::class], + ['bad_bucket_id', 400, InvalidBucketIdException::class], + ['invalid_file_id', 400, InvalidFileIdException::class], + + ['unsupported', 401, UnsupportedException::class], + ['unauthorized', 401, UnauthorizedException::class], + ['bad_auth_token', 401, BadAuthTokenException::class], + ['expired_auth_token', 401, ExpiredAuthTokenException::class], + ['access_denied', 401, AccessDeniedException::class], + + ['cap_exceeded', 403, CapExceededException::class], + ['storage_cap_exceeded', 403, StorageCapExceededException::class], + ['transaction_cap_exceeded', 403, TransactionCapExceededException::class], + ['access_denied', 403, AccessDeniedException::class], + ['download_cap_exceeded', 403, DownloadCapExceededException::class], + + ['not_found', 404, NotFoundException::class], + + ['method_not_allowed', 405, MethodNotAllowedException::class], + + ['request_timeout', 408, RequestTimeoutException::class], + + ['conflict', 409, ConflictException::class], + + ['range_not_satisfiable', 416, RangeNotSatisfiableException::class], + + ['service_unavailable', 503, ServiceUnavailableException::class], + ['bad_request', 503, BadRequestException::class], + ]; + } +} diff --git a/tests/ClientFileOperationsTest.php b/tests/ClientFileOperationsTest.php index 23f50bb..fbbfc24 100644 --- a/tests/ClientFileOperationsTest.php +++ b/tests/ClientFileOperationsTest.php @@ -4,7 +4,7 @@ use Zaxbux\BackblazeB2\Response\FileList; use Zaxbux\BackblazeB2\Object\File; -use Zaxbux\BackblazeB2\Exceptions\BadRequestException; +use Zaxbux\BackblazeB2\Exceptions\Request\BadRequestException; class ClientFileOperationsTest extends ClientTestBase { @@ -40,7 +40,7 @@ public function testGetFile() MockResponse::fromFile('get_file.json'), ); - $file = $this->client->getFileById('bucketId', 'fileId'); + $file = $this->client->getFileById('fileId', 'bucketId'); $this->assertInstanceOf(File::class, $file); } @@ -53,7 +53,7 @@ public function testGettingNonExistentFileThrowsException() MockResponse::fromFile('get_file_non_existent.json', 400), ); - $this->client->getFileById('bucketId', 'fileId'); + $this->client->getFileById('fileId', 'bucketId'); } public function testDeleteFile() @@ -63,7 +63,7 @@ public function testDeleteFile() MockResponse::fromFile('delete_file.json'), ); - $fileId = $this->client->getFileByName('bucketId', 'Test file.bin')->getId(); + $fileId = $this->client->getFileByName('Test file.bin', 'bucketId')->getId(); $this->assertInstanceOf(File::class, $this->client->deleteFileVersion('Test file.bin', $fileId)); } @@ -76,10 +76,10 @@ public function testDeleteFileRetrievesFileNameWhenNotProvided() ); $this->guzzler->queueMany(MockResponse::fromFile('delete_file.json'), 3); - $this->guzzler->expects($this->once())->post(Endpoint::LIST_FILE_VERSIONS); - $this->guzzler->expects($this->exactly(3))->post(Endpoint::DELETE_FILE_VERSION); + $this->guzzler->expects($this->once())->post(static::getEndpointUri(Endpoint::LIST_FILE_VERSIONS)); + $this->guzzler->expects($this->exactly(3))->post(static::getEndpointUri(Endpoint::DELETE_FILE_VERSION)); - $response = $this->client->deleteAllFileVersions('bucketId', null, null, 'fileId'); + $response = $this->client->deleteAllFileVersions('fileId', null, null, null, 'bucketId'); $this->assertInstanceOf(FileList::class, $response); @@ -122,10 +122,10 @@ public function testHideFile() ); $this->guzzler->expects($this->once()) - ->post(Endpoint::AUTHORIZE_ACCOUNT) - ->post(Endpoint::HIDE_FILE); + ->post(static::getEndpointUri(Endpoint::AUTHORIZE_ACCOUNT)) + ->post(static::getEndpointUri(Endpoint::HIDE_FILE)); - $file = $this->client->hideFile('bucketId', 'testfile.bin'); + $file = $this->client->hideFile('testfile.bin', 'bucketId'); $this->assertInstanceOf(File::class, $file); } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 479483e..336f89a 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -2,8 +2,10 @@ namespace tests; +use GuzzleHttp\Psr7\Response; use Zaxbux\BackblazeB2\Client; use Zaxbux\BackblazeB2\Config; +use Zaxbux\BackblazeB2\Http\Exceptions\TooManyRequestsException; use Zaxbux\BackblazeB2\Object\AccountAuthorization; class ClientTest extends ClientTestBase @@ -24,10 +26,37 @@ public function testClientConfig() public function testClientAuthorizeAccount() { + $this->guzzler->expects($this->exactly(2)) + ->get(Client::BASE_URI . Client::B2_API_VERSION . Endpoint::AUTHORIZE_ACCOUNT) + ->withHeader('Authorization', 'Basic MDAwMDAwMDAwMDAwYmI4MDAwMDAwMDAwMDphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0'); + $this->guzzler->queueResponse( - MockResponse::fromFile('authorize_account.json'), + MockResponse::json(static::ACCOUNT_AUTHORIZATION), ); $this->assertInstanceOf(AccountAuthorization::class, $this->client->authorizeAccount()); } + + public function testRetryMiddleware() + { + $this->guzzler->queueMany(new Response(429, ['Retry-After' => 1]), 4); + $this->guzzler->queueResponse(MockResponse::json(['files' => []], 200)); + + $this->client->getHttpClient()->request('POST', Endpoint::LIST_BUCKETS); + + $this->guzzler->assertLast(function ($expect) { + return $expect->post(static::getEndpointUri(Endpoint::LIST_BUCKETS)); + }); + } + + public function testThrowsTooManyRequestsException() + { + $this->expectException(TooManyRequestsException::class); + + $this->guzzler->queueMany(new Response(429, ['Retry-After' => 1]), 5); + + $this->client->getHttpClient()->request('POST', static::getEndpointUri(Endpoint::LIST_BUCKETS)); + + + } } diff --git a/tests/ClientTestBase.php b/tests/ClientTestBase.php index 13346d9..30a4fe4 100644 --- a/tests/ClientTestBase.php +++ b/tests/ClientTestBase.php @@ -4,13 +4,30 @@ namespace tests; +use BlastCloud\Guzzler\Expectation; use BlastCloud\Guzzler\UsesGuzzler; use PHPUnit\Framework\TestCase; use Zaxbux\BackblazeB2\Client; +use tests\Traits\EndpointHelpersTrait; abstract class ClientTestBase extends TestCase { use UsesGuzzler; + use EndpointHelpersTrait; + + /** + * The account authorization object for all client tests. + */ + protected const ACCOUNT_AUTHORIZATION = [ + "accountId" => "000000000000bb8", + "allowed" => [], + "apiUrl" => "https://apiNNN.backblaze.com.test", + "authorizationToken" => "0_0000000000008f80000000000_zzzzzzzz_zzzzzz_acct_zzzzzzzzzzzzzzzzzzzzzzzzzzzz", + "downloadUrl" => "https://fNNN.backblaze.com.test", + "recommendedPartSize" => 100000000, + "absoluteMinimumPartSize" => 5000000, + "s3ApiUrl" => "https://s3.us-west-NNN.backblazeb2.com" + ]; /** @var \Zaxbux\BackblazeB2\Client */ protected $client; @@ -22,11 +39,22 @@ protected function setUp(): void $this->client = new Client($this->clientInit()); $this->guzzler->queueResponse( - MockResponse::fromFile('authorize_account.json'), + MockResponse::json(static::ACCOUNT_AUTHORIZATION), ); $this->client->refreshAccountAuthorization(); + Expectation::macro('withAuthorizationToken', function (Expectation $e, $token) { + return $e->withHeaders([ + 'content-type' => 'application/json', + 'accept' => 'application/json', + + ]); + $e->withHeader('authorization', $token); + }); + + $this->guzzler->expects($this->any())->withAuthorizationToken(static::ACCOUNT_AUTHORIZATION['authorizationToken']); + $this->afterSetUp(); } @@ -36,10 +64,9 @@ protected function afterSetUp() { protected function clientInit() { return [ - 'applicationKeyId' => 'testId', - 'applicationKey' => 'testKey', + 'applicationKeyId' => '000000000000bb80000000000', + 'applicationKey' => 'abcdefghijklmnopqrstuvwxyz01234', 'handler' => $this->guzzler->getHandlerStack() - //'maxRetries' => 0, ]; } } diff --git a/tests/ClientUploadTest.php b/tests/ClientUploadTest.php index 67a3dd8..beee87d 100644 --- a/tests/ClientUploadTest.php +++ b/tests/ClientUploadTest.php @@ -2,7 +2,7 @@ namespace tests; -use Zaxbux\BackblazeB2\Utils; +use Zaxbux\BackblazeB2\Utils as ClientUtils; use Zaxbux\BackblazeB2\Helpers\UploadHelper; use Zaxbux\BackblazeB2\Object\File; use Zaxbux\BackblazeB2\Object\File\FileInfo; @@ -15,11 +15,11 @@ public function testUploadingFile() { MockResponse::fromFile('upload.json'), ); - $filePath = Utils::joinFilePaths(__DIR__, 'responses', 'download_content'); + $filePath = ClientUtils::joinFilePaths(__DIR__, 'responses', 'download_content'); $file = UploadHelper::instance($this->client)->uploadFile( - 'bucketId', '/file/name.txt', + 'bucketId', $filePath, 'text/plain' ); @@ -48,14 +48,14 @@ public function testUploadingResource() rewind($resource); $file = $this->client->uploadFile( - 'bucketId', 'test.txt', + 'bucketId', $resource ); $this->assertInstanceOf(File::class, $file); - $this->guzzler->expects($this->once())->post(Endpoint::GET_UPLOAD_URL); + $this->guzzler->expects($this->once())->post(static::getEndpointUri(Endpoint::GET_UPLOAD_URL)); $this->guzzler->expects($this->once()) ->post('https://pod-000-1005-03.backblaze.com/b2api/v2/b2_upload_file?cvt=c001_v0001005_t0027&bucket=4a48fe8875c6214145260818') ->withHeader('Authorization', 'authToken') @@ -75,8 +75,8 @@ public function testUploadingString() $content = 'The quick brown box jumps over the lazy dog'; $file = $this->client->uploadFile( - 'bucketId', 'test.txt', + 'bucketId', $content ); @@ -103,8 +103,8 @@ public function testUploadingWithCustomContentTypeAndLastModified() $contentType = 'text/plain'; $file = $this->client->uploadFile( - 'bucketId', 'test.txt', + 'bucketId', $content, $contentType, [ diff --git a/tests/Endpoint.php b/tests/Endpoint.php index 405ccd2..769d57d 100644 --- a/tests/Endpoint.php +++ b/tests/Endpoint.php @@ -4,33 +4,33 @@ final class Endpoint { - public const AUTHORIZE_ACCOUNT = '/b2_authorize_account'; - public const CANCEL_LARGE_FILE = '/b2_cancel_large_file'; - public const COPY_FILE = '/b2_copy_file'; - public const COPY_PART = '/b2_copy_part'; - public const CREATE_BUCKET = '/b2_create_bucket'; - public const CREATE_KEY = '/b2_create_key'; - public const DELETE_BUCKET = '/b2_delete_bucket'; - public const DELETE_FILE_VERSION = '/b2_delete_file_version'; - public const DELETE_KEY = '/b2_delete_key'; - public const DOWNLOAD_FILE_BY_ID = '/b2_download_file_by_id'; - public const DOWNLOAD_FILE_BY_NAME = '/b2_download_file_by_name'; - public const FINISH_LARGE_FILE = '/b2_finish_large_file'; - public const GET_DOWNLOAD_AUTHORIZATION = '/b2_get_download_authorization'; - public const GET_FILE_INFO = '/b2_get_file_info'; - public const GET_UPLOAD_PART_URL = '/b2_get_upload_part_url'; - public const GET_UPLOAD_URL = '/b2_get_upload_url'; - public const HIDE_FILE = '/b2_hide_file'; - public const LIST_BUCKETS = '/b2_list_buckets'; - public const LIST_FILE_NAMES = '/b2_list_file_names'; - public const LIST_FILE_VERSIONS = '/b2_list_file_versions'; - public const LIST_KEYS = '/b2_list_keys'; - public const LIST_PARTS = '/b2_list_parts'; - public const LIST_UNFINISHED_LARGE_FILES = '/b2_list_unfinished_large_files'; - public const START_LARGE_FILE = '/b2_start_large_file'; - public const UPDATE_BUCKET = '/b2_update_bucket'; - public const UPDATE_FILE_LEGAL_HOLD = '/b2_update_file_legal_hold'; - public const UPDATE_FILE_RETENTION = '/b2_update_file_retention'; - public const UPLOAD_FILE = '/b2_upload_file'; - public const UPLOAD_PART = '/b2_upload_part'; + public const AUTHORIZE_ACCOUNT = 'b2_authorize_account'; + public const CANCEL_LARGE_FILE = 'b2_cancel_large_file'; + public const COPY_FILE = 'b2_copy_file'; + public const COPY_PART = 'b2_copy_part'; + public const CREATE_BUCKET = 'b2_create_bucket'; + public const CREATE_KEY = 'b2_create_key'; + public const DELETE_BUCKET = 'b2_delete_bucket'; + public const DELETE_FILE_VERSION = 'b2_delete_file_version'; + public const DELETE_KEY = 'b2_delete_key'; + public const DOWNLOAD_FILE_BY_ID = 'b2_download_file_by_id'; + public const DOWNLOAD_FILE_BY_NAME = 'b2_download_file_by_name'; + public const FINISH_LARGE_FILE = 'b2_finish_large_file'; + public const GET_DOWNLOAD_AUTHORIZATION = 'b2_get_download_authorization'; + public const GET_FILE_INFO = 'b2_get_file_info'; + public const GET_UPLOAD_PART_URL = 'b2_get_upload_part_url'; + public const GET_UPLOAD_URL = 'b2_get_upload_url'; + public const HIDE_FILE = 'b2_hide_file'; + public const LIST_BUCKETS = 'b2_list_buckets'; + public const LIST_FILE_NAMES = 'b2_list_file_names'; + public const LIST_FILE_VERSIONS = 'b2_list_file_versions'; + public const LIST_KEYS = 'b2_list_keys'; + public const LIST_PARTS = 'b2_list_parts'; + public const LIST_UNFINISHED_LARGE_FILES = 'b2_list_unfinished_large_files'; + public const START_LARGE_FILE = 'b2_start_large_file'; + public const UPDATE_BUCKET = 'b2_update_bucket'; + public const UPDATE_FILE_LEGAL_HOLD = 'b2_update_file_legal_hold'; + public const UPDATE_FILE_RETENTION = 'b2_update_file_retention'; + public const UPLOAD_FILE = 'b2_upload_file'; + public const UPLOAD_PART = 'b2_upload_part'; } diff --git a/tests/MockResponse.php b/tests/MockResponse.php index 52a4650..ef36463 100644 --- a/tests/MockResponse.php +++ b/tests/MockResponse.php @@ -33,4 +33,12 @@ public static function fromFile( return new static($statusCode, $headers, $body); } + + public static function json( + array $data, + int $statusCode = 200, + array $headers = [] + ): MockResponse { + return new static($statusCode, $headers, json_encode($data)); + } } \ No newline at end of file diff --git a/tests/Traits/EndpointHelpersTrait.php b/tests/Traits/EndpointHelpersTrait.php new file mode 100644 index 0000000..c1e4bf7 --- /dev/null +++ b/tests/Traits/EndpointHelpersTrait.php @@ -0,0 +1,20 @@ +getBody()); + print("\n--------------------\n"); + } + } } \ No newline at end of file diff --git a/tests/responses/download_by_incorrect_id.json b/tests/responses/download_by_incorrect_id.json index 2c3e4bf..5c6763f 100644 --- a/tests/responses/download_by_incorrect_id.json +++ b/tests/responses/download_by_incorrect_id.json @@ -1,5 +1,5 @@ { - "code": "bad_value", + "code": "bad_request", "message": "bad fileId: 4_z4c2b957661da9c825f260e1b_f119f1fae240dae93_d20160131_m162947_c001_v0001015_t0020123123", "status": 400 } \ No newline at end of file