@@ -382,6 +382,28 @@ public void SetupConfigFile5()
382382 CreateBrokenConfigFile ( currentUserConfigFile ) ;
383383 }
384384
385+ public void SetupBrokenCurrentUserConfigOnly ( )
386+ {
387+ CleanupConfigFiles ( ) ;
388+
389+ // Only the per-user config file is broken; the system-wide file is absent.
390+ // Used to verify the #27370 fail-open path for ConfigScope.CurrentUser.
391+ CreateBrokenConfigFile ( currentUserConfigFile ) ;
392+ }
393+
394+ public void SetupTypeMismatchedCurrentUserConfig ( )
395+ {
396+ CleanupConfigFiles ( ) ;
397+
398+ // Per-user config is syntactically valid JSON, but PowerShellPolicies has the
399+ // wrong nested shape: ScriptExecution should be an object but is a string.
400+ // This triggers JsonSerializationException inside jToken.ToObject<T>() and
401+ // exercises the per-key fail-open path for #27370.
402+ File . WriteAllText (
403+ currentUserConfigFile ,
404+ "{ \" PowerShellPolicies\" : { \" ScriptExecution\" : \" not-an-object\" } }" ) ;
405+ }
406+
385407 private static void CreateBrokenConfigFile ( string fileName )
386408 {
387409 File . WriteAllText ( fileName , "[abbra" ) ;
@@ -978,14 +1000,105 @@ public void Utils_GetPolicySetting_BothConfigFilesNotExist()
9781000 fixture . CompareConsoleSessionConfiguration ( consoleSessionConfiguration , null ) ;
9791001 }
9801002
981- [ Fact , Priority ( 11 ) ]
982- public void PowerShellConfig_GetPowerShellPolicies_BrokenSystemConfig ( )
1003+ [ Fact ]
1004+ [ Priority ( 11 ) ]
1005+ public void PowerShellConfig_BrokenSystemConfig_Throws_BrokenCurrentUserConfig_FallsBack ( )
9831006 {
9841007 fixture . SetupConfigFile5 ( ) ;
9851008 fixture . ForceReadingFromFile ( ) ;
9861009
1010+ // Broken system-wide config still hard-fails (admin-owned, security-relevant).
9871011 Assert . Throws < System . Management . Automation . PSInvalidOperationException > ( ( ) => PowerShellConfig . Instance . GetPowerShellPolicies ( ConfigScope . AllUsers ) ) ;
988- Assert . Throws < System . Management . Automation . PSInvalidOperationException > ( ( ) => PowerShellConfig . Instance . GetPowerShellPolicies ( ConfigScope . CurrentUser ) ) ;
1012+
1013+ // Broken per-user config falls back to defaults (#27370).
1014+ PowerShellPolicies currentUserPolicies = PowerShellConfig . Instance . GetPowerShellPolicies ( ConfigScope . CurrentUser ) ;
1015+ Assert . Null ( currentUserPolicies ) ;
1016+ }
1017+
1018+ [ Fact ]
1019+ [ Priority ( 12 ) ]
1020+ public void PowerShellConfig_BrokenCurrentUserConfig_FallsBackToDefaults ( )
1021+ {
1022+ fixture . SetupBrokenCurrentUserConfigOnly ( ) ;
1023+ fixture . ForceReadingFromFile ( ) ;
1024+
1025+ // ExecutionPolicy returns null (caller falls back to GP / registry / Restricted).
1026+ string execPolicy = PowerShellConfig . Instance . GetExecutionPolicy ( ConfigScope . CurrentUser , "Microsoft.PowerShell" ) ;
1027+ Assert . Null ( execPolicy ) ;
1028+
1029+ // Module path returns null (caller uses built-in defaults).
1030+ string modulePath = PowerShellConfig . Instance . GetModulePath ( ConfigScope . CurrentUser ) ;
1031+ Assert . Null ( modulePath ) ;
1032+
1033+ // Experimental features list is empty.
1034+ string [ ] features = PowerShellConfig . Instance . GetExperimentalFeatures ( ) ;
1035+ Assert . Empty ( features ) ;
1036+ }
1037+
1038+ [ Fact ]
1039+ [ Priority ( 13 ) ]
1040+ public void PowerShellConfig_TypeMismatchInCurrentUserConfig_FallsBackToDefaults ( )
1041+ {
1042+ // Verifies the per-key fail-open path: the file parses as JSON, but a value
1043+ // can't be materialized as its target type. Without this fallback the
1044+ // JsonSerializationException would propagate out of GetPolicySetting and
1045+ // crash startup just like #27370.
1046+ fixture . SetupTypeMismatchedCurrentUserConfig ( ) ;
1047+ fixture . ForceReadingFromFile ( ) ;
1048+
1049+ PowerShellPolicies policies = PowerShellConfig . Instance . GetPowerShellPolicies ( ConfigScope . CurrentUser ) ;
1050+ Assert . Null ( policies ) ;
1051+ }
1052+
1053+ [ Fact ]
1054+ [ Priority ( 14 ) ]
1055+ public void PowerShellConfig_BrokenCurrentUserConfig_EmitsWarningToStderr ( )
1056+ {
1057+ // A regression in the warning path would still let startup succeed (this is the
1058+ // whole point of the fail-open) -- but the user would silently lose their config
1059+ // with no indication. This test pins the user-visible warning so that doesn't
1060+ // happen quietly.
1061+ fixture . SetupBrokenCurrentUserConfigOnly ( ) ;
1062+ fixture . ForceReadingFromFile ( ) ;
1063+
1064+ string stderr = CaptureStderr ( ( ) =>
1065+ PowerShellConfig . Instance . GetExecutionPolicy ( ConfigScope . CurrentUser , "Microsoft.PowerShell" ) ) ;
1066+
1067+ Assert . Contains ( "WARNING:" , stderr ) ;
1068+ Assert . Contains ( "Falling back to default settings" , stderr ) ;
1069+ }
1070+
1071+ [ Fact ]
1072+ [ Priority ( 15 ) ]
1073+ public void PowerShellConfig_TypeMismatchInCurrentUserConfig_EmitsWarningToStderr ( )
1074+ {
1075+ // Pin the user-visible warning for the per-key fail-open path as well.
1076+ fixture . SetupTypeMismatchedCurrentUserConfig ( ) ;
1077+ fixture . ForceReadingFromFile ( ) ;
1078+
1079+ string stderr = CaptureStderr ( ( ) =>
1080+ PowerShellConfig . Instance . GetPowerShellPolicies ( ConfigScope . CurrentUser ) ) ;
1081+
1082+ Assert . Contains ( "WARNING:" , stderr ) ;
1083+ Assert . Contains ( "PowerShellPolicies" , stderr ) ;
1084+ Assert . Contains ( "Falling back to default for this setting" , stderr ) ;
1085+ }
1086+
1087+ private static string CaptureStderr ( Action action )
1088+ {
1089+ var captured = new StringWriter ( ) ;
1090+ TextWriter originalError = Console . Error ;
1091+ try
1092+ {
1093+ Console . SetError ( captured ) ;
1094+ action ( ) ;
1095+ }
1096+ finally
1097+ {
1098+ Console . SetError ( originalError ) ;
1099+ }
1100+
1101+ return captured . ToString ( ) ;
9891102 }
9901103 }
9911104}
0 commit comments