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: Disallow search filter with CreatedAfter greater than CreatedBefore #2019

Merged
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
@@ -0,0 +1,7 @@
namespace Digdir.Domain.Dialogporten.Application.Features.V1.Common;

internal static class ValidationErrorStrings
{
internal const string PropertyNameMustBeLessThanOrEqualToComparisonProperty =
"'{PropertyName}' must be less than or equal to '{ComparisonProperty}'.";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Digdir.Domain.Dialogporten.Domain.Common;
using Digdir.Domain.Dialogporten.Domain.Localizations;
using FluentValidation;
using static Digdir.Domain.Dialogporten.Application.Features.V1.Common.ValidationErrorStrings;

namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search;

Expand Down Expand Up @@ -52,5 +53,20 @@ public SearchDialogQueryValidator()

RuleForEach(x => x.Status).IsInEnum();
RuleForEach(x => x.SystemLabel).IsInEnum();

RuleFor(x => x.CreatedAfter)
.LessThanOrEqualTo(x => x.CreatedBefore)
.When(x => x.CreatedAfter is not null && x.CreatedBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);

RuleFor(x => x.DueAfter)
.LessThanOrEqualTo(x => x.DueBefore)
.When(x => x.DueAfter is not null && x.DueBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);

RuleFor(x => x.UpdatedAfter)
.LessThanOrEqualTo(x => x.UpdatedBefore)
.When(x => x.UpdatedAfter is not null && x.UpdatedBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Digdir.Domain.Dialogporten.Domain.Parties;
using Digdir.Domain.Dialogporten.Domain.Parties.Abstractions;
using FluentValidation;
using static Digdir.Domain.Dialogporten.Application.Features.V1.Common.ValidationErrorStrings;

namespace Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Search;

Expand Down Expand Up @@ -58,5 +59,25 @@ public SearchDialogQueryValidator()
.IsValidUri()
.MaximumLength(Constants.DefaultMaxUriLength)
.When(x => x.Process is not null);

RuleFor(x => x.CreatedAfter)
.LessThanOrEqualTo(x => x.CreatedBefore)
.When(x => x.CreatedAfter is not null && x.CreatedBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);

RuleFor(x => x.DueAfter)
.LessThanOrEqualTo(x => x.DueBefore)
.When(x => x.DueAfter is not null && x.DueBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);

RuleFor(x => x.UpdatedAfter)
.LessThanOrEqualTo(x => x.UpdatedBefore)
.When(x => x.UpdatedAfter is not null && x.UpdatedBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);

RuleFor(x => x.VisibleAfter)
.LessThanOrEqualTo(x => x.VisibleBefore)
.When(x => x.VisibleAfter is not null && x.VisibleBefore is not null)
.WithMessage(PropertyNameMustBeLessThanOrEqualToComparisonProperty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static CreateDialogCommand GenerateFakeCreateDialogCommand(
DateTimeOffset? updatedAt = null,
DateTimeOffset? dueAt = null,
DateTimeOffset? expiresAt = null,
DateTimeOffset? visibleFrom = null,
string? process = null,
DialogStatus.Values? status = null,
ContentDto? content = null,
Expand Down Expand Up @@ -62,6 +63,7 @@ public static CreateDialogCommand GenerateFakeCreateDialogCommand(
updatedAt,
dueAt,
expiresAt,
visibleFrom,
process,
status,
content,
Expand All @@ -86,6 +88,7 @@ public static CreateDialogDto GenerateFakeDialog(
DateTimeOffset? updatedAt = null,
DateTimeOffset? dueAt = null,
DateTimeOffset? expiresAt = null,
DateTimeOffset? visibleFrom = null,
string? process = null,
DialogStatus.Values? status = null,
ContentDto? content = null,
Expand All @@ -111,6 +114,7 @@ public static CreateDialogDto GenerateFakeDialog(
updatedAt,
dueAt,
expiresAt,
visibleFrom,
process,
status,
content,
Expand All @@ -137,6 +141,7 @@ public static List<CreateDialogDto> GenerateFakeDialogs(int? seed = null,
DateTimeOffset? updatedAt = null,
DateTimeOffset? dueAt = null,
DateTimeOffset? expiresAt = null,
DateTimeOffset? visibleFrom = null,
string? process = null,
DialogStatus.Values? status = null,
ContentDto? content = null,
Expand All @@ -159,6 +164,7 @@ public static List<CreateDialogDto> GenerateFakeDialogs(int? seed = null,
.RuleFor(o => o.UpdatedAt, f => updatedAt ?? default)
.RuleFor(o => o.DueAt, f => dueAt ?? f.Date.Future(10, RefTime))
.RuleFor(o => o.ExpiresAt, f => expiresAt ?? f.Date.Future(20, RefTime.AddYears(11)))
.RuleFor(o => o.VisibleFrom, _ => visibleFrom ?? null)
.RuleFor(o => o.Status, f => status ?? f.PickRandom<DialogStatus.Values>())
.RuleFor(o => o.Content, _ => content ?? GenerateFakeDialogContent())
.RuleFor(o => o.SearchTags, _ => searchTags ?? GenerateFakeSearchTags())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Digdir.Domain.Dialogporten.Domain.Parties;
using Digdir.Tool.Dialogporten.GenerateFakeData;
using static Digdir.Domain.Dialogporten.Application.Integration.Tests.Common.Common;

namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;

public sealed class DateFilterTestData
{
public int? AfterYear { get; init; }
public int? BeforeYear { get; init; }
public int ExpectedCount { get; init; }
public required int[] ExpectedYears { get; init; }
}

internal static class Common
{
internal static DateTimeOffset CreateDateFromYear(int year) => new(year, 1, 1, 0, 0, 0, TimeSpan.Zero);

internal const string UpdatedAt = "UpdatedAt";
internal const string VisibleFrom = "VisibleFrom";
internal const string DueAt = "DueAt";
internal const string CreatedAt = "CreatedAt";

// Any party will do, required for EndUser search validation
internal static string Party => NorwegianPersonIdentifier.PrefixWithSeparator + "03886595947";
}

internal static class ApplicationExtensions
{
internal static async Task<Guid> CreateDialogWithDateInYear(this DialogApplication application, int year, string dateType)
{
var date = CreateDateFromYear(year);
var createDialogCommand = dateType switch
{
UpdatedAt => DialogGenerator.GenerateFakeCreateDialogCommand(
// Requires CreatedAt to be earlier than UpdatedAt
createdAt: CreateDateFromYear(year - 1), updatedAt: date),

VisibleFrom => DialogGenerator.GenerateFakeCreateDialogCommand(
// Requires DueAt to be later than VisibleFrom
dueAt: CreateDateFromYear(year + 1), visibleFrom: date),

DueAt => DialogGenerator.GenerateFakeCreateDialogCommand(dueAt: date),
CreatedAt => DialogGenerator.GenerateFakeCreateDialogCommand(createdAt: date),
_ => throw new ArgumentException("Invalid date type", nameof(dateType))
};

createDialogCommand.Dto.Party = Party;

var createCommandResponse = await application.Send(createDialogCommand);
return createCommandResponse.AsT0.DialogId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search;
using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;
using FluentAssertions;
using static Digdir.Domain.Dialogporten.Application.Integration.Tests.Common.Common;

namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.EndUser.Dialogs.Queries.Search;

[Collection(nameof(DialogCqrsCollectionFixture))]
public class CreatedAtFilterTests : ApplicationCollectionFixture
{
public CreatedAtFilterTests(DialogApplication application) : base(application) { }

[Theory]
[InlineData(2022, null, 2, new[] { 2022, 2023 })]
[InlineData(null, 2021, 2, new[] { 2020, 2021 })]
[InlineData(2021, 2022, 2, new[] { 2021, 2022 })]
public async Task Should_Filter_On_Created_Date(int? createdAfterYear, int? createdBeforeYear, int expectedCount, int[] expectedYears)
{
// Arrange
var dialogIn2020 = await Application.CreateDialogWithDateInYear(2020, CreatedAt);
var dialogIn2021 = await Application.CreateDialogWithDateInYear(2021, CreatedAt);
var dialogIn2022 = await Application.CreateDialogWithDateInYear(2022, CreatedAt);
var dialogIn2023 = await Application.CreateDialogWithDateInYear(2023, CreatedAt);

// Act
var response = await Application.Send(new SearchDialogQuery
{
Party = [Party],
CreatedAfter = createdAfterYear.HasValue ? CreateDateFromYear(createdAfterYear.Value) : null,
CreatedBefore = createdBeforeYear.HasValue ? CreateDateFromYear(createdBeforeYear.Value) : null
});

// Assert
response.TryPickT0(out var result, out _).Should().BeTrue();
result.Should().NotBeNull();

result.Items.Should().HaveCount(expectedCount);
foreach (var year in expectedYears)
{
var dialogId = year switch
{
2020 => dialogIn2020,
2021 => dialogIn2021,
2022 => dialogIn2022,
2023 => dialogIn2023,
_ => throw new ArgumentOutOfRangeException()
};

result.Items.Should().ContainSingle(x => x.Id == dialogId);
}
}

[Fact]
public async Task Cannot_Filter_On_Created_After_With_Value_Greater_Than_Created_Before()
{
// Act
var response = await Application.Send(new SearchDialogQuery
{
Party = [Party],
CreatedAfter = CreateDateFromYear(2022),
CreatedBefore = CreateDateFromYear(2021)
});

// Assert
response.TryPickT1(out var result, out _).Should().BeTrue();
result.Should().NotBeNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search;
using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common;
using FluentAssertions;
using static Digdir.Domain.Dialogporten.Application.Integration.Tests.Common.Common;

namespace Digdir.Domain.Dialogporten.Application.Integration.Tests.Features.V1.EndUser.Dialogs.Queries.Search;

[Collection(nameof(DialogCqrsCollectionFixture))]
public class DueAtFilterTests : ApplicationCollectionFixture
{
public DueAtFilterTests(DialogApplication application) : base(application) { }

[Theory, MemberData(nameof(DueAtTestData))]
public async Task Should_Filter_On_Due_Date(DateFilterTestData testData)
{
// Arrange
var currentYear = DateTimeOffset.UtcNow.Year;

var oneYearInTheFuture = currentYear + 1;
var twoYearsInTheFuture = currentYear + 2;
var threeYearsInTheFuture = currentYear + 3;
var fourYearsInTheFuture = currentYear + 4;

var dialogOneYearInTheFuture = await Application.CreateDialogWithDateInYear(oneYearInTheFuture, DueAt);
var dialogTwoYearsInTheFuture = await Application.CreateDialogWithDateInYear(twoYearsInTheFuture, DueAt);
var dialogThreeYearsInTheFuture = await Application.CreateDialogWithDateInYear(threeYearsInTheFuture, DueAt);
var dialogFourYearsInTheFuture = await Application.CreateDialogWithDateInYear(fourYearsInTheFuture, DueAt);

// Act
var response = await Application.Send(new SearchDialogQuery
{
Party = [Party],
DueAfter = testData.AfterYear.HasValue ? CreateDateFromYear(testData.AfterYear.Value) : null,
DueBefore = testData.BeforeYear.HasValue ? CreateDateFromYear(testData.BeforeYear.Value) : null
});

// Assert
response.TryPickT0(out var result, out _).Should().BeTrue();
result.Should().NotBeNull();

result.Items.Should().HaveCount(testData.ExpectedCount);
foreach (var year in testData.ExpectedYears)
{
var dialogId = year switch
{
_ when year == oneYearInTheFuture => dialogOneYearInTheFuture,
_ when year == twoYearsInTheFuture => dialogTwoYearsInTheFuture,
_ when year == threeYearsInTheFuture => dialogThreeYearsInTheFuture,
_ when year == fourYearsInTheFuture => dialogFourYearsInTheFuture,
_ => throw new ArgumentOutOfRangeException()
};

result.Items.Should().ContainSingle(x => x.Id == dialogId);
}
}

[Fact]
public async Task Cannot_Filter_On_DueAfter_With_Value_Greater_Than_DueBefore()
{
// Act
var response = await Application.Send(new SearchDialogQuery
{
Party = [Party],
DueAfter = CreateDateFromYear(2022),
DueBefore = CreateDateFromYear(2021)
});

// Assert
response.TryPickT1(out var result, out _).Should().BeTrue();
result.Should().NotBeNull();
}

public static IEnumerable<object[]> DueAtTestData()
{
var currentYear = DateTimeOffset.UtcNow.Year;

// The numbers added to "currentYear" here represent future years relative to the current year.
// This is done to create test data for dialogs that are due "soon" (1 to 4 years ahead).
// This approach ensures that the tests remain valid and relevant regardless of the current date.
return new List<object[]>
{
new object[]
{
new DateFilterTestData
{
AfterYear = currentYear + 3,
BeforeYear = null,
ExpectedCount = 2,
ExpectedYears = [currentYear + 3, currentYear + 4]
}
},
new object[]
{
new DateFilterTestData
{
AfterYear = null,
BeforeYear = currentYear + 2,
ExpectedCount = 2,
ExpectedYears = [currentYear + 1, currentYear + 2]
}
},
new object[]
{
new DateFilterTestData
{
AfterYear = currentYear + 1,
BeforeYear = currentYear + 2,
ExpectedCount = 2,
ExpectedYears = [currentYear + 1, currentYear + 2]
}
}
};
}
}
Loading