From d5d86249dd754514729d8b0be7aaf7894618f69e Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Wed, 7 Jan 2026 13:46:20 +0100 Subject: [PATCH 1/2] Add utilites for reading using xsd files from the models --- .../Controllers/ResourceController.cs | 20 +++++ .../Implementation/AppResourcesSI.cs | 16 ++++ .../Internal/App/IAppResources.cs | 5 ++ .../Controllers/ResourceControllerTests.cs | 57 +++++++++++++ .../models/Skjema.schema.json | 3 + .../contributer-restriction/models/Skjema.xsd | 4 + ...ngeDetection.SaveJsonSwagger.verified.json | 79 ++++++++++++++++++- ...ouldNotChange_Unintentionally.verified.txt | 7 ++ .../Implementation/AppResourcesSITests.cs | 21 +++-- ...ouldNotChange_Unintentionally.verified.txt | 2 + 10 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 test/Altinn.App.Api.Tests/Controllers/ResourceControllerTests.cs create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.schema.json create mode 100644 test/Altinn.App.Api.Tests/Data/apps/tdd/contributer-restriction/models/Skjema.xsd diff --git a/src/Altinn.App.Api/Controllers/ResourceController.cs b/src/Altinn.App.Api/Controllers/ResourceController.cs index e38ad0cc8e..1d3142f6f1 100644 --- a/src/Altinn.App.Api/Controllers/ResourceController.cs +++ b/src/Altinn.App.Api/Controllers/ResourceController.cs @@ -35,6 +35,23 @@ public ActionResult GetModelJsonSchema([FromRoute] string id) return Ok(schema); } + /// + /// Get the xsd schema for the model + /// + [HttpGet] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "application/xml")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Route("{org}/{app}/api/xsdschema/{id}")] + public ActionResult GetModelXsdSchema([FromRoute] string id) + { + string? schema = _appResourceService.GetXsdSchema(id); + if (schema == null) + { + return NotFound(); + } + return Content(schema, "application/xml", System.Text.Encoding.UTF8); + } + /// /// Get the form layout /// @@ -80,6 +97,9 @@ public ActionResult GetLayouts(string org, string app, string id) [ProducesResponseType(typeof(string), StatusCodes.Status200OK, "application/json")] [HttpGet] [Route("{org}/{app}/api/layoutsettings")] + [Obsolete( + "This endpoint is no longer available. Use /{org}/{app}/api/layoutsettings/{id} to get layout settings for a specific layout set." + )] public ActionResult GetLayoutSettings(string org, string app) { string? settings = _appResourceService.GetLayoutSettingsString(); diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs index 95be38aa60..b445cfa906 100644 --- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs +++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs @@ -523,4 +523,20 @@ private static byte[] ReadFileContentsFromLegalPath(string legalPath, string fil return filedata; } + + /// + public string? GetXsdSchema(string dataTypeId) + { + string legalPath = Path.Join(_settings.AppBasePath, _settings.ModelsFolder); + string filename = Path.Join(legalPath, $"{dataTypeId}.xsd"); + PathHelper.EnsureLegalPath(legalPath, filename); + + string? filedata = null; + if (File.Exists(filename)) + { + filedata = File.ReadAllText(filename, Encoding.UTF8); + } + + return filedata; + } } diff --git a/src/Altinn.App.Core/Internal/App/IAppResources.cs b/src/Altinn.App.Core/Internal/App/IAppResources.cs index 6a08382f28..956d8ade61 100644 --- a/src/Altinn.App.Core/Internal/App/IAppResources.cs +++ b/src/Altinn.App.Core/Internal/App/IAppResources.cs @@ -166,4 +166,9 @@ public interface IAppResources /// Gets the validation configuration for a given data type /// string? GetValidationConfiguration(string dataTypeId); + + /// + /// Gets the xsd schema for the provided dataTypeId. + /// + string? GetXsdSchema(string dataTypeId); } diff --git a/test/Altinn.App.Api.Tests/Controllers/ResourceControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ResourceControllerTests.cs new file mode 100644 index 0000000000..d523547cf3 --- /dev/null +++ b/test/Altinn.App.Api.Tests/Controllers/ResourceControllerTests.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit.Abstractions; + +namespace Altinn.App.Api.Tests.Controllers; + +public class ResourceControllerTests : ApiTestBase, IClassFixture> +{ + public ResourceControllerTests(WebApplicationFactory factory, ITestOutputHelper outputHelper) + : base(factory, outputHelper) { } + + private const string Org = "tdd"; + private const string App = "contributer-restriction"; + private const string DefaultDataTypeId = "Skjema"; + + [Fact] + public async Task GetModelJsonSchema_ReturnsOk() + { + var client = GetRootedClient(Org, App); + using var response = await client.GetAsync($"/{Org}/{App}/api/jsonschema/{DefaultDataTypeId}"); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + Assert.NotNull(content); + Assert.Contains("\"$comment\"", content); // TODO: Update when the schema in the test project has more content. + } + + [Fact] + public async Task GetModelJsonSchema_InvalidDataTypeId_ReturnsNotFound() + { + var client = GetRootedClient(Org, App); + await Assert.ThrowsAsync(async () => + { + using var response = await client.GetAsync($"/{Org}/{App}/api/jsonschema/InvalidDataTypeId"); + // TODO: Should probably return 404 NotFound instead of throwing FileNotFoundException, + // but adding test for current behaviour. + // Assert.Equal(System.Net.HttpStatusCode.NotFound, response.StatusCode); + }); + } + + [Fact] + public async Task GetXmlSchema_ReturnsOk() + { + var client = GetRootedClient(Org, App); + using var response = await client.GetAsync($"/{Org}/{App}/api/xsdschema/{DefaultDataTypeId}"); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + Assert.NotNull(content); + Assert.Contains(" + + + diff --git a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json index 3b981634ca..6e922fe919 100644 --- a/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json +++ b/test/Altinn.App.Api.Tests/OpenApi/OpenApiSpecChangeDetection.SaveJsonSwagger.verified.json @@ -5085,6 +5085,82 @@ } } }, + "/{org}/{app}/api/xsdschema/{id}": { + "get": { + "tags": [ + "Resource" + ], + "summary": "Get the xsd schema for the model", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "org", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "app", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/{org}/{app}/api/layouts": { "get": { "tags": [ @@ -5213,7 +5289,8 @@ } } } - } + }, + "deprecated": true } }, "/{org}/{app}/api/layoutsettings/{id}": { diff --git a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 23a2cc3d7c..2b623302a1 100644 --- a/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Api.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -528,6 +528,8 @@ namespace Altinn.App.Api.Controllers [Microsoft.AspNetCore.Mvc.HttpGet] [Microsoft.AspNetCore.Mvc.ProducesResponseType(typeof(string), 200, "application/json", new string[0])] [Microsoft.AspNetCore.Mvc.Route("{org}/{app}/api/layoutsettings")] + [System.Obsolete("This endpoint is no longer available. Use /{org}/{app}/api/layoutsettings/{id} to" + + " get layout settings for a specific layout set.")] public Microsoft.AspNetCore.Mvc.ActionResult GetLayoutSettings(string org, string app) { } [Microsoft.AspNetCore.Mvc.HttpGet] [Microsoft.AspNetCore.Mvc.ProducesResponseType(typeof(string), 200, "application/json", new string[0])] @@ -549,6 +551,11 @@ namespace Altinn.App.Api.Controllers [Microsoft.AspNetCore.Mvc.Route("{org}/{app}/api/jsonschema/{id}")] public Microsoft.AspNetCore.Mvc.ActionResult GetModelJsonSchema([Microsoft.AspNetCore.Mvc.FromRoute] string id) { } [Microsoft.AspNetCore.Mvc.HttpGet] + [Microsoft.AspNetCore.Mvc.ProducesResponseType(404)] + [Microsoft.AspNetCore.Mvc.ProducesResponseType(typeof(string), 200, "application/xml", new string[0])] + [Microsoft.AspNetCore.Mvc.Route("{org}/{app}/api/xsdschema/{id}")] + public Microsoft.AspNetCore.Mvc.ActionResult GetModelXsdSchema([Microsoft.AspNetCore.Mvc.FromRoute] string id) { } + [Microsoft.AspNetCore.Mvc.HttpGet] [Microsoft.AspNetCore.Mvc.ProducesResponseType(204)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(typeof(Microsoft.AspNetCore.Mvc.FileContentResult), 200, "application/json", new string[0])] [Microsoft.AspNetCore.Mvc.Route("{org}/{app}/api/ruleconfiguration/{id}")] diff --git a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs index 14d172bcc9..0de10240ff 100644 --- a/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs +++ b/test/Altinn.App.Core.Tests/Implementation/AppResourcesSITests.cs @@ -1,4 +1,3 @@ -#nullable disable using Altinn.App.Core.Configuration; using Altinn.App.Core.Features.ExternalApi; using Altinn.App.Core.Implementation; @@ -30,7 +29,7 @@ public void GetApplication_desrializes_file_from_disk() AppResourcesSI appResources = new( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -81,7 +80,7 @@ public void GetApplication_handles_onEntry_null() AppResourcesSI appResources = new( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -135,7 +134,7 @@ public void GetApplication_second_read_from_cache() AppResourcesSI appResources = new( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -190,7 +189,7 @@ public void GetApplicationMetadata_throws_ApplicationConfigException_if_file_not IAppResources appResources = new AppResourcesSI( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -206,7 +205,7 @@ public void GetApplicationMetadata_throws_ApplicationConfigException_if_deserial IAppResources appResources = new AppResourcesSI( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -222,7 +221,7 @@ public void GetApplicationXACMLPolicy_return_policyfile_as_string() IAppResources appResources = new AppResourcesSI( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -240,7 +239,7 @@ public void GetApplicationXACMLPolicy_return_null_if_file_not_found() IAppResources appResources = new AppResourcesSI( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -257,7 +256,7 @@ public void GetApplicationBPMNProcess_return_process_as_string() IAppResources appResources = new AppResourcesSI( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -275,7 +274,7 @@ public void GetApplicationBPMNProcess_return_null_if_file_not_found() IAppResources appResources = new AppResourcesSI( settings, appMetadata, - null, + null!, new NullLogger(), _telemetry.Object ); @@ -305,7 +304,7 @@ private AppSettings GetAppSettings( private static IAppMetadata SetupAppMetadata( IOptions appsettings, - IFrontendFeatures frontendFeatures = null + IFrontendFeatures? frontendFeatures = null ) { var featureManagerMock = new Mock(); diff --git a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt index 5e4161778e..23b2e1718c 100644 --- a/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt +++ b/test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt @@ -2353,6 +2353,7 @@ namespace Altinn.App.Core.Implementation public byte[] GetText(string org, string app, string textResource) { } public System.Threading.Tasks.Task GetTexts(string org, string app, string language) { } public string? GetValidationConfiguration(string dataTypeId) { } + public string? GetXsdSchema(string dataTypeId) { } } public class DefaultAppEvents : Altinn.App.Core.Internal.App.IAppEvents { @@ -2928,6 +2929,7 @@ namespace Altinn.App.Core.Internal.App byte[] GetText(string org, string app, string textResource); System.Threading.Tasks.Task GetTexts(string org, string app, string language); string? GetValidationConfiguration(string dataTypeId); + string? GetXsdSchema(string dataTypeId); } public interface IApplicationClient { From f1c903807f282d20bcf514e34c4ababf30abab66 Mon Sep 17 00:00:00 2001 From: Ivar Nesje Date: Thu, 8 Jan 2026 09:08:10 +0100 Subject: [PATCH 2/2] add missing endpoints in scope snapshots --- ...omScopesTests.Metadata_Custom_0_Metadata.verified.txt | 9 +++++++++ ...ScopesTests.Metadata_Standard_0_Metadata.verified.txt | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Custom_0_Metadata.verified.txt b/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Custom_0_Metadata.verified.txt index d4ba4f3935..f521e96afc 100644 --- a/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Custom_0_Metadata.verified.txt +++ b/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Custom_0_Metadata.verified.txt @@ -407,6 +407,15 @@ requiredScopesServiceOwners: null } }, + { + endpoint: GET {org}/{app}/api/xsdschema/{id}, + metadata: { + errorMessageTextResourceKeyUser: null, + errorMessageTextResourceKeyServiceOwner: null, + requiredScopesUsers: null, + requiredScopesServiceOwners: null + } + }, { endpoint: GET {org}/{app}/instances/{instanceOwnerId:int}/{instanceId:guid}/data/{dataGuid:guid}/validate, metadata: { diff --git a/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Standard_0_Metadata.verified.txt b/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Standard_0_Metadata.verified.txt index 189d2581d4..7445415c8a 100644 --- a/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Standard_0_Metadata.verified.txt +++ b/test/Altinn.App.Integration.Tests/CustomScopes/_snapshots/CustomScopesTests.Metadata_Standard_0_Metadata.verified.txt @@ -382,6 +382,15 @@ requiredScopesServiceOwners: null } }, + { + endpoint: GET {org}/{app}/api/xsdschema/{id}, + metadata: { + errorMessageTextResourceKeyUser: null, + errorMessageTextResourceKeyServiceOwner: null, + requiredScopesUsers: null, + requiredScopesServiceOwners: null + } + }, { endpoint: GET {org}/{app}/instances/{instanceOwnerId:int}/{instanceId:guid}/data/{dataGuid:guid}/validate, metadata: {