diff --git a/docs/api/hub-service.yaml b/docs/api/hub-service.yaml index 6abdc0ba..ea2fd62e 100644 --- a/docs/api/hub-service.yaml +++ b/docs/api/hub-service.yaml @@ -39,12 +39,12 @@ paths: in: query description: 'OPTIONAL: Type to filter the response' schema: - $ref: '#/components/schemas/PolicyTypeId' + type: string - name: useCase in: query description: 'OPTIONAL: UseCase to filter the response' schema: - $ref: '#/components/schemas/UseCaseId' + type: string responses: '200': description: OK @@ -75,25 +75,22 @@ paths: in: query description: 'OPTIONAL: The use case' schema: - $ref: '#/components/schemas/UseCaseId' + type: string - name: type in: query description: 'Policy type for which the policy is supposed to get created. Possible values: ''Access'' or ''Usage''' - required: true schema: - $ref: '#/components/schemas/PolicyTypeId' + type: string - name: policyName in: query description: The technical key of the policy - required: true schema: type: string - name: operatorType in: query description: 'Policy Rule operator. Possible values: ''Equals'' or ''In''' - required: true schema: - $ref: '#/components/schemas/OperatorId' + type: string - name: value in: query description: 'OPTIONAL: Value to be used for the rightOperand' diff --git a/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs b/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs index 9c3d5001..70cc0027 100644 --- a/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs +++ b/src/hub/PolicyHub.Service/BusinessLogic/PolicyHubBusinessLogic.cs @@ -65,28 +65,27 @@ 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 ControllerArgumentException.Create(PolicyErrors.INVALID_VALUES, new ErrorParameter[] { new("value", value), new("leftOperand", leftOperand), new("possibleValues", string.Join(",", rightOperands)) }); + throw new ControllerArgumentException($"Invalid values [{value}] set for key {leftOperand}. Possible values [{string.Join(",", rightOperands)}]"); } - rightOperands = rightOperands.Where(r => r.Equals(value)); } - - var useCaseValue = useCase != null ? - useCase.Value.ToString().Insert(0, ".") : - string.Empty; - var rightOperand = $"@{leftOperand}{useCaseValue}-{attributes.Key}"; return rightOperands.Count() > 1 ? - (rightOperand, new AdditionalAttributes(rightOperand, rightOperands)) : + ($"@{leftOperand}{(useCase != null ? + useCase.Value.ToString().Insert(0, ".") : + string.Empty)}-{attributes.Key}", + new AdditionalAttributes($"@{leftOperand}{(useCase != null ? + useCase.Value.ToString().Insert(0, ".") : + string.Empty)}-{attributes.Key}", rightOperands)) : (rightOperands.Single(), null); } @@ -180,7 +179,7 @@ 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 ControllerArgumentException.Create(PolicyErrors.INVALID_VALUES_SET, new ErrorParameter[] { new("values", string.Join(',', x)) }); + throw new ControllerArgumentException($"Invalid values set for {string.Join(',', x)}"); } var policies = await hubRepositories.GetInstance().GetPolicyForOperandContent(requestData.PolicyType, technicalKeys).ToListAsync().ConfigureAwait(false); diff --git a/src/hub/PolicyHub.Service/Controllers/PolicyHubController.cs b/src/hub/PolicyHub.Service/Controllers/PolicyHubController.cs index 78a831c4..f59a27dd 100644 --- a/src/hub/PolicyHub.Service/Controllers/PolicyHubController.cs +++ b/src/hub/PolicyHub.Service/Controllers/PolicyHubController.cs @@ -22,6 +22,7 @@ using Org.Eclipse.TractusX.PolicyHub.Entities.Enums; using Org.Eclipse.TractusX.PolicyHub.Service.BusinessLogic; using Org.Eclipse.TractusX.PolicyHub.Service.Extensions; +using Org.Eclipse.TractusX.PolicyHub.Service.Filters; using Org.Eclipse.TractusX.PolicyHub.Service.Models; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web; @@ -46,7 +47,9 @@ public static RouteGroupBuilder MapPolicyHubApi(this RouteGroupBuilder group) .WithDefaultResponses() .Produces(StatusCodes.Status200OK, typeof(string), Constants.JsonContentType); - policyHub.MapGet("policy-types", (PolicyTypeId? type, UseCaseId? useCase, IPolicyHubBusinessLogic logic) => logic.GetPolicyTypes(type, useCase)) + policyHub.MapGet("policy-types", (string? type, string? useCase, IPolicyHubBusinessLogic logic) => + logic.GetPolicyTypes(ParamEnumHelper.ParseEnum(type), ParamEnumHelper.ParseEnum(useCase))) + .AddEndpointFilter() .WithSwaggerDescription("Provides all current supported policy types incl. a policy description and useCase link.", "Example: GET: api/policy-hub/policy-types", "OPTIONAL: Type to filter the response", @@ -56,12 +59,21 @@ public static RouteGroupBuilder MapPolicyHubApi(this RouteGroupBuilder group) .Produces(StatusCodes.Status200OK, typeof(PolicyTypeResponse), Constants.JsonContentType); policyHub.MapGet("policy-content", - (UseCaseId? useCase, - PolicyTypeId type, - string policyName, - OperatorId operatorType, + (string? useCase, + String? type, + string? policyName, + String? operatorType, string? value, - IPolicyHubBusinessLogic logic) => logic.GetPolicyContentWithFiltersAsync(useCase, type, policyName, operatorType, value)) + IPolicyHubBusinessLogic logic) => + { + if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(policyName) || string.IsNullOrEmpty(operatorType)) + { + throw new ArgumentNullException(nameof(type), "Type parameter cannot be null or empty."); + } + + return logic.GetPolicyContentWithFiltersAsync(ParamEnumHelper.ParseEnum(useCase), Enum.Parse(type), policyName, Enum.Parse(operatorType), value); + }) + .AddEndpointFilter() .WithSwaggerDescription("Receive the policy template 'access' or 'usage' for a single policy rule based on the request parameters submitted by the user.", "Example: GET: api/policy-hub/policy-content", "OPTIONAL: The use case", diff --git a/src/hub/PolicyHub.Service/Filters/BaseQueryParametersFilter.cs b/src/hub/PolicyHub.Service/Filters/BaseQueryParametersFilter.cs new file mode 100644 index 00000000..bbf69b68 --- /dev/null +++ b/src/hub/PolicyHub.Service/Filters/BaseQueryParametersFilter.cs @@ -0,0 +1,93 @@ +/******************************************************************************** + * 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; + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Filters; + +public abstract class BaseQueryParametersFilter : IEndpointFilter +{ + protected readonly Dictionary _queryParameters; + + protected BaseQueryParametersFilter(Dictionary queryParameters) + { + _queryParameters = queryParameters; + } + + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + var queryParams = context.HttpContext.Request.Query; + + // Check for required parameters + EnsureRequiredParameters(queryParams); + + // Validate supported query parameters + ValidateSupportedQueryParameters(queryParams); + + // Validate enum parameter values + ValidateEnumParameters(queryParams); + + // Continue with the next filter or action + return await next(context); + } + + private void EnsureRequiredParameters(IQueryCollection queryParams) + { + var missingRequiredParameters = _queryParameters + .Where(q => q.Value.IsRequired && GetArgument(queryParams, q.Key) == null) + .Select(q => q.Key) + .ToList(); + + if (missingRequiredParameters.Count != 0) + { + throw new NotFoundException($"Missing required parameters: {string.Join(", ", missingRequiredParameters)}."); + } + } + + private void ValidateSupportedQueryParameters(IQueryCollection queryParams) + { + var invalidParameters = queryParams + .Where(q => !_queryParameters.ContainsKey(q.Key)) + .Select(q => $"{q.Key}") + .ToList(); + + if (invalidParameters.Count != 0) + { + throw new ControllerArgumentException($"Invalid query parameters: {string.Join(", ", invalidParameters)}. Supported parameters are: {string.Join(", ", _queryParameters.Keys)}."); + } + } + + private void ValidateEnumParameters(IQueryCollection queryParams) + { + foreach (var param in _queryParameters) + { + var paramValue = GetArgument(queryParams, param.Key); + if (paramValue != null && param.Value.EnumType != null) + { + ParamEnumValidator.ThrowIfInvalidEnumValue(param.Value.EnumType, paramValue, param.Key); + } + } + } + + protected static string? GetArgument(IQueryCollection queryParams, string argumentName) + { + return queryParams.TryGetValue(argumentName, out var value) ? value.ToString() : null; + } +} + diff --git a/src/hub/PolicyHub.Service/Filters/ParamEnumHelper.cs b/src/hub/PolicyHub.Service/Filters/ParamEnumHelper.cs new file mode 100644 index 00000000..8000f437 --- /dev/null +++ b/src/hub/PolicyHub.Service/Filters/ParamEnumHelper.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Filters; + +public static class ParamEnumHelper +{ + public static TEnum? ParseEnum(string? value) where TEnum : struct, Enum + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + return Enum.Parse(value.Trim(), true); + } +} diff --git a/src/hub/PolicyHub.Service/Filters/ParamEnumValidator.cs b/src/hub/PolicyHub.Service/Filters/ParamEnumValidator.cs new file mode 100644 index 00000000..aff80201 --- /dev/null +++ b/src/hub/PolicyHub.Service/Filters/ParamEnumValidator.cs @@ -0,0 +1,34 @@ +/******************************************************************************** + * 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; + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Filters; + +public static class ParamEnumValidator +{ + public static void ThrowIfInvalidEnumValue(Type enumType, string value, string paramName) + { + if (!Enum.IsDefined(enumType, value)) + { + var acceptedValues = string.Join(", ", Enum.GetNames(enumType)); + throw new ControllerArgumentException($"Invalid value '{value}' for parameter '{paramName}'. Accepted values are: {acceptedValues}."); + } + } +} diff --git a/src/hub/PolicyHub.Service/Filters/PolicyContentQueryParametersFilter.cs b/src/hub/PolicyHub.Service/Filters/PolicyContentQueryParametersFilter.cs new file mode 100644 index 00000000..8bb073b6 --- /dev/null +++ b/src/hub/PolicyHub.Service/Filters/PolicyContentQueryParametersFilter.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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.PolicyHub.Entities.Enums; + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Filters; + +public class PolicyContentQueryParametersFilter : BaseQueryParametersFilter +{ + public PolicyContentQueryParametersFilter() : base(new Dictionary + { + { "useCase", new QueryParameterType { IsRequired = false, EnumType = typeof(UseCaseId) } }, + { "type", new QueryParameterType { IsRequired = true, EnumType = typeof(PolicyTypeId) } }, + { "policyName", new QueryParameterType { IsRequired = true } }, + { "operatorType", new QueryParameterType { IsRequired = true, EnumType = typeof(OperatorId) } }, + { "value", new QueryParameterType { IsRequired = false } } + }) + { + } +} diff --git a/src/hub/PolicyHub.Service/Filters/PolicyTypesQueryParametersFilter.cs b/src/hub/PolicyHub.Service/Filters/PolicyTypesQueryParametersFilter.cs new file mode 100644 index 00000000..1713bf2f --- /dev/null +++ b/src/hub/PolicyHub.Service/Filters/PolicyTypesQueryParametersFilter.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * 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.PolicyHub.Entities.Enums; + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Filters; + +public class PolicyTypesQueryParametersFilter : BaseQueryParametersFilter +{ + public PolicyTypesQueryParametersFilter() : base(new Dictionary + { + { "useCase", new QueryParameterType { IsRequired = false, EnumType = typeof(UseCaseId) } }, + { "type", new QueryParameterType { IsRequired = false, EnumType = typeof(PolicyTypeId) } } + }) + { + } +} diff --git a/src/hub/PolicyHub.Service/Filters/QueryParameterType.cs b/src/hub/PolicyHub.Service/Filters/QueryParameterType.cs new file mode 100644 index 00000000..ce61cd9c --- /dev/null +++ b/src/hub/PolicyHub.Service/Filters/QueryParameterType.cs @@ -0,0 +1,48 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Filters; + +public class QueryParameterType +{ + public bool IsRequired { get; set; } + public Type? EnumType { get; set; } + private object? _enumValue; + + public object? EnumValue + { + get => _enumValue; + set + { + if (value is Enum) + { + _enumValue = value; + } + else + { + throw new ArgumentException("Value must be an enum type."); + } + } + } + + public void SetEnumValue(TEnum value) where TEnum : struct, Enum + { + EnumValue = value; + } +} diff --git a/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs b/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs index 47f25270..9be4d76b 100644 --- a/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs +++ b/tests/hub/PolicyHub.Service.Tests/BusinessLogic/PolicyHubBusinessLogicTests.cs @@ -194,7 +194,7 @@ public async Task GetPolicyContentWithFiltersAsync_WithInvalidValue_ExceptionExp { // Arrange A.CallTo(() => _policyRepository.GetPolicyContentAsync(UseCaseId.Traceability, PolicyTypeId.Usage, "multipleAdditionalValues")) - .Returns(new ValueTuple>, string?>(true, "multipleAdditionalValues", new ValueTuple>(AttributeKeyId.Static, new[] { "value1", "value2", "value3" }), null)); + .Returns(new ValueTuple>, string?>(true, "multipleAdditionalValues", new ValueTuple>(AttributeKeyId.Static, ["value1", "value2", "value3"]), null)); // Act async Task Act() => await _sut.GetPolicyContentWithFiltersAsync(UseCaseId.Traceability, PolicyTypeId.Usage, "multipleAdditionalValues", OperatorId.Equals, "test"); @@ -203,7 +203,7 @@ public async Task GetPolicyContentWithFiltersAsync_WithInvalidValue_ExceptionExp var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be(PolicyErrors.INVALID_VALUES.ToString()); + ex.Message.Should().Be("Invalid values [test] set for key multipleAdditionalValues. Possible values [value1,value2,value3]"); } #endregion @@ -263,26 +263,34 @@ public async Task GetPolicyContentAsync_WithUnmatchingTechnicalKeys_ThrowsContro public async Task GetPolicyContentAsync_WithUnmatchingAttributeValues_ThrowsControllerArgumentException() { // Arrange + List<(string TechnicalKey, AttributeKeyId? AttributeKey, IEnumerable Values)> possibleValues = [ + ("test", AttributeKeyId.Version, ["test"]), + ("abc", AttributeKeyId.Version, []) + ]; + var data = new PolicyContentRequest(PolicyTypeId.Access, ConstraintOperandId.Or, new[] { new Constraints("test", OperatorId.In, "abc"), new Constraints("abc", OperatorId.Equals, null) }); + + var technicalKey = "test"; + var attributeId = "1234"; + + var policyForOperand = new ValueTuple>, string?>(technicalKey, attributeId, default, null); + A.CallTo(() => _policyRepository.GetAttributeValuesForTechnicalKeys(data.PolicyType, A>._)) - .Returns([ - ("test", AttributeKeyId.Version, ["test"]), - ("abc", AttributeKeyId.Version, []) - ]); + .Returns(possibleValues); A.CallTo(() => _policyRepository.GetPolicyForOperandContent(data.PolicyType, A>._)) - .Returns(Enumerable.Repeat(new ValueTuple>, string?>("test", "active", default, null), 1).ToAsyncEnumerable()); + .Returns(Enumerable.Repeat(policyForOperand, 1).ToAsyncEnumerable()); async Task Act() => await _sut.GetPolicyContentAsync(data); // Act var ex = await Assert.ThrowsAsync(Act); // Assert - ex.Message.Should().Be(PolicyErrors.INVALID_VALUES_SET.ToString()); + ex.Message.Should().Be("Invalid values set for Key: test, requested value[abc] Possible Values[test]"); } [Fact] diff --git a/tests/hub/PolicyHub.Service.Tests/Filters/PolicyContentQueryParametersFilterTests.cs b/tests/hub/PolicyHub.Service.Tests/Filters/PolicyContentQueryParametersFilterTests.cs new file mode 100644 index 00000000..f584ab79 --- /dev/null +++ b/tests/hub/PolicyHub.Service.Tests/Filters/PolicyContentQueryParametersFilterTests.cs @@ -0,0 +1,171 @@ +/******************************************************************************** + * Copyright (c) 2023 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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Tests.Filters; + +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Org.Eclipse.TractusX.PolicyHub.Service.Filters; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +public class PolicyContentQueryParametersFilterTests +{ + [Fact] + public async Task InvokeAsync_ValidParameters_ContinuesToNext() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "PCF" }, + { "type", "Usage" }, + { "policyName", "ValidPolicyName" }, + { "operatorType", "Equals" }, + { "value", "ValidValue" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + A.CallTo(() => next(A.Ignored)).Returns(ValueTask.FromResult(null).AsTask()); + + var filter = new PolicyContentQueryParametersFilter(); + + // Act + await filter.InvokeAsync(endpointContext, next); + + // Assert + A.CallTo(() => next(A.Ignored)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task InvokeAsync_Missing_And_InvalidQueryParameter_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "invalidParam", "InvalidValue" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyContentQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Equal("Missing required parameters: type, policyName, operatorType.", exception.Message); + } + + [Fact] + public async Task InvokeAsync_InvalidQueryParameters_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "PCF" }, + { "type", "Usage" }, + { "policyName", "ValidPolicyName" }, + { "operatorType", "Equals" }, + { "value", "ValidValue" }, + { "invalidParam1", "InvalidValue1" }, + { "invalidParam2", "InvalidValue2" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyContentQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Equal("Invalid query parameters: invalidParam1, invalidParam2. Supported parameters are: useCase, type, policyName, operatorType, value.", exception.Message); + } + + [Fact] + public async Task InvokeAsync_InvalidEnumParameter_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "InvalidUseCase" }, + { "type", "Usage" }, + { "policyName", "ValidPolicyName" }, + { "operatorType", "Equals" }, + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyContentQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Equal("Invalid value 'InvalidUseCase' for parameter 'useCase'. Accepted values are: Traceability, Quality, PCF, Behavioraltwin, Businesspartner, CircularEconomy, DemandCapacity.", exception.Message); + } + + [Fact] + public async Task InvokeAsync_MissingRequiredParameter_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "PCF" }, + { "policyName", "ValidPolicyName" }, + { "operatorType", "Equals" }, + { "value", "ValidValue" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyContentQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Contains("Missing required parameters: type.", exception.Message); + } +} diff --git a/tests/hub/PolicyHub.Service.Tests/Filters/PolicyTypesQueryParametersFilterTests.cs b/tests/hub/PolicyHub.Service.Tests/Filters/PolicyTypesQueryParametersFilterTests.cs new file mode 100644 index 00000000..021eeca8 --- /dev/null +++ b/tests/hub/PolicyHub.Service.Tests/Filters/PolicyTypesQueryParametersFilterTests.cs @@ -0,0 +1,162 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.PolicyHub.Service.Tests.Filters; + +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Org.Eclipse.TractusX.PolicyHub.Service.Filters; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +public class PolicyTypesQueryParametersFilterTests +{ + [Fact] + public async Task InvokeAsync_ValidParameters_ContinuesToNext() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "PCF" }, + { "type", "Usage" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + A.CallTo(() => next(A.Ignored)).Returns(ValueTask.FromResult(null).AsTask()); + + var filter = new PolicyTypesQueryParametersFilter(); + + // Act + await filter.InvokeAsync(endpointContext, next); + + // Assert + A.CallTo(() => next(A.Ignored)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task InvokeAsync_NoParametersProvided_ContinuesToNext() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + A.CallTo(() => next(A.Ignored)).Returns(ValueTask.FromResult(null).AsTask()); + + var filter = new PolicyTypesQueryParametersFilter(); + + // Act + await filter.InvokeAsync(endpointContext, next); + + // Assert + A.CallTo(() => next(A.Ignored)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task InvokeAsync_Missing_And_InvalidQueryParameter_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "invalidParam", "InvalidValue" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyTypesQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Equal("Invalid query parameters: invalidParam. Supported parameters are: useCase, type.", exception.Message); + } + + [Fact] + public async Task InvokeAsync_InvalidQueryParameters_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "PCF" }, + { "type", "Usage" }, + { "invalidParam1", "InvalidValue1" }, + { "invalidParam2", "InvalidValue2" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyTypesQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Equal("Invalid query parameters: invalidParam1, invalidParam2. Supported parameters are: useCase, type.", exception.Message); + } + + [Fact] + public async Task InvokeAsync_InvalidEnumParameter_ThrowsException() + { + // Arrange + var queryParams = new QueryCollection(new Dictionary + { + { "useCase", "InvalidUseCase" }, + { "type", "Usage" } + }); + + var context = new DefaultHttpContext(); + context.Request.Query = queryParams; + + var endpointContext = A.Fake(); + A.CallTo(() => endpointContext.HttpContext).Returns(context); + + var next = A.Fake(); + + var filter = new PolicyTypesQueryParametersFilter(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => filter.InvokeAsync(endpointContext, next).AsTask()); + Assert.Equal("Invalid value 'InvalidUseCase' for parameter 'useCase'. Accepted values are: Traceability, Quality, PCF, Behavioraltwin, Businesspartner, CircularEconomy, DemandCapacity.", exception.Message); + } +}