From bb2b564e0c4ce3369b679c31f7765e868e53548b Mon Sep 17 00:00:00 2001 From: Erling Hauan Date: Tue, 25 Jun 2024 15:52:56 +0200 Subject: [PATCH 01/16] Add GET endpoint for fetching an options list --- .../Designer/Controllers/OptionsController.cs | 64 +++++++++++++++++++ .../Infrastructure/ServiceRegistration.cs | 1 + .../Services/Implementation/OptionsService.cs | 37 +++++++++++ .../Services/Implementation/TextsService.cs | 2 +- .../Services/Interfaces/IOptionsService.cs | 20 ++++++ .../Controllers/OptionsController/GetTests.cs | 40 ++++++++++++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 backend/src/Designer/Controllers/OptionsController.cs create mode 100644 backend/src/Designer/Services/Implementation/OptionsService.cs create mode 100644 backend/src/Designer/Services/Interfaces/IOptionsService.cs create mode 100644 backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs diff --git a/backend/src/Designer/Controllers/OptionsController.cs b/backend/src/Designer/Controllers/OptionsController.cs new file mode 100644 index 00000000000..86714012c2b --- /dev/null +++ b/backend/src/Designer/Controllers/OptionsController.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +using Altinn.Studio.Designer.Helpers; +using Altinn.Studio.Designer.Services.Interfaces; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Studio.Designer.Controllers; + +/// +/// Controller containing actions related to options (codelists). +/// +[Authorize] +[AutoValidateAntiforgeryToken] +[Route("designer/api/{org}/{repo:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/options")] +public class OptionsController : ControllerBase +{ + private readonly IOptionsService _optionsService; + + /// + /// Initializes a new instance of the class. + /// + /// The options service. + public OptionsController(IOptionsService optionsService) + { + _optionsService = optionsService; + } + + /// + /// Endpoint for getting a specific option list. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Options list identifier specifying the file to read. + [HttpGet] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Route("{optionsListId}")] + public async Task>>> Get(string org, string repo, [FromRoute] string optionsListId) + { + string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); + + try + { + List> optionList = await _optionsService.GetOptions(org, repo, developer, optionsListId); + return Ok(optionList); + } + catch (IOException) + { + return NotFound($"The options file {optionsListId}.json does not exist."); + } + catch (JsonException) + { + return new ObjectResult(new { errorMessage = $"The format of the file {optionsListId}.json might be invalid." }) { StatusCode = 500 }; + } + } +} diff --git a/backend/src/Designer/Infrastructure/ServiceRegistration.cs b/backend/src/Designer/Infrastructure/ServiceRegistration.cs index 6dff9d1a9e4..03c2cac117e 100644 --- a/backend/src/Designer/Infrastructure/ServiceRegistration.cs +++ b/backend/src/Designer/Infrastructure/ServiceRegistration.cs @@ -66,6 +66,7 @@ public static IServiceCollection RegisterServiceImplementations(this IServiceCol services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddHttpClient(); services.AddTransient(); diff --git a/backend/src/Designer/Services/Implementation/OptionsService.cs b/backend/src/Designer/Services/Implementation/OptionsService.cs new file mode 100644 index 00000000000..c0830b2a573 --- /dev/null +++ b/backend/src/Designer/Services/Implementation/OptionsService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Altinn.Studio.Designer.Services.Interfaces; +using Newtonsoft.Json; + +namespace Altinn.Studio.Designer.Services.Implementation; + +/// +/// Service for handling options lists. +/// +public class OptionsService : IOptionsService +{ + private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory; + private readonly IApplicationMetadataService _applicationMetadataService; + + /// + /// Constructor + /// + /// IAltinnGitRepository + /// IApplicationMetadataService + public OptionsService(IAltinnGitRepositoryFactory altinnGitRepositoryFactory, IApplicationMetadataService applicationMetadataService) + { + _altinnGitRepositoryFactory = altinnGitRepositoryFactory; + _applicationMetadataService = applicationMetadataService; + } + + /// + public async Task>> GetOptions(string org, string repo, string developer, string optionsListId) + { + var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer); + + string optionsListString = await altinnAppGitRepository.GetOptions(optionsListId); + var optionsList = JsonConvert.DeserializeObject>>(optionsListString); + + return optionsList; + } +} diff --git a/backend/src/Designer/Services/Implementation/TextsService.cs b/backend/src/Designer/Services/Implementation/TextsService.cs index 84d2e240f48..680db08331e 100644 --- a/backend/src/Designer/Services/Implementation/TextsService.cs +++ b/backend/src/Designer/Services/Implementation/TextsService.cs @@ -14,7 +14,7 @@ namespace Altinn.Studio.Designer.Services.Implementation { /// - /// Interface for dealing with texts in new format in an app repository. + /// Service for handling texts in new format in an app repository. /// public class TextsService : ITextsService { diff --git a/backend/src/Designer/Services/Interfaces/IOptionsService.cs b/backend/src/Designer/Services/Interfaces/IOptionsService.cs new file mode 100644 index 00000000000..095e2ed04de --- /dev/null +++ b/backend/src/Designer/Services/Interfaces/IOptionsService.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Altinn.Studio.Designer.Services.Interfaces; + +/// +/// Interface for handling options lists. +/// +public interface IOptionsService +{ + /// + /// Gets options file in app repository according to specified optionsListId. + /// + /// Organisation + /// Repository + /// Username of developer + /// Options list to fetch + /// The options list as a dictionary + public Task>> GetOptions(string org, string repo, string developer, string optionsListId); +} diff --git a/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs b/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs new file mode 100644 index 00000000000..fe3994fa2cb --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Designer.Tests.Controllers.ApiTests; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.OptionsController; + +public class GetTests : DisagnerEndpointsTestsBase, IClassFixture> +{ + public GetTests(WebApplicationFactory factory) : base(factory) + { + } + + [Theory] + [InlineData("ttd", "app-with-layoutsets", "test-options")] + public async Task Get_Returns_OptionsList(string org, string repo, string optionsListId) + { + var expectedOptionsList = new List> + { + new() { { "label", "label1" }, { "value", "value1" } }, + new() { { "label", "label2" }, { "value", "value2" } } + }; + + string apiUrl = $"/designer/api/{org}/{repo}/options/{optionsListId}"; + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl); + + HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage); + response.EnsureSuccessStatusCode(); + + string responseBody = await response.Content.ReadAsStringAsync(); + var responseList = JsonSerializer.Deserialize>>(responseBody); + + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + Assert.Equal(expectedOptionsList, responseList); + } +} From 98e5a9ee7f0f3560bcd63ecafb3d0b67d234bf6f Mon Sep 17 00:00:00 2001 From: Erling Hauan Date: Wed, 26 Jun 2024 14:18:55 +0200 Subject: [PATCH 02/16] Add PUT endpoint for creating or overwriting an options list --- .../Designer/Controllers/OptionsController.cs | 40 ++++++++++++++- .../GitRepository/AltinnAppGitRepository.cs | 16 ++++++ backend/src/Designer/Models/Options.cs | 49 +++++++++++++++++++ .../Services/Implementation/OptionsService.cs | 14 +++++- .../Services/Interfaces/IOptionsService.cs | 16 ++++-- .../Controllers/OptionsController/GetTests.cs | 6 ++- .../Controllers/OptionsController/PutTests.cs | 48 ++++++++++++++++++ 7 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 backend/src/Designer/Models/Options.cs create mode 100644 backend/tests/Designer.Tests/Controllers/OptionsController/PutTests.cs diff --git a/backend/src/Designer/Controllers/OptionsController.cs b/backend/src/Designer/Controllers/OptionsController.cs index 86714012c2b..6f37163dd10 100644 --- a/backend/src/Designer/Controllers/OptionsController.cs +++ b/backend/src/Designer/Controllers/OptionsController.cs @@ -49,7 +49,7 @@ public async Task>>> Get(string org try { - List> optionList = await _optionsService.GetOptions(org, repo, developer, optionsListId); + List> optionList = await _optionsService.GetOptionsList(org, repo, developer, optionsListId); return Ok(optionList); } catch (IOException) @@ -61,4 +61,42 @@ public async Task>>> Get(string org return new ObjectResult(new { errorMessage = $"The format of the file {optionsListId}.json might be invalid." }) { StatusCode = 500 }; } } + + /// + /// Endpoint for creating a new option list. + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Options list identifier specifying the file to create. + /// The option list to be created. + [HttpPut] + [Produces("application/json")] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Route("{optionsListId}")] + public async Task Put(string org, string repo, [FromRoute] string optionsListId, [FromBody] List> optionsListPayload) + { + string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); + + if (optionsListPayload == null || optionsListPayload.Count == 0) + { + return BadRequest("The option list cannot be null or empty."); + } + + try + { + var newOptionsList = await _optionsService.CreateOrOverwriteOptionsList(org, repo, developer, optionsListId, optionsListPayload); + return Ok(newOptionsList); + } + catch (IOException) + { + return new ObjectResult(new { errorMessage = $"An error occurred while saving the file {optionsListId}.json." }) { StatusCode = 500 }; + } + catch (JsonException) + { + return new ObjectResult(new { errorMessage = $"The format of the provided option list is invalid." }) { StatusCode = 400 }; + } + } + } diff --git a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs index 5f06370e4bd..da7eecb52d1 100644 --- a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs +++ b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs @@ -720,6 +720,22 @@ public async Task GetOptions(string optionsListId, CancellationToken can return fileContent; } + /// + /// Creates a new options list with the provided id. + /// The id of the options list to create. + /// The contents of the new options list as a string + /// A that observes if operation is cancelled. + /// The new options list as a string. + /// + public async Task CreateOrOverwriteOptions(string optionsListId, string optionsListPayload, CancellationToken cancellationToken = default) + { + string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionsListId}.json"); + await WriteTextByRelativePathAsync(optionsFilePath, optionsListPayload, true, cancellationToken); + string fileContent = await ReadTextByRelativePathAsync(optionsFilePath, cancellationToken); + + return fileContent; + } + /// /// Gets a list of file names from the Options folder representing the available options lists. /// A list of option list names. diff --git a/backend/src/Designer/Models/Options.cs b/backend/src/Designer/Models/Options.cs new file mode 100644 index 00000000000..83b50e9d8cc --- /dev/null +++ b/backend/src/Designer/Models/Options.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Newtonsoft.Json; + +namespace Altinn.Studio.Designer.Models; + +/// +/// Individual options. +/// +[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] +public class Options +{ + /// + /// The value that connects the option to the data model. + /// + [JsonProperty(PropertyName = "value")] + public string Value { get; set; } + + /// + /// The label to present to the user. + /// + [JsonProperty(PropertyName = "label")] + public string Label { get; set; } + + /// + /// Description - typically displayed below the label. + /// + [JsonProperty(PropertyName = "description")] + public Optional Description { get; set; } + + /// + /// Help text - typically hidden inside a popover. + /// + [JsonProperty(PropertyName = "helpText")] + public Optional HelpText { get; set; } +} + +/// +/// Options list (code list) to be used in radio buttons or checkboxes. +/// +[JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] +public class OptionsList +{ + /// + /// The options in the list. + /// + [JsonProperty(PropertyName = "options")] + public List Options { get; set; } +} diff --git a/backend/src/Designer/Services/Implementation/OptionsService.cs b/backend/src/Designer/Services/Implementation/OptionsService.cs index c0830b2a573..712a3742503 100644 --- a/backend/src/Designer/Services/Implementation/OptionsService.cs +++ b/backend/src/Designer/Services/Implementation/OptionsService.cs @@ -25,7 +25,7 @@ public OptionsService(IAltinnGitRepositoryFactory altinnGitRepositoryFactory, IA } /// - public async Task>> GetOptions(string org, string repo, string developer, string optionsListId) + public async Task>> GetOptionsList(string org, string repo, string developer, string optionsListId) { var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer); @@ -34,4 +34,16 @@ public async Task>> GetOptions(string org, strin return optionsList; } + + /// + public async Task>> CreateOrOverwriteOptionsList(string org, string repo, string developer, string optionsListId, List> optionsListPayload) + { + var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer); + + string optionsListPayloadString = JsonConvert.SerializeObject(optionsListPayload); + string createdOptionsListString = await altinnAppGitRepository.CreateOrOverwriteOptions(optionsListId, optionsListPayloadString); + var createdOptionsList = JsonConvert.DeserializeObject>>(createdOptionsListString); + + return createdOptionsList; + } } diff --git a/backend/src/Designer/Services/Interfaces/IOptionsService.cs b/backend/src/Designer/Services/Interfaces/IOptionsService.cs index 095e2ed04de..041287f3da2 100644 --- a/backend/src/Designer/Services/Interfaces/IOptionsService.cs +++ b/backend/src/Designer/Services/Interfaces/IOptionsService.cs @@ -9,12 +9,22 @@ namespace Altinn.Studio.Designer.Services.Interfaces; public interface IOptionsService { /// - /// Gets options file in app repository according to specified optionsListId. + /// Gets a options file from the app repository with the specified optionsListId. /// /// Organisation /// Repository /// Username of developer - /// Options list to fetch + /// Name of the options list to fetch /// The options list as a dictionary - public Task>> GetOptions(string org, string repo, string developer, string optionsListId); + public Task>> GetOptionsList(string org, string repo, string developer, string optionsListId); + + /// + /// Creates a new options file in the app repository. + /// + /// Organisation + /// Repository + /// Username of developer + /// Name of the new options list + /// The options list contents + public Task>> CreateOrOverwriteOptionsList(string org, string repo, string developer, string optionsListId, List> optionsListPayload); } diff --git a/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs b/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs index fe3994fa2cb..2ff65b3672c 100644 --- a/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs +++ b/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; @@ -19,6 +20,7 @@ public GetTests(WebApplicationFactory factory) : base(factory) [InlineData("ttd", "app-with-layoutsets", "test-options")] public async Task Get_Returns_OptionsList(string org, string repo, string optionsListId) { + // Arrange var expectedOptionsList = new List> { new() { { "label", "label1" }, { "value", "value1" } }, @@ -28,12 +30,12 @@ public async Task Get_Returns_OptionsList(string org, string repo, string option string apiUrl = $"/designer/api/{org}/{repo}/options/{optionsListId}"; HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl); + // Act HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage); - response.EnsureSuccessStatusCode(); - string responseBody = await response.Content.ReadAsStringAsync(); var responseList = JsonSerializer.Deserialize>>(responseBody); + // Assert Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); Assert.Equal(expectedOptionsList, responseList); } diff --git a/backend/tests/Designer.Tests/Controllers/OptionsController/PutTests.cs b/backend/tests/Designer.Tests/Controllers/OptionsController/PutTests.cs new file mode 100644 index 00000000000..03fc545fd05 --- /dev/null +++ b/backend/tests/Designer.Tests/Controllers/OptionsController/PutTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; +using Designer.Tests.Controllers.ApiTests; +using Designer.Tests.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; + +namespace Designer.Tests.Controllers.OptionsController; + +public class PutTests : DisagnerEndpointsTestsBase, IClassFixture> +{ + public PutTests(WebApplicationFactory factory) : base(factory) + { + } + + [Theory] + [InlineData("ttd", "empty-app", "testUser", "new-options")] + public async Task Create_Returns_200_With_New_OptionsList(string org, string repo, string developer, string optionsListId) + { + // Arrange + string targetRepository = TestDataHelper.GenerateTestRepoName(); + await CopyRepositoryForTest(org, repo, developer, targetRepository); + + var expectedOptionsList = new List> + { + new() { { "label", "label1" }, { "value", "value1" } }, + new() { { "label", "label2" }, { "value", "value2" } } + }; + + string apiUrl = $"/designer/api/{org}/{targetRepository}/options/{optionsListId}"; + HttpRequestMessage httpRequestMessage = new(HttpMethod.Put, apiUrl); + httpRequestMessage.Content = JsonContent.Create(expectedOptionsList); + + // Act + HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage); + string responseBody = await response.Content.ReadAsStringAsync(); + var responseList = JsonSerializer.Deserialize>>(responseBody); + + // Assert + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + Assert.Equal(expectedOptionsList, responseList); + } +} From 68f05f44f774383ab01e7748fc50333a889c1bbd Mon Sep 17 00:00:00 2001 From: Erling Hauan Date: Thu, 27 Jun 2024 10:30:39 +0200 Subject: [PATCH 03/16] Implement 'Option' model --- .../Designer/Controllers/OptionsController.cs | 9 ++-- backend/src/Designer/Models/Option.cs | 34 +++++++++++++ backend/src/Designer/Models/Options.cs | 49 ------------------- .../Services/Implementation/OptionsService.cs | 18 +++---- .../Services/Interfaces/IOptionsService.cs | 5 +- .../Controllers/OptionsController/GetTests.cs | 33 ++++++++++--- .../Controllers/OptionsController/PutTests.cs | 30 +++++++++--- 7 files changed, 98 insertions(+), 80 deletions(-) create mode 100644 backend/src/Designer/Models/Option.cs delete mode 100644 backend/src/Designer/Models/Options.cs diff --git a/backend/src/Designer/Controllers/OptionsController.cs b/backend/src/Designer/Controllers/OptionsController.cs index 6f37163dd10..e0e54424be7 100644 --- a/backend/src/Designer/Controllers/OptionsController.cs +++ b/backend/src/Designer/Controllers/OptionsController.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Altinn.Studio.Designer.Helpers; +using Altinn.Studio.Designer.Models; using Altinn.Studio.Designer.Services.Interfaces; using Microsoft.AspNetCore.Authorization; @@ -43,13 +44,13 @@ public OptionsController(IOptionsService optionsService) [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [Route("{optionsListId}")] - public async Task>>> Get(string org, string repo, [FromRoute] string optionsListId) + public async Task>> Get(string org, string repo, [FromRoute] string optionsListId) { string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); try { - List> optionList = await _optionsService.GetOptionsList(org, repo, developer, optionsListId); + List public async Task CreateOrOverwriteOptions(string optionListId, string payload, CancellationToken cancellationToken = default) { string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionListId}.json"); @@ -738,24 +759,18 @@ public async Task CreateOrOverwriteOptions(string optionListId, string p } /// - /// Gets a list of file names from the Options folder representing the available options lists. - /// A list of option list names. + /// Deletes the option list with the provided id. /// - public string[] GetOptionListIds() + /// The name of the option list to create. + public void DeleteOptions(string optionListId) { - string optionsFolder = Path.Combine(OptionsFolderPath); - if (!DirectoryExistsByRelativePath(optionsFolder)) - { - throw new NotFoundException("Options folder not found."); - } - string[] fileNames = GetFilesByRelativeDirectory(optionsFolder); - List optionListIds = new(); - foreach (string fileName in fileNames.Select(Path.GetFileNameWithoutExtension)) + string optionsFilePath = Path.Combine(OptionsFolderPath, $"{optionListId}.json"); + if (!FileExistsByRelativePath(optionsFilePath)) { - optionListIds.Add(fileName); + throw new NotFoundException($"Options file {optionListId}.json was not found."); } - return optionListIds.ToArray(); + DeleteFileByRelativePath(optionsFilePath); } /// diff --git a/backend/src/Designer/Services/Implementation/OptionsService.cs b/backend/src/Designer/Services/Implementation/OptionsService.cs index 9338315943e..cb2fae4ef92 100644 --- a/backend/src/Designer/Services/Implementation/OptionsService.cs +++ b/backend/src/Designer/Services/Implementation/OptionsService.cs @@ -44,4 +44,12 @@ public async Task> UpdateOptions(string org, string repo, string de return updatedOptions; } + + /// + public void DeleteOptions(string org, string repo, string developer, string optionListId) + { + var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, repo, developer); + + altinnAppGitRepository.DeleteOptions(optionListId); + } } diff --git a/backend/src/Designer/Services/Interfaces/IOptionsService.cs b/backend/src/Designer/Services/Interfaces/IOptionsService.cs index 9475649e0a7..ddcf815c1f5 100644 --- a/backend/src/Designer/Services/Interfaces/IOptionsService.cs +++ b/backend/src/Designer/Services/Interfaces/IOptionsService.cs @@ -29,4 +29,13 @@ public interface IOptionsService /// Name of the new options list /// The options list contents public Task> UpdateOptions(string org, string repo, string developer, string optionListId, List public interface IOptionsService { + /// + /// Gets a list of file names from the Options folder representing the available options lists. + /// + /// + /// + /// + /// + public string[] GetOptionListIds(string org, string repo, string developer); + /// /// Gets a options file from the app repository with the specified optionListId. /// diff --git a/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs b/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs index 5d8b2ce282e..d1fe2dee598 100644 --- a/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs +++ b/backend/tests/Designer.Tests/Controllers/OptionsController/GetTests.cs @@ -16,9 +16,27 @@ public GetTests(WebApplicationFactory factory) : base(factory) { } + [Theory] + [InlineData("ttd", "app-with-options")] + public async Task GetOptionListIds_Returns_OptionListIds(string org, string repo) + { + // Arrange + string apiUrl = $"/designer/api/{org}/{repo}/options"; + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, apiUrl); + + // Act + HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage); + string responseBody = await response.Content.ReadAsStringAsync(); + var responseList = JsonSerializer.Deserialize(responseBody); + + // Assert + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + Assert.Equal(2, responseList.Length); + } + [Theory] [InlineData("ttd", "app-with-layoutsets", "test-options")] - public async Task Get_Returns_OptionList(string org, string repo, string optionListId) + public async Task GetSingleOptionList_Returns_OptionList(string org, string repo, string optionListId) { // Arrange // This expected list matches the list in 'app-with-layoutsets' From d2418eafcdeea3bb02a09f80420f3333fdd94750 Mon Sep 17 00:00:00 2001 From: Erling Hauan Date: Fri, 28 Jun 2024 10:46:34 +0200 Subject: [PATCH 07/16] Add POST endpoint --- .../Designer/Controllers/OptionsController.cs | 61 ++++++-- backend/src/Designer/Models/Option.cs | 3 + .../Services/Implementation/OptionsService.cs | 16 +- .../Services/Interfaces/IOptionsService.cs | 9 ++ .../OptionsController/PostTests.cs | 147 ++++++++++++++++++ 5 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 backend/tests/Designer.Tests/Controllers/OptionsController/PostTests.cs diff --git a/backend/src/Designer/Controllers/OptionsController.cs b/backend/src/Designer/Controllers/OptionsController.cs index cddfc53d57e..2b75c6f21ae 100644 --- a/backend/src/Designer/Controllers/OptionsController.cs +++ b/backend/src/Designer/Controllers/OptionsController.cs @@ -12,8 +12,9 @@ namespace Altinn.Studio.Designer.Controllers; /// -/// Controller containing actions related to options (codelists). +/// Controller containing actions related to options (code lists). /// +[ApiController] [Authorize] [AutoValidateAntiforgeryToken] [Route("designer/api/{org}/{repo:regex(^(?!datamodels$)[[a-z]][[a-z0-9-]]{{1,28}}[[a-z0-9]]$)}/options")] @@ -31,7 +32,7 @@ public OptionsController(IOptionsService optionsService) } /// - /// Fetches a list the static option lists belonging to the app. + /// Fetches a list of the static option lists belonging to the app. /// /// Unique identifier of the organisation responsible for the app. /// Application identifier which is unique within an organisation. @@ -40,14 +41,13 @@ public OptionsController(IOptionsService optionsService) [Produces("application/json")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public string[] GetOptionListIds(string org, string repo) + public ActionResult GetOptionListIds(string org, string repo) { string developer = AuthenticationHelper.GetDeveloperUserName(HttpContext); string[] optionLists = _optionsService.GetOptionListIds(org, repo, developer); - return optionLists; + return Ok(optionLists); } /// @@ -81,6 +81,40 @@ public async Task>> GetSingleOptionList(string org, st } } + /// + /// Create an option list + /// + /// Unique identifier of the organisation responsible for the app. + /// Application identifier which is unique within an organisation. + /// Name of the new option list + /// The option list contents + /// The created option list + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [Route("{optionListId}")] + public async Task Post(string org, string repo, [FromRoute] string optionListId, [FromBody] List