diff --git a/src/database/PolicyHub.DbAccess/IHubRepositories.cs b/src/database/PolicyHub.DbAccess/IHubRepositories.cs index 14b4630..9c92596 100644 --- a/src/database/PolicyHub.DbAccess/IHubRepositories.cs +++ b/src/database/PolicyHub.DbAccess/IHubRepositories.cs @@ -21,5 +21,5 @@ namespace Org.Eclipse.TractusX.PolicyHub.DbAccess; public interface IHubRepositories { - public T GetInstance(); + T GetInstance(); } diff --git a/src/database/PolicyHub.DbAccess/PolicyHub.DbAccess.csproj b/src/database/PolicyHub.DbAccess/PolicyHub.DbAccess.csproj index a08bd62..03102e2 100644 --- a/src/database/PolicyHub.DbAccess/PolicyHub.DbAccess.csproj +++ b/src/database/PolicyHub.DbAccess/PolicyHub.DbAccess.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/src/database/PolicyHub.Migrations/PolicyHub.Migrations.csproj b/src/database/PolicyHub.Migrations/PolicyHub.Migrations.csproj index a67d0bd..70f5315 100644 --- a/src/database/PolicyHub.Migrations/PolicyHub.Migrations.csproj +++ b/src/database/PolicyHub.Migrations/PolicyHub.Migrations.csproj @@ -45,9 +45,9 @@ - - - + + + diff --git a/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs b/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs index 7d58fb4..7665e09 100644 --- a/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs +++ b/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs @@ -21,6 +21,7 @@ using Org.Eclipse.TractusX.PolicyHub.DbAccess.Models; using Org.Eclipse.TractusX.PolicyHub.DbAccess.Repositories; using Org.Eclipse.TractusX.PolicyHub.Entities.Enums; +using Org.Eclipse.TractusX.PolicyHub.Service.ErrorHandling; using Org.Eclipse.TractusX.PolicyHub.Service.Extensions; using Org.Eclipse.TractusX.PolicyHub.Service.Models; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; @@ -42,13 +43,13 @@ public async Task GetPolicyContentWithFiltersAsync(UseCaseId? us var (exists, leftOperand, attributes, rightOperandValue) = await hubRepositories.GetInstance().GetPolicyContentAsync(useCase, type, credential).ConfigureAwait(false); if (!exists) { - throw new NotFoundException($"Policy for type {type} and technicalKey {credential} does not exists"); + throw NotFoundException.Create(PolicyErrors.POLICY_NOT_EXIST, new ErrorParameter[] { new("type", type.ToString()), new("credential", credential) }); } var rightOperands = attributes.Values.Select(a => rightOperandValue != null ? string.Format(rightOperandValue, a) : a); if (attributes.Key == null && rightOperandValue == null) { - throw new UnexpectedConditionException("There must be one configured rightOperand value"); + throw UnexpectedConditionException.Create(PolicyErrors.NO_RIGHT_OPERAND_CONFIGURED); } var (rightOperand, additionalAttribute) = attributes.Key != null ? @@ -64,18 +65,19 @@ private static (object rightOperand, AdditionalAttributes? additionalAttribute) AttributeKeyId.DynamicValue => (value ?? "{dynamicValue}", null), AttributeKeyId.Regex => (GetRegexValue(attributes, value), null), _ => operatorId == OperatorId.Equals - ? processEqualsOperator(attributes, rightOperands, value, leftOperand, useCase) + ? ProcessEqualsOperator(attributes, rightOperands, value, leftOperand, useCase) : (rightOperands, null) }; - private static (object rightOperand, AdditionalAttributes? additionalAttribute) processEqualsOperator((AttributeKeyId? Key, IEnumerable Values) attributes, IEnumerable rightOperands, string? value, string leftOperand, UseCaseId? useCase) + private static (object rightOperand, AdditionalAttributes? additionalAttribute) ProcessEqualsOperator((AttributeKeyId? Key, IEnumerable Values) attributes, IEnumerable rightOperands, string? value, string leftOperand, UseCaseId? useCase) { if (value != null) { if (!rightOperands.Any(r => r == value)) { - throw new ControllerArgumentException($"Invalid values [{value}] set for key {leftOperand}. Possible values [{string.Join(",", rightOperands)}]"); + throw ControllerArgumentException.Create(PolicyErrors.INVALID_VALUES, new ErrorParameter[] { new("value", value), new("leftOperand", leftOperand), new("possibleValues", string.Join(",", rightOperands)) }); } + rightOperands = rightOperands.Where(r => r.Equals(value)); } @@ -92,17 +94,17 @@ private static object GetRegexValue((AttributeKeyId? Key, IEnumerable Va { if (string.IsNullOrWhiteSpace(value)) { - throw new ControllerArgumentException("you must provide a value for the regex", nameof(value)); + throw ControllerArgumentException.Create(PolicyErrors.NO_VALUE_FOR_REGEX); } if (attributes.Values.Count() != 1) { - throw new UnexpectedConditionException("There should only be one regex pattern defined"); + throw UnexpectedConditionException.Create(PolicyErrors.MULTIPLE_REGEX_DEFINED); } if (!Regex.IsMatch(value, attributes.Values.Single(), RegexOptions.Compiled, TimeSpan.FromSeconds(1))) { - throw new ControllerArgumentException($"The provided value {value} does not match the regex pattern {attributes.Values.Single()}", nameof(value)); + throw ControllerArgumentException.Create(PolicyErrors.VALUE_DOES_NOT_MATCH_REGEX, new ErrorParameter[] { new("value", value), new("values", attributes.Values.Single()) }); } return value; @@ -126,24 +128,24 @@ private static PolicyFileContent CreateFileContent(PolicyTypeId type, OperatorId public async Task GetPolicyContentAsync(PolicyContentRequest requestData) { - if (requestData.PolicyType == PolicyTypeId.Usage && requestData.ConstraintOperand == ConstraintOperandId.Or) + if (requestData is { PolicyType: PolicyTypeId.Usage, ConstraintOperand: ConstraintOperandId.Or }) { - throw new ControllerArgumentException($"The support of OR constraintOperand for Usage constraints are not supported for now"); + throw ControllerArgumentException.Create(PolicyErrors.OR_WITH_USAGE); } if (requestData.PolicyType == PolicyTypeId.Access && requestData.ConstraintOperand == ConstraintOperandId.And && requestData.Constraints.Any(x => x.Key == "BusinessPartnerNumber" && (x.Value!.Split(",").Count() > 1))) { - throw new ControllerArgumentException($"Only a single value BPNL is allowed with an AND constraint"); + throw ControllerArgumentException.Create(PolicyErrors.SINGLE_VALUE_BPNL_CONSTRAINT); } if (requestData.Constraints.Any(x => x.Key == "BusinessPartnerNumber") && !requestData.Constraints.Any(x => x.Operator == OperatorId.Equals)) { - throw new ControllerArgumentException($"The operator for BPNLs should always be Equals"); + throw ControllerArgumentException.Create(PolicyErrors.BPNL_WRONG_OPERATOR); } - if (requestData.PolicyType == PolicyTypeId.Usage && requestData.Constraints.Any(x => x.Key == "BusinessPartnerNumber" && (x.Value!.Split(",").Count() > 1))) + if (requestData.PolicyType == PolicyTypeId.Usage && requestData.Constraints.Any(x => x.Key == "BusinessPartnerNumber" && x.Value!.Split(",").Length > 1)) { - throw new ControllerArgumentException($"For usage policies only a single BPNL is allowed"); + throw ControllerArgumentException.Create(PolicyErrors.USAGE_MULTIPLE_BPNL); } var keyCounts = requestData.Constraints @@ -152,14 +154,14 @@ public async Task GetPolicyContentAsync(PolicyContentRequest req var multipleDefinedKey = keyCounts.Where(x => x.Value != 1); if (multipleDefinedKey.Any()) { - throw new ControllerArgumentException($"Keys {string.Join(",", multipleDefinedKey.Select(x => x.Key).Distinct())} have been defined multiple times"); + throw ControllerArgumentException.Create(PolicyErrors.KEY_DEFINED_MULTIPLE_TIMES, new ErrorParameter[] { new("keys", string.Join(",", multipleDefinedKey.Select(x => x.Key).Distinct())) }); } var technicalKeys = requestData.Constraints.Select(x => x.Key); var attributeValuesForTechnicalKeys = await hubRepositories.GetInstance().GetAttributeValuesForTechnicalKeys(requestData.PolicyType, technicalKeys).ConfigureAwait(false); if (technicalKeys.Except(attributeValuesForTechnicalKeys.Select(a => a.TechnicalKey)).Any()) { - throw new ControllerArgumentException($"Policy for type {requestData.PolicyType} and requested technicalKeys does not exists. TechnicalKeys {string.Join(",", attributeValuesForTechnicalKeys.Select(x => x.TechnicalKey))} are allowed"); + throw ControllerArgumentException.Create(PolicyErrors.POLICY_NOT_EXISTS_FOR_TECHNICAL_KEYS, new ErrorParameter[] { new("policyType", requestData.PolicyType.ToString()), new("technicalKeys", string.Join(",", attributeValuesForTechnicalKeys.Select(x => x.TechnicalKey))) }); } IEnumerable<(string TechnicalKey, IEnumerable Values)> keyValues = requestData.Constraints.GroupBy(x => x.Key).Select(x => new ValueTuple>(x.Key, x.Where(y => y.Value != null).SelectMany(y => y.Value!.Split(",")))); @@ -177,13 +179,13 @@ public async Task GetPolicyContentAsync(PolicyContentRequest req { var x = missingValues.Where(x => invalidValues.Contains(x.TechnicalKey)).Select(x => $"Key: {x.TechnicalKey}, requested value[{string.Join(',', x.Values)}] Possible Values[{string.Join(',', attributeValuesForTechnicalKeys.Where(a => a.TechnicalKey.Equals(x.TechnicalKey)).Select(a => a.Values).First())}]"); - throw new ControllerArgumentException($"Invalid values set for {string.Join(',', x)}"); + throw ControllerArgumentException.Create(PolicyErrors.INVALID_VALUES_SET, new ErrorParameter[] { new("values", string.Join(',', x)) }); } var policies = await hubRepositories.GetInstance().GetPolicyForOperandContent(requestData.PolicyType, technicalKeys).ToListAsync().ConfigureAwait(false); if (policies.Count != requestData.Constraints.Count()) { - throw new NotFoundException($"Policy for type {requestData.PolicyType} and technicalKeys {string.Join(",", technicalKeys.Except(policies.Select(x => x.TechnicalKey)))} does not exists"); + throw NotFoundException.Create(PolicyErrors.POLICY_NOT_EXISTS_FOR_TECHNICAL_KEYS, new ErrorParameter[] { new("policyType", requestData.PolicyType.ToString()), new("technicalKeys", string.Join(",", technicalKeys.Except(policies.Select(x => x.TechnicalKey)))) }); } var constraints = new List(); @@ -194,7 +196,7 @@ public async Task GetPolicyContentAsync(PolicyContentRequest req var rightOperands = policy.Attributes.Values.Select(a => policy.RightOperandValue != null ? string.Format(policy.RightOperandValue, a) : a); if (policy.Attributes.Key == null && policy.RightOperandValue == null) { - throw new UnexpectedConditionException("There must be one configured rightOperand value"); + throw UnexpectedConditionException.Create(PolicyErrors.RIGHT_OPERAND_NOT_CONFIGURED); } if (constraint.Value != null) @@ -216,7 +218,6 @@ public async Task GetPolicyContentAsync(PolicyContentRequest req constraint.Operator.OperatorToJsonString(), rightOperand )); - } } else diff --git a/src/hub/PolicyHub.Service/ErrorHandling/ErrorMessageContainer.cs b/src/hub/PolicyHub.Service/ErrorHandling/ErrorMessageContainer.cs new file mode 100644 index 0000000..abe315f --- /dev/null +++ b/src/hub/PolicyHub.Service/ErrorHandling/ErrorMessageContainer.cs @@ -0,0 +1,64 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Service; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.PolicyHub.Service.ErrorHandling; + +public class ErrorMessageContainer : IErrorMessageContainer +{ + private static readonly IReadOnlyDictionary Messages = ImmutableDictionary.CreateRange([ + new((int)PolicyErrors.POLICY_NOT_EXIST, "Policy for type {type} and technicalKey {credential} does not exists"), + new((int)PolicyErrors.NO_RIGHT_OPERAND_CONFIGURED, "There must be one configured rightOperand value"), + new((int)PolicyErrors.INVALID_VALUES, "Invalid values [{value}] set for key {leftOperand}. Possible values [{possibleValues}]"), + new((int)PolicyErrors.NO_VALUE_FOR_REGEX, "you must provide a value for the regex"), + new((int)PolicyErrors.MULTIPLE_REGEX_DEFINED, "There should only be one regex pattern defined"), + new((int)PolicyErrors.VALUE_DOES_NOT_MATCH_REGEX, "The provided value {value} does not match the regex pattern {values}"), + new((int)PolicyErrors.OR_WITH_USAGE, "The support of OR constraintOperand for Usage constraints are not supported for now"), + new((int)PolicyErrors.SINGLE_VALUE_BPNL_CONSTRAINT, "Only a single value BPNL is allowed with an AND constraint"), + new((int)PolicyErrors.BPNL_WRONG_OPERATOR, "The operator for BPNLs should always be Equals"), + new((int)PolicyErrors.USAGE_MULTIPLE_BPNL, "For usage policies only a single BPNL is allowed"), + new((int)PolicyErrors.KEY_DEFINED_MULTIPLE_TIMES, "Keys {keys} have been defined multiple times"), + new((int)PolicyErrors.POLICY_NOT_EXISTS_FOR_TECHNICAL_KEYS, "Policy for type {policyType} and requested technicalKeys does not exists. TechnicalKeys {technicalKeys} are allowed"), + new((int)PolicyErrors.INVALID_VALUES_SET, "Invalid values set for {values}"), + new((int)PolicyErrors.RIGHT_OPERAND_NOT_CONFIGURED, "There must be one configured rightOperand value") + ]); + + public Type Type { get => typeof(PolicyErrors); } + public IReadOnlyDictionary MessageContainer { get => Messages; } +} + +public enum PolicyErrors +{ + POLICY_NOT_EXIST, + NO_RIGHT_OPERAND_CONFIGURED, + INVALID_VALUES, + NO_VALUE_FOR_REGEX, + MULTIPLE_REGEX_DEFINED, + VALUE_DOES_NOT_MATCH_REGEX, + OR_WITH_USAGE, + SINGLE_VALUE_BPNL_CONSTRAINT, + BPNL_WRONG_OPERATOR, + USAGE_MULTIPLE_BPNL, + KEY_DEFINED_MULTIPLE_TIMES, + POLICY_NOT_EXISTS_FOR_TECHNICAL_KEYS, + INVALID_VALUES_SET, + RIGHT_OPERAND_NOT_CONFIGURED +} diff --git a/src/hub/PolicyHub.Service/ErrorHandling/GeneralHttpErrorHandler.cs b/src/hub/PolicyHub.Service/ErrorHandling/GeneralHttpErrorHandler.cs new file mode 100644 index 0000000..64001ec --- /dev/null +++ b/src/hub/PolicyHub.Service/ErrorHandling/GeneralHttpErrorHandler.cs @@ -0,0 +1,144 @@ +/******************************************************************************** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Service; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web; +using Serilog.Context; +using System.Collections.Immutable; +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.PolicyHub.Service.ErrorHandling; + +public class GeneralHttpErrorHandler( + RequestDelegate next, + ILogger logger, + IErrorMessageService errorMessageService) +{ + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + private readonly ILogger _logger = logger; + + private static readonly IReadOnlyDictionary Metadata = ImmutableDictionary.CreateRange(new[] + { + KeyValuePair.Create(HttpStatusCode.BadRequest, new MetaData("https://tools.ietf.org/html/rfc7231#section-6.5.1", "One or more validation errors occurred.")), + KeyValuePair.Create(HttpStatusCode.Conflict, new MetaData("https://tools.ietf.org/html/rfc7231#section-6.5.8", "The resource is in conflict with the current request.")), + KeyValuePair.Create(HttpStatusCode.NotFound, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4", "Cannot find representation of target resource.")), + KeyValuePair.Create(HttpStatusCode.Forbidden, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3", "Access to requested resource is not permitted.")), + KeyValuePair.Create(HttpStatusCode.UnsupportedMediaType, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.13", "The server cannot process this type of content")), + KeyValuePair.Create(HttpStatusCode.BadGateway, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.3", "Error accessing external resource.")), + KeyValuePair.Create(HttpStatusCode.ServiceUnavailable, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.4", "Service is currently unavailable.")), + KeyValuePair.Create(HttpStatusCode.InternalServerError, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", "The server encountered an unexpected condition.")), + }); + + private static KeyValuePair)>? MessageFunc, LogLevel LogLevel)> CreateErrorEntry( + HttpStatusCode httpStatusCode, + Func)>? messageFunc = null, + LogLevel logLevel = LogLevel.Information + ) where T : class => + KeyValuePair.Create)>?, LogLevel)>( + typeof(T), + (httpStatusCode, + messageFunc == null + ? null + : e => messageFunc.Invoke(e as T ?? throw new UnexpectedConditionException($"Exception type {e.GetType()} should always be of type {typeof(T)} here")), + logLevel)); + + private static readonly IReadOnlyDictionary)>? MessageFunc, LogLevel LogLevel)> ErrorTypes = ImmutableDictionary.CreateRange(new[] + { + CreateErrorEntry(HttpStatusCode.BadRequest, argumentException => (argumentException.ParamName, Enumerable.Repeat(argumentException.Message, 1))), + CreateErrorEntry(HttpStatusCode.BadRequest, caException => (caException.ParamName, Enumerable.Repeat(caException.Message, 1))), + CreateErrorEntry(HttpStatusCode.NotFound), + CreateErrorEntry(HttpStatusCode.Conflict), + CreateErrorEntry(HttpStatusCode.Forbidden), + CreateErrorEntry(HttpStatusCode.BadGateway, serviceException => (serviceException.Source, new[] { serviceException.StatusCode == null ? "remote service call failed" : $"remote service returned status code: {(int) serviceException.StatusCode} {serviceException.StatusCode}", serviceException.Message })), + CreateErrorEntry(HttpStatusCode.UnsupportedMediaType), + CreateErrorEntry(HttpStatusCode.InternalServerError, configurationException => (configurationException.Source, new[] { $"Invalid service configuration: {configurationException.Message}" })) + }); + + public async Task Invoke(HttpContext context) + { + try + { + await next(context).ConfigureAwait(ConfigureAwaitOptions.None); + } + catch (Exception error) + { + var errorId = Guid.NewGuid().ToString(); + var details = GetErrorDetails(error); + var message = GetErrorMessage(error); + LogErrorInformation(errorId, error); + var (statusCode, messageFunc, logLevel) = GetErrorInformation(error); + _logger.Log(logLevel, error, "GeneralErrorHandler caught {Error} with errorId: {ErrorId} resulting in response status code {StatusCode}, message '{Message}'", error.GetType().Name, errorId, (int)statusCode, message); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = (int)statusCode; + await context.Response.WriteAsync(JsonSerializer.Serialize(CreateErrorResponse(statusCode, error, errorId, message, details, messageFunc), Options)).ConfigureAwait(ConfigureAwaitOptions.None); + } + } + + private static (HttpStatusCode StatusCode, Func)>? MessageFunc, LogLevel LogLevel) GetErrorInformation(Exception error) => + ErrorTypes.TryGetValue(error.GetType(), out var mapping) + ? mapping + : (HttpStatusCode.InternalServerError, null, LogLevel.Error); + + private ErrorResponse CreateErrorResponse(HttpStatusCode statusCode, Exception error, string errorId, string message, IEnumerable? details, Func)>? getSourceAndMessages) + { + var meta = Metadata.GetValueOrDefault(statusCode, Metadata[HttpStatusCode.InternalServerError]); + var (source, messages) = getSourceAndMessages?.Invoke(error) ?? (error.Source, Enumerable.Repeat(message, 1)); + + var messageMap = new Dictionary> { { source ?? "unknown", messages } }; + while (error.InnerException != null) + { + error = error.InnerException; + source = error.Source ?? "inner"; + + messageMap[source] = messageMap.TryGetValue(source, out messages) + ? messages.Append(GetErrorMessage(error)) + : Enumerable.Repeat(GetErrorMessage(error), 1); + } + + return new ErrorResponse( + meta.Url, + meta.Description, + (int)statusCode, + messageMap, + errorId, + details + ); + } + + private string GetErrorMessage(Exception exception) => + exception is DetailException { HasDetails: true } detail + ? detail.GetErrorMessage(errorMessageService) + : exception.Message; + + private IEnumerable GetErrorDetails(Exception exception) => + exception is DetailException { HasDetails: true } detail + ? detail.GetErrorDetails(errorMessageService) + : Enumerable.Empty(); + + private static void LogErrorInformation(string errorId, Exception exception) + { + LogContext.PushProperty("ErrorId", errorId); + LogContext.PushProperty("StackTrace", exception.StackTrace); + } + + private sealed record MetaData(string Url, string Description); +} diff --git a/src/hub/PolicyHub.Service/PolicyHub.Service.csproj b/src/hub/PolicyHub.Service/PolicyHub.Service.csproj index 79c1951..d2fc6a6 100644 --- a/src/hub/PolicyHub.Service/PolicyHub.Service.csproj +++ b/src/hub/PolicyHub.Service/PolicyHub.Service.csproj @@ -42,7 +42,8 @@ - + + diff --git a/src/hub/PolicyHub.Service/Program.cs b/src/hub/PolicyHub.Service/Program.cs index 9cfee94..6c7b4ec 100644 --- a/src/hub/PolicyHub.Service/Program.cs +++ b/src/hub/PolicyHub.Service/Program.cs @@ -21,6 +21,8 @@ using Org.Eclipse.TractusX.PolicyHub.DbAccess.DependencyInjection; using Org.Eclipse.TractusX.PolicyHub.Service.Authentication; using Org.Eclipse.TractusX.PolicyHub.Service.Controllers; +using Org.Eclipse.TractusX.PolicyHub.Service.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Service; using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Extensions; using Org.Eclipse.TractusX.Portal.Backend.Framework.Web; using System.Text.Json.Serialization; @@ -31,20 +33,24 @@ await WebApplicationBuildRunner .BuildAndRunWebApplicationAsync(args, "policy-hub", version, ".Hub", builder => { - builder.Services.AddTransient(); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddHubRepositories(builder.Configuration); - builder.Services.ConfigureHttpJsonOptions(options => - { - options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); - builder.Services.Configure(options => - { - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); + builder.Services + .AddSingleton() + .AddSingleton() + .AddTransient() + .AddEndpointsApiExplorer() + .AddHubRepositories(builder.Configuration) + .ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }) + .Configure(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); }, (app, _) => { + app.UseMiddleware(); app.MapGroup("/api") .WithOpenApi() .MapPolicyHubApi(); diff --git a/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs b/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs index 2d04570..47f2527 100644 --- a/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs +++ b/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs @@ -22,6 +22,7 @@ using Org.Eclipse.TractusX.PolicyHub.DbAccess.Repositories; using Org.Eclipse.TractusX.PolicyHub.Entities.Enums; using Org.Eclipse.TractusX.PolicyHub.Service.BusinessLogic; +using Org.Eclipse.TractusX.PolicyHub.Service.ErrorHandling; using Org.Eclipse.TractusX.PolicyHub.Service.Models; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; @@ -111,7 +112,7 @@ public async Task GetPolicyContentWithFiltersAsync_WithNotExistingInDatabase_Thr var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be($"Policy for type {policyTypeId} and technicalKey membership does not exists"); + ex.Message.Should().Be(PolicyErrors.POLICY_NOT_EXIST.ToString()); } [Fact] @@ -127,7 +128,7 @@ public async Task GetPolicyContentWithFiltersAsync_AttributeAndRightOperandNull_ var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("There must be one configured rightOperand value"); + ex.Message.Should().Be(PolicyErrors.NO_RIGHT_OPERAND_CONFIGURED.ToString()); } [Fact] @@ -158,7 +159,7 @@ public async Task GetPolicyContentWithFiltersAsync_WithMultipleRegexValues_Throw var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("There should only be one regex pattern defined"); + ex.Message.Should().Be(PolicyErrors.MULTIPLE_REGEX_DEFINED.ToString()); } [Fact] @@ -202,7 +203,7 @@ public async Task GetPolicyContentWithFiltersAsync_WithInvalidValue_ExceptionExp var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("Invalid values [test] set for key multipleAdditionalValues. Possible values [value1,value2,value3]"); + ex.Message.Should().Be(PolicyErrors.INVALID_VALUES.ToString()); } #endregion @@ -232,7 +233,7 @@ public async Task GetPolicyContentAsync_WithUnmatchingPoliciesAndConstraints_Thr var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be($"Policy for type {data.PolicyType} and technicalKeys abc does not exists"); + ex.Message.Should().Be(PolicyErrors.POLICY_NOT_EXISTS_FOR_TECHNICAL_KEYS.ToString()); } [Fact] @@ -255,7 +256,7 @@ public async Task GetPolicyContentAsync_WithUnmatchingTechnicalKeys_ThrowsContro var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be($"Policy for type {data.PolicyType} and requested technicalKeys does not exists. TechnicalKeys test are allowed"); + ex.Message.Should().Be(PolicyErrors.POLICY_NOT_EXISTS_FOR_TECHNICAL_KEYS.ToString()); } [Fact] @@ -281,7 +282,7 @@ public async Task GetPolicyContentAsync_WithUnmatchingAttributeValues_ThrowsCont var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("Invalid values set for Key: test, requested value[abc] Possible Values[test]"); + ex.Message.Should().Be(PolicyErrors.INVALID_VALUES_SET.ToString()); } [Fact] @@ -303,7 +304,7 @@ public async Task GetPolicyContentAsync_WithAttributeAndRightOperandNull_ThrowsU var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("There must be one configured rightOperand value"); + ex.Message.Should().Be(PolicyErrors.RIGHT_OPERAND_NOT_CONFIGURED.ToString()); } [Fact] @@ -325,8 +326,7 @@ public async Task GetPolicyContentAsync_WithRegexWithoutValue_ThrowsControllerAr var ex = await Assert.ThrowsAsync(Act); // Assert - ex.ParamName.Should().Be("value"); - ex.Message.Should().Be("you must provide a value for the regex (Parameter 'value')"); + ex.Message.Should().Be(PolicyErrors.NO_VALUE_FOR_REGEX.ToString()); } [Fact] @@ -348,8 +348,7 @@ public async Task GetPolicyContentAsync_WithRegexWithoutMatchingRegexPattern_Thr var ex = await Assert.ThrowsAsync(Act); // Assert - ex.ParamName.Should().Be("value"); - ex.Message.Should().Be(@"The provided value testRegValue does not match the regex pattern ^BPNL[\w|\d]{12}$ (Parameter 'value')"); + ex.Message.Should().Be(PolicyErrors.VALUE_DOES_NOT_MATCH_REGEX.ToString()); } [Fact] @@ -370,7 +369,7 @@ public async Task GetPolicyContentAsync_WithUsageConstraintNotAllowedWithOR_Thro var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("The support of OR constraintOperand for Usage constraints are not supported for now"); + ex.Message.Should().Be(PolicyErrors.OR_WITH_USAGE.ToString()); } [Fact] @@ -391,7 +390,7 @@ public async Task GetPolicyContentAsync_WithBPNLAllowingANDOperandWithSingleBPNL var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("Only a single value BPNL is allowed with an AND constraint"); + ex.Message.Should().Be(PolicyErrors.SINGLE_VALUE_BPNL_CONSTRAINT.ToString()); } [Fact] @@ -412,7 +411,7 @@ public async Task GetPolicyContentAsync_WithBPNLOperatorShouldEquals_ThrowsContr var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("The operator for BPNLs should always be Equals"); + ex.Message.Should().Be(PolicyErrors.BPNL_WRONG_OPERATOR.ToString()); } [Fact] @@ -433,7 +432,7 @@ public async Task GetPolicyContentAsync_WithUsagePolicyOnlySingleBPNLAllowed_Thr var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("For usage policies only a single BPNL is allowed"); + ex.Message.Should().Be(PolicyErrors.USAGE_MULTIPLE_BPNL.ToString()); } [Fact] @@ -459,7 +458,7 @@ public async Task GetPolicyContentAsync_WithMultipleDefinedKeys_ThrowsNotFoundEx var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be("Keys test have been defined multiple times"); + ex.Message.Should().Be(PolicyErrors.KEY_DEFINED_MULTIPLE_TIMES.ToString()); } [Fact] diff --git a/tests/hub/PolicyHub.Service.Tests/Controllers/PolicyHubControllerTests.cs b/tests/hub/PolicyHub.Service.Tests/Controllers/PolicyHubControllerTests.cs index 97485ee..2f12338 100644 --- a/tests/hub/PolicyHub.Service.Tests/Controllers/PolicyHubControllerTests.cs +++ b/tests/hub/PolicyHub.Service.Tests/Controllers/PolicyHubControllerTests.cs @@ -131,8 +131,8 @@ public async Task GetPolicyContent_WithRegexWithIncorrectValue_ReturnsExpected() response.Should().NotBeNull(); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var error = await response.Content.ReadFromJsonAsync(JsonOptions); - error!.Errors.Should().ContainSingle().And.Satisfy( - x => x.Value.Single() == @"The provided value notmatching does not match the regex pattern ^BPNL[\w|\d]{12}$ (Parameter 'value')"); + error!.Details.Should().ContainSingle().And.Satisfy( + x => x.Message == "The provided value {value} does not match the regex pattern {values}" && x.Parameters.Count() == 2); } [Fact] @@ -145,8 +145,8 @@ public async Task GetPolicyContent_WithRegexWithoutValue_ReturnsExpected() response.Should().NotBeNull(); response.StatusCode.Should().Be(HttpStatusCode.BadRequest); var error = await response.Content.ReadFromJsonAsync(JsonOptions); - error!.Errors.Should().ContainSingle().And.Satisfy( - x => x.Value.Single() == "you must provide a value for the regex (Parameter 'value')"); + error!.Details.Should().ContainSingle().And.Satisfy( + x => x.Message == "you must provide a value for the regex"); } [Fact]