Skip to content

Commit 4ce8a38

Browse files
committed
Preview Changes for action function only
1 parent 0226a0e commit 4ce8a38

6 files changed

Lines changed: 270 additions & 76 deletions

File tree

src/Microsoft.PowerApps.TestEngine/Config/TestSettingExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public class TestSettingExtensions
6060
public HashSet<string> DenyPowerFxNamespaces { get; set; } = new HashSet<string>();
6161

6262

63+
// <summary>
64+
// List of action class names (or wildcard patterns) that are allowed to be registered in the root namespace
65+
// </summary>
66+
public HashSet<string> AllowActionsInRoot { get; set; } = new HashSet<string>() { "PauseFunction" };
67+
6368
/// <summary>
6469
/// Additional optional parameters for extension modules
6570
/// </summary>

src/Microsoft.PowerApps.TestEngine/Modules/TestEngineExtensionChecker.cs

Lines changed: 160 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -313,14 +313,6 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
313313
{
314314
var isValid = true;
315315

316-
#if DEBUG
317-
// Add Experimenal namespaces in Debug compile if it has not been added in allow list
318-
if (!settings.AllowPowerFxNamespaces.Contains(NAMESPACE_PREVIEW))
319-
{
320-
settings.AllowPowerFxNamespaces.Add(NAMESPACE_PREVIEW);
321-
}
322-
#endif
323-
324316
#if RELEASE
325317
// Add Deprecated namespaces in Release compile if it has not been added in deny list
326318
if (!settings.DenyPowerFxNamespaces.Contains(NAMESPACE_DEPRECATED))
@@ -334,22 +326,29 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
334326
stream.Position = 0;
335327
ModuleDefinition module = ModuleDefinition.ReadModule(stream);
336328

337-
// Check if PauseModule exists and inspect its IsPreviewNamespaceEnabled property
338-
var pauseModule = module.Types.FirstOrDefault(t => t.Name == "PauseModule");
339-
if (pauseModule != null)
340-
{
341-
// Check if the PauseModule has IsPreviewNamespaceEnabled property
342-
var previewProperty = pauseModule.Properties.FirstOrDefault(p => p.Name == "IsPreviewNamespaceEnabled");
343-
if (previewProperty != null)
344-
{
345-
// If PauseModule has IsPreviewNamespaceEnabled property, enable Preview namespace
346-
// The property's value will be determined at runtime based on YAML settings
347-
settings.AllowPowerFxNamespaces.Add(NAMESPACE_PREVIEW);
348-
Logger?.LogInformation("Auto-enabled Preview namespace due to PauseModule.IsPreviewNamespaceEnabled property.");
349-
}
350-
}
351-
352-
// Get the source code of the assembly as will be used to check Power FX Namespaces
329+
// Detect if this assembly contains provider types so we can allow Preview for provider assemblies
330+
var assemblyHasProvider = module.GetAllTypes().Any(t =>
331+
t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Providers.ITestWebProvider).FullName) ||
332+
t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Users.IUserManager).FullName) ||
333+
t.Interfaces.Any(i => i.InterfaceType.FullName == typeof(Config.IUserCertificateProvider).FullName)
334+
);
335+
336+
// Check if PauseModule exists and inspect its IsPreviewNamespaceEnabled property
337+
var pauseModule = module.Types.FirstOrDefault(t => t.Name == "PauseModule"); // Local flag indicating PauseModule declares IsPreviewNamespaceEnabled
338+
if (pauseModule != null)
339+
{
340+
// Check if the PauseModule has IsPreviewNamespaceEnabled property
341+
var previewProperty = pauseModule.Properties.FirstOrDefault(p => p.Name == "IsPreviewNamespaceEnabled");
342+
if (previewProperty != null)
343+
{
344+
// Do not modify the global settings here. Instead record that PauseModule exposes the preview toggle.
345+
// The property's value will be determined at runtime based on YAML settings; use the flag to
346+
// selectively allow the Preview namespace for providers only.
347+
Logger?.LogInformation("Detected PauseModule.IsPreviewNamespaceEnabled; preview semantics will be applied per-type.");
348+
}
349+
}
350+
351+
// Get the source code of the assembly as will be used to check Power FX Namespaces
353352
var code = DecompileModuleToCSharp(assembly);
354353

355354
foreach (TypeDefinition type in module.GetAllTypes())
@@ -365,38 +364,45 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
365364
{
366365
if (CheckPropertyArrayContainsValue(type, "Namespaces", out var values))
367366
{
368-
foreach (var name in values)
367+
// For provider types, always allow Preview namespace (preview namespace checks apply to actions only)
368+
var allowedForProvider = settings.AllowPowerFxNamespaces.ToList();
369+
if (!allowedForProvider.Contains(NAMESPACE_PREVIEW))
369370
{
370-
// Check against deny list using regular expressions
371-
if (settings.DenyPowerFxNamespaces.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))))
372-
{
373-
Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}");
374-
return false;
375-
}
376-
377-
// Check against deny wildcard and allow list using regular expressions
378-
if (settings.DenyPowerFxNamespaces.Any(pattern => pattern == "*") &&
379-
(!settings.AllowPowerFxNamespaces.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))) &&
371+
allowedForProvider.Add(NAMESPACE_PREVIEW);
372+
}
373+
374+
foreach (var name in values)
375+
{
376+
// Check against deny list using regular expressions
377+
if (settings.DenyPowerFxNamespaces.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))))
378+
{
379+
Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}");
380+
return false;
381+
}
382+
383+
// Check against deny wildcard and allow list using regular expressions
384+
if (settings.DenyPowerFxNamespaces.Any(pattern => pattern == "*") &&
385+
(!allowedForProvider.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))) &&
380386
name != NAMESPACE_TEST_ENGINE))
381-
{
382-
Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}");
383-
return false;
384-
}
387+
{
388+
Logger.LogInformation($"Deny Power FX Namespace {name} for {type.Name}");
389+
return false;
390+
}
385391

386-
// Check against allow list using regular expressions
387-
if (!settings.AllowPowerFxNamespaces.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))) &&
392+
// Check against allow list using regular expressions
393+
if (!allowedForProvider.Any(pattern => Regex.IsMatch(name, WildcardToRegex(pattern))) &&
388394
name != NAMESPACE_TEST_ENGINE)
389-
{
390-
Logger.LogInformation($"Not allow Power FX Namespace {name} for {type.Name}");
391-
return false;
392-
}
393-
}
394-
}
395-
}
396-
397-
// Extension Module Check are based on constructor
398-
if (type.BaseType != null && type.BaseType.Name == "ReflectionFunction")
399-
{
395+
{
396+
Logger.LogInformation($"Not allow Power FX Namespace {name} for {type.Name}");
397+
return false;
398+
}
399+
}
400+
}
401+
}
402+
403+
// Extension Module Check are based on constructor
404+
if (type.BaseType != null && type.BaseType.Name == "ReflectionFunction")
405+
{
400406
// Special handling for PauseFunction - allow root namespace when PauseModule is present
401407
if (type.Name == "PauseFunction" && pauseModule != null)
402408
{
@@ -461,6 +467,13 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
461467
return false;
462468
}
463469

470+
// For functions defined in assemblies that are providers, allow Preview namespace
471+
var allowedForFunction = settings.AllowPowerFxNamespaces.ToList();
472+
if (assemblyHasProvider && !allowedForFunction.Contains(NAMESPACE_PREVIEW))
473+
{
474+
allowedForFunction.Add(NAMESPACE_PREVIEW);
475+
}
476+
464477
if (settings.DenyPowerFxNamespaces.Contains(name))
465478
{
466479
// Deny list match
@@ -469,8 +482,8 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
469482
}
470483

471484
if ((settings.DenyPowerFxNamespaces.Contains("*") && (
472-
!settings.AllowPowerFxNamespaces.Contains(name) ||
473-
(!settings.AllowPowerFxNamespaces.Contains(name) && name != NAMESPACE_TEST_ENGINE)
485+
!allowedForFunction.Contains(name) ||
486+
(!allowedForFunction.Contains(name) && name != NAMESPACE_TEST_ENGINE)
474487
)
475488
))
476489
{
@@ -479,13 +492,104 @@ public bool VerifyContainsValidNamespacePowerFxFunctions(TestSettingExtensions s
479492
return false;
480493
}
481494

482-
if (!settings.AllowPowerFxNamespaces.Contains(name) && name != NAMESPACE_TEST_ENGINE)
495+
if (!allowedForFunction.Contains(name) && name != NAMESPACE_TEST_ENGINE)
483496
{
484497
Logger.LogInformation($"Do not allow Power FX Namespace {name} for {type.Name}");
485498
// Not in allow list or the Reserved TestEngine namespace
486499
return false;
487500
}
488-
}
501+
}
502+
503+
// Special validation for ReflectionAction types. Actions must not declare the Preview namespace
504+
if (type.BaseType != null && type.BaseType.Name == "ReflectionAction")
505+
{
506+
// If PauseModule is present, allow certain actions to be declared in the root namespace (skip namespace validation)
507+
if (pauseModule != null)
508+
{
509+
// Check configured allow-list of action class names/wildcards
510+
var allowActions = settings.AllowActionsInRoot ?? new HashSet<string>();
511+
var isAllowedAction = allowActions.Any(pattern => Regex.IsMatch(type.Name, WildcardToRegex(pattern)));
512+
if (isAllowedAction)
513+
{
514+
Logger?.LogInformation($"Allowing action {type.Name} in root namespace due to PauseModule presence and AllowActionsInRoot setting.");
515+
continue; // Skip namespace validation for this action
516+
}
517+
}
518+
519+
var constructors = type.GetConstructors();
520+
521+
if (constructors.Count() == 0)
522+
{
523+
Logger.LogInformation($"No constructor defined for {type.Name}. Found {constructors.Count()} expected 1 or more");
524+
return false;
525+
}
526+
527+
var constructor = constructors.Where(c => c.HasBody).FirstOrDefault();
528+
529+
if (constructor == null || !constructor.HasBody)
530+
{
531+
Logger.LogInformation($"No constructor body defined for {type.Name}");
532+
return false;
533+
}
534+
535+
var baseCall = constructor.Body.Instructions?.FirstOrDefault(i => i.OpCode == OpCodes.Call && i.Operand is MethodReference && ((MethodReference)i.Operand).Name == ".ctor");
536+
if (baseCall == null)
537+
{
538+
Logger.LogInformation($"No base constructor defined for {type.Name}");
539+
return false;
540+
}
541+
542+
MethodReference baseConstructor = (MethodReference)baseCall.Operand;
543+
if (baseConstructor.Parameters?.Count() < 2)
544+
{
545+
Logger.LogInformation($"No not enough parameters for {type.Name}");
546+
return false;
547+
}
548+
549+
if (baseConstructor.Parameters[0].ParameterType.FullName != "Microsoft.PowerFx.Core.Utils.DPath")
550+
{
551+
Logger.LogInformation($"No Power FX Namespace for {type.Name}");
552+
return false;
553+
}
554+
555+
// Extract namespace from decompiled source
556+
var actionName = GetPowerFxNamespace(type.Name, code);
557+
if (string.IsNullOrEmpty(actionName))
558+
{
559+
Logger.LogInformation($"No Power FX Namespace found for {type.Name}");
560+
return false;
561+
}
562+
563+
// Actions must not use the Preview namespace
564+
if (string.Equals(actionName, NAMESPACE_PREVIEW, StringComparison.OrdinalIgnoreCase))
565+
{
566+
Logger.LogInformation($"Deny Preview Power FX Namespace {actionName} for action {type.Name}");
567+
return false;
568+
}
569+
570+
// Continue with the same allow/deny validation as functions
571+
if (settings.DenyPowerFxNamespaces.Contains(actionName))
572+
{
573+
Logger.LogInformation($"Deny Power FX Namespace {actionName} for {type.Name}");
574+
return false;
575+
}
576+
577+
if ((settings.DenyPowerFxNamespaces.Contains("*") && (
578+
!settings.AllowPowerFxNamespaces.Contains(actionName) ||
579+
(!settings.AllowPowerFxNamespaces.Contains(actionName) && actionName != NAMESPACE_TEST_ENGINE)
580+
)
581+
))
582+
{
583+
Logger.LogInformation($"Deny Power FX Namespace {actionName} for {type.Name}");
584+
return false;
585+
}
586+
587+
if (!settings.AllowPowerFxNamespaces.Contains(actionName) && actionName != NAMESPACE_TEST_ENGINE)
588+
{
589+
Logger.LogInformation($"Do not allow Power FX Namespace {actionName} for {type.Name}");
590+
return false;
591+
}
592+
}
489593
}
490594
}
491595
return isValid;

src/testengine.module.pause.tests/PauseFunctionTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,40 @@ public void SkipExecute()
101101
It.IsAny<Exception>(),
102102
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.AtLeastOnce);
103103
}
104+
105+
[Fact]
106+
public void PauseExecuteWithPreviewNamespaceTracking()
107+
{
108+
// Arrange - Test that Preview namespace tracking works correctly
109+
var pauseModuleMock = new Mock<PauseModule>();
110+
pauseModuleMock.SetupGet(p => p.IsPreviewNamespaceEnabled).Returns(true);
111+
112+
var module = new PauseFunction(MockTestInfraFunctions.Object, MockTestState.Object, MockLogger.Object, pauseModuleMock.Object);
113+
var settings = new TestSettings() { Headless = false };
114+
var mockContext = new Mock<IBrowserContext>(MockBehavior.Strict);
115+
var mockPage = new Mock<IPage>(MockBehavior.Strict);
116+
117+
MockTestState.Setup(x => x.GetTestSettings()).Returns(settings);
118+
MockTestInfraFunctions.Setup(x => x.GetContext()).Returns(mockContext.Object);
119+
mockContext.Setup(x => x.Pages).Returns(new List<IPage>() { mockPage.Object });
120+
mockPage.Setup(x => x.PauseAsync()).Returns(Task.CompletedTask);
121+
122+
MockLogger.Setup(x => x.Log(
123+
It.IsAny<LogLevel>(),
124+
It.IsAny<EventId>(),
125+
It.IsAny<It.IsAnyType>(),
126+
It.IsAny<Exception>(),
127+
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));
128+
129+
// Act
130+
module.Execute();
131+
132+
// Assert - Function should still work and log Preview tracking information
133+
MockLogger.Verify(l => l.Log(It.Is<LogLevel>(l => l == LogLevel.Information),
134+
It.IsAny<EventId>(),
135+
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Preview namespace enabled in configuration: True")),
136+
It.IsAny<Exception>(),
137+
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.AtLeastOnce);
138+
}
104139
}
105140
}

src/testengine.module.pause.tests/PauseModuleTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ public void RegisterPowerFxFunction()
6868
// Act
6969
module.RegisterPowerFxFunction(TestConfig, MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object);
7070

71-
// Assert
71+
// Assert - Updated expected message to reflect new logging format
7272
MockLogger.Verify(l => l.Log(It.Is<LogLevel>(l => l == LogLevel.Information),
7373
It.IsAny<EventId>(),
74-
It.Is<It.IsAnyType>((v, t) => v.ToString() == "Registered Pause()"),
74+
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Registered Pause() function")),
7575
It.IsAny<Exception>(),
7676
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.AtLeastOnce);
7777
}

0 commit comments

Comments
 (0)