Files
Umbraco-CMS/tests/Umbraco.Tests.Common/Builders/ContentDataBuilder.cs
Andy Butland d623476902 Use UTC for system dates in Umbraco (#19822)
* Persist and expose Umbraco system dates as UTC (#19705)

* Updated persistence DTOs defining default dates to use UTC.

* Remove ForceToUtc = false from all persistence DTO attributes (default when not specified is true).

* Removed use of SpecifyKind setting dates to local.

* Removed unnecessary Utc suffixes on properties.

* Persist current date time with UtcNow.

* Removed further necessary Utc suffixes and fixed failing unit tests.

* Added migration for SQL server to update database date default constraints.

* Added comment justifying not providing a migration for SQLite default date constraints.

* Ensure UTC for datetimes created from persistence DTOs.

* Ensure UTC when creating dates for published content rendering in Razor and outputting in delivery API.

* Fixed migration SQL syntax.

* Introduced AuditItemFactory for creating entries for the backoffice document history, so we can control the UTC setting on the retrieved persisted dates.

* Ensured UTC dates are retrieved for document versions.

* Ensured UTC is returned for backoffice display of last edited and published for variant content.

* Fixed SQLite syntax for default current datetime.

* Apply suggestions from code review

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Further updates from code review.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

* Migrate system dates from local server time to UTC (#19798)

* Add settings for the migration.

* Add migration and implement for SQL server.

* Implement for SQLite.

* Fixes from testing with SQL Server.

* Fixes from testing with SQLite.

* Code tidy.

* Cleaned up usings.

* Removed audit log date from conversion.

* Removed webhook log date from conversion.

* Updated update date initialization on saving dictionary items.

* Updated filter on log queries.

* Use timezone ID instead of system name to work cross-culture.

---------

Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
2025-08-22 11:59:23 +02:00

216 lines
7.2 KiB
C#

using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.HybridCache;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Common.Builders;
internal sealed class ContentDataBuilder : BuilderBase<ContentData>, IWithNameBuilder
{
private Dictionary<string, CultureVariation> _cultureInfos;
private string _name;
private DateTime? _now;
private Dictionary<string, PropertyData[]> _properties;
private bool? _published;
private string _segment;
private int? _templateId;
private int? _versionId;
private int? _writerId;
string IWithNameBuilder.Name
{
get => _name;
set => _name = value;
}
public ContentDataBuilder WithVersionDate(DateTime now)
{
_now = now;
return this;
}
public ContentDataBuilder WithUrlSegment(string segment)
{
_segment = segment;
return this;
}
public ContentDataBuilder WithVersionId(int versionId)
{
_versionId = versionId;
return this;
}
public ContentDataBuilder WithWriterId(int writerId)
{
_writerId = writerId;
return this;
}
public ContentDataBuilder WithTemplateId(int templateId)
{
_templateId = templateId;
return this;
}
public ContentDataBuilder WithPublished(bool published)
{
_published = published;
return this;
}
public ContentDataBuilder WithProperties(Dictionary<string, PropertyData[]> properties)
{
_properties = properties;
return this;
}
public ContentDataBuilder WithCultureInfos(Dictionary<string, CultureVariation> cultureInfos)
{
_cultureInfos = cultureInfos;
return this;
}
/// <summary>
/// Build and dynamically update an existing content type
/// </summary>
/// <typeparam name="TContentType"></typeparam>
/// <param name="shortStringHelper"></param>
/// <param name="propertyDataTypes"></param>
/// <param name="contentType"></param>
/// <param name="contentTypeAlias">
/// Will configure the content type with this alias/name if supplied when it's not already set on the content type.
/// </param>
/// <param name="autoCreateCultureNames"></param>
/// <returns></returns>
public ContentData Build<TContentType>(
IShortStringHelper shortStringHelper,
Dictionary<string, IDataType> propertyDataTypes,
TContentType contentType,
string contentTypeAlias = null,
bool autoCreateCultureNames = false) where TContentType : class, IContentTypeComposition
{
if (_name.IsNullOrWhiteSpace())
{
throw new InvalidOperationException("Cannot build without a name");
}
_segment ??= _name.ToLower().ReplaceNonAlphanumericChars('-');
// create or copy the current culture infos for the content
var contentCultureInfos = _cultureInfos == null
? new Dictionary<string, CultureVariation>()
: new Dictionary<string, CultureVariation>(_cultureInfos);
contentType.Alias ??= contentTypeAlias;
contentType.Name ??= contentTypeAlias;
contentType.Key = contentType.Key == default ? Guid.NewGuid() : contentType.Key;
contentType.Id = contentType.Id == default ? Math.Abs(contentTypeAlias.GetHashCode()) : contentType.Id;
if (_properties == null)
{
_properties = new Dictionary<string, PropertyData[]>();
}
foreach (var prop in _properties)
{
//var dataType = new DataType(new VoidEditor("Label", Mock.Of<IDataValueEditorFactory>()), new ConfigurationEditorJsonSerializer())
//{
// Id = 4
//};
if (!propertyDataTypes.TryGetValue(prop.Key, out var dataType))
{
dataType = propertyDataTypes.First().Value;
}
var propertyType = new PropertyType(shortStringHelper, dataType, prop.Key);
// check each property for culture and set variations accordingly,
// this will also ensure that we have the correct culture name on the content
// set for each culture too.
foreach (var cultureValue in prop.Value.Where(x => !x.Culture.IsNullOrWhiteSpace()))
{
// set the property type to vary based on the values
propertyType.Variations |= ContentVariation.Culture;
// if there isn't already a culture, then add one with the default name
if (autoCreateCultureNames &&
!contentCultureInfos.TryGetValue(cultureValue.Culture, out var cultureVariation))
{
cultureVariation = new CultureVariation
{
Date = DateTime.UtcNow,
IsDraft = true,
Name = _name,
UrlSegment = _segment
};
contentCultureInfos[cultureValue.Culture] = cultureVariation;
}
}
// set variations for segments if there is any
if (prop.Value.Any(x => !x.Segment.IsNullOrWhiteSpace()))
{
propertyType.Variations |= ContentVariation.Segment;
contentType.Variations |= ContentVariation.Segment;
}
if (!contentType.PropertyTypeExists(propertyType.Alias))
{
contentType.AddPropertyType(propertyType);
}
}
if (contentCultureInfos.Count > 0)
{
contentType.Variations |= ContentVariation.Culture;
WithCultureInfos(contentCultureInfos);
}
var result = Build();
return result;
}
public override ContentData Build()
{
var now = _now ?? DateTime.UtcNow;
var versionId = _versionId ?? 1;
var writerId = _writerId ?? -1;
var templateId = _templateId ?? 0;
var published = _published ?? true;
var properties = _properties ?? new Dictionary<string, PropertyData[]>();
var cultureInfos = _cultureInfos ?? new Dictionary<string, CultureVariation>();
var segment = _segment ?? _name.ToLower().ReplaceNonAlphanumericChars('-');
var contentData = new ContentData(
_name,
segment,
versionId,
now,
writerId,
templateId,
published,
properties,
cultureInfos);
return contentData;
}
public static ContentData CreateBasic(string name, DateTime? versionDate = null)
=> new ContentDataBuilder()
.WithName(name)
.WithVersionDate(versionDate ?? DateTime.UtcNow)
.Build();
public static ContentData CreateVariant(string name, Dictionary<string, CultureVariation> cultureInfos, DateTime? versionDate = null, bool published = true)
=> new ContentDataBuilder()
.WithName(name)
.WithVersionDate(versionDate ?? DateTime.UtcNow)
.WithCultureInfos(cultureInfos)
.WithPublished(published)
.Build();
}