Move test projects from src/ to tests/ (#11357)
* Update gitignore * Move csproj * Update project references * Update solutions * Update build scripts * Tests used to share editorconfig with projects in src * Fix broken tests. * Stop copying around .editorconfig merged root one with linting * csharp_style_expression_bodied -> suggestion * Move StyleCop rulesets to matching directories and update shared build properties * Remove legacy build files, update NuGet.cofig and solution files * Restore myget source * Clean up .gitignore * Update .gitignore * Move new test classes to tests after merge * Gitignore + nuget config * Move new test Co-authored-by: Ronald Barendse <ronald@barend.se>
This commit is contained in:
16
tests/Umbraco.TestData/Configuration/TestDataSettings.cs
Normal file
16
tests/Umbraco.TestData/Configuration/TestDataSettings.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.TestData.Configuration
|
||||
{
|
||||
public class TestDataSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the test data generation is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether persisted local database cache files for content and media are disabled.
|
||||
/// </summary>
|
||||
/// <value>The URL path.</value>
|
||||
public bool IgnoreLocalDb { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Infrastructure.PublishedCache;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
using Umbraco.TestData.Configuration;
|
||||
|
||||
namespace Umbraco.TestData.Extensions
|
||||
{
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddUmbracoTestData(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Services.Any(x => x.ServiceType == typeof(LoadTestController)))
|
||||
{
|
||||
// We assume the test data project is composed if any implementations of LoadTestController exist.
|
||||
return builder;
|
||||
}
|
||||
|
||||
IConfigurationSection testDataSection = builder.Config.GetSection("Umbraco:CMS:TestData");
|
||||
TestDataSettings config = testDataSection.Get<TestDataSettings>();
|
||||
if (config == null || config.Enabled == false)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
|
||||
builder.Services.Configure<TestDataSettings>(testDataSection);
|
||||
|
||||
if (config.IgnoreLocalDb)
|
||||
{
|
||||
builder.Services.AddSingleton(factory => new PublishedSnapshotServiceOptions
|
||||
{
|
||||
IgnoreLocalDb = true
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.Configure<UmbracoPipelineOptions>(options =>
|
||||
options.AddFilter(new UmbracoPipelineFilter(nameof(LoadTestController))
|
||||
{
|
||||
Endpoints = app => app.UseEndpoints(endpoints =>
|
||||
endpoints.MapControllerRoute(
|
||||
"LoadTest",
|
||||
"/LoadTest/{action}",
|
||||
new { controller = "LoadTest", Action = "Index" }))
|
||||
}));
|
||||
|
||||
builder.Services.AddScoped(typeof(LoadTestController));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
tests/Umbraco.TestData/LoadTestComposer.cs
Normal file
13
tests/Umbraco.TestData/LoadTestComposer.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.TestData.Extensions;
|
||||
|
||||
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
public class LoadTestComposer : IComposer
|
||||
{
|
||||
public void Compose(IUmbracoBuilder builder) => builder.AddUmbracoTestData();
|
||||
}
|
||||
}
|
||||
387
tests/Umbraco.TestData/LoadTestController.cs
Normal file
387
tests/Umbraco.TestData/LoadTestController.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
|
||||
// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
public class LoadTestController : Controller
|
||||
{
|
||||
private static readonly Random s_random = new Random();
|
||||
private static readonly object s_locko = new object();
|
||||
|
||||
private static volatile int s_containerId = -1;
|
||||
|
||||
private const string ContainerAlias = "LoadTestContainer";
|
||||
private const string ContentAlias = "LoadTestContent";
|
||||
private const int TextboxDefinitionId = -88;
|
||||
private const int MaxCreate = 1000;
|
||||
|
||||
private static readonly string s_headHtml = @"<html>
|
||||
<head>
|
||||
<title>LoadTest</title>
|
||||
<style>
|
||||
body { font-family: arial; }
|
||||
a,a:visited { color: blue; }
|
||||
h1 { margin: 0; padding: 0; font-size: 120%; font-weight: bold; }
|
||||
h1 a { text-decoration: none; }
|
||||
div.block { margin: 20px 0; }
|
||||
ul { margin:0; }
|
||||
div.ver { font-size: 80%; }
|
||||
div.head { padding:0 0 10px 0; margin: 0 0 20px 0; border-bottom: 1px solid #cccccc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class=""head"">
|
||||
<h1><a href=""/LoadTest"">LoadTest</a></h1>
|
||||
<div class=""ver"">@_umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild()</div>
|
||||
</div>
|
||||
";
|
||||
|
||||
private const string FootHtml = @"</body>
|
||||
</html>";
|
||||
|
||||
private static readonly string s_containerTemplateText = @"
|
||||
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
|
||||
@inject Umbraco.Cms.Core.Configuration.IUmbracoVersion _umbracoVersion
|
||||
@{
|
||||
Layout = null;
|
||||
var container = Umbraco.ContentAtRoot().OfTypes(""" + ContainerAlias + @""").FirstOrDefault();
|
||||
var contents = container.Children().ToArray();
|
||||
var groups = contents.GroupBy(x => x.Value<string>(""origin""));
|
||||
var id = contents.Length > 0 ? contents[0].Id : -1;
|
||||
var wurl = Context.Request.Query[""u""] == ""1"";
|
||||
var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length;
|
||||
}
|
||||
" + s_headHtml + @"
|
||||
<div class=""block"">
|
||||
<span @Html.Raw(missing ? ""style=\""color:red;\"""" : """")>@contents.Length items</span>
|
||||
<ul>
|
||||
@foreach (var group in groups)
|
||||
{
|
||||
<li>@group.Key: @group.Count()</li>
|
||||
}
|
||||
</ul></div>
|
||||
<div class=""block"">
|
||||
@foreach (var content in contents)
|
||||
{
|
||||
while (content.Id > id)
|
||||
{
|
||||
<div style=""color:red;"">@id :: MISSING</div>
|
||||
id++;
|
||||
}
|
||||
if (wurl)
|
||||
{
|
||||
<div>@content.Id :: @content.Name :: @content.Url()</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@content.Id :: @content.Name</div>
|
||||
} id++;
|
||||
}
|
||||
</div>
|
||||
" + FootHtml;
|
||||
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly Cms.Core.Hosting.IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
|
||||
public LoadTestController(
|
||||
IContentTypeService contentTypeService,
|
||||
IContentService contentService,
|
||||
IDataTypeService dataTypeService,
|
||||
IFileService fileService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
Cms.Core.Hosting.IHostingEnvironment hostingEnvironment,
|
||||
IHostApplicationLifetime hostApplicationLifetime)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_contentService = contentService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_fileService = fileService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
}
|
||||
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
IActionResult res = EnsureInitialize();
|
||||
if (res != null)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
var html = @"Welcome. You can:
|
||||
<ul>
|
||||
<li><a href=""/LoadTestContainer"">List existing contents</a> (u:url)</li>
|
||||
<li><a href=""/LoadTest/Create?o=browser"">Create a content</a> (o:origin, r:restart, n:number)</li>
|
||||
<li><a href=""/LoadTest/Clear"">Clear all contents</a></li>
|
||||
<li><a href=""/LoadTest/Domains"">List the current domains in w3wp.exe</a></li>
|
||||
<li><a href=""/LoadTest/Restart"">Restart the current AppDomain</a></li>
|
||||
<li><a href=""/LoadTest/Recycle"">Recycle the AppPool</a></li>
|
||||
<li><a href=""/LoadTest/Die"">Cause w3wp.exe to die</a></li>
|
||||
</ul>
|
||||
";
|
||||
|
||||
return ContentHtml(html);
|
||||
}
|
||||
|
||||
private IActionResult EnsureInitialize()
|
||||
{
|
||||
if (s_containerId > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (s_locko)
|
||||
{
|
||||
if (s_containerId > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IContentType contentType = _contentTypeService.Get(ContentAlias);
|
||||
if (contentType == null)
|
||||
{
|
||||
return ContentHtml("Not installed, first you must <a href=\"/LoadTest/Install\">install</a>.");
|
||||
}
|
||||
|
||||
IContentType containerType = _contentTypeService.Get(ContainerAlias);
|
||||
if (containerType == null)
|
||||
{
|
||||
return ContentHtml("Panic! Container type is missing.");
|
||||
}
|
||||
|
||||
IContent container = _contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault();
|
||||
if (container == null)
|
||||
{
|
||||
return ContentHtml("Panic! Container is missing.");
|
||||
}
|
||||
|
||||
s_containerId = container.Id;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ContentHtml(string s) => Content(s_headHtml + s + FootHtml, "text/html");
|
||||
|
||||
public IActionResult Install()
|
||||
{
|
||||
var contentType = new ContentType(_shortStringHelper, -1)
|
||||
{
|
||||
Alias = ContentAlias,
|
||||
Name = "LoadTest Content",
|
||||
Description = "Content for LoadTest",
|
||||
Icon = "icon-document"
|
||||
};
|
||||
IDataType def = _dataTypeService.GetDataType(TextboxDefinitionId);
|
||||
contentType.AddPropertyType(new PropertyType(_shortStringHelper, def)
|
||||
{
|
||||
Name = "Origin",
|
||||
Alias = "origin",
|
||||
Description = "The origin of the content.",
|
||||
});
|
||||
_contentTypeService.Save(contentType);
|
||||
|
||||
Template containerTemplate = ImportTemplate(
|
||||
"LoadTestContainer",
|
||||
"LoadTestContainer",
|
||||
s_containerTemplateText);
|
||||
|
||||
var containerType = new ContentType(_shortStringHelper, -1)
|
||||
{
|
||||
Alias = ContainerAlias,
|
||||
Name = "LoadTest Container",
|
||||
Description = "Container for LoadTest content",
|
||||
Icon = "icon-document",
|
||||
AllowedAsRoot = true,
|
||||
IsContainer = true
|
||||
};
|
||||
containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[]
|
||||
{
|
||||
new ContentTypeSort(new Lazy<int>(() => contentType.Id), 0, contentType.Alias),
|
||||
});
|
||||
containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate });
|
||||
containerType.SetDefaultTemplate(containerTemplate);
|
||||
_contentTypeService.Save(containerType);
|
||||
|
||||
IContent content = _contentService.Create("LoadTestContainer", -1, ContainerAlias);
|
||||
_contentService.SaveAndPublish(content);
|
||||
|
||||
return ContentHtml("Installed.");
|
||||
}
|
||||
|
||||
private Template ImportTemplate(string name, string alias, string text, ITemplate master = null)
|
||||
{
|
||||
var t = new Template(_shortStringHelper, name, alias) { Content = text };
|
||||
if (master != null)
|
||||
{
|
||||
t.SetMasterTemplate(master);
|
||||
}
|
||||
|
||||
_fileService.SaveTemplate(t);
|
||||
return t;
|
||||
}
|
||||
|
||||
public IActionResult Create(int n = 1, int r = 0, string o = null)
|
||||
{
|
||||
IActionResult res = EnsureInitialize();
|
||||
if (res != null)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
if (r < 0)
|
||||
{
|
||||
r = 0;
|
||||
}
|
||||
|
||||
if (r > 100)
|
||||
{
|
||||
r = 100;
|
||||
}
|
||||
|
||||
var restart = GetRandom(0, 100) > (100 - r);
|
||||
|
||||
if (n < 1)
|
||||
{
|
||||
n = 1;
|
||||
}
|
||||
|
||||
if (n > MaxCreate)
|
||||
{
|
||||
n = MaxCreate;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o;
|
||||
IContent content = _contentService.Create(name, s_containerId, ContentAlias);
|
||||
content.SetValue("origin", o);
|
||||
_contentService.SaveAndPublish(content);
|
||||
}
|
||||
|
||||
if (restart)
|
||||
{
|
||||
DoRestart();
|
||||
}
|
||||
|
||||
return ContentHtml("Created " + n + " content"
|
||||
+ (restart ? ", and restarted" : "")
|
||||
+ ".");
|
||||
}
|
||||
|
||||
private static int GetRandom(int minValue, int maxValue)
|
||||
{
|
||||
lock (s_locko)
|
||||
{
|
||||
return s_random.Next(minValue, maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public IActionResult Clear()
|
||||
{
|
||||
IActionResult res = EnsureInitialize();
|
||||
if (res != null)
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
IContentType contentType = _contentTypeService.Get(ContentAlias);
|
||||
_contentService.DeleteOfType(contentType.Id);
|
||||
|
||||
return ContentHtml("Cleared.");
|
||||
}
|
||||
|
||||
private void DoRestart()
|
||||
{
|
||||
HttpContext.User = null;
|
||||
Thread.CurrentPrincipal = null;
|
||||
_hostApplicationLifetime.StopApplication();
|
||||
}
|
||||
|
||||
public IActionResult ColdBootRestart()
|
||||
{
|
||||
Directory.Delete(_hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.TempData,"DistCache")), true);
|
||||
|
||||
DoRestart();
|
||||
|
||||
return Content("Cold Boot Restarted.");
|
||||
}
|
||||
|
||||
public IActionResult Restart()
|
||||
{
|
||||
DoRestart();
|
||||
|
||||
return ContentHtml("Restarted.");
|
||||
}
|
||||
|
||||
public IActionResult Die()
|
||||
{
|
||||
var timer = new Timer(_ => throw new Exception("die!"));
|
||||
_ = timer.Change(100, 0);
|
||||
|
||||
return ContentHtml("Dying.");
|
||||
}
|
||||
|
||||
public IActionResult Domains()
|
||||
{
|
||||
AppDomain currentDomain = AppDomain.CurrentDomain;
|
||||
var currentName = currentDomain.FriendlyName;
|
||||
var pos = currentName.IndexOf('-');
|
||||
if (pos > 0)
|
||||
{
|
||||
currentName = currentName.Substring(0, pos);
|
||||
}
|
||||
|
||||
var text = new StringBuilder();
|
||||
text.Append("<div class=\"block\">Process ID: " + Process.GetCurrentProcess().Id + "</div>");
|
||||
text.Append("<div class=\"block\">");
|
||||
|
||||
// TODO (V9): Commented out as I assume not available?
|
||||
////text.Append("<div>IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "</div>");
|
||||
|
||||
text.Append("<div>App ID: " + currentName + "</div>");
|
||||
//text.Append("<div>AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "</div>");
|
||||
text.Append("</div>");
|
||||
|
||||
text.Append("<div class=\"block\">Domains:<ul>");
|
||||
text.Append("<li>Not implemented.</li>");
|
||||
/*
|
||||
foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id))
|
||||
{
|
||||
var name = domain.FriendlyName;
|
||||
pos = name.IndexOf('-');
|
||||
if (pos > 0) name = name.Substring(0, pos);
|
||||
text.Append("<li style=\""
|
||||
+ (name != currentName ? "color: #cccccc;" : "")
|
||||
//+ (domain.Id == currentDomain.Id ? "" : "")
|
||||
+ "\">"
|
||||
+"[" + domain.Id + "] " + name
|
||||
+ (domain.IsDefaultAppDomain() ? " (default)" : "")
|
||||
+ (domain.Id == currentDomain.Id ? " (current)" : "")
|
||||
+ "</li>");
|
||||
}
|
||||
*/
|
||||
text.Append("</ul></div>");
|
||||
|
||||
return ContentHtml(text.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
126
tests/Umbraco.TestData/SegmentTestController.cs
Normal file
126
tests/Umbraco.TestData/SegmentTestController.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Web.Website.Controllers;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.TestData.Configuration;
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
public class SegmentTestController : SurfaceController
|
||||
{
|
||||
private IOptions<TestDataSettings> _testDataSettings;
|
||||
|
||||
public SegmentTestController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IOptions<TestDataSettings> testDataSettings)
|
||||
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
||||
{
|
||||
_testDataSettings = testDataSettings;
|
||||
}
|
||||
|
||||
public IActionResult EnableDocTypeSegments(string alias, string propertyTypeAlias)
|
||||
{
|
||||
if(_testDataSettings.Value.Enabled != true)
|
||||
{
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
IContentType ct = Services.ContentTypeService.Get(alias);
|
||||
if (ct == null)
|
||||
{
|
||||
return Content($"No document type found by alias {alias}");
|
||||
}
|
||||
|
||||
IPropertyType propType = ct.PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias);
|
||||
if (propType == null)
|
||||
{
|
||||
return Content($"The document type {alias} does not have a property type {propertyTypeAlias ?? "null"}");
|
||||
}
|
||||
|
||||
if (ct.Variations.VariesBySegment())
|
||||
{
|
||||
return Content($"The document type {alias} already allows segments, nothing has been changed");
|
||||
}
|
||||
|
||||
ct.SetVariesBy(ContentVariation.Segment);
|
||||
propType.SetVariesBy(ContentVariation.Segment);
|
||||
|
||||
Services.ContentTypeService.Save(ct);
|
||||
return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments");
|
||||
}
|
||||
|
||||
private IActionResult HttpNotFound() => throw new NotImplementedException();
|
||||
|
||||
public IActionResult DisableDocTypeSegments(string alias)
|
||||
{
|
||||
if (_testDataSettings.Value.Enabled != true)
|
||||
{
|
||||
return HttpNotFound();
|
||||
}
|
||||
|
||||
IContentType ct = Services.ContentTypeService.Get(alias);
|
||||
if (ct == null)
|
||||
{
|
||||
return Content($"No document type found by alias {alias}");
|
||||
}
|
||||
|
||||
if (!ct.VariesBySegment())
|
||||
{
|
||||
return Content($"The document type {alias} does not allow segments, nothing has been changed");
|
||||
}
|
||||
|
||||
ct.SetVariesBy(ContentVariation.Segment, false);
|
||||
|
||||
Services.ContentTypeService.Save(ct);
|
||||
return Content($"The document type {alias} no longer allows segments");
|
||||
}
|
||||
|
||||
public ActionResult AddSegmentData(int contentId, string propertyAlias, string value, string segment, string culture = null)
|
||||
{
|
||||
IContent content = Services.ContentService.GetById(contentId);
|
||||
if (content == null)
|
||||
{
|
||||
return Content($"No content found by id {contentId}");
|
||||
}
|
||||
|
||||
if (propertyAlias.IsNullOrWhiteSpace() || !content.HasProperty(propertyAlias))
|
||||
{
|
||||
return Content($"The content by id {contentId} does not contain a property with alias {propertyAlias ?? "null"}");
|
||||
}
|
||||
|
||||
if (content.ContentType.VariesByCulture() && culture.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Content($"The content by id {contentId} varies by culture but no culture was specified");
|
||||
}
|
||||
|
||||
if (value.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Content("'value' cannot be null");
|
||||
}
|
||||
|
||||
if (segment.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Content("'segment' cannot be null");
|
||||
}
|
||||
|
||||
content.SetValue(propertyAlias, value, culture, segment);
|
||||
Services.ContentService.Save(content);
|
||||
|
||||
return Content($"Segment value has been set on content {contentId} for property {propertyAlias}");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
tests/Umbraco.TestData/Umbraco.TestData.csproj
Normal file
20
tests/Umbraco.TestData/Umbraco.TestData.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Umbraco.TestData</RootNamespace>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="33.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj" />
|
||||
<ProjectReference Include="..\..\src\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
<ProjectReference Include="..\..\src\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
324
tests/Umbraco.TestData/UmbracoTestDataController.cs
Normal file
324
tests/Umbraco.TestData/UmbracoTestDataController.cs
Normal file
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bogus;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
using Umbraco.Cms.Web.Website.Controllers;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.TestData.Configuration;
|
||||
using Constants = Umbraco.Cms.Core.Constants;
|
||||
|
||||
namespace Umbraco.TestData
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates test data
|
||||
/// </summary>
|
||||
public class UmbracoTestDataController : SurfaceController
|
||||
{
|
||||
private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE";
|
||||
private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker";
|
||||
private const string TextDataTypeName = "UmbracoTestDataContent.Text";
|
||||
private const string TestDataContentTypeAlias = "umbTestDataContent";
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly TestDataSettings _testDataSettings;
|
||||
|
||||
public UmbracoTestDataController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IScopeProvider scopeProvider,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IOptions<TestDataSettings> testDataSettings)
|
||||
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
_propertyEditors = propertyEditors;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_testDataSettings = testDataSettings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a content and associated media tree (hierarchy)
|
||||
/// </summary>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="depth"></param>
|
||||
/// <param name="locale"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two
|
||||
/// </remarks>
|
||||
public IActionResult CreateTree(int count, int depth, string locale = "en")
|
||||
{
|
||||
if (_testDataSettings.Enabled == false)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!Validate(count, depth, out var message, out var perLevel))
|
||||
{
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
var faker = new Faker(locale);
|
||||
var company = faker.Company.CompanyName();
|
||||
|
||||
using (IScope scope = _scopeProvider.CreateScope())
|
||||
{
|
||||
var imageIds = CreateMediaTree(company, faker, count, depth).ToList();
|
||||
var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList();
|
||||
|
||||
Services.ContentService.SaveAndPublishBranch(root, true);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
|
||||
return Content("Done");
|
||||
}
|
||||
|
||||
private static bool Validate(int count, int depth, out string message, out int perLevel)
|
||||
{
|
||||
perLevel = 0;
|
||||
message = null;
|
||||
|
||||
if (count <= 0)
|
||||
{
|
||||
message = "Count must be more than 0";
|
||||
return false;
|
||||
}
|
||||
|
||||
perLevel = count / depth;
|
||||
if (perLevel < 1)
|
||||
{
|
||||
message = "Count not high enough for specified for number of levels required";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility to create a tree hierarchy
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="parent"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="depth"></param>
|
||||
/// <param name="create">
|
||||
/// A callback that returns a tuple of Content and another callback to produce a Container.
|
||||
/// For media, a container will be another folder, for content the container will be the Content itself.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<Udi> CreateHierarchy<T>(
|
||||
T parent, int count, int depth,
|
||||
Func<T, (T content, Func<T> container)> create)
|
||||
where T: class, IContentBase
|
||||
{
|
||||
yield return parent.GetUdi();
|
||||
|
||||
// This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree.
|
||||
var totalDescendants = count - 1;
|
||||
var perLevel = Math.Ceiling(totalDescendants / (double)depth);
|
||||
var perBranch = Math.Ceiling(perLevel / depth);
|
||||
|
||||
var tracked = new Stack<(T parent, int childCount)>();
|
||||
|
||||
var currChildCount = 0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
(T content, Func<T> container) created = create(parent);
|
||||
T contentItem = created.content;
|
||||
|
||||
yield return contentItem.GetUdi();
|
||||
|
||||
currChildCount++;
|
||||
|
||||
if (currChildCount == perBranch)
|
||||
{
|
||||
// move back up...
|
||||
|
||||
(T parent, int childCount) prev = tracked.Pop();
|
||||
|
||||
// restore child count
|
||||
currChildCount = prev.childCount;
|
||||
// restore the parent
|
||||
parent = prev.parent;
|
||||
|
||||
}
|
||||
else if (contentItem.Level < depth)
|
||||
{
|
||||
// track the current parent and it's current child count
|
||||
tracked.Push((parent, currChildCount));
|
||||
|
||||
// not at max depth, create below
|
||||
parent = created.container();
|
||||
|
||||
currChildCount = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the media tree hiearachy
|
||||
/// </summary>
|
||||
/// <param name="company"></param>
|
||||
/// <param name="faker"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="depth"></param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<Udi> CreateMediaTree(string company, Faker faker, int count, int depth)
|
||||
{
|
||||
IMedia parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder);
|
||||
|
||||
return CreateHierarchy(parent, count, depth, currParent =>
|
||||
{
|
||||
var imageUrl = faker.Image.PicsumUrl();
|
||||
|
||||
// we are appending a &ext=.jpg to the end of this for a reason. The result of this URL will be something like:
|
||||
// https://picsum.photos/640/480/?image=106
|
||||
// and due to the way that we detect images there must be an extension so we'll change it to
|
||||
// https://picsum.photos/640/480/?image=106&ext=.jpg
|
||||
// which will trick our app into parsing this and thinking it's an image ... which it is so that's good.
|
||||
// if we don't do this we don't get thumbnails in the back office.
|
||||
imageUrl += "&ext=.jpg";
|
||||
|
||||
IMedia media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image);
|
||||
media.SetValue(Constants.Conventions.Media.File, imageUrl);
|
||||
Services.MediaService.Save(media);
|
||||
return (media, () =>
|
||||
{
|
||||
// create a folder to contain child media
|
||||
IMedia container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder);
|
||||
return container;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the content tree hiearachy
|
||||
/// </summary>
|
||||
/// <param name="company"></param>
|
||||
/// <param name="faker"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <param name="depth"></param>
|
||||
/// <param name="imageIds"></param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<Udi> CreateContentTree(string company, Faker faker, int count, int depth, List<Udi> imageIds, out IContent root)
|
||||
{
|
||||
var random = new Random(company.GetHashCode());
|
||||
|
||||
IContentType docType = GetOrCreateContentType();
|
||||
|
||||
IContent parent = Services.ContentService.Create(company, -1, docType.Alias);
|
||||
|
||||
// give it some reasonable data (100 reviews)
|
||||
parent.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review())));
|
||||
parent.SetValue("desc", company);
|
||||
parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]);
|
||||
Services.ContentService.Save(parent);
|
||||
|
||||
root = parent;
|
||||
|
||||
return CreateHierarchy(parent, count, depth, currParent =>
|
||||
{
|
||||
IContent content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias);
|
||||
|
||||
// give it some reasonable data (100 reviews)
|
||||
content.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review())));
|
||||
content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective())));
|
||||
content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]);
|
||||
|
||||
Services.ContentService.Save(content);
|
||||
return (content, () => content);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private IContentType GetOrCreateContentType()
|
||||
{
|
||||
IContentType docType = Services.ContentTypeService.Get(TestDataContentTypeAlias);
|
||||
if (docType != null)
|
||||
{
|
||||
return docType;
|
||||
}
|
||||
|
||||
docType = new ContentType(_shortStringHelper, -1)
|
||||
{
|
||||
Alias = TestDataContentTypeAlias,
|
||||
Name = "Umbraco Test Data Content",
|
||||
Icon = "icon-science color-green"
|
||||
};
|
||||
docType.AddPropertyGroup("content", "Content");
|
||||
docType.AddPropertyType(new PropertyType(_shortStringHelper, GetOrCreateRichText(), "review")
|
||||
{
|
||||
Name = "Review"
|
||||
});
|
||||
docType.AddPropertyType(new PropertyType(_shortStringHelper, GetOrCreateMediaPicker(), "media")
|
||||
{
|
||||
Name = "Media"
|
||||
});
|
||||
docType.AddPropertyType(new PropertyType(_shortStringHelper, GetOrCreateText(), "desc")
|
||||
{
|
||||
Name = "Description"
|
||||
});
|
||||
Services.ContentTypeService.Save(docType);
|
||||
docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) };
|
||||
Services.ContentTypeService.Save(docType);
|
||||
return docType;
|
||||
}
|
||||
|
||||
private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce);
|
||||
|
||||
private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker);
|
||||
|
||||
private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox);
|
||||
|
||||
private IDataType GetOrCreateDataType(string name, string editorAlias)
|
||||
{
|
||||
IDataType dt = Services.DataTypeService.GetDataType(name);
|
||||
if (dt != null)
|
||||
{
|
||||
return dt;
|
||||
}
|
||||
|
||||
IDataEditor editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias);
|
||||
if (editor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No {editorAlias} editor found");
|
||||
}
|
||||
|
||||
var serializer = new ConfigurationEditorJsonSerializer();
|
||||
|
||||
dt = new DataType(editor, serializer)
|
||||
{
|
||||
Name = name,
|
||||
Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject,
|
||||
DatabaseType = ValueStorageType.Ntext
|
||||
};
|
||||
|
||||
Services.DataTypeService.Save(dt);
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
tests/Umbraco.TestData/readme.md
Normal file
56
tests/Umbraco.TestData/readme.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## Umbraco Test Data
|
||||
|
||||
This project is a utility to be able to generate large amounts of content and media in an
|
||||
Umbraco installation for testing.
|
||||
|
||||
## Usage
|
||||
|
||||
You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data.
|
||||
|
||||
It has to be enabled by an appSetting:
|
||||
|
||||
```json
|
||||
{
|
||||
"Umbraco": {
|
||||
"CMS": {
|
||||
"TestData": {
|
||||
"Enabled" : true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once this is enabled this endpoint can be executed:
|
||||
|
||||
`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5`
|
||||
|
||||
The query string options are:
|
||||
|
||||
* `count` = the number of content and media nodes to create
|
||||
* `depth` = how deep the trees created will be
|
||||
* `locale` (optional, default = "en") = the language that the data will be generated in
|
||||
|
||||
This creates a content and associated media tree (hierarchy). Each content item created is associated
|
||||
to a media item via a media picker and therefore a relation is created between the two. Each content and
|
||||
media tree created have the same root node name so it's easy to know which content branch relates to
|
||||
which media branch.
|
||||
|
||||
All values are generated using the very handy `Bogus` package.
|
||||
|
||||
## Schema
|
||||
|
||||
This will install some schema items:
|
||||
|
||||
* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type
|
||||
* `UmbracoTestDataContent.RTE` Data Type
|
||||
* `UmbracoTestDataContent.MediaPicker` Data Type
|
||||
* `UmbracoTestDataContent.Text` Data Type
|
||||
|
||||
For media, the normal folder and image is used
|
||||
|
||||
## Media
|
||||
|
||||
This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value.
|
||||
This works when viewing the media item in the media section and the image will show up and with recent changes this will also work
|
||||
when editing content to view the thumbnail for the picked media.
|
||||
Reference in New Issue
Block a user