diff --git a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/ShiftedLookupIdsTests.cs b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/ShiftedLookupIdsTests.cs new file mode 100644 index 00000000..91007f94 --- /dev/null +++ b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/ShiftedLookupIdsTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using eFormCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Microting.eForm.Infrastructure; +using Microting.eForm.Infrastructure.Constants; +using Microting.eForm.Infrastructure.Data.Entities; +using Microting.eForm.Infrastructure.Helpers; +using Microting.eFormApi.BasePn.Abstractions; +using Microting.eFormApi.BasePn.Infrastructure.Database.Entities; +using Microting.eFormApi.BasePn.Infrastructure.Helpers.PluginDbOptions; +using Microting.eFormWorkflowBase.Infrastructure.Data; +using Microting.eFormWorkflowBase.Infrastructure.Data.Entities; +using NSubstitute; +using NUnit.Framework; +using Testcontainers.MariaDb; +using Workflow.Pn.Infrastructure.Models.Settings; +using Workflow.Pn.Services.WorkflowCasesService; +using Workflow.Pn.Services.WorkflowLocalizationService; + +#nullable enable +namespace Workflow.Pn.Test; + +/// +/// microting/eform-angular-frontend#8141: on a MariaDB Galera cluster +/// (auto_increment_increment=3) the SDK lookup tables get non-contiguous ids +/// (Picture=15, SingleSelect=24, Dansk=3, ...). The workflow plugin must +/// resolve field types by FieldType.Type, never by a hardcoded numeric id. +/// +[TestFixture] +public class ShiftedLookupIdsTests +{ + // Mimic a Galera node: every AUTO_INCREMENT step is 3, starting at 3, + // so the SDK field types seed as Text=3 .. Picture=15 .. SingleSelect=24. + private readonly MariaDbContainer _container = new MariaDbBuilder("mariadb:11.2") + .WithDatabase("myDb").WithUsername("bla").WithPassword("secretpassword") + .WithCommand("--auto-increment-increment=3", "--auto-increment-offset=3") + .Build(); + + private string _sdkConnection = null!; + private MicrotingDbContext _sdkDbContext = null!; + private WorkflowPnDbContext _workflowDbContext = null!; + private Language _danish = null!; + + // SDK seeding order (SqlController.SettingCheckAll) + private static readonly string[] SdkFieldTypeOrder = + { + Constants.FieldTypes.Text, Constants.FieldTypes.Number, Constants.FieldTypes.None, + Constants.FieldTypes.CheckBox, Constants.FieldTypes.Picture, Constants.FieldTypes.Audio, + Constants.FieldTypes.Movie, Constants.FieldTypes.SingleSelect, Constants.FieldTypes.Comment, + Constants.FieldTypes.MultiSelect, Constants.FieldTypes.Date, Constants.FieldTypes.Signature, + Constants.FieldTypes.Timer, Constants.FieldTypes.EntitySearch, Constants.FieldTypes.EntitySelect, + Constants.FieldTypes.ShowPdf, Constants.FieldTypes.FieldGroup, Constants.FieldTypes.SaveButton, + Constants.FieldTypes.NumberStepper + }; + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + await _container.StartAsync(); + var conn = _container.GetConnectionString(); + _sdkConnection = conn.Replace("myDb", "420_SDK").Replace("bla", "root"); + var pluginConnection = conn.Replace("myDb", "420_eform-angular-workflow-plugin").Replace("bla", "root"); + + _sdkDbContext = new DbContextHelper(_sdkConnection).GetDbContext(); + await _sdkDbContext.Database.MigrateAsync(); + + foreach (var type in SdkFieldTypeOrder) + { + if (!await _sdkDbContext.FieldTypes.AnyAsync(x => x.Type == type)) + { + await new FieldType { Type = type, Description = type }.Create(_sdkDbContext); + } + } + + _danish = await _sdkDbContext.Languages.FirstOrDefaultAsync(x => x.LanguageCode == "da"); + if (_danish == null) + { + _danish = new Language { Name = "Dansk", LanguageCode = "da" }; + await _danish.Create(_sdkDbContext); + } + + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseMySql(pluginConnection, + new MariaDbServerVersion(ServerVersion.AutoDetect(pluginConnection))); + _workflowDbContext = new WorkflowPnDbContext(optionsBuilder.Options); + await _workflowDbContext.Database.MigrateAsync(); + } + + [OneTimeTearDown] + public async Task OneTimeTearDown() + { + await _container.DisposeAsync(); + if (_workflowDbContext != null) await _workflowDbContext.DisposeAsync(); + if (_sdkDbContext != null) await _sdkDbContext.DisposeAsync(); + } + + [SetUp] + public async Task SetUp() + { + _workflowDbContext.PluginConfigurationValues.RemoveRange(_workflowDbContext.PluginConfigurationValues); + await _workflowDbContext.SaveChangesAsync(); + } + + [Test] + public async Task FieldTypeIds_AreShiftedLikeAGaleraInstall() + { + // Guard: the fixture really does produce shifted ids, otherwise the + // tests below would pass for the wrong reason. + Assert.That(await FieldTypeId(Constants.FieldTypes.Picture), Is.EqualTo(15)); + Assert.That(await FieldTypeId(Constants.FieldTypes.SingleSelect), Is.EqualTo(24)); + } + + [Test] + public async Task Read_ResolvesPictureFieldByType_WhenPictureIdIsShifted() + { + var secondEform = await CreateCheckList("second eform", parentId: null); + var child = await CreateCheckList("second eform child", parentId: secondEform.Id); + await CreateField(child.Id, Constants.FieldTypes.Text, "Text"); + await CreateField(child.Id, Constants.FieldTypes.Picture, "Pictures of task done"); + + await AddConfig($"WorkflowBaseSettings:{nameof(WorkflowBaseSettings.SecondEformId)}", secondEform.Id.ToString()); + await AddConfig($"WorkflowBaseSettings:{nameof(WorkflowBaseSettings.IncidentPlaceListId)}", "1"); + await AddConfig($"WorkflowBaseSettings:{nameof(WorkflowBaseSettings.IncidentTypeListId)}", "2"); + + var workflowCase = new WorkflowCase + { + Description = "desc", + DateOfIncident = DateTime.UtcNow, + IncidentPlace = "place", + IncidentType = "type", + }; + await workflowCase.Create(_workflowDbContext); + + var result = await CreateService(new WorkflowBaseSettings()).Read(workflowCase.Id); + + Assert.That(result.Success, Is.True, result.Message); + Assert.That(result.Model.Id, Is.EqualTo(workflowCase.Id)); + } + + [Test] + public async Task GetPlaces_ResolvesSingleSelectFieldsByType_WhenSingleSelectIdIsShifted() + { + var firstEform = await CreateCheckList("first eform", parentId: null); + await CreateField(firstEform.Id, Constants.FieldTypes.Text, "Text"); + await CreateField(firstEform.Id, Constants.FieldTypes.SingleSelect, "Incident type"); + var placesField = await CreateField(firstEform.Id, Constants.FieldTypes.SingleSelect, "Incident place"); + await CreateOption(placesField.Id, "Kontoret"); + await CreateOption(placesField.Id, "Lageret"); + + var result = await CreateService(new WorkflowBaseSettings { FirstEformId = firstEform.Id }).GetPlaces(); + + Assert.That(result.Success, Is.True, result.Message); + Assert.That(result.Model.Select(x => x.Name), Is.EquivalentTo(new[] { "Kontoret", "Lageret" })); + } + + private WorkflowCasesService CreateService(WorkflowBaseSettings settings) + { + var core = new Core { DbContextHelper = new DbContextHelper(_sdkConnection) }; + var coreService = Substitute.For(); + coreService.GetCore().Returns(core); + + var userService = Substitute.For(); + userService.GetCurrentUserLanguage().Returns(_danish); + + var options = Substitute.For>(); + options.Value.Returns(settings); + + return new WorkflowCasesService( + coreService, + userService, + _workflowDbContext, + Substitute.For(), + options, + NullLogger.Instance, + null!); + } + + private async Task FieldTypeId(string type) => + (await _sdkDbContext.FieldTypes.SingleAsync(x => x.Type == type)).Id; + + private async Task CreateCheckList(string label, int? parentId) + { + var checkList = new CheckList { Label = label, ParentId = parentId }; + await checkList.Create(_sdkDbContext); + return checkList; + } + + private async Task CreateField(int checkListId, string type, string label) + { + var field = new Field + { + CheckListId = checkListId, + FieldTypeId = await FieldTypeId(type), + Label = label + }; + await field.Create(_sdkDbContext); + return field; + } + + private async Task CreateOption(int fieldId, string text) + { + var option = new FieldOption { FieldId = fieldId, Key = text }; + await option.Create(_sdkDbContext); + await new FieldOptionTranslation + { + FieldOptionId = option.Id, + LanguageId = _danish.Id, + Text = text + }.Create(_sdkDbContext); + } + + private async Task AddConfig(string name, string value) + { + _workflowDbContext.PluginConfigurationValues.Add(new PluginConfigurationValue { Name = name, Value = value }); + await _workflowDbContext.SaveChangesAsync(); + } +} diff --git a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj index c65d09dd..1a5069d5 100644 --- a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj +++ b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn.Test/Workflow.Pn.Test.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs index 5e0b9dc2..71d10117 100644 --- a/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs +++ b/eFormAPI/Plugins/Workflow.Pn/Workflow.Pn/Services/WorkflowCasesService/WorkflowCasesService.cs @@ -181,7 +181,8 @@ public async Task> Read(int id) var secondEformId = int.Parse(secondEformIdValue!); var childCheckList = await sdkDbContext.CheckLists.FirstAsync(x => x.ParentId == secondEformId); var fieldIdOfTaskCompleted = - await sdkDbContext.Fields.FirstAsync(x => x.CheckListId == childCheckList.Id && x.FieldTypeId == 5); + await sdkDbContext.Fields.FirstAsync(x => + x.CheckListId == childCheckList.Id && x.FieldType.Type == Constants.FieldTypes.Picture); // get query var workflowDbCase = await workflowPnDbContext.WorkflowCases @@ -627,7 +628,7 @@ public async Task>> GetPlaces() var fieldWithPlaces = await sdkDbContext.Fields .Where(x => x.CheckListId == options.Value.FirstEformId) - .Where(x => x.FieldTypeId == 8) // fieldType.id == 8; fieldType.type -> SingleSelect + .Where(x => x.FieldType.Type == Constants.FieldTypes.SingleSelect) .Select(x => x.Id) .Skip(1) .FirstOrDefaultAsync();