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:
Paul Johnson
2021-10-18 08:14:04 +01:00
committed by GitHub
parent c005673a96
commit 00133e880d
752 changed files with 650 additions and 1844 deletions

View 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;
}
}

View File

@@ -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;
}
}
}

View 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();
}
}

View 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());
}
}
}

View 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}");
}
}
}

View 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>

View 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;
}
}
}

View 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.