diff --git a/.editorconfig b/.editorconfig index 009c31a3..6ccb9889 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,27 +1,27 @@ -# top-most EditorConfig file -root = true - -# All PHP files MUST use the Unix LF (linefeed) line ending. -# Code MUST use an indent of 4 spaces, and MUST NOT use tabs for indenting. -# All PHP files MUST end with a single blank line. -# There MUST NOT be trailing whitespace at the end of non-blank lines. -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true - -# PHP-Files, Composer.json, MD-Files -[{*.php,composer.json,*.md}] -indent_style = space -indent_size = 4 - -# HTML-Files LESS-Files SASS-Files CSS-Files JS-Files JSON-Files -[{*.html,*.less,*.sass,*.css,*.js,*.json}] -indent_style = tab -indent_size = 4 - -# Gitlab-CI, Travis-CI -[*.yml] -indent_style = space -indent_size = 2 +# top-most EditorConfig file +root = true + +# All PHP files MUST use the Unix LF (linefeed) line ending. +# Code MUST use an indent of 4 spaces, and MUST NOT use tabs for indenting. +# All PHP files MUST end with a single blank line. +# There MUST NOT be trailing whitespace at the end of non-blank lines. +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# PHP-Files, Composer.json, MD-Files +[{*.php,composer.json,*.md}] +indent_style = space +indent_size = 4 + +# HTML-Files LESS-Files SASS-Files CSS-Files JS-Files JSON-Files +[{*.html,*.less,*.sass,*.css,*.js,*.json}] +indent_style = tab +indent_size = 4 + +# Gitlab-CI, Travis-CI +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 07a2961b..5edd24b4 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -20,6 +20,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.3.4 + - name: Setup PHP and Xdebug for Code Coverage report + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + coverage: xdebug - name: Validate composer file run: | composer validate @@ -28,7 +33,7 @@ jobs: composer install - name: Run tests run : | - vendor/bin/phpunit --exclude-group functional + vendor/bin/phpunit --coverage-text run-static-analysis: runs-on: ubuntu-latest steps: @@ -39,6 +44,6 @@ jobs: - name: Composer install run: | composer install - - name: Run static analysis + - name: Run static analysis run: | vendor/bin/phpstan analyse --memory-limit=500M --error-format=github diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..12bc0561 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,188 @@ +# Microsoft Graph PHP SDK Upgrade Guide + +This guide highlights backward compatibility breaking changes introduced during major upgrades. + + +## 1.x to 2.0 + +Version `2.0` highlights: +- [Support for National Clouds.](#support-for-national-clouds) +- Changes in [creating a Graph client.](#creating-a-graph-client) +- Changes in [configuring your HTTP client](#configuring-http-clients-for-use-with-the-graph-api) (including support for PSR-18 and HTTPlug's HttpAsyncClient implementations). +- Introducing standardised Graph exception types [`GraphClientException`](#introducing-the-graphclientexception) and [`GraphServiceException`](#introducing-the-graphserviceexception) as more specific `GraphException` types. +- [`GraphRequest`](#graphrequest-changes) and [`GraphCollectionRequest`](#graphcollectionrequest-changes): + - PSR compliance & other standardisation efforts in request classes and methods. + - Throwing `Psr\Http\Client\ClientExceptionInterface` instead of `\GuzzleHttp\Exception\GuzzleException` in request methods. + - Accepting and returning `Psr\Http\Message\StreamInterface` request bodies instead of `GuzzleHttp\Psr7\Stream`. + - Allow overwriting the default Guzzle client with `Psr\Http\Client\ClientExceptionInterface` for synchronous requests. + - Allow overwriting the default Guzzle client with HTTPlug's `Http\Client\HttpAsyncClient` for asynchronous requests. +- [Introduces a resumable `PageIterator`](#introducing-the-pageiterator) that asynchronously pages through a collection response payload while running a custom callback function against each entity. +- Deprecates support for Guzzle `^6.0`. +- Strongly typed method parameters and return type declarations where possible. + +### Support for National Clouds +We have introduced `NationalCloud` containing Microsoft Graph API endpoint constants to enable you to easily +set base URLs and in future authenticate against the various supported National Clouds. + + +### Creating a Graph client +- Version 2 deprecates setting HTTP-specific config via methods e.g. `setProxyPort()`. +- Deprecates `setBaseUrl()` and `setApiVersion()` in favour of passing these into the constructor. + The public cloud endpoint, `https://graph.microsoft.com`, will be set as the default base URL. +- By default, the SDK will create a Guzzle HTTP client using our default config. +The HTTP client can be customised as shown in the next section. + + +```php +$graphClient = new Graph(); // uses https://graph.microsoft.com as base URL and a Guzzle client as defaults +$response = $graphClient->setAccessToken("abc") + ->setReturnType(Model\User::class) + ->createRequest("GET", "/me") + ->execute(); +``` + + +### Configuring HTTP clients for use with the Graph API +We now support use of any HTTP client library that implements PSR-18 and HTTPlug's HttpAsyncClient interfaces. +In addition, we provide a `HttpClientInterface` that you can implement with your HTTP client library of choice to support both sync and async calls. + +```php +$graphClient = new Graph(NationalCloud::GLOBAL); // creates & uses a default Guzzle client under the hood +``` + +#### 1. Custom configure a Guzzle client using the `HttpClientFactory` +------------------------------------------------------------------------ + To configure a Guzzle client to use with the SDK + ```php + $config = []; // your desired Guzzle client config + $httpClient = HttpClientFactory::setClientConfig($config)::createAdapter(); + $graphClient = new Graph(NationalCloud::GLOBAL, $httpClient); + ``` + + If you'd like to use the raw Guzzle client directly + ```php + $config = [ + // custom request options + ]; + $guzzleClient = HttpClientFactory::setClientConfig($config)::create(); + $response = $guzzleClient->get("/users/me"); + ``` + + We would have loved to allow you to pass your guzzle client directly to the HttpClientFactory, however we would not be able to attach our recommended configs since Guzzle's `getConfig()` method is set to be deprecated in Guzzle 8. + + +#### 2. Configure any other HTTP client +---------------------------------------- +Implement the `Microsoft\Graph\Http\HttpClientInterface` and pass your implementation to the `Graph` constructor. + +#### 3. Overwrite the HTTP client while making synchronous requests +-------------------------------------------------------------------- +The SDK supports use of any PSR-18 compliant client for synchronous requests. + +```php +$customPsr18Client = new Psr18Client(); +$graphClient = new Graph(); +$response = $graphClient->setAccessToken("abc) + ->createRequest("GET", "/user/id") + ->execute($customPsr18Client); // overwrites the default Guzzle client created by Graph() +``` + +#### 4. Overwrite the HTTP client while making asynchronous requests +---------------------------------------------------------------------- +The SDK supports using any HTTPlug HttpAsyncClient implementation for asynchronous requests +```php +$customClient = new HttpAsyncClientImpl(); +$graphClient = new Graph(); +$response = $graphClient->setAccessToken("abc") + ->createRequest("GET", "/user/id") + ->executeAsync($customClient); // overwrite the default Guzzle client created by Graph() +``` + + +### Introducing the `GraphClientException` +This will be the exception type thrown going forward with regard to Graph client and GraphRequest configuration issues. + + +### Introducing the `GraphServiceException` +This is the new standard exception to be thrown for `4xx` and `5xx` responses from the Graph. The exception contains the error payload returned by the Graph API via `getError()`. + +### `GraphRequest` changes + +#### 1. Deprecated functionality +--------------------------------- +- Deprecates Guzzle-specific config and methods. We recommend using `HttpClientFactory` to configure your client: + - Deprecated `setHttpErrors()`, `setTimeout()`. + - Deprecated `proxyPort` and `proxyVerifySSL`. +- `$headers` and `$requestBody` are no longer `protected` attributes. Now `private`. +- Deprecates some getters: `getBaseUrl()`, `getApiVersion()`, `getReturnsStream()`. + + +#### 2. Setting return type +---------------------------- +- `setReturnType()` throws a `GraphClientException` if the return class passed is invalid. +- Deprecates setting return type to `GuzzleHttp\Psr7\Stream` in favour of `Psr\Http\Message\StreamInterface` to get a stream returned. This is because of our efforts +to make the SDK PSR compliant. + + +#### 3. Headers +---------------- +- `getHeaders()` now returns `array`. +- `addHeaders()` also supports passing `array`. +- `addHeaders()` throws a `GraphClientException` if you attempt to overwrite the SDK Version header. +- Extra layer of security by preventing sending your authorization tokens to non-Graph endpoints. + +#### 4. Request Body +---------------------- +- Supports passing any PSR-7 `StreamInterface` implementation to `attachBody()`. + + +#### 5. Making Requests +------------------------- +- `execute()`, `download()` and `upload()` all accept any PSR-18 `ClientInterface` implementation to overwrite the SDK's default Guzzle client. +- `execute()` now throws PSR-18 `ClientExceptionInterface` as opposed to `\GuzzleHttp\Exception\GuzzleException`. +- `executeAsync()` now returns a HTTPlug `Http\Promise\Promise` instead of the previous Guzzle `GuzzleHttp\Promise\PromiseInterface`. +- `executeAsync()` fails with a PSR-18 `ClientExceptionInterface` for HTTP client issues or `GraphServiceException` for 4xx/5xx response. +- `download()` throws a `Psr\Http\Client\ClientExceptionInterface` as opposed to the previous `GuzzleHttp\Exception\GuzzleException`. +- `download()` and `upload()` now throw a `GraphClientException` if the SDK is unable to open the file path given and read/write to it. + + +#### 6. Handling responses +---------------------------- +- For `4xx` and `5xx` responses, the SDK will throw a `GraphServiceException` which contains the error payload via `getError()`. +- The status code is now an `int` from the previous `string` i.e. `getStatus()`. + +### `GraphCollectionRequest` changes +- Executing `count()` requests now throws PSR-18 `ClientExceptionInterface` in case of any HTTP related issues & a `GraphClientException` if the `@odata.count` does not exist in the payload. +- `setPageSize()` throws a `GraphClientException` from the previous `GraphException`. +- `getPage()` throws `Psr\Http\Client\ClientExceptionInterface` from previous `GuzzleHttp\Exception\GuzzleException`. +- `getPage()` has been aligned to `execute()` to return a `GraphResponse` object if no return type is specified from previous JSON-decoded payload array. + You can call `getBody()` on the `GraphResponse` returned to get the JSON-decoded array. If a return type is specified, `getPage()` + still returns the deserialized response body. +- makes `setPageCallInfo()` and `processPageCallReturn()` `private` as these methods provide low level implementation detail. +- See `GraphRequest` changes above as well. + +### Introducing the `PageIterator` +- The `PageIterator` allows you to now asynchronously process each entity in a paged collection response without having to fetch each page synchronously via `getPage()`. +- A callback function that processes each entity is required. Note that the type of the object passed to your callback is matches the return type set on the request. + If no return type is specified, a JSON-decoded entity array will be passed to your callback. +- You control when to pause processing and resume from the last entity processed using the callback's return value. If the callback returns `true`, iteration continues. If it returns `false` the iterator pauses. +- Calling `resume()` continues iteration from the next entity in the collection. +- The `PageIterator` returns a promise which resolves to `true` and throws exceptions should any be encountered. +- Should your access token expire during iteration you can `setAccessToken()` then `resume()`. + +```php +$callback = function (\Microsoft\Graph\Model\Message $message) { + // your logic +}; +$iterator = $graphClient->createCollectionRequest("GET", "/me/messages") + ->setReturnType(\Microsoft\Graph\Model\Message::class) + ->pageIterator($callback); +$promise = $iterator->iterate(); + +# Resuming iteration +$promise = $iterator->resume(); + +# Setting a new access token in case it expires during iteration +$promise = $iterator->setAccessToken("abc")->resume(); +``` + diff --git a/composer.json b/composer.json index 677c45e9..c25f4f27 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^8.0 || ^9.0", + "phpunit/phpunit": "^9.0", "mikey179/vfsstream": "^1.2", "phpstan/phpstan": "^0.12.90" }, @@ -31,8 +31,7 @@ }, "autoload-dev": { "psr-4": { - "Microsoft\\Graph\\Test\\": "tests/", - "Microsoft\\Graph\\Http\\Test\\": "tests/Http/" + "Microsoft\\Graph\\Test\\": "tests/" } } -} \ No newline at end of file +} diff --git a/phpunit.xml b/phpunit.xml index 6ade4e06..ad90053a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,10 +5,13 @@ tests - - + + src - src/Model - - - \ No newline at end of file + + + + + + diff --git a/src/Core/GraphConstants.php b/src/Core/GraphConstants.php index 39a79cdb..be83a353 100644 --- a/src/Core/GraphConstants.php +++ b/src/Core/GraphConstants.php @@ -18,6 +18,9 @@ final class GraphConstants { + const BETA_API_VERSION = "beta"; + const V1_API_VERSION = "v1.0"; + // These can be overwritten in setters in the Graph object const REST_ENDPOINT = "https://graph.microsoft.com/"; diff --git a/src/Core/NationalCloud.php b/src/Core/NationalCloud.php index e863d5da..4a3b52e4 100644 --- a/src/Core/NationalCloud.php +++ b/src/Core/NationalCloud.php @@ -39,13 +39,23 @@ final class NationalCloud * @return bool */ public static function containsNationalCloudHost(string $url): bool { - self::initHosts(); $validUrlParts = parse_url($url); - return $validUrlParts - && array_key_exists("scheme", $validUrlParts) - && $validUrlParts["scheme"] == "https" - && array_key_exists("host", $validUrlParts) - && array_key_exists(strtolower($validUrlParts["host"]), self::$hosts); + return self::containsNationalCloudHostFromUrlParts($validUrlParts); + } + + /** + * Checks if $urlParts contain a valid National Cloud host + * + * @param array|false $urlParts return value of parse_url() + * @return bool + */ + public static function containsNationalCloudHostFromUrlParts($urlParts): bool { + self::initHosts(); + return $urlParts + && array_key_exists("scheme", $urlParts) + && $urlParts["scheme"] == "https" + && array_key_exists("host", $urlParts) + && array_key_exists(strtolower($urlParts["host"]), self::$hosts); } /** diff --git a/src/Exception/GraphClientException.php b/src/Exception/GraphClientException.php index d944d391..02b09b7c 100644 --- a/src/Exception/GraphClientException.php +++ b/src/Exception/GraphClientException.php @@ -8,8 +8,7 @@ namespace Microsoft\Graph\Exception; /** - * Class ClientInitialisationException - * + * Class GraphClientException * @package Microsoft\Graph\Exception * @copyright 2021 Microsoft Corporation * @license https://opensource.org/licenses/MIT MIT License diff --git a/src/Http/AbstractGraphClient.php b/src/Http/AbstractGraphClient.php index 20ab94a9..59a34d03 100644 --- a/src/Http/AbstractGraphClient.php +++ b/src/Http/AbstractGraphClient.php @@ -7,10 +7,8 @@ namespace Microsoft\Graph\Http; -use Microsoft\Graph\Core\GraphConstants; use Microsoft\Graph\Core\NationalCloud; use Microsoft\Graph\Exception\GraphClientException; -use Microsoft\Graph\Exception\GraphException; /** * Class BaseClient @@ -58,7 +56,7 @@ public function __construct(?string $nationalCloud = NationalCloud::GLOBAL, ?HttpClientInterface $httpClient = null) { $this->nationalCloud = ($nationalCloud) ?: NationalCloud::GLOBAL; - $this->httpClient = ($httpClient) ?: HttpClientFactory::nationalCloud($nationalCloud)::createAdapter(); + $this->httpClient = ($httpClient) ?: HttpClientFactory::setNationalCloud($this->nationalCloud)::createAdapter(); } /** @@ -79,7 +77,7 @@ public function setAccessToken(string $accessToken): self /** * @return string */ - public function getAccessToken(): string { + public function getAccessToken(): ?string { return $this->accessToken; } @@ -107,7 +105,7 @@ public function getHttpClient(): HttpClientInterface * * @return GraphRequest The request object, which can be used to * make queries against Graph - * @throws GraphException + * @throws GraphClientException */ public function createRequest(string $requestType, string $endpoint): GraphRequest { @@ -127,9 +125,9 @@ public function createRequest(string $requestType, string $endpoint): GraphReque * * @return GraphCollectionRequest The request object, which can be * used to make queries against Graph - * @throws GraphException + * @throws GraphClientException */ - public function createCollectionRequest(string $requestType, string $endpoint): GraphRequest + public function createCollectionRequest(string $requestType, string $endpoint): GraphCollectionRequest { return new GraphCollectionRequest( $requestType, diff --git a/src/Http/GraphCollectionRequest.php b/src/Http/GraphCollectionRequest.php index 87d6dbb3..687aa141 100644 --- a/src/Http/GraphCollectionRequest.php +++ b/src/Http/GraphCollectionRequest.php @@ -8,9 +8,10 @@ namespace Microsoft\Graph\Http; use GuzzleHttp\Psr7\Uri; +use Microsoft\Graph\Core\GraphConstants; use Microsoft\Graph\Exception\GraphClientException; use Microsoft\Graph\Exception\GraphException; -use Microsoft\Graph\Core\GraphConstants; +use Psr\Http\Client\ClientExceptionInterface; /** * Class GraphCollectionRequest @@ -48,7 +49,7 @@ class GraphCollectionRequest extends GraphRequest /** * The return type that the user specified * - * @var object + * @var string */ protected $originalReturnType; @@ -76,22 +77,25 @@ public function __construct(string $requestType, string $endpoint, AbstractGraph * Gets the number of entries in the collection * * @return int the number of entries - * @throws \Psr\Http\Client\ClientExceptionInterface + * @throws ClientExceptionInterface|GraphClientException */ public function count() { $query = '$count=true'; $requestUri = $this->getRequestUri(); $this->setRequestUri(new Uri( $requestUri . GraphRequestUtil::getQueryParamConcatenator($requestUri) . $query)); - $result = $this->execute()->getBody(); + // Temporarily disable returnType in order to get GraphResponse object returned by execute() + $this->originalReturnType = $this->returnType; + $this->returnType = null; + $result = $this->execute(); - if (array_key_exists("@odata.count", $result)) { - return $result['@odata.count']; + if (is_a($result, GraphResponse::class) && $result->getCount()) { + return $result->getCount(); } /* The $count query parameter for the Graph API is available on several models but not all */ - trigger_error('Count unavailable for this collection'); + throw new GraphClientException('Count unavailable for this collection'); } /** @@ -117,7 +121,7 @@ public function setPageSize(int $pageSize): self * Gets the next page of results * * @return array of objects of class $returnType - * @throws \Psr\Http\Client\ClientExceptionInterface + * @throws ClientExceptionInterface */ public function getPage() { @@ -132,7 +136,7 @@ public function getPage() * * @return GraphCollectionRequest */ - public function setPageCallInfo(): self + private function setPageCallInfo(): self { // Store these to add temporary query data to request $this->originalReturnType = $this->returnType; @@ -167,7 +171,7 @@ public function setPageCallInfo(): self * @return mixed result of the call, formatted according * to the returnType set by the user */ - public function processPageCallReturn(GraphResponse $response) + private function processPageCallReturn(GraphResponse $response) { $this->nextLink = $response->getNextLink(); $this->deltaLink = $response->getDeltaLink(); @@ -211,4 +215,12 @@ public function getDeltaLink(): ?string { return $this->deltaLink; } + + /** + * Get page size + * @return int + */ + public function getPageSize(): int { + return $this->pageSize; + } } diff --git a/src/Http/GraphRequest.php b/src/Http/GraphRequest.php index 2da531a9..9dd81026 100644 --- a/src/Http/GraphRequest.php +++ b/src/Http/GraphRequest.php @@ -15,10 +15,10 @@ use Microsoft\Graph\Core\GraphConstants; use Microsoft\Graph\Core\NationalCloud; use Microsoft\Graph\Exception\GraphClientException; -use Microsoft\Graph\Exception\GraphException; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; /** * Class GraphRequest @@ -32,13 +32,13 @@ class GraphRequest /** * An array of headers to send with the request * - * @var array(string => string) + * @var array */ private $headers; /** * The body of the request (optional) * - * @var string + * @var StreamInterface|string */ private $requestBody = null; /** @@ -57,7 +57,7 @@ class GraphRequest /** * The object type to cast the response to * - * @var string + * @var string|null */ protected $returnType; /** @@ -75,7 +75,7 @@ class GraphRequest /** * Full Request URI (base URL + endpoint) * - * @var Uri + * @var UriInterface */ private $requestUri; @@ -106,30 +106,15 @@ public function __construct(string $requestType, string $endpoint, AbstractGraph $this->initPsr7HttpRequest(); } - public function getHttpRequest(): Request - { - return $this->httpRequest; - } - - protected function setRequestUri(Uri $uri): void { + protected function setRequestUri(UriInterface $uri): void { $this->requestUri = $uri; - $this->initPsr7HttpRequest(); + $this->httpRequest = $this->httpRequest->withUri($uri); } - protected function getRequestUri(): Uri { + public function getRequestUri(): UriInterface { return $this->requestUri; } - /** - * Gets whether request returns a stream or not - * - * @return boolean - */ - public function getReturnsStream() - { - return $this->returnsStream; - } - /** * Sets a new accessToken * @@ -139,37 +124,44 @@ public function getReturnsStream() */ public function setAccessToken(string $accessToken): self { + unset($this->headers['Authorization']); // Prevents appending new token $this->addHeaders(['Authorization' => 'Bearer '.$accessToken]); return $this; } /** - * Sets the return type of the response object - * - * @param string $returnClass The class name to use - * - * @return $this object - */ + * Sets the return type of the response object + * Can be set to a model or \Psr\Http\Message\StreamInterface + * + * @param string $returnClass The class name to use + * + * @return $this object + * @throws GraphClientException when $returnClass is not an existing class + */ public function setReturnType(string $returnClass): self { - $this->returnType = $returnClass; - if ($this->returnType == "GuzzleHttp\Psr7\Stream") { - $this->returnsStream = true; - } else { - $this->returnsStream = false; + if (!class_exists($returnClass) && !interface_exists($returnClass)) { + throw new GraphClientException("Return type specified does not match an existing class definition"); } + $this->returnType = $returnClass; + $this->returnsStream = ($returnClass === StreamInterface::class); return $this; } /** - * Adds custom headers to the request - * - * @param array $headers An array of custom headers - * - * @return GraphRequest object - */ + * Adds custom headers to the request + * + * @param array $headers An array of custom headers + * + * @return GraphRequest object + * @throws GraphClientException if attempting to overwrite SdkVersion header + */ public function addHeaders(array $headers): self { + if (array_key_exists("SdkVersion", $headers)) { + throw new GraphClientException("Cannot overwrite SdkVersion header"); + } + // Recursive merge to support appending values to multi-value headers $this->headers = array_merge_recursive($this->headers, $headers); $this->initPsr7HttpRequest(); return $this; @@ -178,30 +170,30 @@ public function addHeaders(array $headers): self /** * Get the request headers * - * @return array of headers + * @return array of headers */ public function getHeaders(): array { - return $this->headers; + return $this->httpRequest->getHeaders(); } /** * Attach a body to the request. Will JSON encode * any Microsoft\Graph\Model objects as well as arrays * - * @param string|StreamInterface|object $obj The object to include in the request + * @param string|StreamInterface|object|array $body The payload to include in the request * * @return $this object */ - public function attachBody($obj): self + public function attachBody($body): self { // Attach streams & JSON automatically - if (is_string($obj) || is_a($obj, StreamInterface::class)) { - $this->requestBody = $obj; + if (is_string($body) || is_a($body, StreamInterface::class)) { + $this->requestBody = $body; } // By default, JSON-encode else { - $this->requestBody = json_encode($obj); + $this->requestBody = json_encode($body); } $this->initPsr7HttpRequest(); return $this; @@ -313,7 +305,7 @@ public function download(string $path, ?ClientInterface $client = null): void $resource = Utils::tryFopen($path, 'w'); $stream = Utils::streamFor($resource); $response = $client->sendRequest($this->httpRequest); - $stream->write($response->getBody()); + $stream->write($response->getBody()->getContents()); $stream->close(); } catch (\RuntimeException $ex) { throw new GraphClientException(GraphConstants::INVALID_FILE, $ex->getCode(), $ex); @@ -349,8 +341,12 @@ public function upload(string $path, ?ClientInterface $client = null) private function initHeaders(string $baseUrl): void { $coreSdkVersion = "graph-php-core/".GraphConstants::SDK_VERSION; - $serviceLibSdkVersion = "Graph-php-".$this->graphClient->getSdkVersion(); - if (NationalCloud::containsNationalCloudHost($baseUrl)) { + if ($this->graphClient->getApiVersion() === GraphConstants::BETA_API_VERSION) { + $serviceLibSdkVersion = "graph-php-beta/".$this->graphClient->getSdkVersion(); + } else { + $serviceLibSdkVersion = "graph-php/".$this->graphClient->getSdkVersion(); + } + if (NationalCloud::containsNationalCloudHost($this->requestUri)) { $this->headers = [ 'Content-Type' => 'application/json', 'SdkVersion' => $coreSdkVersion.", ".$serviceLibSdkVersion, @@ -367,18 +363,14 @@ private function initHeaders(string $baseUrl): void * Creates full request URI by resolving $baseUrl and $endpoint based on RFC 3986 * * @param string $baseUrl - * @param $endpoint + * @param string $endpoint * @throws GraphClientException */ - protected function initRequestUri(string $baseUrl, $endpoint): void { + protected function initRequestUri(string $baseUrl, string $endpoint): void { try { $this->requestUri = GraphRequestUtil::getRequestUri($baseUrl, $endpoint, $this->graphClient->getApiVersion()); - if (!$this->requestUri) { - // $endpoint is a full URL but doesn't meet criteria - throw new GraphClientException("Endpoint is not a valid URL. Must contain national cloud host."); - } } catch (\InvalidArgumentException $ex) { - throw new GraphClientException("Unable to resolve base URL=".$baseUrl."\" with endpoint=".$endpoint."\"", 0, $ex); + throw new GraphClientException($ex->getMessage(), 0, $ex); } } diff --git a/src/Http/GraphRequestUtil.php b/src/Http/GraphRequestUtil.php index 94dd15e3..fd230005 100644 --- a/src/Http/GraphRequestUtil.php +++ b/src/Http/GraphRequestUtil.php @@ -30,16 +30,36 @@ class GraphRequestUtil * @param string $baseUrl if empty, is overwritten with $client's national cloud * @param string $endpoint can be a full URL * @param string $apiVersion - * @return UriInterface|null + * @return UriInterface + * @throws \InvalidArgumentException */ - public static function getRequestUri(string $baseUrl, string $endpoint, string $apiVersion = "v1.0"): ?UriInterface { - // If endpoint is a full url, ensure the host is a national cloud or custom host + public static function getRequestUri(string $baseUrl, string $endpoint, string $apiVersion = "v1.0"): UriInterface { + // If endpoint is a full url, ensure the host is a national cloud if (parse_url($endpoint, PHP_URL_SCHEME)) { - return (NationalCloud::containsNationalCloudHost($endpoint)) ? new Uri($endpoint) : null; + return new Uri($endpoint); } - $relativeUrl = (NationalCloud::containsNationalCloudHost($baseUrl)) ? "/".$apiVersion : ""; - $relativeUrl .= (substr($endpoint, 0, 1) == "/") ? $endpoint : "/".$endpoint; - return UriResolver::resolve(new Uri($baseUrl), new Uri($relativeUrl)); + if ($baseUrl) { + $baseUrlParts = parse_url($baseUrl); + if (!self::isValidBaseUrl($baseUrlParts)) { + throw new \InvalidArgumentException("Invalid baseUrl=".$baseUrl.". Ensure URL has scheme and host"); + } + $relativeUrl = (NationalCloud::containsNationalCloudHostFromUrlParts($baseUrlParts)) ? "/".$apiVersion : ""; + $relativeUrl .= (substr($endpoint, 0, 1) === "/") ? $endpoint : "/".$endpoint; + return UriResolver::resolve(new Uri($baseUrl), new Uri($relativeUrl)); + } + throw new \InvalidArgumentException("Unable to create uri with empty baseUrl and endpoint=".$endpoint); + } + + /** + * Check whether $urlParts meet criteria for a valid base url + * + * @param array|false $urlParts return value of parse_url() + * @return bool + */ + public static function isValidBaseUrl($urlParts): bool { + return $urlParts + && array_key_exists("scheme", $urlParts) + && array_key_exists("host", $urlParts); } /** diff --git a/src/Http/GraphResponse.php b/src/Http/GraphResponse.php index 42b371f8..ebf8134b 100644 --- a/src/Http/GraphResponse.php +++ b/src/Http/GraphResponse.php @@ -17,8 +17,8 @@ namespace Microsoft\Graph\Http; -use Microsoft\Graph\Exception\GraphException; -use Microsoft\Graph\Core\GraphConstants; +use GuzzleHttp\Psr7\Utils; +use Psr\Http\Message\StreamInterface; /** * Class GraphResponse @@ -40,7 +40,7 @@ class GraphResponse /** * The body of the response * - * @var string + * @var StreamInterface */ private $_body; /** @@ -59,19 +59,19 @@ class GraphResponse /** * The status code of the response * - * @var string + * @var int */ private $_httpStatusCode; /** * Creates a new Graph HTTP response entity * - * @param object $request The request - * @param string $body The body of the response - * @param string $httpStatusCode The returned status code + * @param GraphRequest $request The request + * @param ?StreamInterface $body The body of the response + * @param int $httpStatusCode The returned status code * @param array $headers The returned headers */ - public function __construct($request, $body = null, $httpStatusCode = null, $headers = null) + public function __construct(GraphRequest $request, ?StreamInterface $body = null, int $httpStatusCode = 0, array $headers = []) { $this->_request = $request; $this->_body = $body; @@ -88,6 +88,9 @@ public function __construct($request, $body = null, $httpStatusCode = null, $hea private function _decodeBody() { $decodedBody = json_decode($this->_body, true); + if ($this->_body) { + $this->_body->rewind(); //rewind stream so that it can be read again + } if ($decodedBody === null) { $decodedBody = array(); } @@ -111,15 +114,15 @@ public function getBody() */ public function getRawBody() { - return $this->_body; + return ($this->_body) ?: $this->_body->getContents(); } /** * Get the status of the HTTP response * - * @return string|null The HTTP status + * @return int The HTTP status */ - public function getStatus() + public function getStatus(): int { return $this->_httpStatusCode; } @@ -138,7 +141,7 @@ public function getHeaders() * Converts the response JSON object to a Graph SDK object * * @param mixed $returnType The type to convert the object(s) to - * +// * * @return mixed object or array of objects of type $returnType */ public function getResponseAsObject($returnType) @@ -169,11 +172,10 @@ public function getResponseAsObject($returnType) * * @return string|null nextLink, if provided */ - public function getNextLink() + public function getNextLink(): ?string { if (array_key_exists("@odata.nextLink", $this->getBody())) { - $nextLink = $this->getBody()['@odata.nextLink']; - return $nextLink; + return $this->getBody()['@odata.nextLink']; } return null; } @@ -185,11 +187,23 @@ public function getNextLink() * * @return string|null deltaLink */ - public function getDeltaLink() + public function getDeltaLink(): ?string { if (array_key_exists("@odata.deltaLink", $this->getBody())) { - $deltaLink = $this->getBody()['@odata.deltaLink']; - return $deltaLink; + return $this->getBody()['@odata.deltaLink']; + } + return null; + } + + /** + * Gets the number of items in the response payload + * + * @return int|null + */ + public function getCount(): ?int + { + if (array_key_exists("@odata.count", $this->getBody())) { + return $this->getBody()["@odata.count"]; } return null; } diff --git a/src/Http/HttpClientFactory.php b/src/Http/HttpClientFactory.php index 4a5a503f..37bbd81e 100644 --- a/src/Http/HttpClientFactory.php +++ b/src/Http/HttpClientFactory.php @@ -55,9 +55,15 @@ final class HttpClientFactory /** * HttpClientFactory constructor. - * Creates only one instance to be reused in the static methods */ - private function __construct() { + private function __construct() {} + + /** + * Returns singleton instance + * + * @return HttpClientFactory + */ + private static function getInstance(): HttpClientFactory { if (!self::$instance) { self::$instance = new HttpClientFactory(); } @@ -71,12 +77,12 @@ private function __construct() { * @return $this * @throws GraphClientException if $nationalCloud is empty or an invalid national cloud Host */ - public static function nationalCloud(string $nationalCloud = NationalCloud::GLOBAL): HttpClientFactory { + public static function setNationalCloud(string $nationalCloud = NationalCloud::GLOBAL): HttpClientFactory { if (!$nationalCloud || !NationalCloud::containsNationalCloudHost($nationalCloud)) { throw new GraphClientException("Invalid national cloud passed. See https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints."); } self::$nationalCloud = $nationalCloud; - return new HttpClientFactory(); + return self::getInstance(); } /** @@ -85,9 +91,9 @@ public static function nationalCloud(string $nationalCloud = NationalCloud::GLOB * @param array $config * @return $this */ - public static function clientConfig(array $config): HttpClientFactory { + public static function setClientConfig(array $config): HttpClientFactory { self::$clientConfig = $config; - return new HttpClientFactory(); + return self::getInstance(); } /** diff --git a/tests/Core/NationalCloudTest.php b/tests/Core/NationalCloudTest.php index ff3cae8d..fba25ebb 100644 --- a/tests/Core/NationalCloudTest.php +++ b/tests/Core/NationalCloudTest.php @@ -1,29 +1,29 @@ getConstants()); foreach ($nationalClouds as $nationalCloud) { $this->assertTrue(NationalCloud::containsNationalCloudHost($nationalCloud)); } } - function testNationalCloudWithPortIsValid() { + function testContainsNationalCloudHostWithPortInUrl() { $this->assertTrue(NationalCloud::containsNationalCloudHost(NationalCloud::GLOBAL.":1234")); } - function testNationalCloudWithTrailingForwardSlashIsValid() { + function testContainsNationalCloudHostWithTrailingForwardSlash() { $this->assertTrue(NationalCloud::containsNationalCloudHost(NationalCloud::GLOBAL."/")); } - function testNationalCloudWithPathIsValid() { + function testContainsNationalCloudHostWithPathInUrl() { $this->assertTrue(NationalCloud::containsNationalCloudHost(NationalCloud::GLOBAL."/v1.0/")); } @@ -32,11 +32,11 @@ function testContainsNationalCloudWithCapitalisedHost() { self::assertTrue(NationalCloud::containsNationalCloudHost($url)); } - function testEmptyNationalCloudUrlInvalid() { + function testContainsNationalCloudHostWithEmptyUrl() { $this->assertFalse(NationalCloud::containsNationalCloudHost("")); } - function testNullNationalCloudThrowsError() { + function testContainsNationalCloudHostWithNullUrlThrowsError() { $this->expectException(\TypeError::class); NationalCloud::containsNationalCloudHost(null); } @@ -45,11 +45,11 @@ function testInvalidNationalCloud() { $this->assertFalse(NationalCloud::containsNationalCloudHost("https://www.microsoft.com")); } - function testNationalCloudWithoutSchemeInvalid() { + function testContainsNationalCloudHostWithoutSchemeInvalid() { $this->assertFalse(NationalCloud::containsNationalCloudHost("graph.microsoft.com")); } - function testMalformedNationalCloudInvalid() { + function testContainsNationalCloudHostWithMalformedUrlInvalid() { $this->assertFalse(NationalCloud::containsNationalCloudHost("https:///")); } } diff --git a/tests/Exception/ExceptionTest.php b/tests/Exception/GraphExceptionTest.php similarity index 87% rename from tests/Exception/ExceptionTest.php rename to tests/Exception/GraphExceptionTest.php index 8086b42e..2d333650 100644 --- a/tests/Exception/ExceptionTest.php +++ b/tests/Exception/GraphExceptionTest.php @@ -1,9 +1,12 @@ assertNotNull($graph); - } - - public function testInitializeEmptyGraph() - { - $this->expectException(Microsoft\Graph\Exception\GraphException::class); - $graph = new Graph(); - $request = $graph->createRequest("GET", "/me"); - } - - public function testInitializeGraphWithToken() - { - $graph = new Graph(); - $graph->setAccessToken('abc'); - $request = $graph->createRequest("GET", "/me"); - - $this->assertInstanceOf(GraphRequest::class, $request); - } - - public function testCreateCollectionRequest() - { - $graph = new Graph(); - $graph->setAccessToken('abc'); - $request = $graph->createCollectionRequest("GET", "/me"); - - $this->assertInstanceOf(GraphRequest::class, $request); - } - - public function testRequestWithCustomEndpoint() - { - $graph = new Graph(); - $graph->setAccessToken('abc'); - $graph->setBaseUrl('url2'); - - $request = $graph->createRequest("GET", "/me"); - $this->assertEquals('url2', $request->getNationalCloud()); - } - - public function testBetaRequest() - { - $graph = new Graph(); - $graph->setAccessToken('abc') - ->setApiVersion('beta'); - $request = $graph->createRequest("GET", "/me"); - - $this->assertEquals('beta', $request->getApiVersion()); - } - - public function testMultipleGraphObjects() - { - $graph = new Graph(); - $graph2 = new Graph(); - - $graph->setAccessToken('abc'); - $graph2->setAccessToken('abc'); - $graph2->setApiVersion('beta'); - - $request = $graph->createRequest("GET", "/me"); - $request2 = $graph2->createRequest("GET", "/me"); - - $this->assertEquals(GraphConstants::API_VERSION, $request->getApiVersion()); - $this->assertEquals('beta', $request2->getApiVersion()); - } -} diff --git a/tests/Http/AbstractGraphClientTest.php b/tests/Http/AbstractGraphClientTest.php new file mode 100644 index 00000000..e7d7f0ae --- /dev/null +++ b/tests/Http/AbstractGraphClientTest.php @@ -0,0 +1,88 @@ +defaultGraphClient = $this->getMockForAbstractClass(AbstractGraphClient::class); + } + + public function testGraphConstructorWithDefaultParams() { + $this->assertEquals(NationalCloud::GLOBAL, $this->defaultGraphClient->getNationalCloud()); + $this->assertInstanceOf(HttpClientInterface::class, $this->defaultGraphClient->getHttpClient()); + } + + public function testGraphConstructor() { + $httpClient = HttpClientFactory::createAdapter(); + $graphClient = $this->getMockBuilder(AbstractGraphClient::class) + ->setConstructorArgs([NationalCloud::US_DOD, $httpClient]) + ->getMockForAbstractClass(); + $this->assertInstanceOf(AbstractGraphClient::class, $graphClient); + } + + public function testConstructorWithNullParams() { + $graphClient = $this->getMockBuilder(AbstractGraphClient::class) + ->setConstructorArgs([null, null]) + ->getMockForAbstractClass(); + $this->assertEquals(NationalCloud::GLOBAL, $graphClient->getNationalCloud()); + $this->assertInstanceOf(HttpClientInterface::class, $graphClient->getHttpClient()); + } + + public function testConstructorWithInvalidNationalCloud() { + $this->expectException(GraphClientException::class); + $graphClient = $this->getMockBuilder(AbstractGraphClient::class) + ->setConstructorArgs(["https://www.microsoft.com", null]) + ->getMockForAbstractClass(); + } + + public function testSetAndRetrieveAccessToken() { + $accessToken = "123"; + $result = $this->defaultGraphClient->setAccessToken($accessToken); + $this->assertInstanceOf(AbstractGraphClient::class, $result); + $this->assertEquals($accessToken, $this->defaultGraphClient->getAccessToken()); + } + + public function testCreateRequestReturnsGraphRequest() { + $request = $this->defaultGraphClient->setAccessToken("abc") + ->createRequest("GET", "/me"); + $this->assertInstanceOf(GraphRequest::class, $request); + } + + public function testCreateRequestWithoutSettingAccessTokenThrowsException() { + $this->expectException(GraphClientException::class); + $request = $this->defaultGraphClient->createRequest("GET", "/"); + } + + public function testCreateRequestWithInvalidParamsThrowsException() { + $this->expectException(GraphClientException::class); + $this->defaultGraphClient->createRequest("", ""); + } + + public function testCreateCollectionRequestReturnsGraphCollectionRequest() { + $request = $this->defaultGraphClient->setAccessToken("abc") + ->createCollectionRequest("GET", "/me/users"); + $this->assertInstanceOf(GraphCollectionRequest::class, $request); + } + + public function testCreateCollectionRequestWithoutAccessTokenThrowsException() { + $this->expectException(GraphClientException::class); + $request = $this->defaultGraphClient->createCollectionRequest("GET", "/me/users"); + } + + public function testCreateCollectionRequestWithInvalidParamsThrowsException() { + $this->expectException(GraphClientException::class); + $this->defaultGraphClient->createCollectionRequest("", ""); + } +} diff --git a/tests/Http/GraphCollectionRequestTest.php b/tests/Http/GraphCollectionRequestTest.php deleted file mode 100644 index 93edeaec..00000000 --- a/tests/Http/GraphCollectionRequestTest.php +++ /dev/null @@ -1,84 +0,0 @@ -collectionRequest = new GraphCollectionRequest("GET", "/endpoint", "token", "url", "version"); - $this->collectionRequest->setReturnType(Model\User::class); - $this->collectionRequest->setPageSize(2); - - $body = json_encode(array('body' => 'content', '@odata.nextLink' => 'https://url/version/endpoint?skiptoken=link')); - $body2 = json_encode(array('body' => 'content')); - $mock = new GuzzleHttp\Handler\MockHandler([ - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body2), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body2), - ]); - $handler = GuzzleHttp\HandlerStack::create($mock); - $this->client = new GuzzleHttp\Client(['handler' => $handler]); - - $this->reflectedRequestUrlHandler = new ReflectionMethod('Microsoft\Graph\Http\GraphRequest', '_getRequestUrl'); - $this->reflectedRequestUrlHandler->setAccessible(true); - } - - public function testHitEndOfCollection() - { - $this->expectError(); - - //First page - $this->collectionRequest->setPageCallInfo(); - $response = $this->collectionRequest->execute($this->client); - $this->collectionRequest->processPageCallReturn($response); - - //Last page - $this->collectionRequest->setPageCallInfo(); - $response = $this->collectionRequest->execute($this->client); - $result1 = $this->collectionRequest->processPageCallReturn($response); - - $this->assertTrue($this->collectionRequest->isEnd()); - - //Expect error - $this->collectionRequest->setPageCallInfo(); - } - - public function testProcessPageCallReturn() - { - $this->collectionRequest->setPageCallInfo(); - $response = $this->collectionRequest->execute($this->client); - $result = $this->collectionRequest->processPageCallReturn($response); - $this->assertInstanceOf(Model\User::class, $result); - } - - public function testEndpointManipulationWithoutNextLink() - { - //Page should be 1 - $this->assertFalse($this->collectionRequest->isEnd()); - - $requestUrl = $this->reflectedRequestUrlHandler->invokeArgs($this->collectionRequest, array()); - - $this->assertEquals($requestUrl, 'version/endpoint'); - - $this->collectionRequest->setPageCallInfo(); - - $requestUrl = $this->reflectedRequestUrlHandler->invokeArgs($this->collectionRequest, array()); - $this->assertEquals('version/endpoint?$top=2', $requestUrl); - } - - public function testEndpointManipulationWhenNextLinkExists() - { - $this->collectionRequest->setPageCallInfo(); - $response = $this->collectionRequest->execute($this->client); - $this->collectionRequest->processPageCallReturn($response); - $this->collectionRequest->setPageCallInfo(); - $requestUrl = $this->reflectedRequestUrlHandler->invokeArgs($this->collectionRequest, array()); - $this->assertEquals('version/endpoint?skiptoken=link', $requestUrl); - } -} \ No newline at end of file diff --git a/tests/Http/GraphRequestTest.php b/tests/Http/GraphRequestTest.php deleted file mode 100644 index 6228dbcf..00000000 --- a/tests/Http/GraphRequestTest.php +++ /dev/null @@ -1,242 +0,0 @@ -requests = array( - new GraphRequest("GET", "/endpoint", "token", "baseUrl", "version"), - new GraphRequest("PATCH", "/endpoint?query", "token", "baseUrl", "version"), - new GraphRequest("GET", "/endpoint?query&query2", "token", "baseUrl", "version") - ); - - $this->defaultHeaders = array( - "Host" => "baseUrl", - "Content-Type" => "application/json", - "SdkVersion" => "Graph-php-" . GraphConstants::SDK_VERSION, - "Authorization" => "Bearer token" - ); - - $body = json_encode(array('body' => 'content')); - $mock = new GuzzleHttp\Handler\MockHandler([ - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body), - new GuzzleHttp\Psr7\Response(201, ['foo' => 'bar']), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body) - ]); - $handler = GuzzleHttp\HandlerStack::create($mock); - $this->client = new GuzzleHttp\Client(['handler' => $handler]); - } - - public function testSetReturnType() - { - //Temporarily make getRequestUrl() public - $reflectionMethod = new ReflectionMethod('Microsoft\Graph\Http\GraphRequest', '_getRequestUrl'); - $reflectionMethod->setAccessible(true); - - $graph = new Graph(); - $graph->setApiVersion('beta'); - $graph->setAccessToken('token'); - $request = $graph->createRequest("get", "/me"); - $graph->setApiVersion('v1.0'); - - $requestUrl = $reflectionMethod->invokeArgs($request, array()); - $this->assertEquals($requestUrl, "beta/me"); - - $request2 = $graph->createRequest("get", "/me"); - $requestUrl = $reflectionMethod->invokeArgs($request2, array()); - $this->assertEquals("v1.0/me", $requestUrl); - } - - public function testAddHeaders() - { - $testHeader = array("test" => "header"); - $request = $this->requests[0]->addHeaders($testHeader); - $headers = $request->getHeaders(); - - $expectedHeaders = array( - "Host" => "baseUrl", - "Content-Type" => "application/json", - "SdkVersion" => "Graph-php-" . GraphConstants::SDK_VERSION, - "Authorization" => "Bearer token", - "test" => "header" - ); - - $this->assertEquals($expectedHeaders, $headers); - } - - public function testCustomHeadersOverwriteDefaults() - { - $testHeader = array("Content-Type" => "application/x-www-form-urlencoded"); - $request = $this->requests[0]->addHeaders($testHeader); - $headers = $request->getHeaders(); - - $expectedHeaders = array( - "Host" => "baseUrl", - "Content-Type" => "application/x-www-form-urlencoded", - "SdkVersion" => "Graph-php-" . GraphConstants::SDK_VERSION, - "Authorization" => "Bearer token" - ); - - $this->assertEquals($expectedHeaders, $headers); - } - - public function testDefaultHeaders() - { - $headers = $this->requests[0]->getHeaders(); - - $this->assertEquals($this->defaultHeaders, $headers); - } - - public function testGetBody() - { - $testBody = json_encode(array('body' => 'content')); - $this->requests[0]->attachBody($testBody); - $body = $this->requests[0]->getBody(); - $this->assertEquals($testBody, $body); - } - - public function testAttachPropertyDictionary() - { - $model = new Model\User(array("id" => 1, "manager" => new Model\User(array("id" => 2)))); - $this->requests[0]->attachBody($model); - $body = $this->requests[0]->getBody(); - $this->assertEquals('{"id":1,"manager":{"id":2}}', $body); - } - - public function testAttachDoubleNestedDictionary() - { - $testBody = json_encode(array("data"=> array("key" => array("key2" => "val")))); - $this->requests[0]->attachBody(array("data"=> array("key" => array("key2" => "val")))); - $body = $this->requests[0]->getBody(); - $this->assertEquals($testBody, $body); - } - - public function testSetTimeout() - { - $this->requests[0]->setTimeout('200'); - $this->assertEquals('200', $this->requests[0]->getTimeout()); - } - - public function testDefaultTimeout() - { - $this->assertEquals('100', $this->requests[0]->getTimeout()); - } - - public function testCreateGuzzleClient() - { - $reflectionMethod = new ReflectionMethod('Microsoft\Graph\Http\GraphRequest', 'createGuzzleClient'); - $reflectionMethod->setAccessible(true); - - $request = $this->requests[0]; - $client = $reflectionMethod->invokeArgs($request, array()); - - $this->assertInstanceOf(GuzzleHttp\Client::class, $client); - - } - - public function testExecute() - { - $response = $this->requests[0]->execute($this->client); - - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - } - - public function testExecuteWithTimeout() - { - $response = $this->requests[0]->setTimeout(300)->execute($this->client); - - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - } - - public function testExecuteAsync() - { - $promise = $this->requests[0] - ->executeAsync($this->client); - $this->assertInstanceOf(GuzzleHttp\Promise\PromiseInterface::class, $promise); - - $promise = $this->requests[1] - ->executeAsync($this->client); - $this->assertInstanceOf(GuzzleHttp\Promise\PromiseInterface::class, $promise); - - $promise = $this->requests[0] - ->executeAsync($this->client); - $promise2 = $this->requests[2] - ->executeAsync($this->client); - - $response = \GuzzleHttp\Promise\unwrap(array($promise)); - foreach ($response as $responseItem) { - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $responseItem); - } - } - - public function testGetRequestUrl() - { - //Temporarily make getRequestUrl() public - $reflectionMethod = new ReflectionMethod('Microsoft\Graph\Http\GraphRequest', '_getRequestUrl'); - $reflectionMethod->setAccessible(true); - - $requestUrl = $reflectionMethod->invokeArgs($this->requests[0], array()); - $this->assertEquals($requestUrl, "version/endpoint"); - } - - public function testGetConcatenator() - { - //Temporarily make getConcatenator() public - $reflectionMethod = new ReflectionMethod('Microsoft\Graph\Http\GraphRequest', 'getConcatenator'); - $reflectionMethod->setAccessible(true); - - $concatenator = $reflectionMethod->invokeArgs($this->requests[0], array()); - $this->assertEquals($concatenator, "?"); - - $concatenator = $reflectionMethod->invokeArgs($this->requests[1], array()); - $this->assertEquals($concatenator, "&"); - - $concatenator = $reflectionMethod->invokeArgs($this->requests[2], array()); - $this->assertEquals($concatenator, "&"); - } - - public function testExecuteWith4xxResponse() - { - $this->expectException(GuzzleHttp\Exception\ClientException::class); - $mockResponse = array(new Response(400)); - $client = MockClientFactory::create(['http_errors' => true], $mockResponse); - $this->requests[0]->execute($client); - } - - public function testExecuteWith5xxResponse() - { - $this->expectException(GuzzleHttp\Exception\ServerException::class); - $mockResponse = array(new Response(500)); - $client = MockClientFactory::create(['http_errors' => true], $mockResponse); - $this->requests[0]->execute($client); - } - - public function testExecuteAsyncWithBadResponseTriggersNotice() - { - $this->expectNotice(); - $mockResponse = array(new Response(400)); - $client = MockClientFactory::create(['http_errors' => true], $mockResponse); - $promise = $this->requests[0]->executeAsync($client); - $promise->wait(); - } - - public function testExecuteAsyncWithBadResponseReturnsNull() - { - $mockResponse = array(new Response(400)); - $client = MockClientFactory::create(['http_errors' => true], $mockResponse); - $promise = $this->requests[0]->executeAsync($client); - $result = @$promise->wait(); - $this->assertNull($result); - } -} diff --git a/tests/Http/GraphResponseTest.php b/tests/Http/GraphResponseTest.php index 3ad6d1cd..27ef8eb6 100644 --- a/tests/Http/GraphResponseTest.php +++ b/tests/Http/GraphResponseTest.php @@ -1,152 +1,99 @@ responseBody = array('body' => 'content', 'displayName' => 'Bob Barker'); - - $body = json_encode($this->responseBody); - $multiBody = json_encode(array('value' => array('1' => array('givenName' => 'Bob'), '2' => array('givenName' => 'Drew')))); - $valueBody = json_encode(array('value' => 'Bob Barker')); - $emptyMultiBody = json_encode(array('value' => array())); - - $mock = new GuzzleHttp\Handler\MockHandler([ - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $body), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $multiBody), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $valueBody), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $emptyMultiBody), - ]); - $handler = GuzzleHttp\HandlerStack::create($mock); - $this->client = new GuzzleHttp\Client(['handler' => $handler]); - - $this->request = new GraphRequest("GET", "/endpoint", "token", "baseUrl", "version"); - $this->response = new GraphResponse($this->request, "{response}", "200", ["foo" => "bar"]); - } - - public function testGetResponseAsObject() - { - $this->request->setReturnType(Model\User::class); - $response = $this->request->execute($this->client); - - $this->assertInstanceOf(Model\User::class, $response); - $this->assertEquals($this->responseBody['displayName'], $response->getDisplayName()); + $this->mockGraphRequest = $this->createMock(GraphRequest::class); + $this->defaultBody = SampleGraphResponsePayload::COLLECTION_PAYLOAD; + $this->defaultHeaders = ['foo' => 'bar']; + $this->defaultGraphResponse = new GraphResponse( + $this->mockGraphRequest, + Utils::streamFor(json_encode($this->defaultBody)), + $this->defaultStatusCode, + $this->defaultHeaders + ); } public function testGetResponseHeaders() { - $response = $this->request->execute($this->client); - $headers = $response->getHeaders(); - - $this->assertEquals(["foo" => ["bar"]], $headers); + $this->assertEquals($this->defaultHeaders, $this->defaultGraphResponse->getHeaders()); } public function testGetNextLink() { - $body = json_encode(array('@odata.nextLink' => 'https://url.com/resource?$top=4&skip=4')); - $response = new GraphResponse($this->request, $body); - - $nextLink = $response->getNextLink(); - $this->assertEquals('https://url.com/resource?$top=4&skip=4', $nextLink); + $nextLink = $this->defaultGraphResponse->getNextLink(); + $this->assertEquals($this->defaultBody['@odata.nextLink'], $nextLink); } - public function testDecodeBody() + public function testGetBodyReturnsDecodedBody() { - //Temporarily make decodeBody() public - $reflectionMethod = new ReflectionMethod('Microsoft\Graph\Http\GraphResponse', '_decodeBody'); - $reflectionMethod->setAccessible(true); - - $response = new GraphResponse($this->request, json_encode($this->responseBody)); - $decodedBody = $reflectionMethod->invokeArgs($response, array()); - - $this->assertEquals($this->responseBody, $decodedBody); + $this->assertEquals($this->defaultBody, $this->defaultGraphResponse->getBody()); } - public function testDecodeEmptyBody() + public function testGetBodyWithNullBodyReturnsEmptyArray() { - //Temporarily make decodeBody() public - $reflectionMethod = new ReflectionMethod('Microsoft\Graph\Http\GraphResponse', '_decodeBody'); - $reflectionMethod->setAccessible(true); - - $response = new GraphResponse($this->request); - $decodedBody = $reflectionMethod->invokeArgs($response, array()); - - $this->assertEquals(array(), $decodedBody); - } - - public function testGetHeaders() - { - $headers = $this->response->getHeaders(); - $this->assertEquals(["foo" => "bar"], $headers); - } - - public function testGetBody() - { - $response = $this->request->execute($this->client); - $this->assertInstanceOf(GraphResponse::class, $response); - - $body = $response->getBody(); - $this->assertEquals($this->responseBody, $body); + $response = new GraphResponse($this->mockGraphRequest, null); + $this->assertEquals(array(), $response->getBody()); } public function testGetRawBody() { - $response = $this->request->execute($this->client); - - $body = $response->getRawBody(); - $this->assertEquals(json_encode($this->responseBody), $body); + $rawBody = $this->defaultGraphResponse->getRawBody(); + $this->assertEquals(json_encode($this->defaultBody), $rawBody); } public function testGetStatus() { - $response = $this->request->execute($this->client); - - $this->assertEquals('200', $response->getStatus()); + $this->assertEquals($this->defaultStatusCode, $this->defaultGraphResponse->getStatus()); } public function testGetMultipleObjects() { - $this->request->execute($this->client); - $this->request->execute($this->client); - $hosts = $this->request->setReturnType(Model\User::class)->execute($this->client); - - $this->assertIsArray($hosts); - $this->assertContainsOnlyInstancesOf(Model\User::class, $hosts); - $this->assertSame(array_values($hosts), $hosts); - $this->assertEquals(2, count($hosts)); - $this->assertEquals("Bob", $hosts[0]->getGivenName()); + $obj = $this->defaultGraphResponse->getResponseAsObject(User::class); + $this->assertIsArray($obj); + $this->assertContainsOnlyInstancesOf(User::class, $obj); + $this->assertSameSize($this->defaultBody['value'], $obj); + $this->assertEquals(1, $obj[0]->getId()); } public function testGetValueObject() { - $this->request->execute($this->client); - $this->request->execute($this->client); - $this->request->execute($this->client); - $response = $this->request->setReturnType(Model\User::class)->execute($this->client); - - $this->assertInstanceOf(Model\User::class, $response); + $response = new GraphResponse( + $this->mockGraphRequest, + Utils::streamFor(json_encode(SampleGraphResponsePayload::ENTITY_PAYLOAD)), + $this->defaultStatusCode, + $this->defaultHeaders + ); + + $obj = $response->getResponseAsObject(User::class); + $this->assertInstanceOf(User::class, $obj); } public function testGetZeroMultipleObjects() { - $this->request->execute($this->client); - $this->request->execute($this->client); - $this->request->execute($this->client); - $this->request->execute($this->client); - $response = $this->request->setReturnType(Model\User::class)->execute($this->client); + $response = new GraphResponse( + $this->mockGraphRequest, + Utils::streamFor(json_encode(['value' => []])), + ); - $this->assertSame(array(), $response); + $obj = $response->getResponseAsObject(User::class); + $this->assertSame(array(), $obj); } -} \ No newline at end of file +} diff --git a/tests/Http/HttpClientFactoryTest.php b/tests/Http/HttpClientFactoryTest.php index 2f214349..9af63608 100644 --- a/tests/Http/HttpClientFactoryTest.php +++ b/tests/Http/HttpClientFactoryTest.php @@ -13,12 +13,12 @@ class HttpClientFactoryTest extends \PHPUnit\Framework\TestCase { function testNationalCloudWithEmptyString() { $this->expectException(GraphClientException::class); - HttpClientFactory::nationalCloud(""); + HttpClientFactory::setNationalCloud(""); } function testNationalCloudWithInvalidUrl() { $this->expectException(GraphClientException::class); - HttpClientFactory::nationalCloud("https://www.microsoft.com"); + HttpClientFactory::setNationalCloud("https://www.microsoft.com"); } function testCreateWithNoConfigReturnsDefaultClient() { @@ -31,12 +31,12 @@ function testCreateWithConfigCreatesClient() { "proxy" => "localhost:8000", "verify" => false ]; - $client = HttpClientFactory::clientConfig($config)::nationalCloud(NationalCloud::GERMANY)::create(); + $client = HttpClientFactory::setClientConfig($config)::setNationalCloud(NationalCloud::GERMANY)::create(); $this->assertInstanceOf(\GuzzleHttp\Client::class, $client); } function testCreateAdapterReturnsHttpClientInterface() { - $adapter = HttpClientFactory::nationalCloud(NationalCloud::US_DOD)::createAdapter(); + $adapter = HttpClientFactory::setNationalCloud(NationalCloud::US_DOD)::createAdapter(); $this->assertInstanceOf(HttpClientInterface::class, $adapter); } diff --git a/tests/Http/HttpTest.php b/tests/Http/HttpTest.php deleted file mode 100644 index f89b5cac..00000000 --- a/tests/Http/HttpTest.php +++ /dev/null @@ -1,160 +0,0 @@ - 'bar']), - new Response(200, ['foo' => 'bar']) - ]); - $this->container = []; - $history = GuzzleHttp\Middleware::history($this->container); - $handler = HandlerStack::create($mock); - $handler->push($history); - $this->client = new Client(['handler' => $handler]); - - $this->getRequest = new GraphRequest("GET", "/endpoint", "token", "baseUrl", "version"); - } - - public function testGet() - { - $response = $this->getRequest->execute($this->client); - $code = $response->getStatus(); - - $this->assertEquals("200", $code); - } - - public function testPost() - { - $request = new GraphRequest("POST", "/endpoint", "token", "baseUrl", "version"); - $response = $request->execute($this->client); - $code = $response->getStatus(); - - $this->assertEquals("200", $code); - } - - public function testPut() - { - $request = new GraphRequest("PUT", "/endpoint", "token", "baseUrl", "version"); - $response = $request->execute($this->client); - $code = $response->getStatus(); - - $this->assertEquals("200", $code); - } - - public function testPatch() - { - $request = new GraphRequest("PATCH", "/endpoint", "token", "baseUrl", "version"); - $response = $request->execute($this->client); - $code = $response->getStatus(); - - $this->assertEquals("200", $code); - } - - public function testUpdate() - { - $request = new GraphRequest("UPDATE", "/endpoint", "token", "baseUrl", "version"); - $response = $request->execute($this->client); - $code = $response->getStatus(); - - $this->assertEquals("200", $code); - } - - public function testDelete() - { - $request = new GraphRequest("DELETE", "/endpoint", "token", "baseUrl", "version"); - $response = $request->execute($this->client); - $code = $response->getStatus(); - - $this->assertEquals("200", $code); - } - - public function testInvalidVerb() - { - $this->expectException(GuzzleHttp\Exception\ClientException::class); - - $mock = new MockHandler([ - new Response(400, ['foo' => 'bar']) - ]); - - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $request = new GraphRequest("OBLITERATE", "/endpoint", "token", "baseUrl", "version"); - $response = $request->execute($client); - $code = $response->getStatus(); - - $this->assertEquals("400", $code); - } - - public function testSendJson() - { - $body = json_encode(array('1' => 'a', '2' => 'b')); - - $request = $this->getRequest->attachBody($body); - $this->assertInstanceOf(GraphRequest::class, $request); - - $response = $request->execute($this->client); - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - $this->assertEquals($body, $this->container[0]['request']->getBody()->getContents()); - } - - public function testSendArray() - { - $body = array('1' => 'a', '2' => 'b'); - $request = $this->getRequest->attachBody($body); - $this->assertInstanceOf(GraphRequest::class, $request); - - $response = $request->execute($this->client); - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - $this->assertEquals(json_encode($body), $this->container[0]['request']->getBody()->getContents()); - } - - public function testSendObject() - { - $user = new Model\User(); - $user->setDisplayName('Bob Barker'); - $request = $this->getRequest->attachBody($user); - $this->assertInstanceOf(GraphRequest::class, $request); - - $response = $request->execute($this->client); - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - $this->assertEquals(json_encode($user->getProperties()), $this->container[0]['request']->getBody()->getContents()); - } - - public function testSendString() - { - $body = '{"1":"a","2":"b"}'; - $request = $this->getRequest->attachBody($body); - $this->assertInstanceOf(GraphRequest::class, $request); - - $response = $request->execute($this->client); - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - $this->assertEquals($body, $this->container[0]['request']->getBody()->getContents()); - } - - public function testSendStream() - { - $body = GuzzleHttp\Psr7\Utils::streamFor('stream'); - $request = $this->getRequest->attachBody($body); - $this->assertInstanceOf(GraphRequest::class, $request); - - $response = $request->execute($this->client); - $this->assertInstanceOf(Microsoft\Graph\Http\GraphResponse::class, $response); - $this->assertEquals($body, $this->container[0]['request']->getBody()->getContents()); - } -} \ No newline at end of file diff --git a/tests/Http/MockClientFactory.php b/tests/Http/MockClientFactory.php deleted file mode 100644 index f9910f40..00000000 --- a/tests/Http/MockClientFactory.php +++ /dev/null @@ -1,28 +0,0 @@ -mockHttpClient = $this->createMock(HttpClientInterface::class); + $this->setupMockGraphClient(); + $this->defaultGraphRequest = new GraphRequest("GET", self::DEFAULT_REQUEST_ENDPOINT, $this->mockGraphClient); + } + + private function setupMockGraphClient(): void { + $this->mockGraphClient = $this->createStub(AbstractGraphClient::class); + + $this->mockGraphClient->method('getHttpClient') + ->willReturn($this->mockHttpClient); + + $this->mockGraphClient->method('getSdkVersion') + ->willReturn("2.0.0"); + + $this->mockGraphClient->method('getApiVersion') + ->willReturn("v1.0"); + + $this->mockGraphClient->method('getAccessToken') + ->willReturn("abc"); + + $this->mockGraphClient->method('getNationalCloud') + ->willReturn(NationalCloud::GLOBAL); + + } +} diff --git a/tests/Http/Request/GraphCollectionRequestTest.php b/tests/Http/Request/GraphCollectionRequestTest.php new file mode 100644 index 00000000..488eafeb --- /dev/null +++ b/tests/Http/Request/GraphCollectionRequestTest.php @@ -0,0 +1,75 @@ +defaultCollectionRequest = new GraphCollectionRequest("GET", $this->defaultEndpoint, $this->mockGraphClient); + $this->defaultCollectionRequest->setPageSize($this->defaultPageSize); + $this->defaultCollectionRequest->setReturnType(User::class); + } + + public function testSetPageSizeReturnsInstance(): void { + $this->assertInstanceOf(GraphCollectionRequest::class, $this->defaultCollectionRequest->setPageSize(1)); + } + + public function testSetPageSizeExceedingMaxSizeThrowsException(): void { + $this->expectException(GraphClientException::class); + $this->defaultCollectionRequest->setPageSize(GraphConstants::MAX_PAGE_SIZE + 1); + } + + public function testGetPageAppendsPageSizeToInitialCollectionRequestUrl(): void { + MockHttpClientResponseConfig::configureWithCollectionPayload($this->mockHttpClient); + $this->defaultCollectionRequest->getPage(); + $expectedRequestUrl = GraphRequestUtil::getRequestUri($this->mockGraphClient->getNationalCloud(), $this->defaultEndpoint)."?\$top=".$this->defaultPageSize; + $this->assertEquals($expectedRequestUrl, strval($this->defaultCollectionRequest->getRequestUri())); + } + + public function testGetPageUsesNextLinkForSubsequentRequests(): void { + MockHttpClientResponseConfig::configureWithCollectionPayload($this->mockHttpClient); + // First page + $this->defaultCollectionRequest->getPage(); + // Next page + $this->defaultCollectionRequest->getPage(); + $expectedRequestUrl = SampleGraphResponsePayload::COLLECTION_PAYLOAD['@odata.nextLink']; + $this->assertEquals($expectedRequestUrl, strval($this->defaultCollectionRequest->getRequestUri())); + } + + public function testHitEndOfCollection() + { + $this->expectError(); + MockHttpClientResponseConfig::configureWithLastPageCollectionPayload($this->mockHttpClient); + //Last page + $this->defaultCollectionRequest->getPage(); + $this->assertTrue($this->defaultCollectionRequest->isEnd()); + //Expect error + $this->defaultCollectionRequest->getPage(); + } + + public function testCount(): void { + MockHttpClientResponseConfig::configureWithCollectionPayload($this->mockHttpClient); + $count = $this->defaultCollectionRequest->count(); + $this->assertEquals(SampleGraphResponsePayload::COLLECTION_PAYLOAD["@odata.count"], $count); + } + + public function testCountThrowsErrorIfNoOdataCountFound(): void { + $this->expectException(GraphException::class); + MockHttpClientResponseConfig::configureWithEmptyPayload($this->mockHttpClient); + $count = $this->defaultCollectionRequest->count(); + } +} diff --git a/tests/Http/Request/GraphRequestAsyncTest.php b/tests/Http/Request/GraphRequestAsyncTest.php new file mode 100644 index 00000000..47ea2e2b --- /dev/null +++ b/tests/Http/Request/GraphRequestAsyncTest.php @@ -0,0 +1,94 @@ +createMock(HttpAsyncClient::class); + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise($customClient); + $customClient->expects($this->once())->method('sendAsyncRequest'); + $this->mockHttpClient->expects($this->never())->method('sendAsyncRequest'); + $this->defaultGraphRequest->executeAsync($customClient); + } + + public function testExecuteAsyncThrowsPsr18ExceptionsWithRejectedPromise(): void { + $this->expectException(ClientExceptionInterface::class); + MockHttpClientAsyncResponseConfig::configureWithRejectedPromise( + $this->mockHttpClient, + $this->createMock(NetworkExceptionInterface::class) + ); + $resultPromise = $this->defaultGraphRequest->executeAsync(); + $resultPromise->wait(); + } + + public function testExecuteAsyncReturnsFulfilledPromise(): void { + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise($this->mockHttpClient); + $response = $this->defaultGraphRequest->executeAsync(); + $this->assertInstanceOf(Promise::class, $response); + } + + public function testExecuteAsyncPromiseResolvesToStream(): void { + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise( + $this->mockHttpClient, + SampleGraphResponsePayload::STREAM_PAYLOAD() + ); + $promise = $this->defaultGraphRequest->setReturnType(StreamInterface::class) + ->executeAsync(); + $this->assertInstanceOf(StreamInterface::class, $promise->wait()); + } + + public function testExecuteAsyncPromiseResolvesToGraphResponseIfNoReturnType(): void { + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise($this->mockHttpClient); + $promise = $this->defaultGraphRequest->executeAsync(); + $this->assertInstanceOf(GraphResponse::class, $promise->wait()); + } + + public function testExecuteAsyncPromiseResolvesToGraphResponseForErrorPayload(): void { + MockHttpClientAsyncResponseConfig::statusCode(400)::configureWithFulfilledPromise( + $this->mockHttpClient, + SampleGraphResponsePayload::ERROR_PAYLOAD + ); + $promise = $this->defaultGraphRequest->executeAsync(); + $this->assertInstanceOf(GraphResponse::class, $promise->wait()); + } + + public function testExecuteAsyncResolvesToModelForModelReturnType(): void { + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise( + $this->mockHttpClient, + SampleGraphResponsePayload::ENTITY_PAYLOAD + ); + $promise = $this->defaultGraphRequest->setReturnType(User::class)->executeAsync(); + $this->assertInstanceOf(User::class, $promise->wait()); + } + + public function testExecuteAsyncResolvesToModelArrayForCollectionRequest(): void { + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise( + $this->mockHttpClient, + SampleGraphResponsePayload::COLLECTION_PAYLOAD + ); + $promise = $this->defaultGraphRequest->setReturnType(User::class)->executeAsync(); + $response = $promise->wait(); + $this->assertIsArray($response); + $this->assertEquals(2, sizeof($response)); + $this->assertContainsOnlyInstancesOf(User::class, $response); + } +} diff --git a/tests/Http/Request/GraphRequestStreamTest.php b/tests/Http/Request/GraphRequestStreamTest.php new file mode 100644 index 00000000..e2952a7b --- /dev/null +++ b/tests/Http/Request/GraphRequestStreamTest.php @@ -0,0 +1,55 @@ +rootDir = vfsStream::setup('testDir'); + parent::setUp(); + } + + public function testUpload() + { + MockHttpClientResponseConfig::configureWithEmptyPayload($this->mockHttpClient); + $file = vfsStream::newFile('foo.txt') + ->withContent("content") + ->at($this->rootDir); + $this->defaultGraphRequest->upload($file->url()); + $this->assertEquals($this->defaultGraphRequest->getBody()->getContents(), $file->getContent()); + } + + public function testInvalidUpload() + { + $this->expectException(GraphClientException::class); + $file = new VfsStreamFile('foo.txt', 0000); + $this->rootDir->addChild($file); + $this->defaultGraphRequest->upload($file->url()); + } + + public function testDownload() + { + $file = new VfsStreamFile('foo.txt'); + $this->rootDir->addChild($file); + + MockHttpClientResponseConfig::configureWithStreamPayload($this->mockHttpClient); + $this->defaultGraphRequest->download($file->url()); + $this->assertEquals(SampleGraphResponsePayload::STREAM_PAYLOAD()->getContents(), $file->getContent()); + } + + public function testInvalidDownload() + { + $this->expectException(GraphClientException::class); + $file = new VfsStreamFile('foo.txt', 0000); + $this->rootDir->addChild($file); + $this->defaultGraphRequest->download($file->url()); + } +} diff --git a/tests/Http/Request/GraphRequestSyncTest.php b/tests/Http/Request/GraphRequestSyncTest.php new file mode 100644 index 00000000..9f5be8b2 --- /dev/null +++ b/tests/Http/Request/GraphRequestSyncTest.php @@ -0,0 +1,136 @@ +mockHttpClient); + $this->mockHttpClient->expects($this->once()) + ->method('sendRequest'); + $this->defaultGraphRequest->execute(null); + } + + public function testExecuteWithCustomClientUsesCustomClient(): void { + $customClient = $this->createMock(ClientInterface::class); + MockHttpClientResponseConfig::configureWithEmptyPayload($customClient); + $customClient->expects($this->once())->method('sendRequest'); + $this->mockHttpClient->expects($this->never())->method('sendRequest'); + $this->defaultGraphRequest->execute($customClient); + } + + public function testExecuteThrowsPsr18Exceptions(): void { + $this->expectException(ClientExceptionInterface::class); + $this->mockHttpClient->method('sendRequest') + ->will($this->throwException($this->createMock(NetworkExceptionInterface::class))); + $this->defaultGraphRequest->execute(); + } + + public function testExecuteWithoutReturnTypeReturnsGraphResponseForSuccessPayload(): void { + MockHttpClientResponseConfig::configureWithEntityPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->execute(); + $this->assertInstanceOf(GraphResponse::class, $response); + } + + public function testExecuteWithoutReturnTypeReturnsGraphResponseForEmptyPayload(): void { + MockHttpClientResponseConfig::configureWithEmptyPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->execute(); + $this->assertInstanceOf(GraphResponse::class, $response); + } + + public function testExecuteWithoutReturnTypeReturnsGraphResponseForErrorPayload(): void { + MockHttpClientResponseConfig::configureWithErrorPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->execute(); + $this->assertInstanceOf(GraphResponse::class, $response); + } + + public function testExecuteWithoutReturnTypeReturnsGraphResponseForStreamPayload(): void { + MockHttpClientResponseConfig::configureWithStreamPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->execute(); + $this->assertInstanceOf(GraphResponse::class, $response); + } + + public function testExecuteWithoutReturnTypeReturnsGraphResponseForCollectionPayload(): void { + MockHttpClientResponseConfig::configureWithCollectionPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->execute(); + $this->assertInstanceOf(GraphResponse::class, $response); + } + + public function testExecuteWithModelReturnTypeReturnsModelForSuccessPayload(): void { + MockHttpClientResponseConfig::configureWithEntityPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(User::class)->execute(); + $this->assertInstanceOf(User::class, $response); + } + + public function testExecuteWithModelReturnTypeReturnsModelForEmptyPayload(): void { + MockHttpClientResponseConfig::configureWithEmptyPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(User::class)->execute(); + $this->assertInstanceOf(User::class, $response); + } + + public function testExecuteWithModelReturnTypeReturnsModelForErrorPayload(): void { + MockHttpClientResponseConfig::configureWithErrorPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(User::class)->execute(); + $this->assertInstanceOf(User::class, $response); + } + + public function testExecuteWithModelReturnTypeReturnsModelForStreamPayload(): void { + MockHttpClientResponseConfig::configureWithStreamPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(User::class)->execute(); + $this->assertInstanceOf(User::class, $response); + } + + public function testExecuteWithModelReturnTypeReturnsArrayOfModelsForCollectionPayload(): void { + MockHttpClientResponseConfig::configureWithCollectionPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(User::class)->execute(); + $this->assertIsArray($response); + $this->assertEquals(2, sizeof($response)); + $this->assertContainsOnlyInstancesOf(User::class, $response); + } + + public function testExecuteWithStreamReturnTypeReturnsStreamForSuccessPayload(): void { + MockHttpClientResponseConfig::configureWithEntityPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(StreamInterface::class)->execute(); + $this->assertInstanceOf(StreamInterface::class, $response); + } + + public function testExecuteWithStreamReturnTypeReturnsStreamForEmptyPayload(): void { + MockHttpClientResponseConfig::configureWithEmptyPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(StreamInterface::class)->execute(); + $this->assertInstanceOf(StreamInterface::class, $response); + } + + public function testExecuteWithStreamReturnTypeReturnsStreamForErrorPayload(): void { + MockHttpClientResponseConfig::configureWithErrorPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(StreamInterface::class)->execute(); + $this->assertInstanceOf(StreamInterface::class, $response); + } + + public function testExecuteWithStreamReturnTypeReturnsStreamForStreamPayload(): void { + MockHttpClientResponseConfig::configureWithStreamPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(StreamInterface::class)->execute(); + $this->assertInstanceOf(StreamInterface::class, $response); + } + + public function testExecuteWithStreamReturnTypeReturnsStreamForCollectionPayload(): void { + MockHttpClientResponseConfig::configureWithCollectionPayload($this->mockHttpClient); + $response = $this->defaultGraphRequest->setReturnType(StreamInterface::class)->execute(); + $this->assertInstanceOf(StreamInterface::class, $response); + } +} diff --git a/tests/Http/Request/GraphRequestTest.php b/tests/Http/Request/GraphRequestTest.php new file mode 100644 index 00000000..dfd91e57 --- /dev/null +++ b/tests/Http/Request/GraphRequestTest.php @@ -0,0 +1,233 @@ +expectException(\TypeError::class); + $request = new GraphRequest(null, null, null); + } + + public function testConstructorWithEmptyParametersThrowsException(): void { + $this->expectException(GraphClientException::class); + $request = new GraphRequest("", "", $this->mockGraphClient); + } + + public function testConstructorWithoutAccessTokenThrowsException(): void { + $graphClient = $this->getMockForAbstractClass(AbstractGraphClient::class); + $this->expectException(GraphClientException::class); + $request = new GraphRequest("GET", "/me", $graphClient); + } + + public function testConstructorWithInvalidCustomBaseUrlThrowsException(): void { + $this->expectException(GraphClientException::class); + $baseUrl = "www.outlook.com"; # no scheme + $request = new GraphRequest("GET", "/me", $this->mockGraphClient, $baseUrl); + } + + public function testConstructorWithAccessTokenCreatesGraphRequest(): void { + $request = new GraphRequest("GET", "/me", $this->mockGraphClient); + $this->assertInstanceOf(GraphRequest::class, $request); + } + + public function testConstructorWithValidCustomBaseUrlCreatesGraphRequest(): void { + $baseUrl = "https://www.onedrive.com"; + $request = new GraphRequest("GET", "/me", $this->mockGraphClient, $baseUrl); + $this->assertInstanceOf(GraphRequest::class, $request); + } + + public function testConstructorSetsExpectedRequestUri(): void { + $apiVersion = $this->mockGraphClient->getApiVersion(); + // Sample baseUrls, endpoints and the final expected url + $baseUrlEndpointCombis = [ + [NationalCloud::GLOBAL, "/me", NationalCloud::GLOBAL."/".$apiVersion."/me"], + [NationalCloud::GLOBAL, "me/users/", NationalCloud::GLOBAL."/".$apiVersion."/me/users/"], + [NationalCloud::GLOBAL, "me/users?\$count=true", NationalCloud::GLOBAL."/".$apiVersion."/me/users?\$count=true"], + [NationalCloud::GLOBAL."/beta", "/me/users", NationalCloud::GLOBAL."/".$apiVersion."/me/users"], + [NationalCloud::GLOBAL."/beta/", "me/users", NationalCloud::GLOBAL."/".$apiVersion."/me/users"] + ]; + + foreach ($baseUrlEndpointCombis as $combi) { + $this->mockGraphClient->method('getNationalCloud') + ->willReturn($combi[0]); + $endpoint = $combi[1]; + $expectedRequestUrl = $combi[2]; + + $request = new GraphRequest("GET", $endpoint, $this->mockGraphClient); + $this->assertEquals($expectedRequestUrl, strval($request->getRequestUri())); + } + } + + public function testConstructorSetsExpectedRequestUriGivenValidCustomBaseUrl(): void { + $baseUrl = "https://www.onedrive.com"; + $request = new GraphRequest("GET", "/me", $this->mockGraphClient, $baseUrl); + $expectedUrl = $baseUrl."/me"; + $this->assertEquals($expectedUrl, strval($request->getRequestUri())); + } + + public function testConstructorSetsExpectedRequestUriGivenValidFullEndpointUri(): void { + $endpoint = NationalCloud::GLOBAL."/v1.0/me/users?\$top=10&\$skip=500"; + $request = new GraphRequest("GET", $endpoint, $this->mockGraphClient); + $expectedUrl = $endpoint; + $this->assertEquals($expectedUrl, strval($request->getRequestUri())); + } + + public function testConstructorGivenInvalidFullEndpointUriAppendsItToDefaultBaseUrl(): void { + $invalidEndpoint = "http/microsoft.com:localhost\$endpoint"; # Not https + $request = new GraphRequest("GET", $invalidEndpoint, $this->mockGraphClient); + $expected = NationalCloud::GLOBAL."/".$this->mockGraphClient->getApiVersion()."/".$invalidEndpoint; + $this->assertEquals($expected, strval($request->getRequestUri())); + } + + public function testConstructorSetsExpectedHeadersGivenValidGraphBaseUrl(): void { + $expectedHeaders = [ + 'Content-Type' => ['application/json'], + 'SdkVersion' => ["graph-php-core/".GraphConstants::SDK_VERSION.", graph-php/".$this->mockGraphClient->getSdkVersion()], + 'Authorization' => ['Bearer ' . $this->mockGraphClient->getAccessToken()], + 'Host' => [substr($this->mockGraphClient->getNationalCloud(), strlen("https://"))] + ]; + $request = new GraphRequest("GET", "/me", $this->mockGraphClient); + $this->assertEquals($expectedHeaders, $request->getHeaders()); + } + + public function testConstructorSetsExpectedBetaSdkVersionHeader(): void { + $graphClient = $this->createMock(AbstractGraphClient::class); + $graphClient->method('getAccessToken')->willReturn("abc"); + $graphClient->method('getNationalCloud')->willReturn(NationalCloud::GLOBAL); + $graphClient->method('getSdkVersion')->willReturn('2.0.0'); + $graphClient->method('getApiVersion')->willReturn(GraphConstants::BETA_API_VERSION); + + $request = new GraphRequest("GET", "/me", $graphClient); + $expected = ["graph-php-core/".GraphConstants::SDK_VERSION.", graph-php-beta/".$graphClient->getSdkVersion()]; + $this->assertEquals($expected, $request->getHeaders()["SdkVersion"]); + } + + public function testConstructorSetsExpectedHeadersGivenValidCustomBaseUrl(): void { + $baseUrl = "https://www.outlook.com"; + $expectedHeaders = [ + 'Content-Type' => ['application/json'], + 'Host' => [substr($baseUrl, strlen("https://"))] + ]; + $request = new GraphRequest("GET", "/me", $this->mockGraphClient, $baseUrl); + $this->assertEquals($expectedHeaders, $request->getHeaders()); + } + + public function testConstructorSetsExpectedHeadersGivenGraphEndpointUrl(): void { + $endpoint = "https://graph.microsoft.com/v1.0/me/users\$skip=10&\$top=5"; + $expectedHeaders = [ + 'Content-Type' => ['application/json'], + 'SdkVersion' => ["graph-php-core/".GraphConstants::SDK_VERSION.", graph-php/".$this->mockGraphClient->getSdkVersion()], + 'Authorization' => ['Bearer ' . $this->mockGraphClient->getAccessToken()], + 'Host' => [substr($this->mockGraphClient->getNationalCloud(), strlen("https://"))] + ]; + $request = new GraphRequest("GET", $endpoint, $this->mockGraphClient); + $this->assertEquals($expectedHeaders, $request->getHeaders()); + + } + + public function testConstructorSetsExpectedHeadersGivenNonGraphEndpointUrl(): void { + $endpoint = "https://www.outlook.com/messages"; + $expectedHeaders = [ + 'Content-Type' => ['application/json'], + 'Host' => ["www.outlook.com"] + ]; + $request = new GraphRequest("GET", $endpoint, $this->mockGraphClient); + $this->assertEquals($expectedHeaders, $request->getHeaders()); + } + + public function testSetAccessTokenReturnsGraphRequestInstance(): void { + $this->assertInstanceOf(GraphRequest::class, $this->defaultGraphRequest->setAccessToken("123")); + } + + public function testSetAccessTokenChangesAuthorizationHeaderValue(): void { + $accessToken = "newAccessToken"; + $this->defaultGraphRequest->setAccessToken($accessToken); + $expectedHeaderValue = "Bearer ".$accessToken; + $actualHeaders = $this->defaultGraphRequest->getHeaders()['Authorization']; + $this->assertEquals(1, sizeof($actualHeaders)); + $this->assertEquals($expectedHeaderValue, $actualHeaders[0]); + } + + public function testSetReturnTypeReturnsGraphRequestInstance(): void { + $this->assertInstanceOf(GraphRequest::class, $this->defaultGraphRequest->setReturnType(User::class)); + } + + public function testSetReturnTypeWithInvalidClassThrowsException(): void { + $this->expectException(GraphClientException::class); + $this->defaultGraphRequest->setReturnType("Model\User"); + } + + public function testSetReturnTypeToGuzzleStreamIsValid(): void { + $this->assertInstanceOf(GraphRequest::class, $this->defaultGraphRequest->setReturnType("GuzzleHttp\\Psr7\\Stream")); + } + + public function testAddHeadersReturnsGraphRequestInstance(): void { + $this->assertInstanceOf(GraphRequest::class, $this->defaultGraphRequest->addHeaders([])); + } + + public function testAddHeadersCannotAppendOrOverwriteSdkVersionValue(): void { + $this->expectException(GraphClientException::class); + $this->defaultGraphRequest->addHeaders([ + 'SdkVersion' => 'Version1', + 'Content-Encoding' => 'gzip' + ]); + } + + public function testAddHeadersWithStringValueAppendsNewHeader(): void { + $this->defaultGraphRequest->addHeaders(['Connection' => 'keep-alive']); + $this->assertEquals(['keep-alive'], $this->defaultGraphRequest->getHeaders()['Connection']); + } + + public function testAddHeadersWithArrayOfValuesAppendsNewHeaders(): void { + $values = ['de', 'en', 'fr']; + $this->defaultGraphRequest->addHeaders(['Accept-Language' => $values]); + $this->assertEquals($values, $this->defaultGraphRequest->getHeaders()['Accept-Language']); + } + + public function testAddHeadersWithExistingHeaderNameDoesCaseInsensitiveAppend(): void { + $this->assertEquals(['application/json'], $this->defaultGraphRequest->getHeaders()['Content-Type']); + $this->defaultGraphRequest->addHeaders(['conTeNT-tyPe' => 'text']); + $this->assertEquals(['application/json', 'text'], $this->defaultGraphRequest->getHeaders()['Content-Type']); + } + + public function testAttachBodyReturnsGraphRequestInstance(): void { + $instance = $this->defaultGraphRequest->attachBody(''); + $this->assertInstanceOf(GraphRequest::class, $instance); + } + + public function testAttachBodyWithNullObjectSetsNullStringBody(): void { + $this->defaultGraphRequest->attachBody(null); + $this->assertEquals("null", $this->defaultGraphRequest->getBody()); + } + + public function testAttachBodyWithStringSetsStringBody(): void { + $this->defaultGraphRequest->attachBody("Body"); + $this->assertEquals("Body", $this->defaultGraphRequest->getBody()); + } + + public function testAttachBodyWithObjectSetsJsonSerializedStringBody(): void { + $model = new User(array("id" => 1, "child" => new User(["id" => 2]))); + $this->defaultGraphRequest->attachBody($model); + $this->assertEquals('{"id":1,"child":{"id":2}}', $this->defaultGraphRequest->getBody()); + } + + public function testAttachBodyWithArraySetsJsonArrayBody(): void { + $this->defaultGraphRequest->attachBody(["id" => 1, "name" => "user"]); + $this->assertEquals('{"id":1,"name":"user"}', $this->defaultGraphRequest->getBody()); + } + + public function testExecuteAsyncWithNullClientUsesGraphClientHttpClient(): void { + MockHttpClientAsyncResponseConfig::configureWithFulfilledPromise($this->mockHttpClient); + $this->mockHttpClient->expects($this->once())->method('sendAsyncRequest'); + $this->defaultGraphRequest->executeAsync(null); + } +} diff --git a/tests/Http/GraphRequestUtilTest.php b/tests/Http/Request/GraphRequestUtilTest.php similarity index 74% rename from tests/Http/GraphRequestUtilTest.php rename to tests/Http/Request/GraphRequestUtilTest.php index 21c55d8f..8e19b4d7 100644 --- a/tests/Http/GraphRequestUtilTest.php +++ b/tests/Http/Request/GraphRequestUtilTest.php @@ -5,7 +5,7 @@ * for license information. */ -namespace Http; +namespace Microsoft\Graph\Test\Http\Request; use GuzzleHttp\Psr7\Uri; @@ -31,15 +31,15 @@ public function getApiVersion(): string { } function testGetRequestUriWithFullNationalCloudEndpointUrlReturnsUri() { - $endpoint = NationalCloud::GLOBAL."/me/events?\$skip=100&\$top=10"; + $endpoint = NationalCloud::GLOBAL.'/me/events?$skip=100&$top=10'; $result = GraphRequestUtil::getRequestUri("", $endpoint, $this->apiVersion); self::assertEquals($endpoint, strval($result)); } - function testGetRequestUriWithFullNonNationalCloudEndpointReturnsNull() { + function testGetRequestUriWithFullNonNationalCloudEndpointReturnsUri() { $endpoint = "https://www.outlook.com/mail?user=me"; $uri = GraphRequestUtil::getRequestUri("", $endpoint, $this->apiVersion); - self::assertNull($uri); + $this->assertEquals($endpoint, strval($uri)); } function testGetRequestUriWithValidBaseUrlResolvesCorrectly() { @@ -59,17 +59,14 @@ function testGetRequestUriWithValidBaseUrlResolvesCorrectly() { } } - function testGetRequestUriWithEmptyBaseUriUsesNationalCloudByDefault() { - $endpoints= ["/me/events", "me/events"]; - $expected = "https://graph.microsoft.com/v1.0/me/events"; - foreach ($endpoints as $endpoint) { - $uri = GraphRequestUtil::getRequestUri("", $endpoint, $this->apiVersion); - self::assertEquals($expected, strval($uri)); - } + function testGetRequestUriWithEmptyBaseUriThrowsException() { + $this->expectException(\InvalidArgumentException::class); + $endpoint= "/me/events"; + GraphRequestUtil::getRequestUri("", $endpoint); } function testGetRequestUriWithoutNationalCloudHostDoesntSetApiVersion() { - $baseUrl = "https://outlook.microsoft.com/mail/"; + $baseUrl = "https://outlook.microsoft.com/"; $endpoint = "?startDate=2020-10-02&sort=desc"; $expected = $baseUrl.$endpoint; $uri = GraphRequestUtil::getRequestUri($baseUrl, $endpoint, $this->apiVersion); @@ -79,19 +76,12 @@ function testGetRequestUriWithoutNationalCloudHostDoesntSetApiVersion() { function testGetRequestUriWithInvalidFullEndpointUrlThrowsException() { $this->expectException(\InvalidArgumentException::class); - $endpoint = "http:/microsoft.com:localhost\$endpoint"; + $endpoint = 'http/microsoft.com:localhost$endpoint'; $uri = GraphRequestUtil::getRequestUri("", $endpoint, $this->apiVersion); } - function testGetRequestUrlWithInvalidBaseUrlAndEndpointThrowsException() { - $this->expectException(\InvalidArgumentException::class); - $baseUrl = "https://graph.microsoft.com"; - $endpoint = "http:/microsoft.com:localhost\$endpoint"; - $uri = GraphRequestUtil::getRequestUri($baseUrl, $endpoint, $this->apiVersion); - } - function testGetQueryParamConcatenatorWithExistingQueryParams() { - $uri = new Uri("https://graph.microsoft.com?\$skip=10"); + $uri = new Uri('https://graph.microsoft.com?$skip=10'); $result = GraphRequestUtil::getQueryParamConcatenator($uri); self::assertEquals("&", $result); } diff --git a/tests/Http/Request/MockHttpClientAsyncResponseConfig.php b/tests/Http/Request/MockHttpClientAsyncResponseConfig.php new file mode 100644 index 00000000..cc534595 --- /dev/null +++ b/tests/Http/Request/MockHttpClientAsyncResponseConfig.php @@ -0,0 +1,32 @@ +method(self::METHOD_NAME)->willReturn($promise); + } + + public static function configureWithRejectedPromise($mockHttpClient, $exception) { + $promise = new RejectedPromise($exception); + $mockHttpClient->method(self::METHOD_NAME)->willReturn($promise); + } +} diff --git a/tests/Http/Request/MockHttpClientResponseConfig.php b/tests/Http/Request/MockHttpClientResponseConfig.php new file mode 100644 index 00000000..9e36fc51 --- /dev/null +++ b/tests/Http/Request/MockHttpClientResponseConfig.php @@ -0,0 +1,86 @@ +method(self::METHOD_NAME) + ->willReturn( + new Response( + self::$statusCode, + self::$headers, + json_encode(SampleGraphResponsePayload::ENTITY_PAYLOAD) + ) + ); + return $mockHttpClient; + } + + public static function configureWithEmptyPayload($mockHttpClient) { + $mockHttpClient->method(self::METHOD_NAME) + ->willReturn(new Response(self::$statusCode, self::$headers)); + return $mockHttpClient; + } + + public static function configureWithCollectionPayload($mockHttpClient) { + $mockHttpClient->method(self::METHOD_NAME) + ->willReturn( + new Response( + self::$statusCode, + self::$headers, + json_encode(SampleGraphResponsePayload::COLLECTION_PAYLOAD) + ) + ); + return $mockHttpClient; + } + + public static function configureWithLastPageCollectionPayload($mockHttpClient) { + $mockHttpClient->method(self::METHOD_NAME) + ->willReturn( + new Response( + self::$statusCode, + self::$headers, + json_encode(SampleGraphResponsePayload::LAST_PAGE_COLLECTION_PAYLOAD) + ) + ); + return $mockHttpClient; + } + + public static function configureWithErrorPayload($mockHttpClient, $statusCode = 400) { + $mockHttpClient->method(self::METHOD_NAME) + ->willReturn( + new Response( + $statusCode, + self::$headers, + json_encode(SampleGraphResponsePayload::ERROR_PAYLOAD) + ) + ); + return $mockHttpClient; + } + + public static function configureWithStreamPayload($mockHttpClient) { + $mockHttpClient->method(self::METHOD_NAME) + ->willReturn( + new Response( + self::$statusCode, + self::$headers, + SampleGraphResponsePayload::STREAM_PAYLOAD() + ) + ); + return $mockHttpClient; + } +} diff --git a/tests/Http/Request/MockHttpClientResponseConfigTrait.php b/tests/Http/Request/MockHttpClientResponseConfigTrait.php new file mode 100644 index 00000000..b447b90c --- /dev/null +++ b/tests/Http/Request/MockHttpClientResponseConfigTrait.php @@ -0,0 +1,26 @@ + "https://graph.microsoft.com/v2/dcd219dd-bc68-4b9b-bf0b-4a33a796be35", + "jobTitle" => "developer", + "givenName" => "user1" + ]; + + const COLLECTION_PAYLOAD = [ + "@odata.count" => 2, + "@odata.nextLink" => 'https://graph.microsoft.com/me/users?$skip=2&$top=2', + "value" => [ + [ + "id" => 1, + "givenName" => "user1" + ], + [ + "id" => 2, + "givenName" => "user2" + ] + ] + ]; + + const LAST_PAGE_COLLECTION_PAYLOAD = [ + "@odata.count" => 2, + "value" => [ + [ + "id" => 1, + "givenName" => "user1" + ], + [ + "id" => 2, + "givenName" => "user2" + ] + ] + ]; + + const ERROR_PAYLOAD = [ + "error" => [ + "code" => "BadRequest", + "message" => "Resource not found for the segment", + "innerError" => [ + "date" => "2021-07-02T01:40:19", + "request-id" => "1a0ffbc0-086f-4e8f-93f9-bf99881c65f6", + "client-request-id" => "225aed2b-cf4a-d456-b313-16ab196c2364" + ] + ] + ]; + + public static function STREAM_PAYLOAD(): StreamInterface { + return Utils::streamFor("content"); + } +} diff --git a/tests/Http/StreamTest.php b/tests/Http/StreamTest.php deleted file mode 100644 index bbf8a57d..00000000 --- a/tests/Http/StreamTest.php +++ /dev/null @@ -1,100 +0,0 @@ -root = vfsStream::setup('testDir'); - - $this->body = json_encode(array('body' => 'content')); - $stream = GuzzleHttp\Psr7\Utils::streamFor('content'); - - $mock = new GuzzleHttp\Handler\MockHandler([ - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], $this->body), - new GuzzleHttp\Psr7\Response(200,['foo' => 'bar'], $stream), - new GuzzleHttp\Psr7\Response(200, ['foo' => 'bar'], 'hello') - ]); - - $this->container = []; - $history = GuzzleHttp\Middleware::history($this->container); - $handler = GuzzleHttp\HandlerStack::create($mock); - $handler->push($history); - $this->client = new GuzzleHttp\Client(['handler' => $handler]); - } - - public function testUpload() - { - $file = new VfsStreamFile('foo.txt'); - $this->root->addChild($file); - $file->setContent('data'); - - $request = new GraphRequest("GET", "/me", "token", "url", "v1.0"); - $request->upload($file->url(), $this->client); - - $this->assertEquals($this->container[0]['request']->getBody()->getContents(), $file->getContent()); - } - - public function testInvalidUpload() - { - $this->expectException(Microsoft\Graph\Exception\GraphException::class); - - $file = new VfsStreamFile('foo.txt', 0000); - $this->root->addChild($file); - - $request = new GraphRequest("GET", "/me", "token", "url", "v1.0"); - $request->upload($file->url(), $this->client); - } - - public function testDownload() - { - $request = new GraphRequest("GET", "/me", "token", "url", "v1.0"); - $file = new VfsStreamFile('foo.txt'); - $this->root->addChild($file); - - $request->download($file->url(), $this->client); - $this->assertEquals($this->body, $file->getContent()); - } - - public function testInvalidDownload() - { - set_error_handler(function() {}); - try { - $this->expectException(Microsoft\Graph\Exception\GraphException::class); - - $file = new VfsStreamFile('foo.txt', 0000); - $this->root->addChild($file); - - $request = new GraphRequest("GET", "/me", "token", "url", "v1.0"); - $request->download($file->url(), $this->client); - } finally { - restore_error_handler(); - } - } - - public function testSetReturnStream() - { - $request = new GraphRequest("GET", "/me", "token", "url", "v1.0"); - $request->setReturnType(GuzzleHttp\Psr7\Stream::class); - - $this->assertTrue($request->getReturnsStream()); - - $response = $request->execute($this->client); - $this->assertInstanceOf(GuzzleHttp\Psr7\Stream::class, $response); - - $response = $request->execute($this->client); - $this->assertInstanceOf(GuzzleHttp\Psr7\Stream::class, $response); - } -} \ No newline at end of file diff --git a/tests/TestData/Model/Entity.php b/tests/TestData/Model/Entity.php index 3a5e879a..ec7c5bb0 100644 --- a/tests/TestData/Model/Entity.php +++ b/tests/TestData/Model/Entity.php @@ -1,7 +1,7 @@ _propDict; } - + /** * Gets the id * Read-only. @@ -69,7 +67,7 @@ public function getId() return null; } } - + /** * Sets the id * Read-only. @@ -83,7 +81,7 @@ public function setId($val) $this->_propDict["id"] = $val; return $this; } - + /** * Gets the ODataType * @@ -93,7 +91,7 @@ public function getODataType() { return $this->_propDict["@odata.type"]; } - + /** * Sets the ODataType * @@ -106,7 +104,7 @@ public function setODataType($val) $this->_propDict["@odata.type"] = $val; return $this; } - + /** * Serializes the object by property array * Manually serialize DateTime into RFC3339 format @@ -118,7 +116,7 @@ public function jsonSerialize() $serializableProperties = $this->getProperties(); foreach ($serializableProperties as $property => $val) { if (is_a($val, "\DateTime")) { - $serializableProperties[$property] = $val->format(\DateTime::RFC3339); + $serializableProperties[$property] = $val->format(\DateTimeInterface::RFC3339); } else if (is_a($val, "\Microsoft\Graph\Core\Enum")) { $serializableProperties[$property] = $val->value(); } diff --git a/tests/TestData/Model/User.php b/tests/TestData/Model/User.php index 06130549..740a0813 100644 --- a/tests/TestData/Model/User.php +++ b/tests/TestData/Model/User.php @@ -1,7 +1,7 @@ _propDict["displayName"] = $val; return $this; } - + /** * Gets the givenName * The given name (first name) of the user. Maximum length is 64 characters. Supports $filter (eq, ne, NOT , ge, le, in, startsWith). @@ -68,7 +66,7 @@ public function getGivenName() return null; } } - + /** * Sets the givenName * The given name (first name) of the user. Maximum length is 64 characters. Supports $filter (eq, ne, NOT , ge, le, in, startsWith). @@ -82,5 +80,5 @@ public function setGivenName($val) $this->_propDict["givenName"] = $val; return $this; } - -} \ No newline at end of file + +}