Skip to content

Commit

Permalink
fix: Use object type for option value and custom converter to restric…
Browse files Browse the repository at this point in the history
…t invalid types (#13968)
  • Loading branch information
standeren authored Nov 6, 2024
1 parent e845708 commit 3d9bf61
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Altinn.Studio.Designer.Helpers.JsonConverterHelpers;

public class OptionConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
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}")
};
}

public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
switch (value)
{
case string s:
writer.WriteStringValue(s);
break;
case double d:
writer.WriteNumberValue(d);
break;
case bool b:
writer.WriteBooleanValue(b);
break;
default:
throw new JsonException("Unsupported type for Option.Value.");
}
}
}
4 changes: 3 additions & 1 deletion backend/src/Designer/Models/Option.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Altinn.Studio.Designer.Helpers.JsonConverterHelpers;

namespace Altinn.Studio.Designer.Models;

Expand All @@ -13,7 +14,8 @@ public class Option
/// </summary>
[Required]
[JsonPropertyName("value")]
public string Value { get; set; }
[JsonConverter(typeof(OptionConverter))]
public object Value { get; set; }

/// <summary>
/// Label to present to the user.
Expand Down
12 changes: 11 additions & 1 deletion backend/src/Designer/Services/Implementation/OptionsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public async Task<List<Option>> UploadNewOption(string org, string repo, string
List<Option> deserializedOptions = JsonSerializer.Deserialize<List<Option>>(payload.OpenReadStream(),
new JsonSerializerOptions { WriteIndented = true, AllowTrailingCommas = true });

IEnumerable<Option> result = deserializedOptions.Where(option => string.IsNullOrEmpty(option.Value) || string.IsNullOrEmpty(option.Label));
IEnumerable<Option> result = deserializedOptions.Where(option => IsNullOrEmptyOptionValue(option) || string.IsNullOrEmpty(option.Label));
if (result.Any())
{
throw new JsonException("Uploaded file is missing one of the following attributes for an option: value or label.");
Expand All @@ -85,6 +85,16 @@ public async Task<List<Option>> UploadNewOption(string org, string repo, string
return deserializedOptions;
}

bool IsNullOrEmptyOptionValue(object value)
{
if (value == null)
{
return true;
}

return value is string stringOption && string.IsNullOrEmpty(stringOption);
}

/// <inheritdoc />
public void DeleteOptionsList(string org, string repo, string developer, string optionsListId)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public async Task Put_Returns_200OK_When_Creating_New_OptionsList()

var newOptionsList = new List<Option>
{
new Option
new ()
{
Label = "label1",
Value = "value1",
},
new Option
new ()
{
Label = "label2",
Value = "value2",
Expand Down Expand Up @@ -67,6 +67,45 @@ public async Task Put_Returns_200OK_When_Creating_New_OptionsList()
}
}

[Fact]
public async Task Put_Returns_200OK_When_Option_Values_Are_Bool_String_Double()
{
string repo = "app-with-options";
string optionsListId = "test-options";
// Arrange
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(Org, repo, Developer, targetRepository);

string apiUrl = $"/designer/api/{Org}/{targetRepository}/options/{optionsListId}";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Put, apiUrl);

var stringBoolDoubleOptionsList = new List<Option>
{
new ()
{
Label = "StringValue",
Value = "value1",
},
new ()
{
Label = "BoolValue",
Value = true,
},
new ()
{
Label = "DoubleValue",
Value = 3.1415,
}
};
httpRequestMessage.Content = JsonContent.Create(stringBoolDoubleOptionsList);

// Act
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode);
}

[Fact]
public async Task Put_Returns_200OK_And_Overwrites_Existing_OptionsList()
{
Expand All @@ -79,12 +118,12 @@ public async Task Put_Returns_200OK_And_Overwrites_Existing_OptionsList()

var newOptionsList = new List<Option>
{
new Option
new ()
{
Label = "aNewLabelThatDidNotExistBefore",
Value = "aNewValueThatDidNotExistBefore",
},
new Option
new ()
{
Label = "label2",
Value = "value2",
Expand Down Expand Up @@ -129,12 +168,12 @@ public async Task Put_Returns_400BadRequest_When_OptionsList_Format_Is_Invalid(s
{
var invalidOptionsList = new List<Option>
{
new Option
new ()
{
// Missing Label
Value = "value1",
},
new Option
new ()
{
Label = "label2",
Value = "value2",
Expand All @@ -153,4 +192,33 @@ public async Task Put_Returns_400BadRequest_When_OptionsList_Format_Is_Invalid(s
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
}

[Fact]
public async Task Put_Returns_400BadRequest_When_Option_Value_Is_Invalid()
{
string repo = "app-with-options";
string optionsListId = "test-options";
// Arrange
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(Org, repo, Developer, targetRepository);

string apiUrl = $"/designer/api/{Org}/{targetRepository}/options/{optionsListId}";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Put, apiUrl);

var invalidOptionsList = new List<Option>
{
new ()
{
Label = "ObjectValue",
Value = { },
},
};
httpRequestMessage.Content = JsonContent.Create(invalidOptionsList);

// Act
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode);
}
}

0 comments on commit 3d9bf61

Please sign in to comment.