From 06c081397eb0ea14d1a04c7b4ec6d04bf790639b Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Tue, 10 Mar 2026 07:28:14 +0300 Subject: [PATCH 1/3] Add testing APIs --- OrchardCoreContrib.Testing.sln | 11 +- .../AgencySiteContext.cs | 6 + .../BlogSiteContext.cs | 6 + .../ISiteContext.cs | 20 +++ .../ISiteContextOfT.cs | 6 + .../ModuleNamesProvider.cs | 21 +++ .../OrchardCoreContrib.Testing.csproj | 13 ++ .../OrchardCoreStartup.cs | 29 ++++ .../OrchardCoreWebApplicationFactory.cs | 27 ++++ .../SaasSiteContext.cs | 6 + .../PermissionContextAuthorizationHandler.cs | 83 ++++++++++++ .../Security/PermissionsContext.cs | 15 +++ .../SiteContextBase.cs | 125 ++++++++++++++++++ .../SiteContextOptionDefaults.cs | 16 +++ .../SiteContextOptions.cs | 32 +++++ 15 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 src/OrchardCoreContrib.Testing/AgencySiteContext.cs create mode 100644 src/OrchardCoreContrib.Testing/BlogSiteContext.cs create mode 100644 src/OrchardCoreContrib.Testing/ISiteContext.cs create mode 100644 src/OrchardCoreContrib.Testing/ISiteContextOfT.cs create mode 100644 src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs create mode 100644 src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj create mode 100644 src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs create mode 100644 src/OrchardCoreContrib.Testing/OrchardCoreWebApplicationFactory.cs create mode 100644 src/OrchardCoreContrib.Testing/SaasSiteContext.cs create mode 100644 src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs create mode 100644 src/OrchardCoreContrib.Testing/Security/PermissionsContext.cs create mode 100644 src/OrchardCoreContrib.Testing/SiteContextBase.cs create mode 100644 src/OrchardCoreContrib.Testing/SiteContextOptionDefaults.cs create mode 100644 src/OrchardCoreContrib.Testing/SiteContextOptions.cs diff --git a/OrchardCoreContrib.Testing.sln b/OrchardCoreContrib.Testing.sln index 5d6aa80..57b5195 100644 --- a/OrchardCoreContrib.Testing.sln +++ b/OrchardCoreContrib.Testing.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34701.34 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{20F306B8-D63F-4A61-AF6E-64B4E0918E30}" EndProject @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Testing. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Testing.UI.Tests", "test\OrchardCoreContrib.Testing.UI.Tests\OrchardCoreContrib.Testing.UI.Tests.csproj", "{985F5AD6-8C18-4E63-A35C-A5673F237A4D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Testing", "src\OrchardCoreContrib.Testing\OrchardCoreContrib.Testing.csproj", "{60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +27,10 @@ Global {985F5AD6-8C18-4E63-A35C-A5673F237A4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {985F5AD6-8C18-4E63-A35C-A5673F237A4D}.Release|Any CPU.ActiveCfg = Release|Any CPU {985F5AD6-8C18-4E63-A35C-A5673F237A4D}.Release|Any CPU.Build.0 = Release|Any CPU + {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -32,6 +38,7 @@ Global GlobalSection(NestedProjects) = preSolution {A9E5A40B-78F8-4F02-9E73-C74F395B5BBD} = {20F306B8-D63F-4A61-AF6E-64B4E0918E30} {985F5AD6-8C18-4E63-A35C-A5673F237A4D} = {27507B03-7D7C-492D-92A4-FF5812B9D7DB} + {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58} = {20F306B8-D63F-4A61-AF6E-64B4E0918E30} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DD153137-BF4D-4977-A4D7-4C448D23479A} diff --git a/src/OrchardCoreContrib.Testing/AgencySiteContext.cs b/src/OrchardCoreContrib.Testing/AgencySiteContext.cs new file mode 100644 index 0000000..1a58442 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/AgencySiteContext.cs @@ -0,0 +1,6 @@ +namespace OrchardCoreContrib.Testing; + +public class AgencySiteContext : SiteContextBase> where TEntryPoint : class +{ + public AgencySiteContext() => Options.RecipeName = "Agency"; +} diff --git a/src/OrchardCoreContrib.Testing/BlogSiteContext.cs b/src/OrchardCoreContrib.Testing/BlogSiteContext.cs new file mode 100644 index 0000000..0730384 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/BlogSiteContext.cs @@ -0,0 +1,6 @@ +namespace OrchardCoreContrib.Testing; + +public class BlogSiteContext : SiteContextBase> where TEntryPoint : class +{ + public BlogSiteContext() => Options.RecipeName = "Blog"; +} diff --git a/src/OrchardCoreContrib.Testing/ISiteContext.cs b/src/OrchardCoreContrib.Testing/ISiteContext.cs new file mode 100644 index 0000000..5aba3c2 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/ISiteContext.cs @@ -0,0 +1,20 @@ +using OrchardCore.Environment.Shell; + +namespace OrchardCoreContrib.Testing; + +public interface ISiteContext : IDisposable +{ + static IShellHost ShellHost { get; } + + static IShellSettingsManager ShellSettingsManager { get; } + + static HttpClient DefaultTenantClient { get; } + + SiteContextOptions Options { init; get; } + + HttpClient Client { get; } + + string TenantName { get; } + + Task InitializeAsync(); +} diff --git a/src/OrchardCoreContrib.Testing/ISiteContextOfT.cs b/src/OrchardCoreContrib.Testing/ISiteContextOfT.cs new file mode 100644 index 0000000..fe30b25 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/ISiteContextOfT.cs @@ -0,0 +1,6 @@ +namespace OrchardCoreContrib.Testing; + +public interface ISiteContext : ISiteContext where TSiteStartup : class +{ + static OrchardCoreWebApplicationFactory Site { get; } +} diff --git a/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs b/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs new file mode 100644 index 0000000..842ef64 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs @@ -0,0 +1,21 @@ +using OrchardCore.Modules; +using OrchardCore.Modules.Manifest; +using System.Reflection; + +namespace OrchardCoreContrib.Testing; + +internal sealed class ModuleNamesProvider : IModuleNamesProvider +{ + private readonly IEnumerable _moduleNames; + + public ModuleNamesProvider(Assembly assembly) + { + ArgumentNullException.ThrowIfNull(assembly); + + _moduleNames = Assembly.Load(new AssemblyName(assembly.GetName().Name)) + .GetCustomAttributes() + .Select(m => m.Name); + } + + public IEnumerable GetModuleNames() => _moduleNames; +} diff --git a/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj b/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj new file mode 100644 index 0000000..d73bab8 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + + + + + + + + diff --git a/src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs b/src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs new file mode 100644 index 0000000..c4749b5 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Modules; +using OrchardCoreContrib.Testing.Security; + +namespace OrchardCoreContrib.Testing; + +public class OrchardCoreStartup : StartupBase where TEntryPoint : class +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddOrchardCms(builder => builder + .AddSetupFeatures("OrchardCore.Tenants") + .ConfigureServices(serviceCollection => + { + serviceCollection.AddScoped(sp => + new PermissionContextAuthorizationHandler(sp.GetRequiredService(), SiteContextOptions.PermissionsContexts)); + }) + .Configure(appBuilder => appBuilder.UseAuthorization())); + + services.AddSingleton(new ModuleNamesProvider(typeof(TEntryPoint).Assembly)); + } + + public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + => app.UseOrchardCore(); +} \ No newline at end of file diff --git a/src/OrchardCoreContrib.Testing/OrchardCoreWebApplicationFactory.cs b/src/OrchardCoreContrib.Testing/OrchardCoreWebApplicationFactory.cs new file mode 100644 index 0000000..0bfe02f --- /dev/null +++ b/src/OrchardCoreContrib.Testing/OrchardCoreWebApplicationFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Hosting; + +namespace OrchardCoreContrib.Testing; + +public class OrchardCoreWebApplicationFactory : WebApplicationFactory where TEntryPoint : class +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + var shellsApplicationDataPath = Path.Combine(Directory.GetCurrentDirectory(), "App_Data"); + + if (Directory.Exists(shellsApplicationDataPath)) + { + Directory.Delete(shellsApplicationDataPath, true); + } + + builder.UseContentRoot(Directory.GetCurrentDirectory()); + } + + protected override IWebHostBuilder CreateWebHostBuilder() + => WebHostBuilderFactory.CreateFromAssemblyEntryPoint(typeof(TEntryPoint).Assembly, []); + + protected override IHostBuilder CreateHostBuilder() + => Host.CreateDefaultBuilder().ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); +} diff --git a/src/OrchardCoreContrib.Testing/SaasSiteContext.cs b/src/OrchardCoreContrib.Testing/SaasSiteContext.cs new file mode 100644 index 0000000..28a7a70 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/SaasSiteContext.cs @@ -0,0 +1,6 @@ +namespace OrchardCoreContrib.Testing; + +public class SaasSiteContext : SiteContextBase> where TEntryPoint : class +{ + public SaasSiteContext() => Options.RecipeName = "Saas"; +} diff --git a/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs b/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs new file mode 100644 index 0000000..bc6654b --- /dev/null +++ b/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using OrchardCore.Security; +using OrchardCore.Security.Permissions; + +namespace OrchardCoreContrib.Testing.Security; + +internal sealed class PermissionContextAuthorizationHandler : AuthorizationHandler +{ + private readonly PermissionsContext _permissionsContext; + + public PermissionContextAuthorizationHandler(IHttpContextAccessor httpContextAccessor, IDictionary permissionsContexts) + { + _permissionsContext = new PermissionsContext(); + + if (httpContextAccessor.HttpContext is null) + { + return; + } + + var request = httpContextAccessor.HttpContext.Request; + + if (request?.Headers.ContainsKey(nameof(PermissionsContext)) == true && + permissionsContexts.TryGetValue(request.Headers[nameof(PermissionsContext)], out var permissionsContext)) + { + _permissionsContext = permissionsContext; + } + } + + public PermissionContextAuthorizationHandler(PermissionsContext permissionsContext) + { + _permissionsContext = permissionsContext; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) + { + var permissions = (_permissionsContext.AuthorizedPermissions ?? []).ToList(); + + if (!_permissionsContext.UsePermissionsContext) + { + context.Succeed(requirement); + } + else if (permissions.Contains(requirement.Permission)) + { + context.Succeed(requirement); + } + else + { + var grantingNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + GetGrantingNamesInternal(requirement.Permission, grantingNames); + + if (permissions.Any(p => grantingNames.Contains(p.Name))) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + + return Task.CompletedTask; + } + + private static void GetGrantingNamesInternal(Permission permission, HashSet stack) + { + stack.Add(permission.Name); + + if (permission.ImpliedBy != null && permission.ImpliedBy.Any()) + { + foreach (var impliedBy in permission.ImpliedBy) + { + if (impliedBy == null || stack.Contains(impliedBy.Name)) + { + continue; + } + + GetGrantingNamesInternal(impliedBy, stack); + } + } + } +} diff --git a/src/OrchardCoreContrib.Testing/Security/PermissionsContext.cs b/src/OrchardCoreContrib.Testing/Security/PermissionsContext.cs new file mode 100644 index 0000000..d58ddbc --- /dev/null +++ b/src/OrchardCoreContrib.Testing/Security/PermissionsContext.cs @@ -0,0 +1,15 @@ +using OrchardCore.Security.Permissions; + +namespace OrchardCoreContrib.Testing.Security; + +public class PermissionsContext +{ + public PermissionsContext() + { + AuthorizedPermissions = []; + UsePermissionsContext = false; + } + public IEnumerable AuthorizedPermissions { get; set; } + + public bool UsePermissionsContext { get; set; } +} diff --git a/src/OrchardCoreContrib.Testing/SiteContextBase.cs b/src/OrchardCoreContrib.Testing/SiteContextBase.cs new file mode 100644 index 0000000..d39166c --- /dev/null +++ b/src/OrchardCoreContrib.Testing/SiteContextBase.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.BackgroundTasks; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Scope; +using OrchardCore.Tenants.ViewModels; +using OrchardCoreContrib.Testing.Security; +using System.Net.Http.Json; + +namespace OrchardCoreContrib.Testing; + +public abstract class SiteContextBase : ISiteContext where TSiteStartup : class +{ + static SiteContextBase() + { + Site = new OrchardCoreWebApplicationFactory(); + ShellHost = Site.Services.GetRequiredService(); + ShellSettingsManager = Site.Services.GetRequiredService(); + HttpContextAccessor = Site.Services.GetRequiredService(); + DefaultTenantClient = Site.CreateDefaultClient(); + } + + public SiteContextBase() + { + Options = new SiteContextOptions(); + } + + public static OrchardCoreWebApplicationFactory Site { get; } + + public static IShellHost ShellHost { get; private set; } + + public static IShellSettingsManager ShellSettingsManager { get; private set; } + + public static IHttpContextAccessor HttpContextAccessor { get; } + + public static HttpClient DefaultTenantClient { get; } + + public SiteContextOptions Options { init; get; } + + public HttpClient Client { get; private set; } + + public string TenantName { get; private set; } + + public virtual async Task InitializeAsync() + { + var tenantName = Guid.NewGuid().ToString("n"); + + var response = await CreateSiteAsync(tenantName); + + var content = await response.Content.ReadAsStringAsync(); + + await SetupSiteAsync(tenantName); + + lock (Site) + { + var url = new Uri(content.Trim('"')); + url = new Uri(url.Scheme + "://" + url.Authority + url.LocalPath + "/"); + + Client = Site.CreateDefaultClient(url); + + TenantName = tenantName; + } + + if (Options.PermissionsContext is not null) + { + var permissionContextKey = Guid.NewGuid().ToString(); + + SiteContextOptions.PermissionsContexts.TryAdd(permissionContextKey, Options.PermissionsContext); + + Client.DefaultRequestHeaders.Add(nameof(PermissionsContext), permissionContextKey); + } + } + + public void Dispose() => Client?.Dispose(); + + private async Task CreateSiteAsync(string tenantName) + { + var model = new CreateApiViewModel + { + DatabaseProvider = Options.DatabaseProvider, + TablePrefix = Options.TablePrefix, + ConnectionString = Options.ConnectionString, + RecipeName = Options.RecipeName, + Name = tenantName, + RequestUrlPrefix = tenantName + }; + + var result = await DefaultTenantClient.PostAsJsonAsync("api/tenants/create", model); + + result.EnsureSuccessStatusCode(); + + return result; + } + + private async Task SetupSiteAsync(string tenantName) + { + var model = new SetupApiViewModel + { + SiteName = Options.SiteName, + DatabaseProvider = Options.DatabaseProvider, + TablePrefix = Options.TablePrefix, + ConnectionString = Options.ConnectionString, + RecipeName = Options.RecipeName, + UserName = Options.Username, + Password = Options.Password, + Name = tenantName, + Email = Options.Email + }; + + var result = await DefaultTenantClient.PostAsJsonAsync("api/tenants/setup", model); + + result.EnsureSuccessStatusCode(); + } + + public async Task UsingTenantScopeAsync(Func execute, bool activateShell = true) + { + var shellScope = await ShellHost.GetScopeAsync(TenantName); + + HttpContextAccessor.HttpContext = shellScope.ShellContext.CreateHttpContext(); + + await shellScope.UsingAsync(execute, activateShell); + + HttpContextAccessor.HttpContext = null; + } +} diff --git a/src/OrchardCoreContrib.Testing/SiteContextOptionDefaults.cs b/src/OrchardCoreContrib.Testing/SiteContextOptionDefaults.cs new file mode 100644 index 0000000..4d73bbb --- /dev/null +++ b/src/OrchardCoreContrib.Testing/SiteContextOptionDefaults.cs @@ -0,0 +1,16 @@ +namespace OrchardCoreContrib.Testing; + +internal sealed class SiteContextOptionDefaults +{ + public const string RecipeName = "Blog"; + + public const string DatabaseProvider = "Sqlite"; + + public const string SiteName = "Orchard Core Contrib"; + + public const string Username = "admin"; + + public const string Password = "P@ssw0rd"; + + public const string Email = "admin@orchardcorecontrib.com"; +} diff --git a/src/OrchardCoreContrib.Testing/SiteContextOptions.cs b/src/OrchardCoreContrib.Testing/SiteContextOptions.cs new file mode 100644 index 0000000..8c3b133 --- /dev/null +++ b/src/OrchardCoreContrib.Testing/SiteContextOptions.cs @@ -0,0 +1,32 @@ +using OrchardCoreContrib.Testing.Security; +using System.Collections.Concurrent; + +namespace OrchardCoreContrib.Testing; + +public class SiteContextOptions +{ + static SiteContextOptions() + { + PermissionsContexts = new(); + } + + public static ConcurrentDictionary PermissionsContexts { get; set; } + + public string SiteName { get; set; } = SiteContextOptionDefaults.SiteName; + + public string Username { get; set; } = SiteContextOptionDefaults.Username; + + public string Password { get; set; } = SiteContextOptionDefaults.Password; + + public string Email { get; set; } = SiteContextOptionDefaults.Email; + + public string RecipeName { get; set; } = SiteContextOptionDefaults.RecipeName; + + public string DatabaseProvider { get; set; } = SiteContextOptionDefaults.DatabaseProvider; + + public string ConnectionString { get; set; } + + public string TablePrefix { get; set; } + + public PermissionsContext PermissionsContext { get; set; } +} From e1e53aa3629f646cda59389b8d86ef99e01464d0 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Tue, 10 Mar 2026 22:25:58 +0300 Subject: [PATCH 2/3] Remove ubuntu from GitHub workflow --- .github/workflows/build.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bdfced8..e1138fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,12 +10,8 @@ env: jobs: test: - runs-on: ${{ matrix.os }} + runs-on: windows-latest name: Build & Test - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, windows-latest ] steps: - uses: actions/checkout@v4 - name: Setup .NET Core @@ -29,10 +25,5 @@ jobs: - name: Install Playwright browsers & dependencies run: pwsh test/OrchardCoreContrib.Testing.UI.Tests/bin/Release/net8.0/playwright.ps1 install --with-deps - name: Test - run: | - if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then - xvfb-run dotnet test test/OrchardCoreContrib.Testing.UI.Tests -c Release --no-restore --verbosity normal - else + run: dotnet test test/OrchardCoreContrib.Testing.UI.Tests -c Release --no-restore --verbosity normal - fi - shell: bash From db35b8504bad30187f72dac956715c9050ad230e Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Wed, 11 Mar 2026 00:11:13 +0300 Subject: [PATCH 3/3] Add unit tests --- .gitignore | 1 + OrchardCoreContrib.Testing.sln | 14 ++ .../NLog.config | 32 +++++ .../OrchardCoreContrib.Testing.Web.csproj | 25 ++++ src/OrchardCoreContrib.Testing.Web/Program.cs | 35 +++++ .../Properties/launchSettings.json | 27 ++++ .../appsettings.json | 9 ++ .../wwwroot/.placeholder | 1 + .../AgencySiteContext.cs | 6 - .../BlogSiteContext.cs | 6 - .../ModuleNamesProvider.cs | 2 +- .../OrchardCoreContrib.Testing.csproj | 4 +- .../SaasSiteContext.cs | 6 - .../PermissionContextAuthorizationHandler.cs | 2 +- .../SiteContextBase.cs | 6 +- .../BlogSiteContext.cs | 6 + .../OrchardCoreApplicationTests.cs | 43 ++++++ .../OrchardCoreContrib.Testing.Tests.csproj | 26 ++++ .../OrchardCoreStartup.cs | 12 +- .../SiteContextTests.cs | 122 ++++++++++++++++++ 20 files changed, 354 insertions(+), 31 deletions(-) create mode 100644 src/OrchardCoreContrib.Testing.Web/NLog.config create mode 100644 src/OrchardCoreContrib.Testing.Web/OrchardCoreContrib.Testing.Web.csproj create mode 100644 src/OrchardCoreContrib.Testing.Web/Program.cs create mode 100644 src/OrchardCoreContrib.Testing.Web/Properties/launchSettings.json create mode 100644 src/OrchardCoreContrib.Testing.Web/appsettings.json create mode 100644 src/OrchardCoreContrib.Testing.Web/wwwroot/.placeholder delete mode 100644 src/OrchardCoreContrib.Testing/AgencySiteContext.cs delete mode 100644 src/OrchardCoreContrib.Testing/BlogSiteContext.cs delete mode 100644 src/OrchardCoreContrib.Testing/SaasSiteContext.cs create mode 100644 test/OrchardCoreContrib.Testing.Tests/BlogSiteContext.cs create mode 100644 test/OrchardCoreContrib.Testing.Tests/OrchardCoreApplicationTests.cs create mode 100644 test/OrchardCoreContrib.Testing.Tests/OrchardCoreContrib.Testing.Tests.csproj rename {src/OrchardCoreContrib.Testing => test/OrchardCoreContrib.Testing.Tests}/OrchardCoreStartup.cs (65%) create mode 100644 test/OrchardCoreContrib.Testing.Tests/SiteContextTests.cs diff --git a/.gitignore b/.gitignore index 8a30d25..3b81dcb 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +src/OrchardCoreContrib.Testing.Web/Localization diff --git a/OrchardCoreContrib.Testing.sln b/OrchardCoreContrib.Testing.sln index 57b5195..c2560c1 100644 --- a/OrchardCoreContrib.Testing.sln +++ b/OrchardCoreContrib.Testing.sln @@ -13,6 +13,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCoreContrib.Testing. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Testing", "src\OrchardCoreContrib.Testing\OrchardCoreContrib.Testing.csproj", "{60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Testing.Web", "src\OrchardCoreContrib.Testing.Web\OrchardCoreContrib.Testing.Web.csproj", "{A0FE9046-5B92-4C73-AB51-33CE15F14917}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCoreContrib.Testing.Tests", "test\OrchardCoreContrib.Testing.Tests\OrchardCoreContrib.Testing.Tests.csproj", "{F0C2E52D-4412-4A18-B0F7-FD99F85F192C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,6 +35,14 @@ Global {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Debug|Any CPU.Build.0 = Debug|Any CPU {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Release|Any CPU.ActiveCfg = Release|Any CPU {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58}.Release|Any CPU.Build.0 = Release|Any CPU + {A0FE9046-5B92-4C73-AB51-33CE15F14917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0FE9046-5B92-4C73-AB51-33CE15F14917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0FE9046-5B92-4C73-AB51-33CE15F14917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0FE9046-5B92-4C73-AB51-33CE15F14917}.Release|Any CPU.Build.0 = Release|Any CPU + {F0C2E52D-4412-4A18-B0F7-FD99F85F192C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0C2E52D-4412-4A18-B0F7-FD99F85F192C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0C2E52D-4412-4A18-B0F7-FD99F85F192C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0C2E52D-4412-4A18-B0F7-FD99F85F192C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -39,6 +51,8 @@ Global {A9E5A40B-78F8-4F02-9E73-C74F395B5BBD} = {20F306B8-D63F-4A61-AF6E-64B4E0918E30} {985F5AD6-8C18-4E63-A35C-A5673F237A4D} = {27507B03-7D7C-492D-92A4-FF5812B9D7DB} {60B9E226-55FF-4B5C-BFFE-57A14DE6CC58} = {20F306B8-D63F-4A61-AF6E-64B4E0918E30} + {A0FE9046-5B92-4C73-AB51-33CE15F14917} = {20F306B8-D63F-4A61-AF6E-64B4E0918E30} + {F0C2E52D-4412-4A18-B0F7-FD99F85F192C} = {27507B03-7D7C-492D-92A4-FF5812B9D7DB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DD153137-BF4D-4977-A4D7-4C448D23479A} diff --git a/src/OrchardCoreContrib.Testing.Web/NLog.config b/src/OrchardCoreContrib.Testing.Web/NLog.config new file mode 100644 index 0000000..410f7bf --- /dev/null +++ b/src/OrchardCoreContrib.Testing.Web/NLog.config @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCoreContrib.Testing.Web/OrchardCoreContrib.Testing.Web.csproj b/src/OrchardCoreContrib.Testing.Web/OrchardCoreContrib.Testing.Web.csproj new file mode 100644 index 0000000..31bf943 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.Web/OrchardCoreContrib.Testing.Web.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + InProcess + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCoreContrib.Testing.Web/Program.cs b/src/OrchardCoreContrib.Testing.Web/Program.cs new file mode 100644 index 0000000..6de8170 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.Web/Program.cs @@ -0,0 +1,35 @@ +using OrchardCore.Logging; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseNLogHost(); + +builder.Services + .AddOrchardCms() +// // Orchard Specific Pipeline +// .ConfigureServices( services => { +// }) +// .Configure( (app, routes, services) => { +// }) +; + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseOrchardCore(); + +app.Run(); + +namespace OrchardCoreContrib.Testing.Web +{ + public partial class Program; +} diff --git a/src/OrchardCoreContrib.Testing.Web/Properties/launchSettings.json b/src/OrchardCoreContrib.Testing.Web/Properties/launchSettings.json new file mode 100644 index 0000000..87329d2 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.Web/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8080", + "sslPort": 44300 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "OrchardCoreContrib.Testing.Web": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/OrchardCoreContrib.Testing.Web/appsettings.json b/src/OrchardCoreContrib.Testing.Web/appsettings.json new file mode 100644 index 0000000..6d27889 --- /dev/null +++ b/src/OrchardCoreContrib.Testing.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, +} diff --git a/src/OrchardCoreContrib.Testing.Web/wwwroot/.placeholder b/src/OrchardCoreContrib.Testing.Web/wwwroot/.placeholder new file mode 100644 index 0000000..46b134b --- /dev/null +++ b/src/OrchardCoreContrib.Testing.Web/wwwroot/.placeholder @@ -0,0 +1 @@ +ÿþ \ No newline at end of file diff --git a/src/OrchardCoreContrib.Testing/AgencySiteContext.cs b/src/OrchardCoreContrib.Testing/AgencySiteContext.cs deleted file mode 100644 index 1a58442..0000000 --- a/src/OrchardCoreContrib.Testing/AgencySiteContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OrchardCoreContrib.Testing; - -public class AgencySiteContext : SiteContextBase> where TEntryPoint : class -{ - public AgencySiteContext() => Options.RecipeName = "Agency"; -} diff --git a/src/OrchardCoreContrib.Testing/BlogSiteContext.cs b/src/OrchardCoreContrib.Testing/BlogSiteContext.cs deleted file mode 100644 index 0730384..0000000 --- a/src/OrchardCoreContrib.Testing/BlogSiteContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OrchardCoreContrib.Testing; - -public class BlogSiteContext : SiteContextBase> where TEntryPoint : class -{ - public BlogSiteContext() => Options.RecipeName = "Blog"; -} diff --git a/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs b/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs index 842ef64..c3183ad 100644 --- a/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs +++ b/src/OrchardCoreContrib.Testing/ModuleNamesProvider.cs @@ -4,7 +4,7 @@ namespace OrchardCoreContrib.Testing; -internal sealed class ModuleNamesProvider : IModuleNamesProvider +public sealed class ModuleNamesProvider : IModuleNamesProvider { private readonly IEnumerable _moduleNames; diff --git a/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj b/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj index d73bab8..4fb5f7f 100644 --- a/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj +++ b/src/OrchardCoreContrib.Testing/OrchardCoreContrib.Testing.csproj @@ -6,7 +6,9 @@ - + + + diff --git a/src/OrchardCoreContrib.Testing/SaasSiteContext.cs b/src/OrchardCoreContrib.Testing/SaasSiteContext.cs deleted file mode 100644 index 28a7a70..0000000 --- a/src/OrchardCoreContrib.Testing/SaasSiteContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OrchardCoreContrib.Testing; - -public class SaasSiteContext : SiteContextBase> where TEntryPoint : class -{ - public SaasSiteContext() => Options.RecipeName = "Saas"; -} diff --git a/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs b/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs index bc6654b..63cb97f 100644 --- a/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs +++ b/src/OrchardCoreContrib.Testing/Security/PermissionContextAuthorizationHandler.cs @@ -5,7 +5,7 @@ namespace OrchardCoreContrib.Testing.Security; -internal sealed class PermissionContextAuthorizationHandler : AuthorizationHandler +public sealed class PermissionContextAuthorizationHandler : AuthorizationHandler { private readonly PermissionsContext _permissionsContext; diff --git a/src/OrchardCoreContrib.Testing/SiteContextBase.cs b/src/OrchardCoreContrib.Testing/SiteContextBase.cs index d39166c..456bb37 100644 --- a/src/OrchardCoreContrib.Testing/SiteContextBase.cs +++ b/src/OrchardCoreContrib.Testing/SiteContextBase.cs @@ -9,11 +9,11 @@ namespace OrchardCoreContrib.Testing; -public abstract class SiteContextBase : ISiteContext where TSiteStartup : class +public abstract class SiteContextBase : ISiteContext where TEntryPoint : class { static SiteContextBase() { - Site = new OrchardCoreWebApplicationFactory(); + Site = new OrchardCoreWebApplicationFactory(); ShellHost = Site.Services.GetRequiredService(); ShellSettingsManager = Site.Services.GetRequiredService(); HttpContextAccessor = Site.Services.GetRequiredService(); @@ -25,7 +25,7 @@ public SiteContextBase() Options = new SiteContextOptions(); } - public static OrchardCoreWebApplicationFactory Site { get; } + public static OrchardCoreWebApplicationFactory Site { get; } public static IShellHost ShellHost { get; private set; } diff --git a/test/OrchardCoreContrib.Testing.Tests/BlogSiteContext.cs b/test/OrchardCoreContrib.Testing.Tests/BlogSiteContext.cs new file mode 100644 index 0000000..e62aba4 --- /dev/null +++ b/test/OrchardCoreContrib.Testing.Tests/BlogSiteContext.cs @@ -0,0 +1,6 @@ +namespace OrchardCoreContrib.Testing.Tests; + +public class BlogSiteContext : SiteContextBase +{ + public BlogSiteContext() => Options.RecipeName = "Blog"; +} diff --git a/test/OrchardCoreContrib.Testing.Tests/OrchardCoreApplicationTests.cs b/test/OrchardCoreContrib.Testing.Tests/OrchardCoreApplicationTests.cs new file mode 100644 index 0000000..cafba5a --- /dev/null +++ b/test/OrchardCoreContrib.Testing.Tests/OrchardCoreApplicationTests.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.ContentManagement; +using System.Net; + +namespace OrchardCoreContrib.Testing.Tests; + +public class OrchardCoreApplicationTests +{ + [Fact] + public async Task IndexPage_ShouldContainsBlogInItsContent() + { + // Arrange + var context = new BlogSiteContext(); + + await context.InitializeAsync(); + + // Act + var response = await context.Client.GetAsync("/"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Blog", content); + } + + [Fact] + public async Task Tenant_ShouldAccessServiceFromDIContainer() + { + // Arrange + var context = new BlogSiteContext(); + + await context.InitializeAsync(); + + // Act & Assert + await context.UsingTenantScopeAsync(async scope => + { + var siteService = scope.ServiceProvider.GetRequiredService(); + + Assert.NotNull(siteService); + }); + } +} \ No newline at end of file diff --git a/test/OrchardCoreContrib.Testing.Tests/OrchardCoreContrib.Testing.Tests.csproj b/test/OrchardCoreContrib.Testing.Tests/OrchardCoreContrib.Testing.Tests.csproj new file mode 100644 index 0000000..0315bc7 --- /dev/null +++ b/test/OrchardCoreContrib.Testing.Tests/OrchardCoreContrib.Testing.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + false + true + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs b/test/OrchardCoreContrib.Testing.Tests/OrchardCoreStartup.cs similarity index 65% rename from src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs rename to test/OrchardCoreContrib.Testing.Tests/OrchardCoreStartup.cs index c4749b5..86b560b 100644 --- a/src/OrchardCoreContrib.Testing/OrchardCoreStartup.cs +++ b/test/OrchardCoreContrib.Testing.Tests/OrchardCoreStartup.cs @@ -1,16 +1,15 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using OrchardCore.Modules; using OrchardCoreContrib.Testing.Security; -namespace OrchardCoreContrib.Testing; +namespace OrchardCoreContrib.Testing.Tests; -public class OrchardCoreStartup : StartupBase where TEntryPoint : class +public class OrchardCoreStartup { - public override void ConfigureServices(IServiceCollection services) + public void ConfigureServices(IServiceCollection services) { services.AddOrchardCms(builder => builder .AddSetupFeatures("OrchardCore.Tenants") @@ -21,9 +20,8 @@ public override void ConfigureServices(IServiceCollection services) }) .Configure(appBuilder => appBuilder.UseAuthorization())); - services.AddSingleton(new ModuleNamesProvider(typeof(TEntryPoint).Assembly)); + services.AddSingleton(new ModuleNamesProvider(typeof(Web.Program).Assembly)); } - public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) - => app.UseOrchardCore(); + public void Configure(IApplicationBuilder app) => app.UseOrchardCore(); } \ No newline at end of file diff --git a/test/OrchardCoreContrib.Testing.Tests/SiteContextTests.cs b/test/OrchardCoreContrib.Testing.Tests/SiteContextTests.cs new file mode 100644 index 0000000..7f566ba --- /dev/null +++ b/test/OrchardCoreContrib.Testing.Tests/SiteContextTests.cs @@ -0,0 +1,122 @@ +namespace OrchardCoreContrib.Testing.Tests; + +public class SiteContextTests +{ + [Fact] + public async Task Site_ShouldBeSameForAllSites() + { + // Arrange & Act + var _ = new BlogSiteContext(); + var site1 = BlogSiteContext.Site; + + var __ = new BlogSiteContext(); + var site2 = BlogSiteContext.Site; + + // Assert + Assert.Same(site1, site2); + } + + [Fact] + public async Task ShellHost_ShouldBeSameForAllSites() + { + // Arrange & Act + var _ = new BlogSiteContext(); + var shellHost1 = BlogSiteContext.ShellHost; + + var __ = new BlogSiteContext(); + var shellHost2 = BlogSiteContext.ShellHost; + + // Assert + Assert.Same(shellHost1, shellHost2); + } + + [Fact] + public async Task ShellSettingsManager_ShouldBeSameForAllSites() + { + // Arrange & Act + var _ = new BlogSiteContext(); + var shellSettingsManager1 = BlogSiteContext.ShellSettingsManager; + + var __ = new BlogSiteContext(); + var shellSettingsManager2 = BlogSiteContext.ShellSettingsManager; + + // Assert + Assert.Same(shellSettingsManager1, shellSettingsManager2); + } + + [Fact] + public async Task HttpContextAccessor_ShouldBeSameForAllSites() + { + // Arrange & Act + var _ = new BlogSiteContext(); + var httpContextAccessor1 = BlogSiteContext.HttpContextAccessor; + + var __ = new BlogSiteContext(); + var httpContextAccessor2 = BlogSiteContext.HttpContextAccessor; + + // Assert + Assert.Same(httpContextAccessor1, httpContextAccessor2); + } + + [Fact] + public async Task Options_ShouldBeSetOnConstructor() + { + // Arrange + BlogSiteContext siteContext; + + // Act + siteContext = new BlogSiteContext(); + + // Assert + Assert.NotNull(siteContext.Options); + } + + [Fact] + public async Task Client_ShouldBeSetAfterCallingInitializeAsync() + { + // Arrange + var siteContext = new BlogSiteContext(); + + Assert.Null(siteContext.Client); + + // Act + await siteContext.InitializeAsync(); + + // Assert + Assert.NotNull(siteContext.Client); + } + + [Fact] + public async Task TenantName_ShouldBeSetAfterCallingInitializeAsync() + { + // Arrange + var siteContext = new BlogSiteContext(); + + Assert.Null(siteContext.TenantName); + + // Act + await siteContext.InitializeAsync(); + + // Assert + Assert.NotNull(siteContext.TenantName); + } + + [Fact] + public async Task CallingInitializeAsyncMultipleTimes_ShouldChangeTenantName() + { + // Arrange + var siteContext = new BlogSiteContext(); + + // Act + await siteContext.InitializeAsync(); + + var tenant1 = siteContext.TenantName; + + await siteContext.InitializeAsync(); + + var tenant2 = siteContext.TenantName; + + // Assert + Assert.NotEqual(tenant1, tenant2); + } +}