Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Allow setting UpdatedAt when creating Dialog #1105

Merged
merged 10 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ public CreateDialogCommandValidator(
.IsValidUuidV7()
.UuidV7TimestampIsInPast();

RuleFor(x => x.CreatedAt)
MagnusSandgren marked this conversation as resolved.
Show resolved Hide resolved
.IsInPast();

RuleFor(x => x.CreatedAt)
.NotEmpty()
.WithMessage($"{{PropertyName}} must not be empty when '{nameof(CreateDialogCommand.UpdatedAt)} is set.")
.When(x => x.UpdatedAt != default);

RuleFor(x => x.UpdatedAt)
.IsInPast()
.GreaterThanOrEqualTo(x => x.CreatedAt)
.WithMessage($"'{{PropertyName}}' must be greater than or equal to '{nameof(CreateDialogCommand.CreatedAt)}'.")
.When(x => x.CreatedAt != default && x.UpdatedAt != default);

RuleFor(x => x.ServiceResource)
.NotNull()
.IsValidUri()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class CreateDialogDto
/// If not supplied, the current date /time will be used.
/// </summary>
/// <example>2022-12-31T23:59:59Z</example>
public DateTimeOffset UpdatedAt { get; set; } // TODO: This does not work, as UpdatedAt is always overwritten
public DateTimeOffset UpdatedAt { get; set; }

/// <summary>
/// The aggregated status of the dialog.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using Digdir.Library.Entity.Abstractions.Features.Updatable;

namespace Digdir.Library.Entity.Abstractions.Features.Aggregate;

Expand Down Expand Up @@ -88,6 +89,34 @@ internal static AggregateNode Create(Type type, object entity, AggregateNodeStat
/// Convenience method to check if the state of the node is <see cref="AggregateNodeState.Modified"/> and not by a child node.
/// </summary>
public bool IsDirectlyModified() => State is AggregateNodeState.Modified && !ModifiedByChild;

/// <summary>
/// Convenience method to check if the state of the node is <see cref="AggregateNodeState.Added"/>.
/// </summary>
public bool IsAdded() => State is AggregateNodeState.Added;

/// <summary>
/// Convenience method to check if the state of the node is <see cref="AggregateNodeState.Modified"/>.
/// </summary>
public bool IsModified() => State is AggregateNodeState.Modified;

/// <summary>
/// Convenience method to check if the state of the node is <see cref="AggregateNodeState.Restored"/>.
/// </summary>
public bool IsRestored() => State is AggregateNodeState.Restored;

/// <summary>
/// Convenience method to check if the state of the node is <see cref="AggregateNodeState.Deleted"/>.
/// </summary>
public bool IsDeleted() => State is AggregateNodeState.Deleted;

/// <summary>
/// Convenience method to check if the state of the node is <see cref="AggregateNodeState.Added"/>,
/// and that UpdatedAt is the default value.
/// </summary>
/// <param name="updatable"></param>
public bool IsAddedWithDefaultUpdatedAt(IUpdateableEntity updatable)
=> IsAdded() && updatable.UpdatedAt == default;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,32 @@ internal static async Task HandleAggregateEntities(this ChangeTracker changeTrac

foreach (var (_, aggregateNode) in aggregateNodeByEntry)
{
if (aggregateNode.Entity is IAggregateCreatedHandler created && aggregateNode.State is AggregateNodeState.Added)
if (aggregateNode.Entity is IAggregateCreatedHandler created && aggregateNode.IsAdded())
{
created.OnCreate(aggregateNode, utcNow);
}

if (aggregateNode.Entity is IAggregateUpdatedHandler updated && aggregateNode.State is AggregateNodeState.Modified)
if (aggregateNode.Entity is IAggregateUpdatedHandler updated && aggregateNode.IsModified())
{
updated.OnUpdate(aggregateNode, utcNow);
}

if (aggregateNode.Entity is IAggregateDeletedHandler deleted && aggregateNode.State is AggregateNodeState.Deleted)
if (aggregateNode.Entity is IAggregateDeletedHandler deleted && aggregateNode.IsDeleted())
{
deleted.OnDelete(aggregateNode, utcNow);
}

if (aggregateNode.Entity is IAggregateRestoredHandler restored && aggregateNode.State is AggregateNodeState.Restored)
if (aggregateNode.Entity is IAggregateRestoredHandler restored && aggregateNode.IsRestored())
{
restored.OnRestore(aggregateNode, utcNow);
}

if (aggregateNode.Entity is IUpdateableEntity updatable)
{
updatable.Update(utcNow);
if (aggregateNode.IsModified() || aggregateNode.IsAddedWithDefaultUpdatedAt(updatable))
{
updatable.Update(utcNow);
}
}

if (aggregateNode.Entity is IVersionableEntity versionable)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;
using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get;
using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;
using Digdir.Tool.Dialogporten.GenerateFakeData;
using FluentAssertions;
using static Digdir.Domain.Dialogporten.Application.Integration.Tests.UuiDv7Utils;
Expand Down Expand Up @@ -108,5 +109,98 @@ public async Task Create_CreateDialog_WhenDialogIsComplex()
success.Value.Should().Be(expectedDialogId);
}

// TODO: Add tests
[Fact]
public async Task Can_Create_Dialog_With_UpdatedAt_Supplied()
{
// Arrange
var dialogId = GenerateBigEndianUuidV7();
var createdAt = DateTimeOffset.UtcNow.AddYears(-20);
var updatedAt = DateTimeOffset.UtcNow.AddYears(-15);
var createDialogCommand = DialogGenerator.GenerateFakeDialog(id: dialogId, updatedAt: updatedAt, createdAt: createdAt);

// Act
var createDialogResult = await Application.Send(createDialogCommand);
var getDialogQuery = new GetDialogQuery
{
DialogId = dialogId
};

var getDialogResponse = await Application.Send(getDialogQuery);

// Assert
createDialogResult.TryPickT0(out var dialogCreatedSuccess, out _).Should().BeTrue();
dialogCreatedSuccess.Value.Should().Be(dialogId);

getDialogQuery.Should().NotBeNull();
getDialogResponse.TryPickT0(out var dialog, out _).Should().BeTrue();
dialog.Should().NotBeNull();
dialog.CreatedAt.Should().BeCloseTo(createdAt, precision: TimeSpan.FromMicroseconds(10));
dialog.UpdatedAt.Should().BeCloseTo(updatedAt, precision: TimeSpan.FromMicroseconds(10));
}

[Fact]
public async Task Cant_Create_Dialog_With_UpdatedAt_Supplied_Without_CreatedAt_Supplied()
{
// Arrange
var updatedAt = DateTimeOffset.UtcNow.AddYears(-15);
var createDialogCommand = DialogGenerator.GenerateFakeDialog(updatedAt: updatedAt);

// Act
var response = await Application.Send(createDialogCommand);

// Assert
response.TryPickT2(out var validationError, out _).Should().BeTrue();
validationError.Should().NotBeNull();
validationError.Errors.Should().Contain(e => e.ErrorMessage.Contains(nameof(createDialogCommand.UpdatedAt)));
}

[Fact]
public async Task Cant_Create_Dialog_With_UpdatedAt_Date_Earlier_Than_CreatedAt_Date()
{
// Arrange
var createdAt = DateTimeOffset.UtcNow.AddYears(-10);
var updatedAt = DateTimeOffset.UtcNow.AddYears(-15);
var createDialogCommand = DialogGenerator.GenerateFakeDialog(updatedAt: updatedAt, createdAt: createdAt);

// Act
var response = await Application.Send(createDialogCommand);

// Assert
response.TryPickT2(out var validationError, out _).Should().BeTrue();
validationError.Should().NotBeNull();
validationError.Errors.Should().Contain(e => e.ErrorMessage.Contains(nameof(createDialogCommand.CreatedAt)));
}

[Fact]
public async Task Cant_Create_Dialog_With_UpdatedAt_Or_CreatedAt_In_The_Future()
{
// Arrange
var aYearFromNow = DateTimeOffset.UtcNow.AddYears(1);
var createDialogCommand = DialogGenerator
.GenerateFakeDialog(updatedAt: aYearFromNow, createdAt: aYearFromNow);

// Act
var response = await Application.Send(createDialogCommand);

//
response.TryPickT2(out var validationError, out _).Should().BeTrue();
validationError.Should().NotBeNull();
validationError.Errors.Should().Contain(e => e.ErrorMessage.Contains("in the past"));
}

[Fact]
public async Task Can_Create_Dialog_With_UpdatedAt_And_CreatedAt_Being_Equal()
{
// Arrange
var aYearAgo = DateTimeOffset.UtcNow.AddYears(-1);
var createDialogCommand = DialogGenerator
.GenerateFakeDialog(updatedAt: aYearAgo, createdAt: aYearAgo);

// Act
var response = await Application.Send(createDialogCommand);

// Assert
response.TryPickT0(out var success, out _).Should().BeTrue();
success.Should().NotBeNull();
}
}
Loading