From 4c50a5c5a5ac6db6bbb03972bf4b6eee843c3103 Mon Sep 17 00:00:00 2001 From: andreastanderen Date: Mon, 11 Nov 2024 15:49:16 +0100 Subject: [PATCH] Add custom attribute for OptionValue and OptionLabel and allow empty string --- .../Options/InvalidOptionsFormatException.cs | 25 ++ .../Filters/Options/OptionsErrorCodes.cs | 6 + .../OptionsExceptionFilterAttribute.cs | 26 +++ .../Filters/ProblemDetailsExtensionsCodes.cs | 2 +- .../NotNullableAttribute.cs | 15 ++ .../OptionConverterHelper.cs | 7 +- .../Infrastructure/MvcConfiguration.cs | 2 + backend/src/Designer/Models/Option.cs | 11 +- .../Services/Implementation/OptionsService.cs | 17 -- .../OptionsController/UpdateOptionsTests.cs | 220 ++++++++++-------- .../OptionsController/UploadOptionsTests.cs | 159 +++++++++++++ 11 files changed, 364 insertions(+), 126 deletions(-) create mode 100644 backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs create mode 100644 backend/src/Designer/Filters/Options/OptionsErrorCodes.cs create mode 100644 backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs create mode 100644 backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs create mode 100644 backend/tests/Designer.Tests/Controllers/OptionsController/UploadOptionsTests.cs diff --git a/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs b/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs new file mode 100644 index 00000000000..0dc69bc495d --- /dev/null +++ b/backend/src/Designer/Exceptions/Options/InvalidOptionsFormatException.cs @@ -0,0 +1,25 @@ +using System; + +namespace Altinn.Studio.Designer.Exceptions.Options; + +/// +/// Indicates that an error occurred during json serialization of options. +/// +[Serializable] +public class InvalidOptionsFormatException : Exception +{ + /// + public InvalidOptionsFormatException() + { + } + + /// + public InvalidOptionsFormatException(string message) : base(message) + { + } + + /// + public InvalidOptionsFormatException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs b/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs new file mode 100644 index 00000000000..9053087b1b5 --- /dev/null +++ b/backend/src/Designer/Filters/Options/OptionsErrorCodes.cs @@ -0,0 +1,6 @@ +namespace Altinn.Studio.Designer.Filters.Options; + +public class OptionsErrorCodes +{ + public const string InvalidOptionsFormat = nameof(InvalidOptionsFormat); +} diff --git a/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs b/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs new file mode 100644 index 00000000000..ead5e0a589a --- /dev/null +++ b/backend/src/Designer/Filters/Options/OptionsExceptionFilterAttribute.cs @@ -0,0 +1,26 @@ +using System.Net; +using Altinn.Studio.Designer.Exceptions.Options; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Altinn.Studio.Designer.Filters.Options; + +public class OptionsExceptionFilterAttribute : ExceptionFilterAttribute +{ + public override void OnException(ExceptionContext context) + { + base.OnException(context); + + if (context.ActionDescriptor is not ControllerActionDescriptor) + { + return; + } + + if (context.Exception is InvalidOptionsFormatException) + { + context.Result = new ObjectResult(ProblemDetailsUtils.GenerateProblemDetails(context.Exception, OptionsErrorCodes.InvalidOptionsFormat, HttpStatusCode.BadRequest)) { StatusCode = (int)HttpStatusCode.BadRequest }; + } + } + +} diff --git a/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs b/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs index 4064393c200..a70c0f6f152 100644 --- a/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs +++ b/backend/src/Designer/Filters/ProblemDetailsExtensionsCodes.cs @@ -3,5 +3,5 @@ namespace Altinn.Studio.Designer.Filters; public static class ProblemDetailsExtensionsCodes { public const string ErrorCode = "errorCode"; - + public const string Detail = "detail"; } diff --git a/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs b/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs new file mode 100644 index 00000000000..c4a556f977c --- /dev/null +++ b/backend/src/Designer/Helpers/JsonConverterHelpers/NotNullableAttribute.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers; + +public class NotNullableAttribute : ValidationAttribute +{ + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + if (value == null) + { + return new ValidationResult("The field is required."); + } + return ValidationResult.Success; + } +} diff --git a/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs b/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs index a15ad94deeb..824ec4f0727 100644 --- a/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs +++ b/backend/src/Designer/Helpers/JsonConverterHelpers/OptionConverterHelper.cs @@ -1,10 +1,11 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Altinn.Studio.Designer.Exceptions.Options; namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers; -public class OptionConverter : JsonConverter +public class OptionValueConverter : JsonConverter { public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -14,7 +15,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS JsonTokenType.Number when reader.TryGetDouble(out double d) => d, JsonTokenType.True => true, JsonTokenType.False => false, - _ => throw new JsonException($"Unsupported JSON token for Option.Value: {reader.TokenType}") + _ => throw new InvalidOptionsFormatException($"Unsupported JSON token for Value field, {typeToConvert}: {reader.TokenType}.") }; } @@ -32,7 +33,7 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp writer.WriteBooleanValue(b); break; default: - throw new JsonException("Unsupported type for Option.Value."); + throw new InvalidOptionsFormatException($"{value} is an unsupported type for Value field."); } } } diff --git a/backend/src/Designer/Infrastructure/MvcConfiguration.cs b/backend/src/Designer/Infrastructure/MvcConfiguration.cs index b135b5a35cc..990b05a5498 100644 --- a/backend/src/Designer/Infrastructure/MvcConfiguration.cs +++ b/backend/src/Designer/Infrastructure/MvcConfiguration.cs @@ -2,6 +2,7 @@ using Altinn.Studio.Designer.Filters.DataModeling; using Altinn.Studio.Designer.Filters.Git; using Altinn.Studio.Designer.Filters.IO; +using Altinn.Studio.Designer.Filters.Options; using Altinn.Studio.Designer.ModelBinding; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -26,6 +27,7 @@ public static IServiceCollection ConfigureMvc(this IServiceCollection services) options.Filters.Add(typeof(DataModelingExceptionFilterAttribute)); options.Filters.Add(typeof(GitExceptionFilterAttribute)); options.Filters.Add(typeof(IoExceptionFilterAttribute)); + options.Filters.Add(typeof(OptionsExceptionFilterAttribute)); }) .AddNewtonsoftJson(options => options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter())); diff --git a/backend/src/Designer/Models/Option.cs b/backend/src/Designer/Models/Option.cs index ddfa6b9559e..b49a499ad28 100644 --- a/backend/src/Designer/Models/Option.cs +++ b/backend/src/Designer/Models/Option.cs @@ -1,4 +1,3 @@ -using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Altinn.Studio.Designer.Helpers.JsonConverterHelpers; @@ -12,17 +11,17 @@ public class Option /// /// Value that connects the option to the data model. /// - [Required] + [NotNullable] [JsonPropertyName("value")] - [JsonConverter(typeof(OptionConverter))] - public object Value { get; set; } + [JsonConverter(typeof(OptionValueConverter))] + public required object Value { get; set; } /// /// Label to present to the user. /// - [Required] + [NotNullable] [JsonPropertyName("label")] - public string Label { get; set; } + public required string Label { get; set; } /// /// Description, typically displayed below the label. diff --git a/backend/src/Designer/Services/Implementation/OptionsService.cs b/backend/src/Designer/Services/Implementation/OptionsService.cs index f1834c3156a..978d473f9fd 100644 --- a/backend/src/Designer/Services/Implementation/OptionsService.cs +++ b/backend/src/Designer/Services/Implementation/OptionsService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -73,28 +72,12 @@ public async Task> UploadNewOption(string org, string repo, string List