src/cmdlets/Test-Graph.ps1

# Copyright 2021, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

. (import-script ../REST/RESTRequest)
. (import-script ../graphservice/GraphEndpoint)
. (import-script ../client/GraphConnection)
. (import-script ../client/GraphContext)

<#
.SYNOPSIS
Determines without authentication whether a Graph endpoint is accessible over the network.

.DESCRIPTION
The Test-Graph command makes a request to an arbitrary URL of a target Graph endpoint. If a response is received, HTTP headers containing diagnostic information typically returned by Graph is converted to a human-readable representation by the command.

.PARAMETER Cloud
Specifies that the target Graph endpoint to test is the Graph endpoint associated with cloud environment indicated by the parameter. By default, Test-Graph makes a request against the current connection, which itself defaults to https://graph.microsoft.com, so this parameter allows the default to be overridden.

.PARAMETER Connection
Specifies a Connection object returned by the New-GraphConnection command whose Graph endpoint will be accessed when making Graph requests with this Connection object.

.PARAMETER EndpointUri
Specifies an arbitrary URI as the Graph endpoint -- the URI must be an absolute URI, e.g. https://graph.microsoft.com.

.PARAMETER RawContent
By default, the output of the cmdlet is deserialized PowerShell objects. If this switch is specified, the output is an object whose properties names correspond to specific HTTP header names returned by Graph. The values of the properties are then the values of the corresponding returned headers.

.OUTPUTS
If successful, this cmdlet returns non-null, non-empty output. If it is not successful, an HTTP status code or other error will be surfaced as an exeption. Note that the exact structure of successful output is NOT a contract as it is based on undocumented aspects of the Graph protocol -- the output structure can change at any time (and has changed in the past). However, the output will usually be deserialized PowerShell objects that provide diagnostic information about the Graph endpoint such as the name of the datacenter that served the request, the time of the request as seen by the system that served the response, and the host name of the system that served the response. The output may be piped to other commands including Select-Object to project specific fields or perform other additional processing.

.EXAMPLE
Test-Graph

TestUri : https://graph.microsoft.com/v1.0/$metadata
ServerTimestamp : 09/20/2021 10:21:07 +00:00
ClientElapsedTime (ms) : 12.0889
RequestId : fa0113c3-c0ab-4f47-8571-d3bc6b891686
DataCenter : West US 2
Ring : 1
RoleInstance : MW2PEPF000031CC
ScaleUnit : 001
Slice : E
NonfatalStatus : 405

When no parameters are specified, the command targets the Graph endpoint of the current connection, in this case https://graph.microsoft.com, and outputs diagnostic information.

.EXAMPLE
Test-Graph -Cloud ChinaCloud

TestUri : https://microsoftgraph.chinacloudapi.cn/v1.0/$metadata
ServerTimestamp : 9/19/2021 2:21:03 AM +00:00
ClientElapsedTime (ms) : 19.0889
RequestId : 6150c057-020f-4f5d-b2c3-cc208570ae6b
DataCenter : China East
Ring : 6
RoleInstance : SH1NEPF00000388
ScaleUnit : 001
Slice : E
NonfatalStatus : 405

This command targets the Graph endpoint for the China cloud, https://microsoftgraph.chinacloudapi.cn, and outputs its diagnostic information.

.EXAMPLE
Test-Graph -RawContent

Date request-id x-ms-ags-diagnostic
---- ---------- -------------------
Tue, 21 Sep 2021 13:27:44 GMT 66a3c612-6887-4533-bc25-c4e7a226b85e @{ServerInfo=}

This command returns the same information as in the first example, but by specifying the RawContent parameter the command is directed not to output the response as deserialized structured objects, but as an object that contains the headers returned by Graph in response to the test request. Note that RawContent's output does not correspond exactly to the output returned when RawContent is not specified as the latter performs interpretation of the Graph response with context about local state such as the local system's time. In contrast, when RawContent is specified no interpretation is made of the results, they are simply returned as-is.

.LINK
Get-GraphCurrentConnection
New-GraphConnection
Connect-GraphApi
#>

function Test-Graph {
    [cmdletbinding(defaultparametersetname='currentconnection')]
    param(
        [parameter(parametersetname='KnownClouds')]
        [validateset("Public", "ChinaCloud", "USGovernmentCloud")]
        [string] $Cloud,

        [parameter(parametersetname='Connection', mandatory=$true)]
        [PSCustomObject] $Connection,

        [parameter(parametersetname='CustomEndpoint', mandatory=$true)]
        [Uri] $EndpointUri,

        [switch] $RawContent
    )
    Enable-ScriptClassVerbosePreference

    $logger = $::.RequestLog |=> GetDefault

    $graphEndpointUri = if ( $Connection ) {
        $Connection.GraphEndpoint.Graph
    } elseif ( $Cloud ) {
        (new-so GraphEndpoint $Cloud).Graph
    } elseif ( $endpointUri ) {
        $endpointUri
    } else {
        ($::.GraphContext |=> GetConnection).GraphEndpoint.Graph
    }

    $pingUri = [Uri]::new($graphEndpointUri, 'v1.0/$metadata')
    $request = new-so RESTRequest $pingUri HEAD
    $logEntry = if ( $logger ) { $logger |=> NewLogEntry $null $request }
    $responseException = $null
    $responseStatus = 0

    $response = try {
        $successfulResponse = $request |=> Invoke -logEntry $logEntry
        $successfulResponse
    } catch {
        $responseException = $_.Exception.InnerException.InnerException
        $responseException.Response
    } finally {
        if ( $logEntry ) { $logger |=> CommitLogEntry $logEntry }
    }

    $dateHeader = $null
    $requestId = $null

    $diagnosticHeaderName = 'x-ms-ags-diagnostic'

    $diagnosticInfo = if ( $response | get-member Headers -erroraction ignore ) {
        $responseHeaders = $::.HttpUtilities |=> NormalizeHeaders $response.headers
        $dateHeader = $responseHeaders['Date']
        $requestId = $responseHeaders['request-id']
        $responseHeaders[$diagnosticHeaderName]
    }

    $serverTime = if ( $dateHeader ) {
        $dateTime = [DateTimeOffset]::Now
        if ( [DateTimeOffset]::TryParse($dateHeader, [ref] $dateTime) ) {
            $dateTime
        } else {
            $dateHeader
        }
    }

    $logData = $logEntry.ToDisplayableObject()
    $responseStatus = $logData.Status
    $clientRequestTime = $logData.RequestTimestamp
    $clientResponseTime = $logData.ResponseTimestamp
    $clientElapsedTime = $clientResponseTime - $clientRequestTime

    if ( ! $diagnosticInfo ) {
        if ( $responseException ) {
            throw $responseException
        } else {
            throw "Graph URI '$graphEndpointUri' was unreachable or returned an unexpected response"
        }
    } elseif ( ! $RawContent.ispresent ) {
        # The [ordered] type adapter will ensure that enumeration of items in a hashtable
        # is sorted by insertion order
        $result = [ordered] @{
            TestUri = ($pingUri.ToString())
            ServerTimestamp = $serverTime
            ClientRequestTimestamp = $clientRequestTime
            ClientResponseTimestamp = $clientResponseTime
            ClientElapsedTime = $clientElapsedTime
            RequestId = $requestId
        }

        $content = $diagnosticInfo | convertfrom-json | select-object -ExpandProperty ServerInfo

        # Sort by name to get consistent sort formatting
        $content | gm -membertype noteproperty | sort-object name | foreach {
            $value = ($content | select -expandproperty $_.name)
            $result[$_.name] = $value
        }

        $result['NonfatalStatus'] = $responseStatus

        $asObject = [PSCustomObject] $result
        $asObject.pstypenames.insert(0, 'GraphEndpointTest')
        $asObject
    } else {
        [PSCustomObject] (
            [ordered] @{
                Date = $dateHeader
                'request-id' = $requestId
                $diagnosticHeaderName = $diagnosticInfo | ConvertFrom-Json
            }
        )
    }
}