diff --git a/docs/schema/V1/swagger.verified.json b/docs/schema/V1/swagger.verified.json index b382f4cf1..5493bb968 100644 --- a/docs/schema/V1/swagger.verified.json +++ b/docs/schema/V1/swagger.verified.json @@ -70,7 +70,6 @@ "properties": { "mediaType": { "description": "Media type of the content (plaintext, Markdown). Can also indicate that the content is embeddable.", - "example": "text/plain\ntext/markdown\napplication/vnd.dialogporten.frontchannelembed", "type": "string" }, "value": { @@ -297,7 +296,7 @@ "additionalProperties": false, "properties": { "additionalInfo": { - "description": "Additional information about the dialog, this may contain Markdown.", + "description": "Additional information about the dialog.\nSupported media types: text/plain, text/markdown", "nullable": true, "oneOf": [ { @@ -306,7 +305,7 @@ ] }, "extendedStatus": { - "description": "Used as the human-readable label used to describe the \u0022ExtendedStatus\u0022 field. Must be text/plain.", + "description": "Used as the human-readable label used to describe the \u0022ExtendedStatus\u0022 field.\nSupported media types: text/plain", "nullable": true, "oneOf": [ { @@ -315,7 +314,7 @@ ] }, "mainContentReference": { - "description": "Front-channel embedded content. Used to dynamically embed content in the frontend from an external URL.", + "description": "Front-channel embedded content. Used to dynamically embed content in the frontend from an external URL.\nSupported media types: application/vnd.dialogporten.frontchannelembed\u002Bjson;type=markdown", "nullable": true, "oneOf": [ { @@ -324,7 +323,7 @@ ] }, "senderName": { - "description": "Overridden sender name. If not supplied, assume \u0022org\u0022 as the sender name. Must be text/plain if supplied.", + "description": "Overridden sender name. If not supplied, assume \u0022org\u0022 as the sender name. Must be text/plain if supplied.\nSupported media types: text/plain", "nullable": true, "oneOf": [ { @@ -333,7 +332,7 @@ ] }, "summary": { - "description": "A short summary of the dialog and its current state. Must be text/plain.", + "description": "A short summary of the dialog and its current state.\nSupported media types: text/plain", "oneOf": [ { "$ref": "#/components/schemas/ContentValueDto" @@ -341,7 +340,7 @@ ] }, "title": { - "description": "The title of the dialog. Must be text/plain.", + "description": "The title of the dialog.\nSupported media types: text/plain", "oneOf": [ { "$ref": "#/components/schemas/ContentValueDto" @@ -6474,4 +6473,4 @@ "url": "https://altinn-dev-api.azure-api.net/dialogporten" } ] -} \ No newline at end of file +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs index b322d74cf..2c6a9f77a 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDto.cs @@ -13,10 +13,5 @@ public sealed class ContentValueDto /// /// Media type of the content (plaintext, Markdown). Can also indicate that the content is embeddable. /// - /// - /// text/plain - /// text/markdown - /// application/vnd.dialogporten.frontchannelembed - /// public string MediaType { get; set; } = MediaTypes.PlainText; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDtoValidator.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDtoValidator.cs index e3f77d40c..cffa0a34b 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDtoValidator.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Content/ContentValueDtoValidator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Digdir.Domain.Dialogporten.Application.Common.Authorization; using Digdir.Domain.Dialogporten.Application.Common.Extensions; using Digdir.Domain.Dialogporten.Application.Common.Extensions.FluentValidation; @@ -17,7 +18,6 @@ internal interface IIgnoreOnAssemblyScan; internal sealed class ContentValueDtoValidator : AbstractValidator, IIgnoreOnAssemblyScan { - public const string LegacyHtmlMediaType = "text/html"; public ContentValueDtoValidator(DialogTransmissionContentType contentType) { @@ -68,22 +68,15 @@ x.MediaType is not null .SetValidator(_ => new LocalizationDtosValidator(contentType.MaxLength)); } + [SuppressMessage("Style", "IDE0072:Add missing cases")] private static string[] GetAllowedMediaTypes(DialogContentType contentType, IUser? user) - { - if (user == null) - { - return contentType.AllowedMediaTypes; - } - - if (contentType.Id != DialogContentType.Values.AdditionalInfo) + => contentType.Id switch { - return contentType.AllowedMediaTypes; - } - - var allowHtmlSupport = user.GetPrincipal().HasScope(Constants.LegacyHtmlScope); - - return allowHtmlSupport - ? contentType.AllowedMediaTypes.Append(LegacyHtmlMediaType).ToArray() - : contentType.AllowedMediaTypes; - } + DialogContentType.Values.AdditionalInfo when UserHasLegacyHtmlScope(user) + => contentType.AllowedMediaTypes.Append(MediaTypes.LegacyHtml).ToArray(), + DialogContentType.Values.MainContentReference when UserHasLegacyHtmlScope(user) + => contentType.AllowedMediaTypes.Append(MediaTypes.LegacyEmbeddableHtml).ToArray(), + _ => contentType.AllowedMediaTypes + }; + private static bool UserHasLegacyHtmlScope(IUser? user) => user is not null && user.GetPrincipal().HasScope(Constants.LegacyHtmlScope); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs index 3d706fe9f..d59f59cda 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Commands/Create/CreateDialogDto.cs @@ -209,32 +209,38 @@ public sealed class CreateDialogDialogTransmissionDto public sealed class CreateDialogContentDto { /// - /// The title of the dialog. Must be text/plain. + /// The title of the dialog. + /// Supported media types: text/plain /// public ContentValueDto Title { get; set; } = null!; /// - /// A short summary of the dialog and its current state. Must be text/plain. + /// A short summary of the dialog and its current state. + /// Supported media types: text/plain /// public ContentValueDto Summary { get; set; } = null!; /// /// Overridden sender name. If not supplied, assume "org" as the sender name. Must be text/plain if supplied. + /// Supported media types: text/plain /// public ContentValueDto? SenderName { get; set; } /// - /// Additional information about the dialog, this may contain Markdown. + /// Additional information about the dialog. + /// Supported media types: text/plain, text/markdown /// public ContentValueDto? AdditionalInfo { get; set; } /// - /// Used as the human-readable label used to describe the "ExtendedStatus" field. Must be text/plain. + /// Used as the human-readable label used to describe the "ExtendedStatus" field. + /// Supported media types: text/plain /// public ContentValueDto? ExtendedStatus { get; set; } /// /// Front-channel embedded content. Used to dynamically embed content in the frontend from an external URL. + /// Supported media types: application/vnd.dialogporten.frontchannelembed+json;type=markdown /// public ContentValueDto? MainContentReference { get; set; } } diff --git a/src/Digdir.Domain.Dialogporten.Domain/MediaTypes.cs b/src/Digdir.Domain.Dialogporten.Domain/MediaTypes.cs index e89f528c5..6e2c53f9e 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/MediaTypes.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/MediaTypes.cs @@ -4,7 +4,9 @@ public static class MediaTypes { public const string EmbeddablePrefix = "application/vnd.dialogporten.frontchannelembed"; public const string EmbeddableMarkdown = $"{EmbeddablePrefix}+json;type=markdown"; + public const string LegacyEmbeddableHtml = $"{EmbeddablePrefix}+json;type=html"; + public const string LegacyHtml = "text/html"; public const string Markdown = "text/markdown"; public const string PlainText = "text/plain"; } diff --git a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs index 3b5fc68ac..54f08226b 100644 --- a/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs +++ b/tests/Digdir.Domain.Dialogporten.Application.Integration.Tests/Features/V1/ServiceOwner/Dialogs/Commands/CreateDialogTests.cs @@ -4,6 +4,7 @@ using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations; using Digdir.Domain.Dialogporten.Application.Features.V1.ServiceOwner.Dialogs.Queries.Get; using Digdir.Domain.Dialogporten.Application.Integration.Tests.Common; +using Digdir.Domain.Dialogporten.Domain; using Digdir.Tool.Dialogporten.GenerateFakeData; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -276,7 +277,7 @@ public async Task Cannot_Create_Transmission_With_Empty_Content_Localization_Val .Be(2); } - private const string LegacyHtmlMediaType = ContentValueDtoValidator.LegacyHtmlMediaType; + private const string LegacyHtmlMediaType = MediaTypes.LegacyHtml; private static ContentValueDto CreateHtmlContentValueDto() => new() { @@ -350,4 +351,62 @@ public async Task Cannot_Create_Title_Content_With_Html_MediaType_With_Correct_S .Should() .Be(1); } + + [Fact] + public async Task Cannot_Create_Title_Content_With_Embeddable_Html_MediaType_With_Correct_Scope() + { + // Arrange + var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(); + createDialogCommand.Content.Title = new ContentValueDto + { + MediaType = MediaTypes.LegacyEmbeddableHtml, + Value = [new LocalizationDto { LanguageCode = "en", Value = "https://external.html" }] + }; + + var userWithLegacyScope = new IntegrationTestUser([new("scope", Constants.LegacyHtmlScope)]); + Application.ConfigureServiceCollection(services => + { + services.RemoveAll(); + services.AddSingleton(userWithLegacyScope); + }); + + // Act + var response = await Application.Send(createDialogCommand); + + // Assert + response.TryPickT2(out var validationError, out _).Should().BeTrue(); + validationError.Should().NotBeNull(); + validationError.Errors + .Count(e => e.AttemptedValue.Equals(MediaTypes.LegacyEmbeddableHtml)) + .Should() + .Be(1); + } + + [Fact] + public async Task Can_Create_MainContentRef_Content_With_Embeddable_Html_MediaType_With_Correct_Scope() + { + // Arrange + var expectedDialogId = GenerateBigEndianUuidV7(); + var createDialogCommand = DialogGenerator.GenerateSimpleFakeDialog(id: expectedDialogId); + createDialogCommand.Content.MainContentReference = new ContentValueDto + { + MediaType = MediaTypes.LegacyEmbeddableHtml, + Value = [new LocalizationDto { LanguageCode = "en", Value = "https://external.html" }] + }; + + var userWithLegacyScope = new IntegrationTestUser([new("scope", Constants.LegacyHtmlScope)]); + Application.ConfigureServiceCollection(services => + { + services.RemoveAll(); + services.AddSingleton(userWithLegacyScope); + }); + + // Act + var response = await Application.Send(createDialogCommand); + + // Assert + response.TryPickT0(out var success, out _).Should().BeTrue(); + success.Should().NotBeNull(); + success.Value.Should().Be(expectedDialogId); + } }