From bd2ef00307aac17c67c18070ee489d7053599786 Mon Sep 17 00:00:00 2001 From: Friptu Teodor Date: Wed, 26 Mar 2025 22:43:47 +0200 Subject: [PATCH 1/2] Add MatrixQuestion feature --- .../FormBase/Questions/BaseQuestion.cs | 1 + .../FormBase/Questions/MatrixOption.cs | 37 +++ .../FormBase/Questions/MatrixQuestion.cs | 97 +++++++ .../Entities/FormBase/Questions/MatrixRow.cs | 34 +++ .../FormBase/Questions/QuestionTypes.cs | 1 + .../Mappers/QuestionsMapper.cs | 28 ++ .../Requests/BaseQuestionRequest.cs | 3 +- .../Requests/MatrixOptionRequest.cs | 10 + .../Requests/MatrixQuestionRequest.cs | 7 + .../Requests/MatrixRowRequest.cs | 9 + .../MatrixOptionRequestValidator.cs | 19 ++ .../MatrixQuestionRequestValidator.cs | 54 ++++ .../Validators/MatrixRowRequestValidator.cs | 18 ++ .../Entities/Questions/MatrixOptionTests.cs | 51 ++++ ...atrixQuestionTests.GetTranslationStatus.cs | 145 ++++++++++ .../MatrixQuestionTests.TrimTranslations.cs | 56 ++++ .../Entities/Questions/MatrixQuestionTests.cs | 108 ++++++++ .../Entities/Questions/MatrixRowTests.cs | 51 ++++ .../MatrixQuestionRequestValidatorTests.cs | 249 ++++++++++++++++++ .../Aggregates/Questions/MatrixOptionFaker.cs | 13 + .../Aggregates/Questions/MatrixRowFaker.cs | 12 + 21 files changed, 1002 insertions(+), 1 deletion(-) create mode 100644 api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixOption.cs create mode 100644 api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixQuestion.cs create mode 100644 api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixRow.cs create mode 100644 api/src/Vote.Monitor.Form.Module/Requests/MatrixOptionRequest.cs create mode 100644 api/src/Vote.Monitor.Form.Module/Requests/MatrixQuestionRequest.cs create mode 100644 api/src/Vote.Monitor.Form.Module/Requests/MatrixRowRequest.cs create mode 100644 api/src/Vote.Monitor.Form.Module/Validators/MatrixOptionRequestValidator.cs create mode 100644 api/src/Vote.Monitor.Form.Module/Validators/MatrixQuestionRequestValidator.cs create mode 100644 api/src/Vote.Monitor.Form.Module/Validators/MatrixRowRequestValidator.cs create mode 100644 api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixOptionTests.cs create mode 100644 api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.GetTranslationStatus.cs create mode 100644 api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs create mode 100644 api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs create mode 100644 api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixRowTests.cs create mode 100644 api/tests/Vote.Monitor.Form.Module.UnitTests/Validators/MatrixQuestionRequestValidatorTests.cs create mode 100644 api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixOptionFaker.cs create mode 100644 api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixRowFaker.cs diff --git a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/BaseQuestion.cs b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/BaseQuestion.cs index 4870f92bb..36ab24273 100644 --- a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/BaseQuestion.cs +++ b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/BaseQuestion.cs @@ -10,6 +10,7 @@ namespace Vote.Monitor.Domain.Entities.FormBase.Questions; [PolyJsonConverter.SubType(typeof(SingleSelectQuestion), QuestionTypes.SingleSelectQuestionType)] [PolyJsonConverter.SubType(typeof(MultiSelectQuestion), QuestionTypes.MultiSelectQuestionType)] [PolyJsonConverter.SubType(typeof(RatingQuestion), QuestionTypes.RatingQuestionType)] +[PolyJsonConverter.SubType(typeof(MatrixQuestion), QuestionTypes.MatrixQuestionType)] public abstract record BaseQuestion { [JsonPropertyName("$questionType")] diff --git a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixOption.cs b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixOption.cs new file mode 100644 index 000000000..0ff22ed16 --- /dev/null +++ b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixOption.cs @@ -0,0 +1,37 @@ +using Vote.Monitor.Core.Models; + +namespace Vote.Monitor.Domain.Entities.FormBase.Questions; + +public record MatrixOption +{ + public Guid Id { get; private set; } + public TranslatedString Text { get; private set; } + public bool IsFlagged { get; private set; } + + [JsonConstructor] + internal MatrixOption(Guid id, TranslatedString text, bool isFlagged) + { + Id = id; + Text = text; + IsFlagged = isFlagged; + } + + public static MatrixOption Create(Guid id, + TranslatedString text, bool isFlagged) + => new(id, text, isFlagged); + + public void AddTranslation(string languageCode) + { + Text.AddTranslation(languageCode); + } + + public void RemoveTranslation(string languageCode) + { + Text.RemoveTranslation(languageCode); + } + + public void TrimTranslations(IEnumerable languages) + { + Text.TrimTranslations(languages); + } +} diff --git a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixQuestion.cs b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixQuestion.cs new file mode 100644 index 000000000..6dc9c5503 --- /dev/null +++ b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixQuestion.cs @@ -0,0 +1,97 @@ +using System.Runtime.InteropServices.JavaScript; +using Microsoft.AspNetCore.Http.HttpResults; +using NPOI.SS.Formula.Functions; +using Vote.Monitor.Core.Models; + +namespace Vote.Monitor.Domain.Entities.FormBase.Questions; + +public record MatrixQuestion : BaseQuestion +{ + public IReadOnlyList Options { get; private set; } + public IReadOnlyList Rows { get; private set; } + + [JsonConstructor] + internal MatrixQuestion(Guid id, + string code, + TranslatedString text, + TranslatedString? helptext, + DisplayLogic? displayLogic, + IReadOnlyList options, + IReadOnlyList rows) : base(id, code, text, helptext, displayLogic) + { + Options = options; + Rows = rows; + } + + protected override void AddTranslationsInternal(string languageCode) + { + foreach (var option in Options) + { + option.AddTranslation(languageCode); + } + + foreach (var row in Rows) + { + row.AddTranslation(languageCode); + } + } + + protected override void RemoveTranslationInternal(string languageCode) + { + foreach (var option in Options) + { + option.RemoveTranslation(languageCode); + } + + foreach (var row in Rows) + { + row.RemoveTranslation(languageCode); + } + } + + protected override TranslationStatus InternalGetTranslationStatus(string baseLanguageCode, string languageCode) + { + bool anyMissingInOptions = Options.Any(x => string.IsNullOrWhiteSpace(x.Text[languageCode])); + bool anyMissingInRows = Rows.Any(x => string.IsNullOrWhiteSpace(x.Text[languageCode])); + + return anyMissingInOptions || anyMissingInRows + ? TranslationStatus.MissingTranslations + : TranslationStatus.Translated; + } + + protected override void InternalTrimTranslations(IEnumerable languages) + { + var languagesArray = languages as string[] ?? languages.ToArray(); + foreach (var option in Options) + { + option.TrimTranslations(languagesArray); + } + + foreach (var row in Rows) + { + row.TrimTranslations(languagesArray); + } + } + + public static MatrixQuestion Create(Guid id, + string code, + TranslatedString text, + TranslatedString? helptext, + DisplayLogic? displayLogic, + IReadOnlyList options, + IReadOnlyList rows) + => new(id, code, text, helptext, displayLogic, options, rows); + + public virtual bool Equals(MatrixQuestion? other) + { + bool options = base.Equals(other) && Options.SequenceEqual(other.Options); + bool rows = base.Equals(other) && Rows.SequenceEqual(other.Rows); + + return options && rows; + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), Options, Rows); + } +} diff --git a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixRow.cs b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixRow.cs new file mode 100644 index 000000000..7e0a011ad --- /dev/null +++ b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/MatrixRow.cs @@ -0,0 +1,34 @@ +using Vote.Monitor.Core.Models; + +namespace Vote.Monitor.Domain.Entities.FormBase.Questions; + +public record MatrixRow +{ + public Guid Id { get; private set; } + public TranslatedString Text { get; private set; } + + [JsonConstructor] + internal MatrixRow(Guid id, TranslatedString text) + { + Id = id; + Text = text; + } + + public static MatrixRow Create(Guid id, TranslatedString text) + => new(id, text); + + public void AddTranslation(string languageCode) + { + Text.AddTranslation(languageCode); + } + + public void RemoveTranslation(string languageCode) + { + Text.RemoveTranslation(languageCode); + } + + public void TrimTranslations(IEnumerable languages) + { + Text.TrimTranslations(languages); + } +} diff --git a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/QuestionTypes.cs b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/QuestionTypes.cs index 48af6ce79..87894c366 100644 --- a/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/QuestionTypes.cs +++ b/api/src/Vote.Monitor.Domain/Entities/FormBase/Questions/QuestionTypes.cs @@ -8,4 +8,5 @@ public static class QuestionTypes public const string SingleSelectQuestionType = "singleSelectQuestion"; public const string MultiSelectQuestionType = "multiSelectQuestion"; public const string RatingQuestionType = "ratingQuestion"; + public const string MatrixQuestionType = "matrixQuestion"; } diff --git a/api/src/Vote.Monitor.Form.Module/Mappers/QuestionsMapper.cs b/api/src/Vote.Monitor.Form.Module/Mappers/QuestionsMapper.cs index 6ebfeb760..d29243c55 100644 --- a/api/src/Vote.Monitor.Form.Module/Mappers/QuestionsMapper.cs +++ b/api/src/Vote.Monitor.Form.Module/Mappers/QuestionsMapper.cs @@ -90,6 +90,18 @@ public static BaseQuestion ToEntity(BaseQuestionRequest question) ratingQuestion.LowerLabel, ratingQuestion.UpperLabel, ToEntity(ratingQuestion.DisplayLogic)); + + case MatrixQuestionRequest matrixQuestion: + var matrixQuestionOptions = ToEntities(matrixQuestion.Options); + var matrixQuestionRows = ToEntities(matrixQuestion.Rows); + + return MatrixQuestion.Create(matrixQuestion.Id, + matrixQuestion.Code, + matrixQuestion.Text, + matrixQuestion.Helptext, + ToEntity(matrixQuestion.DisplayLogic), + matrixQuestionOptions, + matrixQuestionRows); default: throw new ApplicationException("Unknown question type received"); } @@ -102,6 +114,22 @@ private static IReadOnlyList ToEntities(IEnumerable ToEntities(IEnumerable options) + { + return options + .Select(o => MatrixOption.Create(o.Id, o.Text, o.IsFlagged)) + .ToList() + .AsReadOnly(); + } + + private static IReadOnlyList ToEntities(IEnumerable options) + { + return options + .Select(o => MatrixRow.Create(o.Id, o.Text)) + .ToList() + .AsReadOnly(); + } private static DisplayLogic? ToEntity(DisplayLogicRequest? displayLogic) { diff --git a/api/src/Vote.Monitor.Form.Module/Requests/BaseQuestionRequest.cs b/api/src/Vote.Monitor.Form.Module/Requests/BaseQuestionRequest.cs index 4392fe70c..d18742059 100644 --- a/api/src/Vote.Monitor.Form.Module/Requests/BaseQuestionRequest.cs +++ b/api/src/Vote.Monitor.Form.Module/Requests/BaseQuestionRequest.cs @@ -12,6 +12,7 @@ namespace Vote.Monitor.Form.Module.Requests; [PolyJsonConverter.SubType(typeof(SingleSelectQuestionRequest), QuestionTypes.SingleSelectQuestionType)] [PolyJsonConverter.SubType(typeof(MultiSelectQuestionRequest), QuestionTypes.MultiSelectQuestionType)] [PolyJsonConverter.SubType(typeof(RatingQuestionRequest), QuestionTypes.RatingQuestionType)] +[PolyJsonConverter.SubType(typeof(MatrixQuestionRequest), QuestionTypes.MatrixQuestionType)] public abstract class BaseQuestionRequest { [JsonPropertyName("$questionType")] public string QuestionType => DiscriminatorValue.Get(GetType())!; @@ -24,4 +25,4 @@ public abstract class BaseQuestionRequest public TranslatedString? Helptext { get; set; } public DisplayLogicRequest? DisplayLogic { get; set; } -} \ No newline at end of file +} diff --git a/api/src/Vote.Monitor.Form.Module/Requests/MatrixOptionRequest.cs b/api/src/Vote.Monitor.Form.Module/Requests/MatrixOptionRequest.cs new file mode 100644 index 000000000..169a524e7 --- /dev/null +++ b/api/src/Vote.Monitor.Form.Module/Requests/MatrixOptionRequest.cs @@ -0,0 +1,10 @@ +using Vote.Monitor.Core.Models; + +namespace Vote.Monitor.Form.Module.Requests; + +public class MatrixOptionRequest +{ + public Guid Id { get; set; } + public TranslatedString Text { get; set; } + public bool IsFlagged { get; set; } +} diff --git a/api/src/Vote.Monitor.Form.Module/Requests/MatrixQuestionRequest.cs b/api/src/Vote.Monitor.Form.Module/Requests/MatrixQuestionRequest.cs new file mode 100644 index 000000000..f410f8118 --- /dev/null +++ b/api/src/Vote.Monitor.Form.Module/Requests/MatrixQuestionRequest.cs @@ -0,0 +1,7 @@ +namespace Vote.Monitor.Form.Module.Requests; + +public class MatrixQuestionRequest: BaseQuestionRequest +{ + public List Options { get; set; } = new(); + public List Rows { get; set; } = new(); +} diff --git a/api/src/Vote.Monitor.Form.Module/Requests/MatrixRowRequest.cs b/api/src/Vote.Monitor.Form.Module/Requests/MatrixRowRequest.cs new file mode 100644 index 000000000..f77024344 --- /dev/null +++ b/api/src/Vote.Monitor.Form.Module/Requests/MatrixRowRequest.cs @@ -0,0 +1,9 @@ +using Vote.Monitor.Core.Models; + +namespace Vote.Monitor.Form.Module.Requests; + +public class MatrixRowRequest +{ + public Guid Id { get; set; } + public TranslatedString Text { get; set; } +} diff --git a/api/src/Vote.Monitor.Form.Module/Validators/MatrixOptionRequestValidator.cs b/api/src/Vote.Monitor.Form.Module/Validators/MatrixOptionRequestValidator.cs new file mode 100644 index 000000000..ae6abd272 --- /dev/null +++ b/api/src/Vote.Monitor.Form.Module/Validators/MatrixOptionRequestValidator.cs @@ -0,0 +1,19 @@ +using FastEndpoints; +using FluentValidation; +using Vote.Monitor.Core.Validators; +using Vote.Monitor.Form.Module.Requests; + +namespace Vote.Monitor.Form.Module.Validators; + +public class MatrixOptionRequestValidator: Validator +{ + public MatrixOptionRequestValidator(List languages) + { + RuleFor(x => x.Id) + .NotEmpty(); + + RuleFor(x => x.Text) + .SetValidator(new PartiallyTranslatedStringValidator(languages)); + + } +} diff --git a/api/src/Vote.Monitor.Form.Module/Validators/MatrixQuestionRequestValidator.cs b/api/src/Vote.Monitor.Form.Module/Validators/MatrixQuestionRequestValidator.cs new file mode 100644 index 000000000..6d3ebabd4 --- /dev/null +++ b/api/src/Vote.Monitor.Form.Module/Validators/MatrixQuestionRequestValidator.cs @@ -0,0 +1,54 @@ +using FastEndpoints; +using FluentValidation; +using Vote.Monitor.Core.Validators; +using Vote.Monitor.Form.Module.Requests; + +namespace Vote.Monitor.Form.Module.Validators; + +public class MatrixQuestionRequestValidator : Validator +{ + public MatrixQuestionRequestValidator(List languages) + { + RuleFor(x => x.Id).NotEmpty(); + + RuleFor(x => x.QuestionType).NotEmpty(); + + RuleFor(x => x.Text) + .SetValidator(new PartiallyTranslatedStringValidator(languages)); + + RuleFor(x => x.Helptext) + .SetValidator(new PartiallyTranslatedStringValidator(languages)) + .When(x => x.Helptext != null); + + RuleFor(x => x.Code) + .NotEmpty() + .MaximumLength(256); + + RuleForEach(x => x.Options) + .SetValidator(new MatrixOptionRequestValidator(languages)); + + RuleFor(x => x.Options) + .Must(options => + { + var groupedOptionIds = options.GroupBy(o => o.Id, (id, group) => new { id, count = group.Count() }); + + return groupedOptionIds.All(g => g.count == 1); + }) + .WithMessage("Duplicated id found"); + + RuleForEach(x => x.Rows) + .SetValidator(new MatrixRowRequestValidator(languages)); + + RuleFor(x => x.Rows) + .Must(options => + { + var groupedOptionIds = options.GroupBy(o => o.Id, (id, group) => new { id, count = group.Count() }); + + return groupedOptionIds.All(g => g.count == 1); + }) + .WithMessage("Duplicated id found"); + + RuleFor(x => x.DisplayLogic) + .SetValidator(new DisplayLogicRequestValidator()); + } +} diff --git a/api/src/Vote.Monitor.Form.Module/Validators/MatrixRowRequestValidator.cs b/api/src/Vote.Monitor.Form.Module/Validators/MatrixRowRequestValidator.cs new file mode 100644 index 000000000..8a399f7eb --- /dev/null +++ b/api/src/Vote.Monitor.Form.Module/Validators/MatrixRowRequestValidator.cs @@ -0,0 +1,18 @@ +using FastEndpoints; +using FluentValidation; +using Vote.Monitor.Core.Validators; +using Vote.Monitor.Form.Module.Requests; + +namespace Vote.Monitor.Form.Module.Validators; + +public class MatrixRowRequestValidator: Validator +{ + public MatrixRowRequestValidator(List languages) + { + RuleFor(x => x.Id) + .NotEmpty(); + + RuleFor(x => x.Text) + .SetValidator(new PartiallyTranslatedStringValidator(languages)); + } +} diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixOptionTests.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixOptionTests.cs new file mode 100644 index 000000000..3a8117f94 --- /dev/null +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixOptionTests.cs @@ -0,0 +1,51 @@ +using Vote.Monitor.Core.Helpers; + +namespace Vote.Monitor.Domain.UnitTests.Entities.Questions; + +public class MatrixOptionTests +{ + [Fact] + public void ComparingToAMatrixOption_WithSameProperties_ReturnsTrue() + { + // Arrange + var text = new TranslatedString + { + {"EN", "text"} + }; + + var id = Guid.NewGuid(); + var option1 = MatrixOption.Create(id, text, true); + var option2 = option1.DeepClone(); + + // Act + var result = option1 == option2; + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void ComparingToAMatrixOption_WithDifferentProperties_ReturnsFalse() + { + // Arrange + var text1 = new TranslatedString + { + {"EN", "text"} + }; + + var text2 = new TranslatedString + { + {"EN", "other tex"} + }; + + var id = Guid.NewGuid(); + var option1 = MatrixOption.Create(id, text1, true); + var option2 = MatrixOption.Create(id, text2, true); + + // Act + var result = option1 == option2; + + // Assert + result.Should().BeFalse(); + } +} diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.GetTranslationStatus.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.GetTranslationStatus.cs new file mode 100644 index 000000000..a03b678b7 --- /dev/null +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.GetTranslationStatus.cs @@ -0,0 +1,145 @@ +using Vote.Monitor.Core.Helpers; + +namespace Vote.Monitor.Domain.UnitTests.Entities.Questions; + +public partial class MatrixQuestionTests +{ + [Theory] + [MemberData(nameof(TestData.EmptyStringsTestCases), MemberType = typeof(TestData))] + public void GetTranslationStatus_WithMissingTranslationForText_ReturnsMissingTranslation(string emptyString) + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString(emptyString); + MatrixOption[] options = [.. new MatrixOptionFaker().Generate(3)]; + MatrixRow[] rows = [.. new MatrixRowFaker().Generate(3)]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, null, null, options, rows); + + // Act + var status = matrixQuestion.GetTranslationStatus(_defaultLanguageCode, _languageCode); + + // Assert + status.Should().Be(TranslationStatus.MissingTranslations); + } + + [Theory] + [MemberData(nameof(TestData.EmptyStringsTestCases), MemberType = typeof(TestData))] + public void GetTranslationStatus_WithMissingTranslationForHelptext_ReturnsMissingTranslation(string emptyString) + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + var helptext = CreateTranslatedString(emptyString); + MatrixOption[] options = [.. new MatrixOptionFaker().Generate(3)]; + MatrixRow[] rows = [.. new MatrixRowFaker().Generate(3)]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, options, rows); + + // Act + var status = matrixQuestion.GetTranslationStatus(_defaultLanguageCode, _languageCode); + + // Assert + status.Should().Be(TranslationStatus.MissingTranslations); + } + + [Fact] + public void GetTranslationStatus_NullHelptext_ReturnsTranslated() + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + TranslatedString? helptext = null; + string[] languages = [_defaultLanguageCode, _languageCode]; + + + MatrixOption[] options = [.. new MatrixOptionFaker(languageList: languages).Generate(3)]; + MatrixRow[] rows = [.. new MatrixRowFaker(languageList: languages).Generate(3)]; + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, options, rows); + + // Act + var status = matrixQuestion.GetTranslationStatus(_defaultLanguageCode, _languageCode); + + // Assert + status.Should().Be(TranslationStatus.Translated); + } + + [Theory] + [MemberData(nameof(TestData.EmptyStringsTestCases), MemberType = typeof(TestData))] + public void GetTranslationStatus_WithHelptextEmptyForBaseLanguage_ReturnsTranslated(string emptyString) + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + var helptext = new TranslatedString + { + [_defaultLanguageCode] = emptyString, + [_languageCode] = "some helptext" + }; + string[] languages = [_defaultLanguageCode, _languageCode]; + + MatrixOption[] options = [.. new MatrixOptionFaker(languageList: languages).Generate(3)]; + MatrixRow[] rows = [.. new MatrixRowFaker(languageList: languages).Generate(3)]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, options, rows); + + // Act + var status = matrixQuestion.GetTranslationStatus(_defaultLanguageCode, _languageCode); + + // Assert + status.Should().Be(TranslationStatus.Translated); + } + + [Theory] + [MemberData(nameof(TestData.EmptyStringsTestCases), MemberType = typeof(TestData))] + public void GetTranslationStatus_WithMissingTranslationForOption_ReturnsMissingTranslation(string emptyString) + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + var helptext = CreateTranslatedString("some helptext"); + var nonTranslatedOption = MatrixOption.Create(Guid.NewGuid(), CreateTranslatedString(emptyString), false); + var translatedOption = MatrixOption.Create(Guid.NewGuid(), new TranslatedString + { + [_defaultLanguageCode] = "some option", + [_languageCode] = "some option translated" + }, false); + + string[] languages = [_defaultLanguageCode, _languageCode]; + + MatrixRow[] rows = [.. new MatrixRowFaker(languageList: languages).Generate(3)]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, [translatedOption, nonTranslatedOption], rows); + + // Act + var status = matrixQuestion.GetTranslationStatus(_defaultLanguageCode, _languageCode); + + // Assert + status.Should().Be(TranslationStatus.MissingTranslations); + } + + // to implement + // GetTranslationStatus_WhenFullyTranslated_ReturnsTranslated() + + [Fact] + public void GetTranslationStatus_WhenFullyTranslated_ReturnsTranslated() + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + var helptext = CreateTranslatedString("some other text"); + + string[] languages = [_defaultLanguageCode, _languageCode]; + MatrixOption[] options = [.. new MatrixOptionFaker(languageList: languages).Generate(3)]; + MatrixRow[] rows = [.. new MatrixRowFaker(languageList: languages).Generate(3)]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, options, rows ); + + // Act + var status = matrixQuestion.GetTranslationStatus(_defaultLanguageCode, _languageCode); + + // Assert + status.Should().Be(TranslationStatus.Translated); + } + +} diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs new file mode 100644 index 000000000..8fb7945e3 --- /dev/null +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs @@ -0,0 +1,56 @@ +namespace Vote.Monitor.Domain.UnitTests.Entities.Questions; + +public partial class MatrixQuestionTests +{ + [Fact] + public void TrimTranslations_RemovesUnusedTranslations() + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + var helptext = CreateTranslatedString("some other text"); + + string[] languages = [_defaultLanguageCode, _languageCode]; + + + MatrixRow[] rows = [.. new MatrixRowFaker(languageList: languages).Generate(1)]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, [_translatedOption],[_translatedRow]); + + + // Act + matrixQuestion.TrimTranslations([_defaultLanguageCode]); + + // Assert + matrixQuestion.Text.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some text for default language")); + matrixQuestion.Text.Should().HaveCount(1); + matrixQuestion.Helptext.Should().HaveCount(1).And.Subject.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some text for default language")); + matrixQuestion.Options.First().Text.Should().HaveCount(1).And.Subject.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some option")); + matrixQuestion.Rows.First().Text.Should().HaveCount(1).And.Subject.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some option")); + + } + + [Fact] + public void TrimTranslations_RemovesUnusedTranslations_Ignores_Null() + { + // Arrange + var id = Guid.NewGuid(); + var text = CreateTranslatedString("some text"); + string[] languages = [_defaultLanguageCode, _languageCode]; + + var matrixQuestion = MatrixQuestion.Create(id, "C!", text, null, null, [_translatedOption],[_translatedRow]); + + // Act + matrixQuestion.TrimTranslations([_defaultLanguageCode]); + + // Assert + matrixQuestion.Text.Should().HaveCount(1).And.Subject.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some text for default language")); + + matrixQuestion.Options.First().Text.Should().HaveCount(1); + matrixQuestion.Options.First().Text.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some option")); + + matrixQuestion.Rows.First().Text.Should().HaveCount(1); + matrixQuestion.Rows.First().Text.Should().Contain(new KeyValuePair(_defaultLanguageCode, "some option")); + matrixQuestion.Helptext.Should().BeNull(); + } +} diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs new file mode 100644 index 000000000..dfe1bc6f8 --- /dev/null +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs @@ -0,0 +1,108 @@ +using Vote.Monitor.Core.Helpers; + +namespace Vote.Monitor.Domain.UnitTests.Entities.Questions; + +public partial class MatrixQuestionTests +{ + private readonly string _defaultLanguageCode = LanguagesList.EN.Iso1; + private readonly string _languageCode = LanguagesList.RO.Iso1; + private readonly MatrixOption _translatedOption; + private readonly MatrixRow _translatedRow; + + public MatrixQuestionTests() + { + _translatedOption = MatrixOption.Create(Guid.NewGuid(), new TranslatedString + { + [_defaultLanguageCode] = "some option", + [_languageCode] = "some option translated" + }, + false); + + _translatedRow = MatrixRow.Create(Guid.NewGuid(), new TranslatedString + { + [_defaultLanguageCode] = "some option", + [_languageCode] = "some option translated" + }); + } + + private TranslatedString CreateTranslatedString(string value) + { + return new TranslatedString + { + [_defaultLanguageCode] = "some text for default language", + [_languageCode] = value + }; + } + + [Fact] + public void ComparingToAMatrixQuestion_WithSameProperties_ReturnsTrue() + { + // Arrange + var text = new TranslatedString + { + {_defaultLanguageCode, "some text"} + }; + + var helptext = new TranslatedString + { + {"EN", "other text"} + }; + + MatrixOption[] options = [.. new MatrixOptionFaker().Generate(3)]; + MatrixRow[] rows = [.. new MatrixRowFaker().Generate(3)]; + + var id = Guid.NewGuid(); + var matrixQuestion1 = MatrixQuestion.Create( + id, "C!", text, helptext,null,options,rows); + var matrixQuestion2 = matrixQuestion1.DeepClone(); + + // Act + var result = matrixQuestion1 == matrixQuestion2; + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void ComparingToAMatrixQuestion_WithDifferentProperties_ReturnsFalse() + { + // Arrange + var text1 = new TranslatedString + { + {_defaultLanguageCode, "some text"} + }; + + var text2 = new TranslatedString + { + {_defaultLanguageCode, "some text"} + }; + + var helptext1 = new TranslatedString + { + {_defaultLanguageCode, "other text"} + }; + + var helptext2 = new TranslatedString + { + {_defaultLanguageCode, "other different"} + }; + + MatrixOption[] options1 = [.. new MatrixOptionFaker().Generate(3)]; + MatrixOption[] options2 = [.. new MatrixOptionFaker().Generate(3)]; + + MatrixRow[] rows1 = [.. new MatrixRowFaker().Generate(3)]; + MatrixRow[] rows2 = [.. new MatrixRowFaker().Generate(3)]; + + + var id = Guid.NewGuid(); + + var textQuestion1 = MatrixQuestion.Create(id, "C!", text1,null,null, options1,rows1); + var textQuestion2 = MatrixQuestion.Create(id, "C!", text2, null,null, options2, rows2); + + // Act + var result = textQuestion1 == textQuestion2; + + // Assert + result.Should().BeFalse(); + } +} diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixRowTests.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixRowTests.cs new file mode 100644 index 000000000..6d0ecd8c2 --- /dev/null +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixRowTests.cs @@ -0,0 +1,51 @@ +using Vote.Monitor.Core.Helpers; + +namespace Vote.Monitor.Domain.UnitTests.Entities.Questions; + +public class MatrixRowTests +{ + [Fact] + public void ComparingToAMatrixRow_WithSameProperties_ReturnsTrue() + { + // Arrange + var text = new TranslatedString + { + {"EN", "text"} + }; + + var id = Guid.NewGuid(); + var row1 = MatrixRow.Create(id, text); + var row2 = row1.DeepClone(); + + // Act + var result = row1 == row2; + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void ComparingToAMatrixRow_WithDifferentProperties_ReturnsFalse() + { + // Arrange + var text1 = new TranslatedString + { + {"EN", "text"} + }; + + var text2 = new TranslatedString + { + {"EN", "other tex"} + }; + + var id = Guid.NewGuid(); + var row1 = MatrixRow.Create(id, text1); + var row2 = MatrixRow.Create(id, text2); + + // Act + var result = row1 == row2; + + // Assert + result.Should().BeFalse(); + } +} diff --git a/api/tests/Vote.Monitor.Form.Module.UnitTests/Validators/MatrixQuestionRequestValidatorTests.cs b/api/tests/Vote.Monitor.Form.Module.UnitTests/Validators/MatrixQuestionRequestValidatorTests.cs new file mode 100644 index 000000000..dc408d919 --- /dev/null +++ b/api/tests/Vote.Monitor.Form.Module.UnitTests/Validators/MatrixQuestionRequestValidatorTests.cs @@ -0,0 +1,249 @@ +using FluentAssertions; +using FluentValidation.TestHelper; +using Vote.Monitor.Core.Constants; +using Vote.Monitor.Core.Models; +using Vote.Monitor.Domain.Entities.FormBase.Questions; +using Vote.Monitor.Form.Module.Requests; +using Vote.Monitor.Form.Module.Validators; + +namespace Vote.Monitor.Form.Module.UnitTests.Validators; + +public class MatrixQuestionRequestValidatorTests +{ + private readonly MatrixQuestionRequestValidator _sut = new([LanguagesList.EN.Iso1, LanguagesList.RO.Iso1]); + + [Fact] + public void Validation_ShouldFail_When_EmptyId() + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Id = Guid.Empty + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor(x => x.Id); + } + + [Theory] + [MemberData(nameof(ValidatorsTestData.InvalidPartiallyTranslatedTestCases), MemberType = typeof(ValidatorsTestData))] + public void Validation_ShouldFail_When_EmptyText(TranslatedString invalidText) + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Text = invalidText + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor(x => x.Text); + } + + [Theory] + [MemberData(nameof(ValidatorsTestData.InvalidPartiallyTranslatedTestCases), MemberType = typeof(ValidatorsTestData))] + public void Validation_ShouldFail_When_EmptyHelptext(TranslatedString invalidHelptext) + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Helptext = invalidHelptext + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor(x => x.Helptext); + } + + [Fact] + public void Validation_ShouldPass_When_NoHelptext() + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Helptext = null + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldNotHaveValidationErrorFor(x => x.Helptext); + } + + [Theory] + [MemberData(nameof(TestData.EmptyStringsTestCases), MemberType = typeof(TestData))] + public void Validation_ShouldFail_When_CodeEmpty(string code) + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Code = code + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor(x => x.Code) + .WithErrorMessage("'Code' must not be empty."); + } + + [Theory] + [MemberData(nameof(ValidatorsTestData.InvalidCodeTestCases), MemberType = typeof(ValidatorsTestData))] + public void Validation_ShouldFail_When_CodeHasInvalidLength(string code) + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Code = code + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor(x => x.Code); + } + + [Fact] + public void Validation_ShouldFail_When_SomeOptionsAreInvalid() + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Options = [ + new MatrixOptionRequest + { + Text = ValidatorsTestData.ValidPartiallyTranslatedTestData.Last() + }, + new MatrixOptionRequest + { + Text = ValidatorsTestData.InvalidPartiallyTranslatedTestData.Last() + } + ] + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor("Options[1].Text"); + } + + [Fact] + public void Validation_ShouldPass_When_EmptyOptions() + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Options = [] + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldNotHaveValidationErrorFor(x => x.Options); + } + + + [Fact] + public void Validation_ShouldFail_When_InvalidDisplayLogic() + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + DisplayLogic = new DisplayLogicRequest + { + ParentQuestionId = Guid.Empty + } + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldHaveValidationErrorFor("DisplayLogic.ParentQuestionId"); + } + + [Theory] + [MemberData(nameof(ValidatorsTestData.ValidDisplayLogicTestCases), MemberType = typeof(ValidatorsTestData))] + public void Validation_ShouldPass_When_ValidDisplayLogic(DisplayLogicRequest? displayLogic) + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + DisplayLogic = displayLogic + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .Errors + .Should() + .AllSatisfy(x => + { + x.PropertyName.Should().NotContain(nameof(MultiSelectQuestionRequest.DisplayLogic)); + }); + } + + [Fact] + public void Validation_ShouldPass_When_ValidRequest() + { + // Arrange + var matrixQuestionRequest = new MatrixQuestionRequest + { + Helptext = ValidatorsTestData.ValidPartiallyTranslatedTestData.First(), + Text = ValidatorsTestData.ValidPartiallyTranslatedTestData.First(), + Code = "A code", + Id = Guid.NewGuid(), + Options = [ + new MatrixOptionRequest + { + Id = Guid.NewGuid(), + Text = ValidatorsTestData.ValidPartiallyTranslatedTestData.Last(), + IsFlagged = false + } + ], + Rows = [ + new MatrixRowRequest + { + Id = Guid.NewGuid(), + Text = ValidatorsTestData.ValidPartiallyTranslatedTestData.Last() + } + ], + DisplayLogic = new DisplayLogicRequest + { + ParentQuestionId = Guid.NewGuid(), + Condition = DisplayLogicCondition.GreaterEqual, + Value = "1" + } + }; + + // Act + var validationResult = _sut.TestValidate(matrixQuestionRequest); + + // Assert + validationResult + .ShouldNotHaveAnyValidationErrors(); + } +} diff --git a/api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixOptionFaker.cs b/api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixOptionFaker.cs new file mode 100644 index 000000000..5ad22b5bd --- /dev/null +++ b/api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixOptionFaker.cs @@ -0,0 +1,13 @@ +using Bogus; +using Vote.Monitor.Domain.Entities.FormBase.Questions; + +namespace Vote.Monitor.TestUtils.Fakes.Aggregates.Questions; + +public sealed class MatrixOptionFaker : Faker +{ + public MatrixOptionFaker(Guid? id = null, string[]? languageList = null, bool isFlagged=false) + { + CustomInstantiator(f => MatrixOption.Create(id ?? f.Random.Guid(), new TranslatedStringFaker(languageList), + isFlagged)); + } +} diff --git a/api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixRowFaker.cs b/api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixRowFaker.cs new file mode 100644 index 000000000..9bc296b50 --- /dev/null +++ b/api/tests/Vote.Monitor.TestUtils/Fakes/Aggregates/Questions/MatrixRowFaker.cs @@ -0,0 +1,12 @@ +using Bogus; +using Vote.Monitor.Domain.Entities.FormBase.Questions; + +namespace Vote.Monitor.TestUtils.Fakes.Aggregates.Questions; + +public sealed class MatrixRowFaker : Faker +{ + public MatrixRowFaker(Guid? id = null, string[]? languageList = null) + { + CustomInstantiator(f => MatrixRow.Create(id ?? f.Random.Guid(), new TranslatedStringFaker(languageList))); + } +} From 32c2de89cfdab77ca517aceca95db0ad87fa52c0 Mon Sep 17 00:00:00 2001 From: Friptu Teodor Date: Thu, 27 Mar 2025 14:55:12 +0200 Subject: [PATCH 2/2] github-advanced-security potential problems checked --- .../Questions/MatrixQuestionTests.TrimTranslations.cs | 6 ------ .../Entities/Questions/MatrixQuestionTests.cs | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs index 8fb7945e3..41dea8c01 100644 --- a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.TrimTranslations.cs @@ -10,11 +10,6 @@ public void TrimTranslations_RemovesUnusedTranslations() var text = CreateTranslatedString("some text"); var helptext = CreateTranslatedString("some other text"); - string[] languages = [_defaultLanguageCode, _languageCode]; - - - MatrixRow[] rows = [.. new MatrixRowFaker(languageList: languages).Generate(1)]; - var matrixQuestion = MatrixQuestion.Create(id, "C!", text, helptext, null, [_translatedOption],[_translatedRow]); @@ -36,7 +31,6 @@ public void TrimTranslations_RemovesUnusedTranslations_Ignores_Null() // Arrange var id = Guid.NewGuid(); var text = CreateTranslatedString("some text"); - string[] languages = [_defaultLanguageCode, _languageCode]; var matrixQuestion = MatrixQuestion.Create(id, "C!", text, null, null, [_translatedOption],[_translatedRow]); diff --git a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs index dfe1bc6f8..67e5fcb24 100644 --- a/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs +++ b/api/tests/Vote.Monitor.Domain.UnitTests/Entities/Questions/MatrixQuestionTests.cs @@ -96,8 +96,8 @@ public void ComparingToAMatrixQuestion_WithDifferentProperties_ReturnsFalse() var id = Guid.NewGuid(); - var textQuestion1 = MatrixQuestion.Create(id, "C!", text1,null,null, options1,rows1); - var textQuestion2 = MatrixQuestion.Create(id, "C!", text2, null,null, options2, rows2); + var textQuestion1 = MatrixQuestion.Create(id, "C!", text1,helptext1,null, options1,rows1); + var textQuestion2 = MatrixQuestion.Create(id, "C!", text2, helptext2,null, options2, rows2); // Act var result = textQuestion1 == textQuestion2;