tests/Client/ReadingRows.Tests.ps1

<#
    Integration Test: reading rows (brief lot 2)
    Get-XrmMultipleRecords -AsArray / -AsEntity, Get-XrmRecord -AsEntity / -IfExists / -Unique / -Attributes,
    Get-XrmAttributeValue (Get-XrmRowValue) on converted rows, Get-XrmMetadataValue, Resolve-XrmEntityReference,
    Get-XrmRecordCount, Get-XrmTotalRecordCount -SkipRefused -AsHashtable, Get-XrmRecordIds.
    Known issues 2.0.0.65: @(... -AsArray) message (K04), FetchExpression paging (K13), aggregate limit detection (K01), Remove-XrmRecord -IfExists (K14).
#>

. "$PSScriptRoot\..\_TestConfig.ps1";

$prefix = Get-TestName -Prefix "Rows";
$accountIds = [System.Collections.Generic.List[Guid]]::new();
$contactIds = [System.Collections.Generic.List[Guid]]::new();

function New-TestAccount([string]$Name, [hashtable]$Attributes = @{}) {
    $columns = @{ name = $Name };
    foreach ($key in $Attributes.Keys) { $columns[$key] = $Attributes[$key]; }
    $record = New-XrmEntity -LogicalName "account" -Attributes $columns;
    $record.Id = $Global:XrmClient | Add-XrmRecord -Record $record;
    $accountIds.Add($record.Id);
    return $record.ToEntityReference();
}

function New-PrefixQuery {
    $query = New-XrmQueryExpression -LogicalName "account" -Columns "name", "industrycode", "donotemail", "creditlimit", "parentaccountid";
    return ($query | Add-XrmQueryCondition -Field "name" -Condition BeginsWith -Values $prefix);
}

Write-Section "Setup";
$account1 = New-TestAccount -Name "$prefix-1" -Attributes @{ industrycode = (New-XrmOptionSetValue -Value 1); donotemail = $true; creditlimit = (New-XrmMoney -Value 100) };
$account2 = New-TestAccount -Name "$prefix-2" -Attributes @{ parentaccountid = $account1 };
$account3 = New-TestAccount -Name "$prefix-3";
foreach ($i in 1..2) {
    $contact = New-XrmEntity -LogicalName "contact" -Attributes @{ lastname = "$prefix-contact-$i"; parentcustomerid = $account1 };
    $contactIds.Add(($Global:XrmClient | Add-XrmRecord -Record $contact));
}
Assert-Test "3 accounts and 2 contacts created" { $accountIds.Count -eq 3 -and $contactIds.Count -eq 2 };

# ============================================================
# L01 / L02 Get-XrmMultipleRecords -AsArray / -AsEntity
# ============================================================
Write-Section "Get-XrmMultipleRecords -AsArray / -AsEntity";

$none = New-XrmQueryExpression -LogicalName "account" | Add-XrmQueryCondition -Field "name" -Condition Equal -Values "$prefix-none";
$noneRows = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $none -AsArray;
Assert-Test "-AsArray: no row gives an empty array" { $null -ne $noneRows -and $noneRows -is [array] -and $noneRows.Count -eq 0 };

$one = New-XrmQueryExpression -LogicalName "account" -Columns "name" | Add-XrmQueryCondition -Field "name" -Condition Equal -Values "$prefix-1";
$oneRows = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $one -AsArray;
Assert-Test "-AsArray: one row gives an array of one" { $oneRows -is [array] -and $oneRows.Count -eq 1 -and $oneRows[0].name -eq "$prefix-1" };

$oneDefault = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $one;
Assert-Test "Default output unchanged: one row gives a single object" { $oneDefault -isnot [array] -and $oneDefault.name -eq "$prefix-1" };

$entities = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query (New-PrefixQuery) -AsEntity -AsArray;
Assert-Test "-AsEntity -AsArray: 3 SDK entities" { $entities.Count -eq 3 -and @($entities | Where-Object { $_ -isnot [Microsoft.Xrm.Sdk.Entity] }).Count -eq 0 };

# @() around -AsArray nests the whole result in a one-element array: Get-XrmRowValue names the probable cause
$wrapped = @(Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query (New-PrefixQuery) -AsArray);
$wrapError = $null;
try { foreach ($row in $wrapped) { Get-XrmRowValue -Record $row -Name "name" | Out-Null; } } catch { $wrapError = $_.Exception.Message; }
Assert-Test "@(... -AsArray): one element, and Get-XrmRowValue points to -AsArray" { $wrapped.Count -eq 1 -and $wrapError -like "*-AsArray*@()*" };

# ============================================================
# K13 Get-XrmMultipleRecords with a FetchExpression
# ============================================================
Write-Section "Get-XrmMultipleRecords with a FetchExpression";

$fetchXml = "<fetch><entity name='account'><attribute name='name' /><filter><condition attribute='name' operator='begins-with' value='$prefix' /></filter><order attribute='name' /></entity></fetch>";
$fetchExpression = New-XrmFetchExpression -FetchXml $fetchXml;
$fetchRows = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $fetchExpression -PageSize 2 -AsArray;
Assert-Test "FetchExpression: the 3 rows, read in pages of 2" { $fetchRows.Count -eq 3 -and $fetchRows[0].name -eq "$prefix-1" -and $fetchRows[2].name -eq "$prefix-3" };
Assert-Test "Caller's FetchExpression unchanged" { $fetchExpression.Query -eq $fetchXml };

$topFetch = New-XrmFetchExpression -FetchXml $fetchXml.Replace("<fetch>", "<fetch top='2'>");
Assert-Test "Fetch with top: one call, top rows only" { (Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $topFetch -PageSize 1 -AsArray).Count -eq 2 };

$aggregateFetch = New-XrmFetchExpression -FetchXml "<fetch aggregate='true'><entity name='account'><attribute name='accountid' alias='total' aggregate='count' /><filter><condition attribute='name' operator='begins-with' value='$prefix' /></filter></entity></fetch>";
$aggregateRows = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $aggregateFetch -AsEntity -AsArray;
Assert-Test "Aggregate fetch: one row, read in one call" { $aggregateRows.Count -eq 1 -and $aggregateRows[0]["total"].Value -eq 3 };

# ============================================================
# L03 Get-XrmAttributeValue / Get-XrmRowValue
# ============================================================
Write-Section "Get-XrmAttributeValue on converted rows";

$row1 = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Id $account1.Id -Columns "industrycode", "donotemail", "creditlimit";
$row2 = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Id $account2.Id -Columns "parentaccountid", "telephone1";
Assert-Test "Converted row keeps the label under the column name (why this cmdlet exists)" { $row1.donotemail -is [string] };
Assert-Test "Boolean read from a converted row is a bool" { ($row1 | Get-XrmRowValue -Name "donotemail") -is [bool] -and ($row1 | Get-XrmRowValue -Name "donotemail") -eq $true };
Assert-Test "-Raw: OptionSetValue => int" { ($row1 | Get-XrmAttributeValue -Name "industrycode" -Raw) -eq 1 };
Assert-Test "-Raw: Money => decimal" { ($row1 | Get-XrmAttributeValue -Name "creditlimit" -Raw) -eq [decimal]100 };
Assert-Test "-FormattedValue: label" { ($row1 | Get-XrmAttributeValue -Name "industrycode" -FormattedValue) -is [string] };
Assert-Test "-AsId: lookup Guid" { ($row2 | Get-XrmAttributeValue -Name "parentaccountid" -AsId) -eq $account1.Id };
Assert-Test "Missing column gives `$null" { $null -eq ($row2 | Get-XrmAttributeValue -Name "telephone1") };
Assert-Test "`$null record gives `$null" { $null -eq (Get-XrmAttributeValue -Record $null -Name "name") };

$aliasQuery = New-XrmQueryExpression -LogicalName "account" -Columns "name" | Add-XrmQueryCondition -Field "accountid" -Condition Equal -Values $account2.Id;
$aliasLink = $aliasQuery | Add-XrmQueryLink -ToEntityName "account" -FromAttributeName "parentaccountid" -ToAttributeName "accountid" -Alias "parent";
$aliasLink | Add-XrmQueryLinkColumns -Columns "industrycode" | Out-Null;
$aliasRow = Get-XrmMultipleRecords -XrmClient $Global:XrmClient -Query $aliasQuery | Select-Object -First 1;
Assert-Test "-Raw unwraps an AliasedValue (linked option set => int)" { ($aliasRow | Get-XrmRowValue -Name "parent.industrycode" -Raw) -eq 1 };

$typeError = $null;
try { Get-XrmAttributeValue -Record "not a row" -Name "name" | Out-Null; } catch { $typeError = $_.Exception.Message; }
Assert-Test "Another object type raises an error" { $typeError -like "*Entity*" };

Assert-Test "Parameter aliases -Row / -Column" { (Get-XrmRowValue -Row $row1 -Column "donotemail") -eq $true };
# Scripts that defined their own Get-XrmRowValue (-Row -Column) or Get-XrmAttributeValue (-Record -Attribute) before the module had the alias
$shadowed = & {
    function Get-XrmRowValue { param($Row, $Column) "caller helper"; }
    function Get-XrmAttributeValue { param($Record, $Attribute) "caller helper"; }
    Get-XrmRowValue -Row $row1 -Column "donotemail";
};
Assert-Test "The alias reaches the module command even when the caller defines functions with the same names" { $shadowed -is [bool] -and $shadowed -eq $true };

# ============================================================
# L04 Get-XrmMetadataValue
# ============================================================
Write-Section "Get-XrmMetadataValue";

$accountMetadata = $Global:XrmClient | Get-XrmEntityMetadata -LogicalName "account" -Filter ([Microsoft.Xrm.Sdk.Metadata.EntityFilters]::Entity);
Assert-Test "Managed property => bool value" { (Get-XrmMetadataValue -Metadata $accountMetadata -Property "IsAuditEnabled") -is [bool] };
Assert-Test "-CanBeChanged => bool" { (Get-XrmMetadataValue -Metadata $accountMetadata -Property "IsAuditEnabled" -CanBeChanged) -is [bool] };
Assert-Test "Label => text for -LanguageCode 1033" { ($accountMetadata | Get-XrmMetadataValue -Property "DisplayName" -LanguageCode 1033) -eq "Account" };
Assert-Test "Dotted path" { ($accountMetadata | Get-XrmMetadataValue -Property "DisplayName.UserLocalizedLabel.LanguageCode") -gt 0 };
Assert-Test "Plain property" { ($accountMetadata | Get-XrmMetadataValue -Property "PrimaryIdAttribute") -eq "accountid" };
$canBeChangedError = $null;
try { $accountMetadata | Get-XrmMetadataValue -Property "LogicalName" -CanBeChanged | Out-Null; } catch { $canBeChangedError = $_.Exception.Message; }
Assert-Test "-CanBeChanged on a plain property raises an error" { $canBeChangedError -like "*not a managed property*" };

$nameColumn = $Global:XrmClient | Get-XrmColumn -EntityLogicalName "account" -LogicalName "name";
Assert-Test "AttributeRequiredLevelManagedProperty => enum value" { ($nameColumn | Get-XrmMetadataValue -Property "RequiredLevel") -is [Microsoft.Xrm.Sdk.Metadata.AttributeRequiredLevel] };

# ============================================================
# L05 / L06 Get-XrmRecord
# ============================================================
Write-Section "Get-XrmRecord -IfExists / -Unique / -Attributes / -AsEntity";

$missingId = [Guid]::NewGuid();
$missingError = $null;
$missing = "not set";
try { $missing = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Id $missingId -IfExists; } catch { $missingError = $_.Exception.Message; }
Assert-Test "-IfExists: missing id gives `$null without error" { $null -eq $missingError -and $null -eq $missing };

$asEntity = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Id $account1.Id -Columns "name" -AsEntity;
Assert-Test "-AsEntity: SDK entity" { $asEntity -is [Microsoft.Xrm.Sdk.Entity] -and $asEntity["name"] -eq "$prefix-1" };

$byAttributes = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Attributes @{ name = "$prefix-1"; industrycode = (New-XrmOptionSetValue -Value 1) } -Columns "name";
Assert-Test "-Attributes: AND of conditions, OptionSetValue compared on value" { $null -ne $byAttributes -and $byAttributes.Id -eq $account1.Id };

$byLookup = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Attributes @{ parentaccountid = $account1; name = "$prefix-2" };
Assert-Test "-Attributes: EntityReference compared on id" { $null -ne $byLookup -and $byLookup.Id -eq $account2.Id };

$byNull = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Attributes @{ name = "$prefix-3"; parentaccountid = $null };
Assert-Test "-Attributes: `$null value matches an empty column" { $null -ne $byNull -and $byNull.Id -eq $account3.Id };

$noMatch = $Global:XrmClient | Get-XrmRecord -LogicalName "account" -Attributes @{ name = "$prefix-3"; parentaccountid = $account1 };
Assert-Test "-Attributes: no match gives `$null" { $null -eq $noMatch };

$duplicate = New-TestAccount -Name "$prefix-1";
$uniqueError = $null;
try { $Global:XrmClient | Get-XrmRecord -LogicalName "account" -AttributeName "name" -Value "$prefix-1" -Unique | Out-Null; } catch { $uniqueError = $_.Exception.Message; }
Assert-Test "-Unique: two matches raise an error" { $uniqueError -like "*More than one*" };

# ============================================================
# L07 Resolve-XrmEntityReference
# ============================================================
Write-Section "Resolve-XrmEntityReference";

$resolved = Resolve-XrmEntityReference -XrmClient $Global:XrmClient -LogicalName "account" -Attributes @{ name = "$prefix-2" };
Assert-Test "Found: EntityReference of the row" { $resolved -is [Microsoft.Xrm.Sdk.EntityReference] -and $resolved.Id -eq $account2.Id -and $resolved.LogicalName -eq "account" };

$notFoundError = $null;
try { Resolve-XrmEntityReference -XrmClient $Global:XrmClient -LogicalName "account" -Attributes @{ name = "$prefix-none" } | Out-Null; } catch { $notFoundError = $_.Exception.Message; }
Assert-Test "Not found raises an error" { $notFoundError -like "*No 'account' row*" };
Assert-Test "-IfExists: not found gives `$null" { $null -eq (Resolve-XrmEntityReference -XrmClient $Global:XrmClient -LogicalName "account" -Attributes @{ name = "$prefix-none" } -IfExists) };

$ambiguousError = $null;
try { Resolve-XrmEntityReference -XrmClient $Global:XrmClient -LogicalName "account" -Attributes @{ name = "$prefix-1" } | Out-Null; } catch { $ambiguousError = $_.Exception.Message; }
Assert-Test "Several matches raise an error" { $ambiguousError -like "*More than one*" };

$cache = @{};
$first = Resolve-XrmEntityReference -XrmClient $Global:XrmClient -LogicalName "account" -Attributes @{ name = "$prefix-3" } -Cache $cache;
$Global:XrmClient | Remove-XrmRecord -LogicalName "account" -Id $account3.Id;
$accountIds.Remove($account3.Id) | Out-Null;
$second = Resolve-XrmEntityReference -XrmClient $Global:XrmClient -LogicalName "account" -Attributes @{ name = "$PREFIX-3".ToUpperInvariant() } -Cache $cache;
Assert-Test "-Cache: second resolution served from the caller's cache (case-insensitive key)" { $cache.Count -eq 1 -and $second.Id -eq $first.Id };

# ============================================================
# L08 Get-XrmRecordCount / Get-XrmTotalRecordCount
# ============================================================
Write-Section "Get-XrmRecordCount";

$countQuery = New-XrmQueryExpression -LogicalName "account" -Columns "name" | Add-XrmQueryCondition -Field "name" -Condition BeginsWith -Values $prefix;
Assert-Test "Aggregate count (3 accounts: 2 + the duplicate)" { (Get-XrmRecordCount -XrmClient $Global:XrmClient -Query $countQuery) -eq 3 };
Assert-Test "-NoAggregate: same count by paging" { (Get-XrmRecordCount -XrmClient $Global:XrmClient -Query $countQuery -NoAggregate) -eq 3 };
Assert-Test "Caller's query columns untouched" { $countQuery.ColumnSet.Columns.Count -eq 1 -and $countQuery.ColumnSet.Columns[0] -eq "name" };

$linkedQuery = New-XrmQueryExpression -LogicalName "account" | Add-XrmQueryCondition -Field "name" -Condition BeginsWith -Values $prefix;
$contactLink = $linkedQuery | Add-XrmQueryLink -ToEntityName "contact" -FromAttributeName "accountid" -ToAttributeName "parentcustomerid";
Assert-Test "One-to-many link: an account with 2 contacts counts once" { (Get-XrmRecordCount -XrmClient $Global:XrmClient -Query $linkedQuery) -eq 1 };
Assert-Test "One-to-many link with -NoAggregate: counts once" { (Get-XrmRecordCount -XrmClient $Global:XrmClient -Query $linkedQuery -NoAggregate) -eq 1 };

$fetch = "<fetch><entity name='contact'><attribute name='lastname' /><filter><condition attribute='lastname' operator='like' value='$prefix-contact-%' /></filter></entity></fetch>";
Assert-Test "-FetchXml: 2 contacts" { (Get-XrmRecordCount -XrmClient $Global:XrmClient -FetchXml $fetch) -eq 2 };

$topQuery = New-XrmQueryExpression -LogicalName "account" -TopCount 2 | Add-XrmQueryCondition -Field "name" -Condition BeginsWith -Values $prefix;
Assert-Test "TopCount caps the count" { (Get-XrmRecordCount -XrmClient $Global:XrmClient -Query $topQuery) -eq 2 };

# K01: the refusal above 50,000 rows comes with two wordings and a fault code; the test instance has no such table, so the detection is checked on built exceptions
$moduleScope = Get-Module PowerDataOps;
Assert-Test "Aggregate limit recognized: 'AggregateQueryRecordLimit' wording" { & $moduleScope { Test-XrmAggregateLimitErrorInternal -Exception ([System.Exception]::new("AggregateQueryRecordLimit exceeded. Cannot perform this operation.")) } };
Assert-Test "Aggregate limit recognized: 'maximum record limit of 50000' wording, as an inner exception" {
    & $moduleScope { Test-XrmAggregateLimitErrorInternal -Exception ([System.Exception]::new("Request failed", [System.Exception]::new("The maximum record limit of 50000 is exceeded. Reduce the number of records."))) };
};
Assert-Test "Aggregate limit recognized: fault code 0x8004E023 whatever the text" {
    & $moduleScope {
        $fault = [Microsoft.Xrm.Sdk.OrganizationServiceFault]::new();
        $fault.ErrorCode = -2147164125;
        Test-XrmAggregateLimitErrorInternal -Exception ([System.ServiceModel.FaultException[Microsoft.Xrm.Sdk.OrganizationServiceFault]]::new($fault, "Some other text"));
    };
};
Assert-Test "Another error is not taken for the limit" { -not (& $moduleScope { Test-XrmAggregateLimitErrorInternal -Exception ([System.Exception]::new("Principal user is missing prvReadAccount privilege")) }) };

$totals = Get-XrmTotalRecordCount -XrmClient $Global:XrmClient -LogicalNames "account", "contact" -AsHashtable;
Assert-Test "Get-XrmTotalRecordCount -AsHashtable" { $totals -is [hashtable] -and $totals.ContainsKey("account") -and $totals.ContainsKey("contact") };

$skipped = Get-XrmTotalRecordCount -XrmClient $Global:XrmClient -LogicalNames "account", "pdo_nosuchtable" -SkipRefused -AsHashtable -WarningAction SilentlyContinue;
Assert-Test "-SkipRefused: an unknown table does not fail the call" { $skipped.ContainsKey("account") -and -not $skipped.ContainsKey("pdo_nosuchtable") };

# ============================================================
# L09 Get-XrmRecordIds
# ============================================================
Write-Section "Get-XrmRecordIds";

$idQuery = New-XrmQueryExpression -LogicalName "account" -Columns "name" | Add-XrmQueryCondition -Field "name" -Condition BeginsWith -Values $prefix;
$ids = Get-XrmRecordIds -XrmClient $Global:XrmClient -Query $idQuery;
Assert-Test "-Query: HashSet of the 3 ids" { $ids -is [System.Collections.Generic.HashSet[Guid]] -and $ids.Count -eq 3 -and $ids.Contains($account1.Id) -and $ids.Contains($duplicate.Id) };
Assert-Test "Caller's query columns restored" { $idQuery.ColumnSet.Columns.Count -eq 1 };

$emptyIds = Get-XrmRecordIds -XrmClient $Global:XrmClient -Query $none;
Assert-Test "No row: empty HashSet, not `$null" { $emptyIds -is [System.Collections.Generic.HashSet[Guid]] -and $emptyIds.Count -eq 0 };

$businessUnitIds = Get-XrmRecordIds -XrmClient $Global:XrmClient -LogicalName "businessunit";
Assert-Test "-LogicalName: all rows of the table" { $businessUnitIds.Count -ge 1 };

# ============================================================
# K14 Remove-XrmRecord -IfExists
# ============================================================
Write-Section "Remove-XrmRecord -IfExists";

$ifExistsError = $null;
try { $Global:XrmClient | Remove-XrmRecord -LogicalName "account" -Id ([Guid]::NewGuid()) -IfExists -ErrorAction Stop; } catch { $ifExistsError = $_.Exception.Message; }
Assert-Test "-IfExists: missing row, no error" { $null -eq $ifExistsError };

# Without -ErrorAction Stop: any error other than "not found" must stop the caller, like the other -IfExists switches
$otherError = $null;
$reached = $false;
try { $Global:XrmClient | Remove-XrmRecord -LogicalName "organization" -Id ([Guid]::NewGuid()) -IfExists; $reached = $true; } catch { $otherError = $_.Exception.Message; }
Assert-Test "-IfExists: another error is raised ($otherError)" { $null -ne $otherError -and -not $reached };

# ============================================================
# CLEANUP
# ============================================================
Write-Section "Cleanup";
foreach ($id in $contactIds) { try { $Global:XrmClient | Remove-XrmRecord -LogicalName "contact" -Id $id; } catch { } }
foreach ($id in $accountIds) { try { $Global:XrmClient | Remove-XrmRecord -LogicalName "account" -Id $id; } catch { } }
Assert-Test "Cleanup complete" { $true };

Write-TestSummary;