@@ -4,23 +4,80 @@ function Get-CIPPTestData {
44 Cached wrapper around New-CIPPDbRequest for test functions
55
66 . DESCRIPTION
7- Returns cached tenant data during test suite execution. The cache is
8- backed by CIPP.TestDataCache (static ConcurrentDictionary in C#) so
9- it is shared across all PowerShell runspaces within the worker process.
7+ Returns cached tenant data during test suite execution. The cache is backed by
8+ CIPP.TestDataCache (static ConcurrentDictionary in C#) so it is shared across all
9+ PowerShell runspaces within the worker process.
10+
11+ RECORDS ARE FIELD-PROJECTED. Only the fields listed for $Type in
12+ Get-CippTestDataFieldManifest are materialized. Reading an unlisted field returns $null
13+ with no error, so the test emits a wrong verdict silently. If you add a field read to a
14+ test, add it to the manifest first. A type absent from the manifest is fetched whole,
15+ which is the safe default.
16+
17+ Projection is record-level: a kept field keeps its entire subtree, so
18+ `$policy.conditions.users.includeRoles` only requires 'conditions'.
19+
20+ Records are PSCustomObject and must stay so. Hashtable output changes scalar semantics
21+ ($x[0] becomes a key lookup returning $null; $x.Count returns the record's field count
22+ instead of 1) and only when a pipeline yields exactly one record — data-dependent, and
23+ easily missed in testing.
1024
1125 . PARAMETER TenantFilter
1226 The tenant domain or GUID to filter by
1327
1428 . PARAMETER Type
1529 The data type to retrieve (e.g., Users, Groups, ConditionalAccessPolicies)
30+
31+ . PARAMETER Fields
32+ Optional. Override the manifest for this call: keep only these top-level fields. Prefer
33+ the manifest; this is for one-off and diagnostic callers, not tests.
34+
35+ The field set is part of the cache key — it must be, or callers asking for the same
36+ tenant+type with different lists would receive each other's projection. Distinct lists
37+ therefore fragment the cache: the same rows parsed once per list, all alive together for
38+ the TTL. Per-call-site lists measured several times worse than one shared per-type entry.
39+ Fields belong to the type — see Get-CippTestDataFieldManifest.
40+
41+ . PARAMETER NoProjection
42+ Return every field, ignoring the manifest.
43+
44+ Required for the custom-script sandbox (Get-CippSandboxData): those scripts are
45+ customer-authored, so the fields they read are unknowable and a manifest built from our
46+ own tests would silently hand them incomplete records. Any caller fetching data on behalf
47+ of code we did not write must pass this.
48+
49+ . EXAMPLE
50+ Get-CIPPTestData -TenantFilter $Tenant -Type 'Users'
51+ Normal use: the manifest decides the fields. This is what tests should do.
52+
53+ . EXAMPLE
54+ Get-CIPPTestData -TenantFilter $Tenant -Type 'Users' -NoProjection
55+ Every field. For callers serving code we did not author.
56+
57+ . NOTES
58+ Verifying a change: "0 errored" proves nothing, because the failure modes here are silent
59+ — a broken test still reports success. Diff test verdicts against a baseline, across more
60+ than one tenant: a bug that needs exactly one matching record will not show on a tenant
61+ that has two.
62+
63+ Useful endpoints: ExecTestRun then ListTests for verdicts; ListDBCache to confirm a field
64+ exists and its real casing before adding it to the manifest (type=_availableTypes lists
65+ the types); ListWorkerHealth?Action=CacheDiag for entries, hit rate and a per-type
66+ breakdown whose keys carry the projected field list.
1667 #>
1768 [CmdletBinding ()]
1869 param (
1970 [Parameter (Mandatory = $false )]
2071 [string ]$TenantFilter ,
2172
2273 [Parameter (Mandatory = $false )]
23- [string ]$Type
74+ [string ]$Type ,
75+
76+ [Parameter (Mandatory = $false )]
77+ [string []]$Fields ,
78+
79+ [Parameter (Mandatory = $false )]
80+ [switch ]$NoProjection
2481 )
2582
2683 # Enforce tenant lock when running inside custom script execution
@@ -32,14 +89,36 @@ function Get-CIPPTestData {
3289 throw ' TenantFilter is required.'
3390 }
3491
35- $CacheKey = ' {0}|{1}' -f $TenantFilter , $Type
92+ # Resolve the field set: an explicit -Fields wins, otherwise consult the per-type manifest.
93+ # -NoProjection suppresses both (the sandbox path must never receive projected records).
94+ $EffectiveFields = if ($NoProjection ) {
95+ $null
96+ } elseif ($Fields ) {
97+ $Fields
98+ } else {
99+ Get-CippTestDataFieldManifest - Type $Type
100+ }
101+
102+ # Normalize the field set (sorted + de-duplicated) so that callers asking for the same
103+ # fields in a different order share one cache entry instead of parsing twice.
104+ $NormalizedFields = if ($EffectiveFields ) { @ ($EffectiveFields | Sort-Object - Unique) } else { $null }
105+
106+ $CacheKey = if ($NormalizedFields ) {
107+ ' {0}|{1}|{2}' -f $TenantFilter , $Type , ($NormalizedFields -join ' ,' )
108+ } else {
109+ ' {0}|{1}' -f $TenantFilter , $Type
110+ }
36111
37112 $CachedValue = $null
38113 if ([CIPP.TestDataCache ]::TryGet($CacheKey , [ref ]$CachedValue )) {
39114 return $CachedValue
40115 }
41116
42- $Data = New-CIPPDbRequest - TenantFilter $TenantFilter - Type $Type
117+ $Data = if ($NormalizedFields ) {
118+ New-CIPPDbRequest - TenantFilter $TenantFilter - Type $Type - Fields $NormalizedFields
119+ } else {
120+ New-CIPPDbRequest - TenantFilter $TenantFilter - Type $Type
121+ }
43122
44123 [CIPP.TestDataCache ]::Set($CacheKey , $Data )
45124
0 commit comments