Skip to content

Commit

Permalink
fix: Allow setting UpdatedAt when creating Dialog (#1105)
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the Title above -->

## Description

* Allows setting UpdatedAt when creating Dialog
* Disallow setting CreatedAt to future date
* Added tests for UpdatedAt/CreatedAt

## Related Issue(s)

- #1003 

## Verification

- [x] **Your** code builds clean without any errors or warnings
- [x] Manual testing done (required)
- [x] Relevant automated test added (if you find this hard, leave it and
we'll help out)

## Documentation

- [ ] Documentation is updated (either in `docs`-directory, Altinnpedia
or a separate linked PR in
[altinn-studio-docs.](https://github.com/Altinn/altinn-studio-docs), if
applicable)
  • Loading branch information
oskogstad authored Sep 12, 2024
1 parent a7e769a commit 481e907
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ public CreateDialogCommandValidator(
.IsValidUuidV7()
.UuidV7TimestampIsInPast();

RuleFor(x => x.CreatedAt)
.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 @@ -95,7 +95,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();
}
}

0 comments on commit 481e907

Please sign in to comment.