Skip to content

Commit

Permalink
Add tests for shadow fields and hidden component data removal in proc…
Browse files Browse the repository at this point in the history
…ess/next
  • Loading branch information
ivarne committed Aug 20, 2024
1 parent e6cda48 commit 3c4ac0c
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ private async Task<bool> RemoveFieldsOnTaskComplete(
// Remove hidden data before validation, ignore hidden rows.
if (_appSettings.Value?.RemoveHiddenData == true)
{
// Backend removal of data is deprecated in favor of
// implementing frontend removal of hidden data, so
//this is not updated to remove from multiple data models at once.
LayoutEvaluatorState evaluationState = await _layoutEvaluatorStateInitializer.Init(
instance,
dataAccessor,
Expand Down Expand Up @@ -184,12 +187,13 @@ await _dataClient.InsertFormData(
else
{
// Remove the shadow fields from the data
// TODO: This does not work!!!

data =
JsonSerializer.Deserialize(serializedData, data.GetType())
?? throw new JsonException(
"Could not deserialize back datamodel after removing shadow fields. Data was \"null\""
);
(dataAccessor as CachedInstanceDataAccessor)?.Set(dataElement, data);
isModified = true; // TODO: Detect if modifications were made
}
}
Expand Down
212 changes: 196 additions & 16 deletions test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Altinn.App.Api.Models;
using Altinn.App.Api.Tests.Data;
using Altinn.App.Api.Tests.Data.apps.tdd.contributer_restriction.models;
using Altinn.App.Core.Features;
using Altinn.App.Core.Internal.Instances;
using Altinn.App.Core.Internal.Pdf;
using Altinn.App.Core.Models.Validation;
using Altinn.Platform.Storage.Interface.Models;
using App.IntegrationTests.Mocks.Services;
using FluentAssertions;
using Json.Patch;
using Json.Pointer;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
Expand All @@ -23,9 +29,9 @@ public class ProcessControllerTests : ApiTestBase, IClassFixture<WebApplicationF
private const string Org = "tdd";
private const string App = "contributer-restriction";
private const int InstanceOwnerPartyId = 500600;
private static readonly Guid InstanceGuid = new("5a2fa5ec-f97c-4816-b57a-dc78a981917e");
private static readonly string InstanceId = $"{InstanceOwnerPartyId}/{InstanceGuid}";
private static readonly Guid DataGuid = new("cd691c32-ae36-4555-8aee-0b7054a413e4");
private static readonly Guid _instanceGuid = new("5a2fa5ec-f97c-4816-b57a-dc78a981917e");
private static readonly string _instanceId = $"{InstanceOwnerPartyId}/{_instanceGuid}";
private static readonly Guid _dataGuid = new("cd691c32-ae36-4555-8aee-0b7054a413e4");
private static new readonly JsonSerializerOptions _jsonSerializerOptions =
new()
{
Expand All @@ -50,8 +56,8 @@ public ProcessControllerTests(WebApplicationFactory<Program> factory, ITestOutpu
services.AddSingleton(_dataProcessorMock.Object);
services.AddSingleton(_formDataValidatorMock.Object);
};
TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, InstanceGuid);
TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, InstanceGuid);
TestData.DeleteInstanceAndData(Org, App, InstanceOwnerPartyId, _instanceGuid);
TestData.PrepareInstance(Org, App, InstanceOwnerPartyId, _instanceGuid);
}

[Fact]
Expand Down Expand Up @@ -146,7 +152,7 @@ public async Task RunProcessNextWithLang_VerifyPdfCallWithLanguage()
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
// both "?lang" and "?language" should work
var nextResponse = await client.PutAsync(
$"{Org}/{App}/instances/{InstanceId}/process/next?lang={language}",
$"{Org}/{App}/instances/{_instanceId}/process/next?lang={language}",
null
);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
Expand Down Expand Up @@ -179,7 +185,7 @@ public async Task RunProcessNextWithLanguage_VerifyPdfCall()
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
// both "?lang" and "?language" should work
var nextResponse = await client.PutAsync(
$"{Org}/{App}/instances/{InstanceId}/process/next?language={language}",
$"{Org}/{App}/instances/{_instanceId}/process/next?language={language}",
null
);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
Expand All @@ -191,7 +197,7 @@ public async Task RunProcessNextWithLanguage_VerifyPdfCall()
public async Task RunProcessNext_PdfFails_DataIsUnlocked()
{
bool sendAsyncCalled = false;
var dataElementPath = TestData.GetDataElementPath(Org, App, InstanceOwnerPartyId, InstanceGuid, DataGuid);
var dataElementPath = TestData.GetDataElementPath(Org, App, InstanceOwnerPartyId, _instanceGuid, _dataGuid);

SendAsync = async message =>
{
Expand All @@ -213,7 +219,7 @@ public async Task RunProcessNext_PdfFails_DataIsUnlocked()
return new HttpResponseMessage(HttpStatusCode.TooManyRequests);
};
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{InstanceId}/process/next", null);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError);
Expand Down Expand Up @@ -256,7 +262,7 @@ public async Task RunProcessNext_FailingValidator_ReturnsValidationErrors()
services.AddSingleton(dataValidator.Object);
};
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{InstanceId}/process/next", null);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
Expand All @@ -270,11 +276,185 @@ public async Task RunProcessNext_FailingValidator_ReturnsValidationErrors()
);

// Verify that the instance is not updated
var instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, InstanceGuid);
var instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid);
instance.Process.CurrentTask.Should().NotBeNull();
instance.Process.CurrentTask!.ElementId.Should().Be("Task_1");
}

[Fact]
public async Task RunProcessNext_DataFromHiddenComponents_GetsRemoved()
{
// Override config to remove hidden data
OverrideAppSetting("AppSettings:RemoveHiddenData", "true");

// Mock pdf generation so that the test does not fail due to pof service not running.
var pdfMock = new Mock<IPdfGeneratorClient>(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
pdfMock.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(pdfReturnStream);
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(pdfMock.Object);
};
// setup data processor
_dataProcessorMock
.Setup(dp =>
dp.ProcessDataWrite(
It.IsAny<Instance>(),
_dataGuid,
It.IsAny<Skjema>(),
It.IsAny<Skjema>(),
It.IsAny<string?>()
)
)
.Returns(Task.CompletedTask)
.Verifiable(Times.Once);

// create client for tests
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
var dataPath = TestData.GetDataBlobPath(Org, App, InstanceOwnerPartyId, _instanceGuid, _dataGuid);

// Update hidden data value
var serializedPatch = JsonSerializer.Serialize(
new DataPatchRequest()
{
Patch = new JsonPatch(
PatchOperation.Add(
JsonPointer.Create("melding", "hidden"),
JsonNode.Parse("\"value that is hidden\"")
)
),
IgnoredValidators = []
},
_jsonSerializerOptions
);
_outputHelper.WriteLine(serializedPatch);
using var updateDataElementContent = new StringContent(serializedPatch, Encoding.UTF8, "application/json");
using var response = await client.PatchAsync(
$"{Org}/{App}/instances/{InstanceOwnerPartyId}/{_instanceGuid}/data/{_dataGuid}",
updateDataElementContent
);
response.Should().HaveStatusCode(HttpStatusCode.OK);

// Verify that hidden is stored
var dataString = await File.ReadAllTextAsync(dataPath);
_outputHelper.WriteLine("Data before process next:");
_outputHelper.WriteLine(dataString);
dataString.Should().Contain("<hidden>value that is hidden</hidden>");

// Run process next
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.OK);

// Verify that the instance is updated to the ended state
dataString = await File.ReadAllTextAsync(dataPath);
_outputHelper.WriteLine("Data after process next:");
_outputHelper.WriteLine(dataString);
dataString.Should().NotContain("<hidden>value that is hidden</hidden>");

_dataProcessorMock.Verify();
}

[Theory]
[InlineData(null)]
[InlineData("copyDataType")]
public async Task RunProcessNext_ShadowFields_GetsRemoved(string? saveToDataType)
{
// Mock pdf generation so that the test does not fail due to pof service not running.
var pdfMock = new Mock<IPdfGeneratorClient>(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
pdfMock.Setup(p => p.GeneratePdf(It.IsAny<Uri>(), It.IsAny<CancellationToken>())).ReturnsAsync(pdfReturnStream);
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(pdfMock.Object);
services.AddSingleton(
new AppMetadataMutationHook(appMetadata =>
{
var defaultDataType = appMetadata.DataTypes.Single(dt => dt.Id == "default");
defaultDataType.AppLogic.ShadowFields = new() { Prefix = "SF_", SaveToDataType = saveToDataType };
if (saveToDataType is not null)
appMetadata.DataTypes.Add(
new DataType()
{
Id = saveToDataType,
TaskId = "Task_1",
AppLogic = new() { ClassRef = defaultDataType.AppLogic.ClassRef }
}
);
})
);
};
// setup data processor
_dataProcessorMock
.Setup(dp =>
dp.ProcessDataWrite(
It.IsAny<Instance>(),
_dataGuid,
It.IsAny<Skjema>(),
It.IsAny<Skjema>(),
It.IsAny<string?>()
)
)
.Returns(Task.CompletedTask)
.Verifiable(Times.Once);

// create client for tests
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);

// Update hidden data value
var serializedPatch = JsonSerializer.Serialize(
new DataPatchRequest()
{
Patch = new JsonPatch(
PatchOperation.Add(
JsonPointer.Create("melding", "SF_test"),
JsonNode.Parse("\"value that is in shadow field\"")
)
),
IgnoredValidators = []
},
_jsonSerializerOptions
);
_outputHelper.WriteLine(serializedPatch);
using var updateDataElementContent = new StringContent(serializedPatch, Encoding.UTF8, "application/json");
using var response = await client.PatchAsync(
$"{Org}/{App}/instances/{InstanceOwnerPartyId}/{_instanceGuid}/data/{_dataGuid}",
updateDataElementContent
);
response.Should().HaveStatusCode(HttpStatusCode.OK);

// Verify that hidden is stored
var dataPath = TestData.GetDataBlobPath(Org, App, InstanceOwnerPartyId, _instanceGuid, _dataGuid);
var dataString = await File.ReadAllTextAsync(dataPath);
_outputHelper.WriteLine("Data before process next:");
_outputHelper.WriteLine(dataString);
dataString.Should().Contain("<SF_test>value that is in shadow field</SF_test>");

// Run process next
using var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.OK);

// Get data path if the data element with shadow fields removed is saved to another data type
if (saveToDataType is not null)
{
var instanceClient = Services.GetRequiredService<IInstanceClient>();
var instance = await instanceClient.GetInstance(App, Org, InstanceOwnerPartyId, _instanceGuid);
var copyDataGuid = Guid.Parse(instance.Data.Single(de => de.DataType == saveToDataType).Id);
dataPath = TestData.GetDataBlobPath(Org, App, InstanceOwnerPartyId, _instanceGuid, copyDataGuid);
}
// Verify that the instance is updated to the ended state
dataString = await File.ReadAllTextAsync(dataPath);
_outputHelper.WriteLine("Data after process next:");
_outputHelper.WriteLine(dataString);
dataString.Should().NotContain("<SF_test>value that is in shadow field</SF_test>");

_dataProcessorMock.Verify();
}

[Fact]
public async Task RunProcessNext_NonErrorValidations_ReturnsOk()
{
Expand Down Expand Up @@ -328,15 +508,15 @@ public async Task RunProcessNext_NonErrorValidations_ReturnsOk()
services.AddSingleton(pdfMock.Object);
};
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{InstanceId}/process/next", null);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", null);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.OK);
using var document = JsonDocument.Parse(nextResponseContent);
document.RootElement.EnumerateObject().Should().NotContain(p => p.Name == "validationIssues");

// Verify that the instance is updated to the ended state
var instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, InstanceGuid);
var instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid);
instance.Process.CurrentTask.Should().BeNull();
instance.Process.EndEvent.Should().Be("EndEvent_1");
}
Expand All @@ -352,13 +532,13 @@ public async Task RunCompleteTask_GoesToEndEvent()
services.AddSingleton(pdfMock.Object);
};
using var client = GetRootedClient(Org, App, 1337, InstanceOwnerPartyId);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{InstanceId}/process/completeProcess", null);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/completeProcess", null);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.OK);

// Verify that the instance is updated to the ended state
var instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, InstanceGuid);
var instance = await TestData.GetInstance(Org, App, InstanceOwnerPartyId, _instanceGuid);
instance.Process.CurrentTask.Should().BeNull();
instance.Process.EndEvent.Should().Be("EndEvent_1");
}
Expand All @@ -379,7 +559,7 @@ public async Task RunNextWithAction_WhenActionIsNotAuthorized_ReturnsUnauthorize
Encoding.UTF8,
"application/json"
);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{InstanceId}/process/next", content);
var nextResponse = await client.PutAsync($"{Org}/{App}/instances/{_instanceId}/process/next", content);
var nextResponseContent = await nextResponse.Content.ReadAsStringAsync();
_outputHelper.WriteLine(nextResponseContent);
nextResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden);
Expand Down
15 changes: 14 additions & 1 deletion test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ public HttpClient GetRootedClient(string org, string app, bool includeTraceConte

var factory = _factory.WithWebHostBuilder(builder =>
{
var configuration = new ConfigurationBuilder().AddJsonFile(appSettingsPath).Build();
var configuration = new ConfigurationBuilder()
.AddJsonFile(appSettingsPath)
.AddInMemoryCollection(_configOverrides)
.Build();
configuration.GetSection("AppSettings:AppBasePath").Value = appRootPath;
IConfigurationSection appSettingSection = configuration.GetSection("AppSettings");
Expand All @@ -79,6 +82,16 @@ public HttpClient GetRootedClient(string org, string app, bool includeTraceConte
return client;
}

/// <summary>
/// Overrides the app settings for the test application.
/// </summary>
public void OverrideAppSetting(string key, string? value)
{
_configOverrides[key] = value;
}

private readonly Dictionary<string, string?> _configOverrides = new();

private sealed class DiagnosticHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
Expand Down
Loading

0 comments on commit 3c4ac0c

Please sign in to comment.