Skip to content

Commit

Permalink
Improve testing for multiple patch endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarne committed Aug 22, 2024
1 parent 3c4ac0c commit 79a918b
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 20 deletions.
32 changes: 26 additions & 6 deletions src/Altinn.App.Api/Controllers/DataController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ public async Task<ActionResult<DataPatchResponse>> PatchFormData(
}

/// <summary>
/// Updates an existing form data element with a patch of changes.
/// Updates an existing form data element with patches to mulitple data elements.
/// </summary>
/// <param name="org">unique identfier of the organisation responsible for the app</param>
/// <param name="app">application identifier which is unique within an organisation</param>
Expand Down Expand Up @@ -507,17 +507,31 @@ public async Task<ActionResult<DataPatchResponseMultiple>> PatchFormDataMultiple
if (!InstanceIsActive(instance))
{
return Conflict(
$"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}"
new ProblemDetails()
{
Title = "Instance is not active",
Detail =
$"Cannot update data element of archived or deleted instance {instanceOwnerPartyId}/{instanceGuid}",
Status = (int)HttpStatusCode.Conflict,
}
);
}

foreach (Guid dataGuid in dataPatchRequest.Patches.Keys)
{
var dataElement = instance.Data.First(m => m.Id.Equals(dataGuid.ToString(), StringComparison.Ordinal));
var dataElement = instance.Data.Find(m => m.Id.Equals(dataGuid.ToString(), StringComparison.Ordinal));

if (dataElement == null)
if (dataElement is null)
{
return NotFound("Did not find data element");
return NotFound(
new ProblemDetails()
{
Title = "Did not find data element",
Detail =
$"Data element with id {dataGuid} not found on instance {instanceOwnerPartyId}/{instanceGuid}",
Status = (int)HttpStatusCode.NotFound,
}
);
}

var dataType = await GetDataType(dataElement);
Expand All @@ -530,7 +544,13 @@ public async Task<ActionResult<DataPatchResponseMultiple>> PatchFormDataMultiple
org,
app
);
return BadRequest($"Could not determine if data type {dataType?.Id} requires application logic.");
return BadRequest(
new ProblemDetails()
{
Title = "Could not determine if data type requires application logic",
Detail = $"Could not determine if data type {dataType?.Id} requires application logic."
}
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,22 @@ public async Task<LayoutEvaluatorState> Init(
var dataTasks = new List<Task<KeyValuePair<DataElement, object>>>();
foreach (var dataType in layouts.GetReferencedDataTypeIds())
{
dataTasks.AddRange(
instance
.Data.Where(dataElement => dataElement.DataType == dataType)
.Select(async dataElement =>
KeyValuePair.Create(dataElement, await dataAccessor.GetData(dataElement))
)
);
// Find first data element of type dataType
var dataElement = instance.Data.Find(d => d.DataType == dataType);
if (dataElement is not null)
{
dataTasks.Add(
Task.Run(async () => KeyValuePair.Create(dataElement, await dataAccessor.GetData(dataElement)))
);
}
// TODO: This will change when subforms use the same data type for multiple data elemetns.
// dataTasks.AddRange(
// instance
// .Data.Where(dataElement => dataElement.DataType == dataType)
// .Select(async dataElement =>
// KeyValuePair.Create(dataElement, await dataAccessor.GetData(dataElement))
// )
// );
}

var extraModels = await Task.WhenAll(dataTasks);
Expand Down
143 changes: 136 additions & 7 deletions test/Altinn.App.Api.Tests/Controllers/DataController_PatchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Altinn.App.Core.Features;
using Altinn.App.Core.Models.Validation;
using Altinn.Platform.Storage.Interface.Models;
using App.IntegrationTests.Mocks.Services;
using FluentAssertions;
using Json.More;
using Json.Patch;
Expand Down Expand Up @@ -48,6 +49,10 @@ public class DataControllerPatchTests : ApiTestBase, IClassFixture<WebApplicatio
private readonly Mock<IDataProcessor> _dataProcessorMock = new(MockBehavior.Strict);
private readonly Mock<IFormDataValidator> _formDataValidatorMock = new(MockBehavior.Strict);

private HttpClient? _client;

private HttpClient GetClient() => _client ??= GetRootedClient(Org, App, UserId, null);

// Constructor with common setup
public DataControllerPatchTests(WebApplicationFactory<Program> factory, ITestOutputHelper outputHelper)
: base(factory, outputHelper)
Expand Down Expand Up @@ -81,14 +86,44 @@ TResponse parsedResponse
url += $"?language={language}";
}
_outputHelper.WriteLine($"Calling PATCH {url}");
using var httpClient = GetRootedClient(Org, App, UserId, null);

var serializedPatch = JsonSerializer.Serialize(
new DataPatchRequest() { Patch = patch, IgnoredValidators = ignoredValidators, },
_jsonSerializerOptions
);
_outputHelper.WriteLine(serializedPatch);
using var updateDataElementContent = new StringContent(serializedPatch, Encoding.UTF8, "application/json");
var response = await httpClient.PatchAsync(url, updateDataElementContent);
var response = await GetClient().PatchAsync(url, updateDataElementContent);
var responseString = await response.Content.ReadAsStringAsync();
using var responseParsedRaw = JsonDocument.Parse(responseString);
_outputHelper.WriteLine("\nResponse:");
_outputHelper.WriteLine(JsonSerializer.Serialize(responseParsedRaw, _jsonSerializerOptions));
response.Should().HaveStatusCode(expectedStatus);
var responseObject = JsonSerializer.Deserialize<TResponse>(responseString, _jsonSerializerOptions)!;
return (response, responseString, responseObject);
}

// Helper method to call the API
private async Task<(
HttpResponseMessage response,
string responseString,
TResponse parsedResponse
)> CallPatchMultipleApi<TResponse>(
DataPatchRequestMultiple requestMultiple,
HttpStatusCode expectedStatus,
string? language = null
)
{
var url = $"/{Org}/{App}/instances/{InstanceId}/data";
if (language is not null)
{
url += $"?language={language}";
}
_outputHelper.WriteLine($"Calling PATCH {url}");
var serializedPatch = JsonSerializer.Serialize(requestMultiple, _jsonSerializerOptions);
_outputHelper.WriteLine(serializedPatch);
using var updateDataElementContent = new StringContent(serializedPatch, Encoding.UTF8, "application/json");
var response = await GetClient().PatchAsync(url, updateDataElementContent);
var responseString = await response.Content.ReadAsStringAsync();
using var responseParsedRaw = JsonDocument.Parse(responseString);
_outputHelper.WriteLine("\nResponse:");
Expand Down Expand Up @@ -143,6 +178,95 @@ public async Task ValidName_ReturnsOk()
_dataProcessorMock.VerifyNoOtherCalls();
}

[Fact]
public async Task MultiplePatches_AppliesCorrectly()
{
const string prefillDataType = "prefill-data-type";
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(
new AppMetadataMutationHook(
(app) =>
{
app.DataTypes.Add(
new DataType()
{
Id = prefillDataType,
AllowedContentTypes = new List<string> { "application/json" },
AppLogic = new()
{
ClassRef =
"Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models.Skjema",
},
}
);
}
)
);
};
_dataProcessorMock
.Setup(p =>
p.ProcessDataWrite(
It.IsAny<Instance>(),
It.IsAny<Guid?>(),
It.IsAny<object>(),
It.IsAny<object?>(),
null
)
)
.Returns(Task.CompletedTask)
.Verifiable(Times.Exactly(2));

// Initialize extra data element
var createExtraElementResponse = await GetClient()
.PostAsync(
$"{Org}/{App}/instances/{InstanceId}/data?dataType={prefillDataType}",
new StringContent("""{"melding":{}}""", Encoding.UTF8, "application/json")
);
var createExtraElementResponseString = await createExtraElementResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(createExtraElementResponseString);
createExtraElementResponse.Should().HaveStatusCode(HttpStatusCode.Created);
var extraDataId = JsonSerializer
.Deserialize<DataElement>(createExtraElementResponseString, _jsonSerializerOptions)
?.Id;
extraDataId.Should().NotBeNull();
var extraDataGuid = Guid.Parse(extraDataId!);

// Update data element
var patch = new JsonPatch(
PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("\"Ola Olsen\""))
);
var patch2 = new JsonPatch(
PatchOperation.Replace(JsonPointer.Create("melding", "name"), JsonNode.Parse("\"Kari Olsen\""))
);
var request = new DataPatchRequestMultiple()
{
Patches = new Dictionary<Guid, JsonPatch> { [DataGuid] = patch, [extraDataGuid] = patch2, },
IgnoredValidators = []
};

var (_, _, parsedResponse) = await CallPatchMultipleApi<DataPatchResponseMultiple>(request, HttpStatusCode.OK);

parsedResponse.ValidationIssues.Should().ContainKey("Required").WhoseValue.Should().BeEmpty();

parsedResponse.NewDataModels.Should().HaveCount(2).And.ContainKey(DataGuid).And.ContainKey(extraDataGuid);
var newData = parsedResponse
.NewDataModels[DataGuid]
.Should()
.BeOfType<JsonElement>()
.Which.Deserialize<Skjema>()!;
newData.Melding!.Name.Should().Be("Ola Olsen");

var newExtraData = parsedResponse
.NewDataModels[extraDataGuid]
.Should()
.BeOfType<JsonElement>()
.Which.Deserialize<Skjema>()!;
newExtraData.Melding!.Name.Should().Be("Kari Olsen");

_dataProcessorMock.Verify();
}

[Fact]
public async Task NullName_ReturnsOkAndValidationError()
{
Expand Down Expand Up @@ -486,7 +610,7 @@ public async Task RemoveStringProperty_ReturnsCorrectDataModel()
}

[Fact]
public async Task SetStringPropertyToEmtpy_ReturnsCorrectDataModel()
public async Task SetStringPropertyToEmpty_ReturnsCorrectDataModel()
{
_dataProcessorMock
.Setup(p =>
Expand Down Expand Up @@ -531,7 +655,7 @@ public async Task SetStringPropertyToEmtpy_ReturnsCorrectDataModel()
}

[Fact]
public async Task SetAttributeTagPropertyToEmtpy_ReturnsCorrectDataModel()
public async Task SetAttributeTagPropertyToEmpty_ReturnsCorrectDataModel()
{
_dataProcessorMock
.Setup(p =>
Expand Down Expand Up @@ -578,7 +702,7 @@ public async Task SetAttributeTagPropertyToEmtpy_ReturnsCorrectDataModel()
public async Task RowId_GetsAddedAutomatically()
{
var rowIdServer = Guid.NewGuid();
var rowIdClinet = Guid.NewGuid();
var rowIdClient = Guid.NewGuid();
_dataProcessorMock
.Setup(p =>
p.ProcessDataWrite(
Expand Down Expand Up @@ -621,7 +745,7 @@ public async Task RowId_GetsAddedAutomatically()
{
"key": "KeyFromClient",
"intValue": 123,
"altinnRowId": "{{rowIdClinet}}"
"altinnRowId": "{{rowIdClient}}"
},
{
"key": "KeyFromClientNoRowId",
Expand All @@ -647,7 +771,7 @@ public async Task RowId_GetsAddedAutomatically()
{
Key = "KeyFromClient",
IntValue = 123,
AltinnRowId = rowIdClinet
AltinnRowId = rowIdClient
},
new()
{
Expand Down Expand Up @@ -862,4 +986,9 @@ public async Task IgnoredValidators_NotExecuted()
_dataProcessorMock.Verify();
_formDataValidatorMock.Verify();
}

~DataControllerPatchTests()
{
_client?.Dispose();
}
}

0 comments on commit 79a918b

Please sign in to comment.