From 85a283e27a0107e3c13b96c75310b85f94f42ac8 Mon Sep 17 00:00:00 2001 From: Mirko Sekulic Date: Mon, 9 Oct 2023 09:11:40 +0200 Subject: [PATCH] feat: process-modeling templates (#11310) * Streaming used in process modelign * temp * GetTemplates tests * Saveproces from template tests * Get appversion tests * use embedded resources for templates to avoid often IO operations on disc * format * fix reading template as embedded resource instead as stream * comments and test added * docs fix --- .../Controllers/AppDevelopmentController.cs | 9 ++ .../Controllers/ProcessModelingController.cs | 54 +++++++----- backend/src/Designer/Designer.csproj | 4 + backend/src/Designer/Helpers/Guard.cs | 27 ++++++ .../Designer/Helpers/PackageVersionHelper.cs | 33 ++++++++ .../GitRepository/AltinnAppGitRepository.cs | 30 ++----- .../GitRepository/GitRepository.cs | 24 +++++- .../Infrastructure/ServiceRegistration.cs | 2 + .../src/Designer/Models/AltinnRepoContext.cs | 1 + .../Models/AltinnRepoEditingContext.cs | 2 + .../Implementation/AppDevelopmentService.cs | 24 +++--- .../ProcessModeling/ProcessModelingService.cs | 72 ++++++++++++++++ .../v8/start-data-confirmation-end.bpmn | 60 +++++++++++++ .../start-data-confirmation-feedback-end.bpmn | 77 +++++++++++++++++ .../Templates/v8/start-data-end.bpmn | 43 ++++++++++ .../Templates/v8/start-data-signing-end.bpmn | 69 +++++++++++++++ .../Interfaces/IAppDevelopmentService.cs | 15 +--- .../Interfaces/IProcessModelingService.cs | 44 ++++++++++ .../ViewModels/Response/VersionResponse.cs | 9 ++ .../GetVersionOfTheAppLibTests.cs | 84 +++++++++++++++++++ .../GetTemplatesTests.cs | 39 +++++++++ .../SaveProcessDefinitionFromTemplateTests.cs | 55 ++++++++++++ .../SaveProcessDefinitionTests.cs | 4 - .../Helpers/PackageVersionHelperTests.cs | 29 +++++++ .../Services/ProcessModelingServiceTests.cs | 51 +++++++++++ .../_TestData/Templates/AppCsprojTemplate.txt | 46 ++++++++++ .../AppCsprojTemplateWithoutAppLib.txt | 42 ++++++++++ 27 files changed, 877 insertions(+), 72 deletions(-) create mode 100644 backend/src/Designer/Helpers/PackageVersionHelper.cs create mode 100644 backend/src/Designer/Services/Implementation/ProcessModeling/ProcessModelingService.cs create mode 100644 backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-end.bpmn create mode 100644 backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-feedback-end.bpmn create mode 100644 backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-end.bpmn create mode 100644 backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-signing-end.bpmn create mode 100644 backend/src/Designer/Services/Interfaces/IProcessModelingService.cs create mode 100644 backend/src/Designer/ViewModels/Response/VersionResponse.cs create mode 100644 backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetVersionOfTheAppLibTests.cs create mode 100644 backend/tests/Designer.Tests/Controllers/ProcessModelingController/GetTemplatesTests.cs create mode 100644 backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionFromTemplateTests.cs create mode 100644 backend/tests/Designer.Tests/Helpers/PackageVersionHelperTests.cs create mode 100644 backend/tests/Designer.Tests/Services/ProcessModelingServiceTests.cs create mode 100644 backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplate.txt create mode 100644 backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplateWithoutAppLib.txt diff --git a/backend/src/Designer/Controllers/AppDevelopmentController.cs b/backend/src/Designer/Controllers/AppDevelopmentController.cs index 548507675ee..d57ec1630d5 100644 --- a/backend/src/Designer/Controllers/AppDevelopmentController.cs +++ b/backend/src/Designer/Controllers/AppDevelopmentController.cs @@ -11,6 +11,7 @@ using Altinn.Studio.Designer.Infrastructure.GitRepository; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; +using Altinn.Studio.Designer.ViewModels.Response; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -517,5 +518,13 @@ public ActionResult GetOptionListIds(string org, string app) return NoContent(); } } + + [HttpGet("app-lib-version")] + public VersionResponse GetVersionOfTheAppLib(string org, string app) + { + string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); + var version = _appDevelopmentService.GetAppLibVersion(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, app, developer)); + return new VersionResponse { Version = version }; + } } } diff --git a/backend/src/Designer/Controllers/ProcessModelingController.cs b/backend/src/Designer/Controllers/ProcessModelingController.cs index 4563c54a1ae..7a6031b0c99 100644 --- a/backend/src/Designer/Controllers/ProcessModelingController.cs +++ b/backend/src/Designer/Controllers/ProcessModelingController.cs @@ -1,11 +1,14 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Text; +using System.Net.Mime; +using System.Threading; using System.Threading.Tasks; using Altinn.Studio.Designer.Helpers; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Altinn.Studio.Designer.Controllers @@ -18,33 +21,29 @@ namespace Altinn.Studio.Designer.Controllers [Route("designer/api/{org}/{repo:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/process-modelling")] public class ProcessModelingController : ControllerBase { - private readonly IAppDevelopmentService _appDevelopmentService; - - public ProcessModelingController(IAppDevelopmentService appDevelopmentService) + private readonly IProcessModelingService _processModelingService; + public ProcessModelingController(IProcessModelingService processModelingService) { - _appDevelopmentService = appDevelopmentService; + _processModelingService = processModelingService; } [HttpGet("process-definition")] - public async Task GetProcessDefinition(string org, string repo) + public FileStreamResult GetProcessDefinition(string org, string repo) { string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - return await _appDevelopmentService.GetProcessDefinition(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer)); + + Stream processDefinitionStream = _processModelingService.GetProcessDefinitionStream(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer)); + + return new FileStreamResult(processDefinitionStream, MediaTypeNames.Text.Plain); } [HttpPut("process-definition")] - public async Task SaveProcessDefinition(string org, string repo) + public async Task SaveProcessDefinition(string org, string repo, CancellationToken cancellationToken) { - string bpmnFileContent = await ReadRequestBodyContentAsync(); - - if (bpmnFileContent.Length > 100_000) - { - return BadRequest("BPMN file is too large"); - } - + Request.EnableBuffering(); try { - Guard.AssertValidXmlContent(bpmnFileContent); + await Guard.AssertValidXmlStreamAndRewindAsync(Request.Body); } catch (ArgumentException) { @@ -52,14 +51,27 @@ public async Task SaveProcessDefinition(string org, string repo) } string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); - await _appDevelopmentService.SaveProcessDefinition(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer), bpmnFileContent); - return Ok(bpmnFileContent); + await _processModelingService.SaveProcessDefinitionAsync(AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer), Request.Body, cancellationToken); + return Ok(); + } + + [HttpGet("templates/{appVersion}")] + public IEnumerable GetTemplates(string org, string repo, Version appVersion) + { + Guard.AssertArgumentNotNull(appVersion, nameof(appVersion)); + return _processModelingService.GetProcessDefinitionTemplates(appVersion); } - private async Task ReadRequestBodyContentAsync() + [HttpPut("templates/{appVersion}/{templateName}")] + public async Task SaveProcessDefinitionFromTemplate(string org, string repo, Version appVersion, string templateName, CancellationToken cancellationToken) { - using StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8); - return await reader.ReadToEndAsync(); + Guard.AssertArgumentNotNull(appVersion, nameof(appVersion)); + string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); + var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, repo, developer); + await _processModelingService.SaveProcessDefinitionFromTemplateAsync(editingContext, templateName, appVersion, cancellationToken); + + Stream processDefinitionStream = _processModelingService.GetProcessDefinitionStream(editingContext); + return new FileStreamResult(processDefinitionStream, MediaTypeNames.Text.Plain); } } } diff --git a/backend/src/Designer/Designer.csproj b/backend/src/Designer/Designer.csproj index 1edc03fce7e..680035ca9d6 100644 --- a/backend/src/Designer/Designer.csproj +++ b/backend/src/Designer/Designer.csproj @@ -134,6 +134,10 @@ + + + + diff --git a/backend/src/Designer/Helpers/Guard.cs b/backend/src/Designer/Helpers/Guard.cs index 293dd01713e..dbf59a49aad 100644 --- a/backend/src/Designer/Helpers/Guard.cs +++ b/backend/src/Designer/Helpers/Guard.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; @@ -127,5 +128,31 @@ public static void AssertValidXmlContent(string xmlContent) throw new ArgumentException("Invalid xml content."); } } + + public static async Task AssertValidXmlStreamAndRewindAsync(Stream xmlStream) + { + XmlReaderSettings settings = new XmlReaderSettings + { + ConformanceLevel = ConformanceLevel.Document, + Async = true + }; + try + { + using XmlReader reader = XmlReader.Create(xmlStream, settings); + while (await reader.ReadAsync()) + { + // Check if node is formatted correctly + } + } + catch (XmlException) + { + throw new ArgumentException("Invalid xml stream."); + } + finally + { + xmlStream.Seek(0, SeekOrigin.Begin); + } + } + } } diff --git a/backend/src/Designer/Helpers/PackageVersionHelper.cs b/backend/src/Designer/Helpers/PackageVersionHelper.cs new file mode 100644 index 00000000000..b8eb167b358 --- /dev/null +++ b/backend/src/Designer/Helpers/PackageVersionHelper.cs @@ -0,0 +1,33 @@ +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Altinn.Studio.Designer.Helpers +{ + public static class PackageVersionHelper + { + public static bool TryGetPackageVersionFromCsprojFile(string csprojFilePath, string packageName, out System.Version version) + { + version = null; + var doc = XDocument.Load(csprojFilePath); + var packageReferences = doc.XPathSelectElements("//PackageReference") + .Where(element => element.Attribute("Include")?.Value == packageName).ToList(); + + if (packageReferences.Count != 1) + { + return false; + } + + var packageReference = packageReferences.First(); + + string versionString = packageReference.Attribute("Version")?.Value; + if (string.IsNullOrEmpty(versionString)) + { + return false; + } + + version = System.Version.Parse(versionString); + return true; + } + } +} diff --git a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs index dbd8d16c3be..ecd154743ff 100644 --- a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs +++ b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs @@ -733,42 +733,30 @@ public string[] GetOptionListIds() } /// - /// Saves bpmn file to disk. + /// Saves the processdefinition file on disk. /// - /// Content of bpmn file. - /// An that observes if operation is cancelled. - /// - /// Throws argument exception if size of syntax validation of the file fails. - public async Task SaveProcessDefinitionFile(string file, CancellationToken cancellationToken = default) + /// Stream of the file to be saved. + /// A that observes if operation is cancelled. + public async Task SaveProcessDefinitionFileAsync(Stream file, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - Guard.AssertNotNullOrEmpty(file, nameof(file)); - if (file.Length > 100_000) + if (file.Length > 1000_000) { throw new ArgumentException("Bpmn file is too large"); } + await Guard.AssertValidXmlStreamAndRewindAsync(file); - Guard.AssertValidXmlContent(file); - - await WriteTextByRelativePathAsync(ProcessDefinitionFilePath, file, true, cancellationToken); - return file; + await WriteStreamByRelativePathAsync(ProcessDefinitionFilePath, file, true, cancellationToken); } - /// - /// Gets the Bpmn file from App/config/process/process.bpmn location - /// - /// Content of Bpmn file - /// An that observes if operation is cancelled. - /// If file doesn't exists. - public async Task GetProcessDefinitionFile(CancellationToken cancellationToken = default) + public Stream GetProcessDefinitionFile() { - cancellationToken.ThrowIfCancellationRequested(); if (!FileExistsByRelativePath(ProcessDefinitionFilePath)) { throw new NotFoundHttpRequestException("Bpmn file not found."); } - return await ReadTextByRelativePathAsync(ProcessDefinitionFilePath, cancellationToken); + return OpenStreamByRelativePath(ProcessDefinitionFilePath); } /// diff --git a/backend/src/Designer/Infrastructure/GitRepository/GitRepository.cs b/backend/src/Designer/Infrastructure/GitRepository/GitRepository.cs index b01bfd9f83d..454c06234d5 100644 --- a/backend/src/Designer/Infrastructure/GitRepository/GitRepository.cs +++ b/backend/src/Designer/Infrastructure/GitRepository/GitRepository.cs @@ -138,6 +138,20 @@ public async Task ReadTextByRelativePathAsync(string relativeFilePath, C } } + /// + /// Returns fileStream of a file path relative to the repository directory. + /// + /// The relative path to the file. + /// A . + public Stream OpenStreamByRelativePath(string relativeFilePath) + { + string absoluteFilePath = GetAbsoluteFileOrDirectoryPathSanitized(relativeFilePath); + + Guard.AssertFilePathWithinParentDirectory(RepositoryDirectory, absoluteFilePath); + + return File.OpenRead(absoluteFilePath); + } + /// /// Creates a new file or overwrites an existing and writes the text to the specified file path. /// @@ -168,8 +182,10 @@ public async Task WriteTextByRelativePathAsync(string relativeFilePath, string t /// File to be created/updated. /// Content to be written to the file. /// False (default) if you don't want missing directory to be created. True will check if the directory exist and create it if it don't exist. - public async Task WriteStreamByRelativePathAsync(string relativeFilePath, Stream stream, bool createDirectory = false) + /// A that observes if operation in cancelled + public async Task WriteStreamByRelativePathAsync(string relativeFilePath, Stream stream, bool createDirectory = false, CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); Guard.AssertNotNullOrEmpty(relativeFilePath, nameof(relativeFilePath)); string absoluteFilePath = GetAbsoluteFileOrDirectoryPathSanitized(relativeFilePath); @@ -181,7 +197,7 @@ public async Task WriteStreamByRelativePathAsync(string relativeFilePath, Stream CreateDirectory(absoluteFilePath); } - await WriteAsync(absoluteFilePath, stream); + await WriteAsync(absoluteFilePath, stream, cancellationToken); } /// @@ -378,10 +394,10 @@ private static async Task WriteTextAsync(string absoluteFilePath, string text, C await sourceStream.WriteAsync(encodedText.AsMemory(0, encodedText.Length), cancellationToken); } - private static async Task WriteAsync(string absoluteFilePath, Stream stream) + private static async Task WriteAsync(string absoluteFilePath, Stream stream, CancellationToken cancellationToken = default) { await using FileStream targetStream = new(absoluteFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); - await stream.CopyToAsync(targetStream, bufferSize: 4096); + await stream.CopyToAsync(targetStream, bufferSize: 4096, cancellationToken: cancellationToken); } } } diff --git a/backend/src/Designer/Infrastructure/ServiceRegistration.cs b/backend/src/Designer/Infrastructure/ServiceRegistration.cs index 5e6b263775b..9fdf5fd57e0 100644 --- a/backend/src/Designer/Infrastructure/ServiceRegistration.cs +++ b/backend/src/Designer/Infrastructure/ServiceRegistration.cs @@ -10,6 +10,7 @@ using Altinn.Studio.Designer.Factories; using Altinn.Studio.Designer.Repository; using Altinn.Studio.Designer.Services.Implementation; +using Altinn.Studio.Designer.Services.Implementation.ProcessModeling; using Altinn.Studio.Designer.Services.Interfaces; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -57,6 +58,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.RegisterDatamodeling(configuration); services.RegisterUserRequestSynchronization(configuration); diff --git a/backend/src/Designer/Models/AltinnRepoContext.cs b/backend/src/Designer/Models/AltinnRepoContext.cs index f2344bef98f..94dd57112a4 100644 --- a/backend/src/Designer/Models/AltinnRepoContext.cs +++ b/backend/src/Designer/Models/AltinnRepoContext.cs @@ -30,6 +30,7 @@ protected AltinnRepoContext(string org, string repo) private void ValidateOrganization(string org) { + Guard.AssertNotNullOrEmpty(org, nameof(org)); if (!Regex.IsMatch(org, "^[a-zA-Z0-9][a-zA-Z0-9-_\\.]*$")) { throw new ArgumentException("Provided organization name is not valid"); diff --git a/backend/src/Designer/Models/AltinnRepoEditingContext.cs b/backend/src/Designer/Models/AltinnRepoEditingContext.cs index a5cc03b605f..649cd488a4d 100644 --- a/backend/src/Designer/Models/AltinnRepoEditingContext.cs +++ b/backend/src/Designer/Models/AltinnRepoEditingContext.cs @@ -1,5 +1,6 @@ using System; using System.Text.RegularExpressions; +using Altinn.Studio.Designer.Helpers; namespace Altinn.Studio.Designer.Models { @@ -22,6 +23,7 @@ private AltinnRepoEditingContext(string org, string repo, string developer) : ba private static void ValidateDeveloper(string developer) { + Guard.AssertNotNullOrEmpty(developer, nameof(developer)); if (!Regex.IsMatch(developer, "^[a-zA-Z0-9][a-zA-Z0-9-_\\.]*$")) { throw new ArgumentException("Provided developer name is not valid"); diff --git a/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs b/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs index 343b38d8f2e..805c1963b1c 100644 --- a/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs +++ b/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs @@ -1,8 +1,11 @@ +using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; +using Altinn.Studio.Designer.Helpers; using Altinn.Studio.Designer.Infrastructure.GitRepository; using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; @@ -215,20 +218,21 @@ public async Task SaveRuleConfig(AltinnRepoEditingContext altinnRepoEditingConte } /// - public Task GetProcessDefinition(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default) + public Version GetAppLibVersion(AltinnRepoEditingContext altinnRepoEditingContext) { - cancellationToken.ThrowIfCancellationRequested(); AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer); - return altinnAppGitRepository.GetProcessDefinitionFile(cancellationToken); - } + var csprojFiles = altinnAppGitRepository.FindFiles(new[] { "*.csproj" }); - /// - public Task SaveProcessDefinition(AltinnRepoEditingContext altinnRepoEditingContext, string bpmnXml, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer); - return altinnAppGitRepository.SaveProcessDefinitionFile(bpmnXml, cancellationToken); + foreach (string csprojFile in csprojFiles) + { + if (PackageVersionHelper.TryGetPackageVersionFromCsprojFile(csprojFile, "Altinn.App.Api", out Version version)) + { + return version; + } + } + + throw new FileNotFoundException("Unable to extract the version of the app-lib from csproj files."); } } } diff --git a/backend/src/Designer/Services/Implementation/ProcessModeling/ProcessModelingService.cs b/backend/src/Designer/Services/Implementation/ProcessModeling/ProcessModelingService.cs new file mode 100644 index 00000000000..be1a47188df --- /dev/null +++ b/backend/src/Designer/Services/Implementation/ProcessModeling/ProcessModelingService.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Infrastructure.GitRepository; +using Altinn.Studio.Designer.Models; +using Altinn.Studio.Designer.Services.Interfaces; + +namespace Altinn.Studio.Designer.Services.Implementation.ProcessModeling +{ + public class ProcessModelingService : IProcessModelingService + { + private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory; + public ProcessModelingService(IAltinnGitRepositoryFactory altinnGitRepositoryFactory) + { + _altinnGitRepositoryFactory = altinnGitRepositoryFactory; + } + + private string TemplatesFolderIdentifier(Version version) => string.Join(".", nameof(Services), nameof(Implementation), nameof(ProcessModeling), "Templates", $"v{version.Major}"); + + /// + public IEnumerable GetProcessDefinitionTemplates(Version version) + { + return EnumerateTemplateResources(version) + .Select( + templateName => templateName.Split(TemplatesFolderIdentifier(version)).Last().TrimStart('.'))!; + } + + /// + public async Task SaveProcessDefinitionFromTemplateAsync(AltinnRepoEditingContext altinnRepoEditingContext, string templateName, Version version, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer); + await using Stream templateStream = GetTemplateStream(version, templateName); + await altinnAppGitRepository.SaveProcessDefinitionFileAsync(templateStream, cancellationToken); + } + + /// + public async Task SaveProcessDefinitionAsync(AltinnRepoEditingContext altinnRepoEditingContext, Stream bpmnStream, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer); + await altinnAppGitRepository.SaveProcessDefinitionFileAsync(bpmnStream, cancellationToken); + } + + /// + public Stream GetProcessDefinitionStream(AltinnRepoEditingContext altinnRepoEditingContext) + { + AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer); + return altinnAppGitRepository.GetProcessDefinitionFile(); + } + + private IEnumerable EnumerateTemplateResources(Version version) + { + return typeof(ProcessModelingService).Assembly.GetManifestResourceNames() + .Where(resourceName => resourceName.Contains(TemplatesFolderIdentifier(version))); + } + + private Stream GetTemplateStream(Version version, string templateName) + { + var templates = EnumerateTemplateResources(version).ToList(); + if (!templates.Exists(template => template.EndsWith(templateName))) + { + throw new FileNotFoundException("Unknown template."); + } + string template = templates.Single(template => template.EndsWith(templateName)); + return typeof(ProcessModelingService).Assembly.GetManifestResourceStream(template); + } + } +} diff --git a/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-end.bpmn b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-end.bpmn new file mode 100644 index 00000000000..bc53a9a325b --- /dev/null +++ b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-end.bpmn @@ -0,0 +1,60 @@ + + + + + SequenceFlow_1n56yn5 + + + Flow_1w5g1cc + + + + + + data + + + SequenceFlow_1n56yn5 + Flow_04zgett + + + + + + confirmation + + + Flow_04zgett + Flow_1w5g1cc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-feedback-end.bpmn b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-feedback-end.bpmn new file mode 100644 index 00000000000..1ac5d86b91e --- /dev/null +++ b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-confirmation-feedback-end.bpmn @@ -0,0 +1,77 @@ + + + + + SequenceFlow_1n56yn5 + + + Flow_0de0tee + + + + + + data + + + SequenceFlow_1n56yn5 + Flow_0wapsmg + + + + + + confirmation + + + Flow_0wapsmg + Flow_1f2wo1h + + + + + + feedback + + + Flow_1f2wo1h + Flow_0de0tee + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-end.bpmn b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-end.bpmn new file mode 100644 index 00000000000..1bac049e31d --- /dev/null +++ b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-end.bpmn @@ -0,0 +1,43 @@ + + + + + SequenceFlow_1n56yn5 + + + Flow_1htunjv + + + + + + data + + + SequenceFlow_1n56yn5 + Flow_1htunjv + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-signing-end.bpmn b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-signing-end.bpmn new file mode 100644 index 00000000000..289ff3d135a --- /dev/null +++ b/backend/src/Designer/Services/Implementation/ProcessModeling/Templates/v8/start-data-signing-end.bpmn @@ -0,0 +1,69 @@ + + + + + SequenceFlow_1n56yn5 + + + Flow_1dvawgt + + + + + + data + + + SequenceFlow_1n56yn5 + Flow_0k6xtdu + + + + + + signing + + sign + reject + + + + Model + + + + + Flow_0k6xtdu + Flow_1dvawgt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/Designer/Services/Interfaces/IAppDevelopmentService.cs b/backend/src/Designer/Services/Interfaces/IAppDevelopmentService.cs index 561c7e37dce..3d42bd9521c 100644 --- a/backend/src/Designer/Services/Interfaces/IAppDevelopmentService.cs +++ b/backend/src/Designer/Services/Interfaces/IAppDevelopmentService.cs @@ -127,21 +127,12 @@ public interface IAppDevelopmentService /// A task that represents the asynchronous operation. public Task SaveRuleConfig(AltinnRepoEditingContext altinnRepoEditingContext, JsonNode ruleConfig, string layoutSetName, CancellationToken cancellationToken = default); - /// - /// Get's process definition for an app. - /// - /// An . - /// An that observes if operation is cancelled. - /// Bpmn file. - public Task GetProcessDefinition(AltinnRepoEditingContext altinnRepoEditingContext, CancellationToken cancellationToken = default); /// - /// Saves the process definition for an app. + /// Get's the version of the app-lib used in repo /// /// An . - /// Content of process definition file to save. - /// An that observes if operation is cancelled. - /// Saved file. - public Task SaveProcessDefinition(AltinnRepoEditingContext altinnRepoEditingContext, string bpmnXml, CancellationToken cancellationToken = default); + /// A holding the version of the app-lib used in app. + public System.Version GetAppLibVersion(AltinnRepoEditingContext altinnRepoEditingContext); } } diff --git a/backend/src/Designer/Services/Interfaces/IProcessModelingService.cs b/backend/src/Designer/Services/Interfaces/IProcessModelingService.cs new file mode 100644 index 00000000000..d7117d1d69d --- /dev/null +++ b/backend/src/Designer/Services/Interfaces/IProcessModelingService.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Models; + +namespace Altinn.Studio.Designer.Services.Interfaces +{ + public interface IProcessModelingService + { + /// + /// Gets defined process definition templates for a given version. + /// + /// Version of the app-lib + /// An IEnumerable containing supported templates for given version. + IEnumerable GetProcessDefinitionTemplates(Version version); + + /// + /// Saves the process definition file for a given app from a template. + /// + /// An . + /// Name of the template. + /// Version of the app-lib. + /// A that observes if operation is cancelled. + Task SaveProcessDefinitionFromTemplateAsync(AltinnRepoEditingContext altinnRepoEditingContext, string templateName, Version version, CancellationToken cancellationToken = default); + + /// + /// Saves the process definition file for a given app. + /// + /// An . + /// A that should be saved to process definition file. + /// A that observes if operation is cancelled. + Task SaveProcessDefinitionAsync(AltinnRepoEditingContext altinnRepoEditingContext, Stream bpmnStream, CancellationToken cancellationToken = default); + + /// + /// Gets the process definition file stream for a given app. + /// + /// An . + /// A that observes if operation is cancelled. + /// A of a process definition file. + Stream GetProcessDefinitionStream(AltinnRepoEditingContext altinnRepoEditingContext); + } +} diff --git a/backend/src/Designer/ViewModels/Response/VersionResponse.cs b/backend/src/Designer/ViewModels/Response/VersionResponse.cs new file mode 100644 index 00000000000..1086105e06a --- /dev/null +++ b/backend/src/Designer/ViewModels/Response/VersionResponse.cs @@ -0,0 +1,9 @@ +using System; + +namespace Altinn.Studio.Designer.ViewModels.Response +{ + public class VersionResponse + { + public Version Version { get; set; } + } +} diff --git a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetVersionOfTheAppLibTests.cs b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetVersionOfTheAppLibTests.cs new file mode 100644 index 00000000000..55143b3b2ef --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetVersionOfTheAppLibTests.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Studio.Designer.ViewModels.Response; +using Designer.Tests.Controllers.ApiTests; +using Designer.Tests.Utils; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.AppDevelopmentController +{ + public class GetVersionOfTheAppLibTests : DisagnerEndpointsTestsBase + { + private static string VersionPrefix(string org, string repository) => $"/designer/api/{org}/{repository}/app-development/app-lib-version"; + public GetVersionOfTheAppLibTests(WebApplicationFactory factory) : base(factory) + { + } + + [Theory] + [InlineData("ttd", "empty-app", "testUser", "7.4.0", "Templates/AppCsprojTemplate.txt")] + public async Task GetAppLibVersion_GivenCsProjFile_ShouldReturnOK(string org, string app, string developer, string version, string csprojTemplate) + { + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, app, developer, targetRepository); + Dictionary replacements = new Dictionary() { { "[[appLibVersion]]", version } }; + await AddCsProjToRepo("App/App.csproj", csprojTemplate, replacements); + + string url = VersionPrefix(org, targetRepository); + + using var response = await HttpClient.Value.GetAsync(url); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var responseVersion = await response.Content.ReadAsAsync(); + + responseVersion.Version.ToString().Should().Be(version); + } + + [Theory] + [InlineData("ttd", "empty-app", "testUser", "Templates/AppCsprojTemplateWithoutAppLib.txt")] + public async Task GetAppLibVersion_GivenCsprojFileWithoutAppLib_ShouldReturn404(string org, string app, string developer, string csprojTemplate) + { + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, app, developer, targetRepository); + await AddCsProjToRepo("App/App.csproj", csprojTemplate); + string url = VersionPrefix(org, targetRepository); + + using var response = await HttpClient.Value.GetAsync(url); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Theory] + [InlineData("ttd", "empty-app")] + public async Task GetAppLibVersion_NotGivenCsprojFile_ShouldReturn404(string org, string app) + { + string url = VersionPrefix(org, app); + + using var response = await HttpClient.Value.GetAsync(url); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + private async Task AddCsProjToRepo(string relativeCopyRepoLocation, string csprojTemplate, Dictionary replacements = null) + { + string fileContent = TestDataHelper.LoadTestDataFromFileAsString(csprojTemplate); + if (replacements is not null) + { + foreach ((string key, string value) in replacements) + { + fileContent = fileContent.Replace(key, value); + } + } + + string filePath = Path.Combine(TestRepoPath, relativeCopyRepoLocation); + string folderPath = Path.GetDirectoryName(filePath); + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath!); + } + await File.WriteAllTextAsync(filePath, fileContent); + } + } +} diff --git a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/GetTemplatesTests.cs b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/GetTemplatesTests.cs new file mode 100644 index 00000000000..989e0fbc7b8 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/GetTemplatesTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Designer.Tests.Controllers.ApiTests; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.ProcessModelingController +{ + public class GetTemplatesTests : DisagnerEndpointsTestsBase + { + private static string VersionPrefix(string org, string repository, string appVersion) => $"/designer/api/{org}/{repository}/process-modelling/templates/{appVersion}"; + + public GetTemplatesTests(WebApplicationFactory factory) : base(factory) + { + } + + [Theory] + [InlineData("ttd", "empty-app", "8.0.0", "start-data-confirmation-end.bpmn", "start-data-confirmation-feedback-end.bpmn", "start-data-end.bpmn", "start-data-signing-end.bpmn")] + [InlineData("ttd", "empty-app", "7.4.0")] + public async Task GetTemplates_ShouldReturnOK(string org, string app, string version, params string[] expectedTemplates) + { + string url = VersionPrefix(org, app, version); + + using var response = await HttpClient.Value.GetAsync(url); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + List responseContent = await response.Content.ReadAsAsync>(); + + responseContent.Count.Should().Be(expectedTemplates.Length); + foreach (string expectedTemplate in expectedTemplates) + { + responseContent.Should().Contain(expectedTemplate); + } + } + } +} diff --git a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionFromTemplateTests.cs b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionFromTemplateTests.cs new file mode 100644 index 00000000000..56cadd0f0f5 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionFromTemplateTests.cs @@ -0,0 +1,55 @@ +using System.Net; +using System.Threading.Tasks; +using System.Xml.Linq; +using Designer.Tests.Controllers.ApiTests; +using Designer.Tests.Utils; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.ProcessModelingController +{ + public class SaveProcessDefinitionFromTemplateTests : DisagnerEndpointsTestsBase + { + + private static string VersionPrefix(string org, string repository, string appVersion, string templateName) => $"/designer/api/{org}/{repository}/process-modelling/templates/{appVersion}/{templateName}"; + + public SaveProcessDefinitionFromTemplateTests(WebApplicationFactory factory) : base(factory) + { + } + + [Theory] + [InlineData("ttd", "empty-app", "testUser", "9.0.0", "start-data-confirmation-end.bpmn")] + public async Task SaveProcessDefinitionFromTemplate_WrongTemplate_ShouldReturn404(string org, string app, string developer, string version, string templateName) + { + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, app, developer, targetRepository); + + string url = VersionPrefix(org, targetRepository, version, templateName); + + using var response = await HttpClient.Value.PutAsync(url, null); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + } + + [Theory] + [InlineData("ttd", "empty-app", "testUser", "8.0.0", "start-data-confirmation-end.bpmn")] + public async Task SaveProcessDefinitionFromTemplate_ShouldReturnOk_AndSaveTemplate(string org, string app, string developer, string version, string templateName) + { + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, app, developer, targetRepository); + + string url = VersionPrefix(org, targetRepository, version, templateName); + + using var response = await HttpClient.Value.PutAsync(url, null); + response.StatusCode.Should().Be(HttpStatusCode.OK); + + string responseContent = await response.Content.ReadAsStringAsync(); + + string savedFile = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/config/process/process.bpmn"); + + XDocument responseXml = XDocument.Parse(responseContent); + XDocument savedXml = XDocument.Parse(savedFile); + XNode.DeepEquals(savedXml, responseXml).Should().BeTrue(); + } + } +} diff --git a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionTests.cs b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionTests.cs index 353547a806b..dd654d6eb86 100644 --- a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionTests.cs +++ b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/SaveProcessDefinitionTests.cs @@ -35,14 +35,10 @@ public async Task SaveProcessDefinition_ShouldReturnOk(string org, string app, s using var response = await HttpClient.Value.PutAsync(url, content); response.StatusCode.Should().Be(HttpStatusCode.OK); - string responseContent = await response.Content.ReadAsStringAsync(); - string savedFile = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/config/process/process.bpmn"); - XDocument responseXml = XDocument.Parse(responseContent); XDocument expectedXml = XDocument.Parse(fileContent); XDocument savedXml = XDocument.Parse(savedFile); - XNode.DeepEquals(responseXml, expectedXml).Should().BeTrue(); XNode.DeepEquals(savedXml, expectedXml).Should().BeTrue(); } diff --git a/backend/tests/Designer.Tests/Helpers/PackageVersionHelperTests.cs b/backend/tests/Designer.Tests/Helpers/PackageVersionHelperTests.cs new file mode 100644 index 00000000000..e5e2eb6b9b6 --- /dev/null +++ b/backend/tests/Designer.Tests/Helpers/PackageVersionHelperTests.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using Altinn.Studio.Designer.Helpers; +using DotNet.Testcontainers.Builders; +using FluentAssertions; +using Xunit; + +namespace Designer.Tests.Helpers +{ + public class PackageVersionHelperTests + { + [Theory] + [InlineData("Altinn.App.Api", true, "7.4.0")] + [InlineData("NonExistingNuget", false)] + public void TryGetPackageVersionFromCsprojFile_GivenValidCsprojFile_ReturnsTrue(string packageName, bool expectedResult, string expectedVersion = "") + { + string testTemplateCsProjPath = Path.Combine(CommonDirectoryPath.GetSolutionDirectory().DirectoryPath, "..", "testdata", "AppTemplates", "AspNet", "App", "App.csproj"); + + bool result = PackageVersionHelper.TryGetPackageVersionFromCsprojFile(testTemplateCsProjPath, packageName, out Version version); + + result.Should().Be(expectedResult); + + if (result) + { + version.ToString().Should().Be(expectedVersion); + } + } + } +} diff --git a/backend/tests/Designer.Tests/Services/ProcessModelingServiceTests.cs b/backend/tests/Designer.Tests/Services/ProcessModelingServiceTests.cs new file mode 100644 index 00000000000..bd5abb938d1 --- /dev/null +++ b/backend/tests/Designer.Tests/Services/ProcessModelingServiceTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Altinn.Studio.Designer.Services.Implementation.ProcessModeling; +using Altinn.Studio.Designer.Services.Interfaces; +using FluentAssertions; +using Moq; +using SharedResources.Tests; +using Xunit; + +namespace Designer.Tests.Services +{ + public class ProcessModelingServiceTests : FluentTestsBase + { + [Theory] + [MemberData(nameof(TemplatesTestData))] + public void GetProcessDefinitionTemplates_GivenVersion_ReturnsListOfTemplates(string versionString, params string[] expectedTemplates) + { + Version version = Version.Parse(versionString); + + IProcessModelingService processModelingService = new ProcessModelingService(new Mock().Object); + + var result = processModelingService.GetProcessDefinitionTemplates(version).ToList(); + + result.Count.Should().Be(expectedTemplates.Length); + + foreach (string expectedTemplate in expectedTemplates) + { + result.Should().Contain(expectedTemplate); + } + } + + public static IEnumerable TemplatesTestData => new List + { + new object[] + { + "8.0.0", new string[] + { + "start-data-confirmation-end.bpmn", + "start-data-confirmation-feedback-end.bpmn", + "start-data-end.bpmn", + "start-data-signing-end.bpmn", + } + }, + new object[] + { + "7.0.0" + } + }; + } +} diff --git a/backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplate.txt b/backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplate.txt new file mode 100644 index 00000000000..66864d00476 --- /dev/null +++ b/backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplate.txt @@ -0,0 +1,46 @@ + + + + net6.0 + Altinn.App + Altinn.App + + + + + lib\$(TargetFramework)\*.xml + + + + + + + + + + + + + + Always + + + PreserveNewest + + + PreserveNewest + + + + + true + $(NoWarn);1591 + + + + + + + + + diff --git a/backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplateWithoutAppLib.txt b/backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplateWithoutAppLib.txt new file mode 100644 index 00000000000..69d51399118 --- /dev/null +++ b/backend/tests/Designer.Tests/_TestData/Templates/AppCsprojTemplateWithoutAppLib.txt @@ -0,0 +1,42 @@ + + + + net6.0 + Altinn.App + Altinn.App + + + + + + + + + + + + + + + Always + + + PreserveNewest + + + PreserveNewest + + + + + true + $(NoWarn);1591 + + + + + + + + +