-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Disallow search filter with CreatedAfter greater than CreatedBef…
…ore (#2019) <!--- Provide a general summary of your changes in the Title above --> ## Description This PR disallows search filtering with `{dateProperty}After > {dateProperty}Before` for all available date filter types, * CreatedAt * DueAt * UpdatedAt * VisibleFrom (ServiceOwner only) Also adding tests to verify these filters work with normal use, and that validation errors are produced when the rule above is broken. ## Related Issue(s) - #2018 ## 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
Showing
13 changed files
with
713 additions
and
12 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/ValidationErrorStrings.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}'."; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Common/Common.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
...tion.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/Search/CreatedAtFilterTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
114 changes: 114 additions & 0 deletions
114
...lication.Integration.Tests/Features/V1/EndUser/Dialogs/Queries/Search/DueAtFilterTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} | ||
} | ||
}; | ||
} | ||
} |
Oops, something went wrong.