diff --git a/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs b/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs index 4acd68d117..edfa84e2b3 100644 --- a/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs +++ b/Application/EdFi.Ods.Api/Container/Modules/ApplicationModule.cs @@ -47,6 +47,7 @@ using EdFi.Ods.Common.Models.Resource; using EdFi.Ods.Common.ProblemDetails; using EdFi.Ods.Common.Providers; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Common.Security; using EdFi.Ods.Common.Specifications; using EdFi.Ods.Common.Validation; @@ -348,6 +349,10 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType() .As() .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); builder.RegisterType() .Named(InterceptorCacheKeys.OdsInstances) diff --git a/Application/EdFi.Ods.Api/EdFi.Ods.Api.csproj b/Application/EdFi.Ods.Api/EdFi.Ods.Api.csproj index fbbfee15d7..f68698b973 100644 --- a/Application/EdFi.Ods.Api/EdFi.Ods.Api.csproj +++ b/Application/EdFi.Ods.Api/EdFi.Ods.Api.csproj @@ -28,7 +28,6 @@ - diff --git a/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderCteAuthorizationDecorator.cs b/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderCteAuthorizationDecorator.cs index 1e69519c73..44857b712a 100644 --- a/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderCteAuthorizationDecorator.cs +++ b/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderCteAuthorizationDecorator.cs @@ -205,6 +205,7 @@ bool TryApplyFilters( filterDefinition.CriteriaApplicator( queryBuilder, conjunctionQueryBuilder, + authorizationPlan.RequestContext.Resource, filterContext.SubjectEndpointNames, parameterValues, joinType, diff --git a/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderJoinAuthorizationDecorator.cs b/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderJoinAuthorizationDecorator.cs index 1c8048852a..62c00ae89c 100644 --- a/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderJoinAuthorizationDecorator.cs +++ b/Application/EdFi.Ods.Api/Security/Authorization/Repositories/AggregateRootQueryBuilderProviderJoinAuthorizationDecorator.cs @@ -224,6 +224,7 @@ bool TryApplyFilters( filterDefinition.CriteriaApplicator( queryBuilder, conjunctionQueryBuilder, + authorizationPlan.RequestContext.Resource, filterContext.SubjectEndpointNames, parameterValues, joinType, diff --git a/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/NamespaceBased/NamespaceBasedAuthorizationFilterDefinitionsFactory.cs b/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/NamespaceBased/NamespaceBasedAuthorizationFilterDefinitionsFactory.cs index a4ade76d5e..c7f54c9843 100644 --- a/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/NamespaceBased/NamespaceBasedAuthorizationFilterDefinitionsFactory.cs +++ b/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/NamespaceBased/NamespaceBasedAuthorizationFilterDefinitionsFactory.cs @@ -15,6 +15,7 @@ using EdFi.Ods.Common.Infrastructure.Filtering; using EdFi.Ods.Common.Models; using EdFi.Ods.Common.Models.Resource; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Common.Security.Authorization; using EdFi.Ods.Common.Security.Claims; @@ -65,6 +66,7 @@ public IReadOnlyList CreatePredefinedAuthorizatio private static void ApplyAuthorizationCriteria( QueryBuilder queryBuilder, QueryBuilder whereBuilder, + Resource resource, string[] subjectEndpointNames, IDictionary parameters, JoinType joinType, @@ -87,14 +89,16 @@ private static void ApplyAuthorizationCriteria( // Ensure the Namespace parameter is represented as an object array var namespacePrefixes = parameterValue as object[] ?? [parameterValue]; + var alias = resource.Entity?.RootTableAlias() ?? "r"; + // Add the final namespaces criteria to the supplied WHERE clause (junction) - whereBuilder.WhereNotNull(subjectEndpointName) + whereBuilder.WhereNotNull($"{alias}.{subjectEndpointName}") .Where( qb => { foreach (var namespacePrefix in namespacePrefixes) { - qb.OrWhereLike(subjectEndpointName, namespacePrefix, MatchMode.Start); + qb.OrWhereLike($"{alias}.{subjectEndpointName}", namespacePrefix, MatchMode.Start); } return qb; diff --git a/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/OwnershipBased/OwnershipBasedAuthorizationFilterDefinitionsFactory.cs b/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/OwnershipBased/OwnershipBasedAuthorizationFilterDefinitionsFactory.cs index 0664bc7234..ca24799a65 100644 --- a/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/OwnershipBased/OwnershipBasedAuthorizationFilterDefinitionsFactory.cs +++ b/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/OwnershipBased/OwnershipBasedAuthorizationFilterDefinitionsFactory.cs @@ -49,6 +49,7 @@ public IReadOnlyList CreatePredefinedAuthorizatio private static void ApplyAuthorizationCriteria( QueryBuilder queryBuilder, QueryBuilder whereQueryBuilder, + Resource _, string[] subjectEndpointNames, IDictionary parameters, JoinType joinType, @@ -67,7 +68,7 @@ private static void ApplyAuthorizationCriteria( private void ApplyTrackedChangesAuthorizationCriteria( AuthorizationFilterDefinition filterDefinition, AuthorizationFilterContext filterContext, - Resource resource, + Resource _, int filterIndex, QueryBuilder queryBuilder, bool useOuterJoins) diff --git a/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/Relationships/Filters/ViewBasedAuthorizationFilterDefinition.cs b/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/Relationships/Filters/ViewBasedAuthorizationFilterDefinition.cs index be3ae80f35..b60fe0c655 100644 --- a/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/Relationships/Filters/ViewBasedAuthorizationFilterDefinition.cs +++ b/Application/EdFi.Ods.Api/Security/AuthorizationStrategies/Relationships/Filters/ViewBasedAuthorizationFilterDefinition.cs @@ -44,7 +44,7 @@ public ViewBasedAuthorizationFilterDefinition( SELECT {{newAlias1}}.{viewTargetEndpointName} FROM {GetFullNameForView($"auth_{viewName}")} {{newAlias1}} WHERE {{newAlias1}}.{viewSourceEndpointName} IN (:{RelationshipAuthorizationConventions.ClaimsParameterName}))", - (queryBuilder, @where, subjectEndpointNames, parameters, joinType, authorizationStrategy) => + (queryBuilder, @where, resource, subjectEndpointNames, parameters, joinType, authorizationStrategy) => { if (subjectEndpointNames?.Length != 1) { @@ -89,7 +89,7 @@ public ViewBasedAuthorizationFilterDefinition( : base( filterName, null, // HQL condition (not supported by view-based authorization) - (criteria, @where, subjectEndpointNames, parameters, joinType, authorizationStrategy) + (criteria, @where, resource, subjectEndpointNames, parameters, joinType, authorizationStrategy) => criteria.ApplyCustomViewJoinFilter( viewName, subjectEndpointNames, diff --git a/Application/EdFi.Ods.Common/Infrastructure/Filtering/AuthorizationFilterDefinition.cs b/Application/EdFi.Ods.Common/Infrastructure/Filtering/AuthorizationFilterDefinition.cs index 8b410a47ac..3e35342d85 100644 --- a/Application/EdFi.Ods.Common/Infrastructure/Filtering/AuthorizationFilterDefinition.cs +++ b/Application/EdFi.Ods.Common/Infrastructure/Filtering/AuthorizationFilterDefinition.cs @@ -37,7 +37,7 @@ public class AuthorizationFilterDefinition public AuthorizationFilterDefinition( string filterName, string friendlyHqlConditionFormat, - Action, JoinType, IAuthorizationStrategy> criteriaApplicator, + Action, JoinType, IAuthorizationStrategy> criteriaApplicator, Action trackedChangesCriteriaApplicator, Func authorizeInstance) { @@ -58,7 +58,7 @@ public AuthorizationFilterDefinition( /// /// Gets the function for applying the filter using NHibernate's API. /// - public Action, JoinType, IAuthorizationStrategy> CriteriaApplicator { get; protected set; } + public Action, JoinType, IAuthorizationStrategy> CriteriaApplicator { get; protected set; } /// /// Gets the function for applying the filter to the for tracked changes queries. diff --git a/Application/EdFi.Ods.Common/Providers/Queries/AggregateRootCriteriaProviderHelpers.cs b/Application/EdFi.Ods.Common/Providers/Queries/AggregateRootCriteriaProviderHelpers.cs index 022f9555b1..08d22f11f3 100644 --- a/Application/EdFi.Ods.Common/Providers/Queries/AggregateRootCriteriaProviderHelpers.cs +++ b/Application/EdFi.Ods.Common/Providers/Queries/AggregateRootCriteriaProviderHelpers.cs @@ -3,7 +3,10 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using EdFi.Ods.Common.Models.Domain; using EdFi.Ods.Common.Specifications; namespace EdFi.Ods.Common.Providers.Criteria; diff --git a/Application/EdFi.Ods.Common/Providers/Queries/Criteria/IdentificationCodeAggregateRootQueryCriteriaApplicator.cs b/Application/EdFi.Ods.Common/Providers/Queries/Criteria/IdentificationCodeAggregateRootQueryCriteriaApplicator.cs new file mode 100644 index 0000000000..f77f7c4fc7 --- /dev/null +++ b/Application/EdFi.Ods.Common/Providers/Queries/Criteria/IdentificationCodeAggregateRootQueryCriteriaApplicator.cs @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using EdFi.Common.Configuration; +using EdFi.Common.Extensions; +using EdFi.Ods.Common.Database.Querying; +using EdFi.Ods.Common.Descriptors; +using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Models.Domain; +using EdFi.Ods.Common.Models.Resource; +using EdFi.Ods.Common.Providers.Criteria; + +namespace EdFi.Ods.Common.Providers.Queries.Criteria; + +/// +/// Applies criteria to a query based on identification codes property values +/// +public class IdentificationCodeAggregateRootQueryCriteriaApplicator : IAggregateRootQueryCriteriaApplicator +{ + private readonly IDescriptorResolver _descriptorResolver; + private readonly IResourceModelProvider _resourceModelProvider; + private readonly IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; + private readonly DatabaseEngine _databaseEngine; + + private const string IdentificationCodeTableAlias = "idct"; + private readonly ConcurrentDictionary _identificationCodeTableJoinByRootEntityName = new(); + + public IdentificationCodeAggregateRootQueryCriteriaApplicator( + IDescriptorResolver descriptorResolver, + IResourceModelProvider resourceModelProvider, + IResourceIdentificationCodePropertiesProvider resourceIdentificationCodePropertiesProvider, + DatabaseEngine databaseEngine) + { + _descriptorResolver = descriptorResolver; + _resourceIdentificationCodePropertiesProvider = resourceIdentificationCodePropertiesProvider; + _resourceModelProvider = resourceModelProvider; + _databaseEngine = databaseEngine; + } + + public void ApplyAdditionalParameters(QueryBuilder queryBuilder, Entity entity, AggregateRootWithCompositeKey specification, + IDictionary additionalParameters) + { + if (additionalParameters == null + || !additionalParameters.Any() + || additionalParameters.All( + ap => AggregateRootCriteriaProviderHelpers.PropertiesToIgnore.Contains(ap.Key, StringComparer.OrdinalIgnoreCase))) + { + return; + } + + var resource = _resourceModelProvider.GetResourceModel().GetResourceByFullName(entity.FullName); + + // If the entity does not have an identificationCodes collection with queryable properties, return + if (!_resourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resource, out List identificationCodeProperties)) + { + return; + } + + // Find any supplied additionalParameters with a non-default value and name matching that of a queryable identificationCode property, if none then return + var applicableAdditionalParameters = additionalParameters + .Where( + x => !x.Value.IsDefaultValue() + && identificationCodeProperties.Any(y => y.PropertyName.Equals(x.Key, StringComparison.OrdinalIgnoreCase))) + .ToArray(); + + if (applicableAdditionalParameters.Length == 0) + { + return; + } + + var identificationCodeTableJoin = + GetIdentificationCodeEntityTableJoin(entity, identificationCodeProperties.First().EntityProperty.Entity); + + queryBuilder.Join(identificationCodeTableJoin.TableName, _ => identificationCodeTableJoin); + + ApplyParameterValuesToQueryBuilder( + queryBuilder, applicableAdditionalParameters, identificationCodeProperties); + } + + private void ApplyParameterValuesToQueryBuilder(QueryBuilder queryBuilder, + IEnumerable> parameterValuesByName, + List identificationCodeProperties) + { + foreach (KeyValuePair parameterKeyValuePair in parameterValuesByName) + { + var identificationCodeProperty = + identificationCodeProperties.FirstOrDefault( + p => p.PropertyName.Equals(parameterKeyValuePair.Key, StringComparison.OrdinalIgnoreCase)); + + if (identificationCodeProperty == null) + { + continue; + } + + var queryParameterValue = GetQueryParameterValueForProperty(identificationCodeProperty, parameterKeyValuePair.Value); + + if (identificationCodeProperty.IsDescriptorUsage && queryParameterValue == null) + { + // Descriptor did not match any value -- criteria should exclude all entries + // Since no additional criteria need to be applied, exit the loop + queryBuilder.WhereRaw("1 = 0"); + break; + } + + queryBuilder.Where( + $"{IdentificationCodeTableAlias}.{identificationCodeProperty.EntityProperty}", queryParameterValue); + } + + return; + + object GetQueryParameterValueForProperty(ResourceProperty property, string parameterValue) + { + if (!property.IsDescriptorUsage) + { + return parameterValue; + } + + var lookupId = _descriptorResolver.GetDescriptorId(property.DescriptorName, parameterValue); + + return lookupId == 0 + ? null + : lookupId; + } + } + + private Join GetIdentificationCodeEntityTableJoin(Entity rootEntity, Entity identificationCodeEntity) + { + return _identificationCodeTableJoinByRootEntityName.GetOrAdd( + rootEntity.FullName, _ => + { + string alias = rootEntity.RootTableAlias(); + + var join = new Join( + $"{identificationCodeEntity.Schema}.{identificationCodeEntity.TableName(_databaseEngine)}" + .Alias(IdentificationCodeTableAlias)); + + foreach (var entityIdentificationCodePropertyColumnName in GetIdentificationCodeEntityTableJoinColumnNames( + identificationCodeEntity)) + { + join.On( + $"{alias}.{entityIdentificationCodePropertyColumnName}", + $"{IdentificationCodeTableAlias}.{entityIdentificationCodePropertyColumnName}"); + } + + return join; + }); + + IEnumerable GetIdentificationCodeEntityTableJoinColumnNames(Entity identificationCodeEntity) + { + return identificationCodeEntity.Identifier.Properties + .Where(p => p.IsFromParent && p.IsIdentifying) + .Select(p => p.ColumnName(_databaseEngine, p.PropertyName)).ToArray(); + } + } + + public static string IdentificationCodeEntityTableAlias() => IdentificationCodeTableAlias; +} diff --git a/Application/EdFi.Ods.Common/Providers/Queries/EntityExtensions.cs b/Application/EdFi.Ods.Common/Providers/Queries/EntityExtensions.cs new file mode 100644 index 0000000000..e4a03642ce --- /dev/null +++ b/Application/EdFi.Ods.Common/Providers/Queries/EntityExtensions.cs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.Common.Models.Domain; + +namespace EdFi.Ods.Common.Providers.Queries; + +public static class EntityExtensions +{ + private const string BaseAlias = "b"; + private const string StandardAlias = "r"; + + /// + /// Determines the appropriate table alias for the given aggregate root entity. + /// + /// The aggregate root entity to determine the alias for. + /// + /// Returns "b" if the entity is derived, otherwise returns "r". + /// + public static string RootTableAlias(this Entity aggregateRootEntity) + { + return aggregateRootEntity.IsDerived ? BaseAlias : StandardAlias; + } +} diff --git a/Application/EdFi.Ods.Common/Providers/Queries/IResourceIdentificationCodePropertyProvider.cs b/Application/EdFi.Ods.Common/Providers/Queries/IResourceIdentificationCodePropertyProvider.cs new file mode 100644 index 0000000000..dc51150953 --- /dev/null +++ b/Application/EdFi.Ods.Common/Providers/Queries/IResourceIdentificationCodePropertyProvider.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using EdFi.Ods.Common.Models.Resource; + +namespace EdFi.Ods.Common.Providers.Queries; + +/// +/// Provides an interface for retrieving the queryable properties of the identificationCode collection of a resource +/// +public interface IResourceIdentificationCodePropertiesProvider +{ + /// + /// Attempts to get the queryable properties of a resource's identificationCode collection. + /// + /// The entity to try and get the identificationCode properties for. + /// The queryable properties of the identificationCode of the . + /// true if the resource has an identificationCode collection with any queryable properties. + public bool TryGetIdentificationCodeProperties(Resource resource, + out List queryableIdentificationCodeProperties); +} diff --git a/Application/EdFi.Ods.Common/Providers/Queries/ResourceIdentificationCodePropertiesProvider.cs b/Application/EdFi.Ods.Common/Providers/Queries/ResourceIdentificationCodePropertiesProvider.cs new file mode 100644 index 0000000000..14e2754d4c --- /dev/null +++ b/Application/EdFi.Ods.Common/Providers/Queries/ResourceIdentificationCodePropertiesProvider.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.Common.Models.Domain; +using EdFi.Ods.Common.Models.Resource; + +namespace EdFi.Ods.Common.Providers.Queries; + +public class ResourceIdentificationCodePropertiesProvider : IResourceIdentificationCodePropertiesProvider +{ + private readonly ConcurrentDictionary> + _identificationCodePropertiesByRootResourceFullName = new(); + + public bool TryGetIdentificationCodeProperties(Resource resource, + out List identificationCodeProperties) + { + if (resource.Entity == null) + { + identificationCodeProperties = null; + return false; + } + + identificationCodeProperties = _identificationCodePropertiesByRootResourceFullName.GetOrAdd( + resource.Entity.FullName, _ => + { + if (TryFindIdentificationCode(resource, out ResourceChildItem identificationCode)) + { + return identificationCode.Properties + .Where(IsQueryableIdentificationCodeProperty).ToList(); + } + + return null; + }); + + return identificationCodeProperties?.Count > 0; + + bool IsQueryableIdentificationCodeProperty(ResourceProperty property) + { + return property.PropertyName.Equals("IdentificationCode") + || !property.EntityProperty.IsPredefinedProperty(); + } + } + + private static bool TryFindIdentificationCode(Resource resource, out ResourceChildItem identificationCodeProperty) + { + identificationCodeProperty = null; + + identificationCodeProperty = resource.Collections + .Select(c => c.ItemType) + .FirstOrDefault(i => i.PropertyByName.ContainsKey("IdentificationCode")); + + return identificationCodeProperty != null; + } +} diff --git a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactory.cs b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactory.cs index fa0e27ccb8..3b32770e36 100644 --- a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactory.cs +++ b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactory.cs @@ -8,6 +8,7 @@ using System.Linq; using EdFi.Ods.Api.Constants; using EdFi.Ods.Common.Configuration; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Features.OpenApiMetadata.Dtos; using EdFi.Ods.Features.OpenApiMetadata.Models; using EdFi.Ods.Features.OpenApiMetadata.Providers; @@ -25,12 +26,14 @@ public class OpenApiMetadataDocumentFactory : IOpenApiMetadataDocumentFactory private readonly IDefaultPageSizeLimitProvider _defaultPageSizeLimitProvider; private readonly IOpenApiIdentityProvider _openApiIdentityProvider; private readonly IOpenApiUpconversionProvider _openApiUpconversionProvider; + private readonly IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; - public OpenApiMetadataDocumentFactory(ApiSettings apiSettings, IDefaultPageSizeLimitProvider defaultPageSizeLimitProvider, IOpenApiUpconversionProvider openApiUpconversionProvider, IOpenApiIdentityProvider openApiIdentityProvider) + public OpenApiMetadataDocumentFactory(ApiSettings apiSettings, IDefaultPageSizeLimitProvider defaultPageSizeLimitProvider, IOpenApiUpconversionProvider openApiUpconversionProvider, IResourceIdentificationCodePropertiesProvider resourceIdentificationCodePropertiesProvider, IOpenApiIdentityProvider openApiIdentityProvider) { _apiSettings = apiSettings; _defaultPageSizeLimitProvider = defaultPageSizeLimitProvider; _openApiIdentityProvider = openApiIdentityProvider; + _resourceIdentificationCodePropertiesProvider = resourceIdentificationCodePropertiesProvider; _openApiUpconversionProvider = openApiUpconversionProvider; } @@ -48,7 +51,7 @@ public string Create(IOpenApiMetadataResourceStrategy resourceStrategy, OpenApiM var pathsFactory = OpenApiMetadataDocumentFactoryHelper.CreateOpenApiMetadataPathsFactory( - documentContext, _openApiIdentityProvider, _apiSettings); + documentContext, _openApiIdentityProvider, _resourceIdentificationCodePropertiesProvider, _apiSettings); var tagsFactory = OpenApiMetadataDocumentFactoryHelper.CreateOpenApiMetadataTagsFactory(documentContext); diff --git a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryHelper.cs b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryHelper.cs index cd6c556c56..007d1242d5 100644 --- a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryHelper.cs +++ b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryHelper.cs @@ -4,6 +4,7 @@ // See the LICENSE and NOTICES files in the project root for more information. using EdFi.Ods.Common.Configuration; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Features.OpenApiMetadata.Dtos; using EdFi.Ods.Features.OpenApiMetadata.Providers; using EdFi.Ods.Features.OpenApiMetadata.Strategies.FactoryStrategies; @@ -77,7 +78,7 @@ public static OpenApiMetadataDefinitionsFactory CreateOpenApiMetadataDefinitions } public static OpenApiMetadataPathsFactory CreateOpenApiMetadataPathsFactory( - OpenApiMetadataDocumentContext openApiMetadataDocumentContext, IOpenApiIdentityProvider openApiIdentityProvider, ApiSettings apiSettings) + OpenApiMetadataDocumentContext openApiMetadataDocumentContext, IOpenApiIdentityProvider openApiIdentityProvider, IResourceIdentificationCodePropertiesProvider resourceIdentificationCodePropertiesProvider, ApiSettings apiSettings) { if (openApiMetadataDocumentContext.IsProfileContext) { @@ -85,7 +86,7 @@ public static OpenApiMetadataPathsFactory CreateOpenApiMetadataPathsFactory( //Profile strategy implements each of the interfaces in the signature of the paths factory constructor //Hence the odd parameter repetition. - return new OpenApiMetadataPathsFactory(profileStrategy, profileStrategy, profileStrategy, openApiIdentityProvider, apiSettings); + return new OpenApiMetadataPathsFactory(profileStrategy, profileStrategy, profileStrategy, openApiIdentityProvider, resourceIdentificationCodePropertiesProvider, apiSettings); } IOpenApiMetadataPathsFactorySelectorStrategy selectorStrategy = null; @@ -109,7 +110,7 @@ public static OpenApiMetadataPathsFactory CreateOpenApiMetadataPathsFactory( selectorStrategy ??= defaultStrategy; resourceNamingStrategy ??= defaultResourceDefinitionNamingStrategy; - return new OpenApiMetadataPathsFactory(selectorStrategy, contentTypeStrategy, resourceNamingStrategy, openApiIdentityProvider, apiSettings); + return new OpenApiMetadataPathsFactory(selectorStrategy, contentTypeStrategy, resourceNamingStrategy, openApiIdentityProvider, resourceIdentificationCodePropertiesProvider, apiSettings); } public static OpenApiMetadataTagsFactory CreateOpenApiMetadataTagsFactory(OpenApiMetadataDocumentContext documentContext) diff --git a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactory.cs b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactory.cs index 45873d0229..a05adbaaa1 100644 --- a/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactory.cs +++ b/Application/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactory.cs @@ -11,6 +11,8 @@ using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Constants; using EdFi.Ods.Common.Models.Domain; +using EdFi.Ods.Common.Models.Resource; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Common.Utils.Profiles; using EdFi.Ods.Features.ChangeQueries; using EdFi.Ods.Features.OpenApiMetadata.Dtos; @@ -29,18 +31,22 @@ public class OpenApiMetadataPathsFactory private readonly IOpenApiMetadataPathsFactorySelectorStrategy _openApiMetadataPathsFactorySelectorStrategy; private readonly IOpenApiMetadataPathsFactoryNamingStrategy _pathsFactoryNamingStrategy; private readonly IOpenApiIdentityProvider _openApiIdentityProvider; + private readonly IResourceIdentificationCodePropertiesProvider + _resourceIdentificationCodePropertiesProvider; public OpenApiMetadataPathsFactory( IOpenApiMetadataPathsFactorySelectorStrategy openApiMetadataPathsFactorySelectorStrategy, IOpenApiMetadataPathsFactoryContentTypeStrategy contentTypeStrategy, IOpenApiMetadataPathsFactoryNamingStrategy pathsFactoryNamingStrategy, IOpenApiIdentityProvider openApiIdentityProvider, + IResourceIdentificationCodePropertiesProvider resourceIdentificationCodePropertiesProvider, ApiSettings apiSettings) { _openApiMetadataPathsFactorySelectorStrategy = openApiMetadataPathsFactorySelectorStrategy; _contentTypeStrategy = contentTypeStrategy; _pathsFactoryNamingStrategy = pathsFactoryNamingStrategy; _openApiIdentityProvider = openApiIdentityProvider; + _resourceIdentificationCodePropertiesProvider = resourceIdentificationCodePropertiesProvider; _apiSettings = apiSettings; } @@ -141,16 +147,10 @@ private PathItem CreatePathItemForAccessByIdsOperations(OpenApiMetadataPathsReso }; private PathItem CreatePathItemForTrackedChangesDeleteOperation(OpenApiMetadataPathsResource openApiMetadataResource) - => new PathItem - { - get = CreateTrackedChangesDeleteOperation(openApiMetadataResource) - }; + => new PathItem { get = CreateTrackedChangesDeleteOperation(openApiMetadataResource) }; private PathItem CreatePathItemForTrackedChangesKeyChangeOperation(OpenApiMetadataPathsResource openApiMetadataResource) - => new PathItem - { - get = CreateTrackedChangesKeyChangeOperation(openApiMetadataResource) - }; + => new PathItem { get = CreateTrackedChangesKeyChangeOperation(openApiMetadataResource) }; private PathItem CreatePathItemForPartitionOperation(OpenApiMetadataPathsResource openApiMetadataResource) => new PathItem @@ -236,24 +236,24 @@ private Operation CreateGetByIdOperation(OpenApiMetadataPathsResource openApiMet { // Path parameters need to be inline in the operation, and not referenced. OpenApiMetadataDocumentHelper.CreateIdParameter(), - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("If-None-Match")} + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("If-None-Match") } }.Concat( openApiMetadataResource.DefaultGetByIdParameters - .Select(p => new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference(p)})) + .Select(p => new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference(p) })) .ToList(); if (_apiSettings.IsFeatureEnabled(ApiFeature.ChangeQueries.GetConfigKeyName())) { - parameters.Add(new Parameter - { - name = "Use-Snapshot", - @in = "header", - description = "Indicates if the configured Snapshot should be used.", - type = "boolean", - required = false, - @default = false - - }); + parameters.Add( + new Parameter + { + name = "Use-Snapshot", + @in = "header", + description = "Indicates if the configured Snapshot should be used.", + type = "boolean", + required = false, + @default = false + }); } return new Operation @@ -276,7 +276,8 @@ private Operation CreateGetByIdOperation(OpenApiMetadataPathsResource openApiMet }; } - private Dictionary CreateReadResponses(OpenApiMetadataPathsResource openApiMetadataResource, bool isArray) + private Dictionary CreateReadResponses(OpenApiMetadataPathsResource openApiMetadataResource, + bool isArray) { var resourceName = _pathsFactoryNamingStrategy.GetResourceName(openApiMetadataResource, ContentTypeUsage.Readable); var responses = OpenApiMetadataDocumentHelper.GetReadOperationResponses( @@ -292,12 +293,12 @@ private Dictionary CreateReadResponses(OpenApiMetadataPathsRes else { responses.Add( - "404", - new Response - { - description = - "Not Found. An attempt to connect to the database snapshot, enabled by the Use-Snapshot header, was unsuccessful (indicating the snapshot may have been removed)." - }); + "404", + new Response + { + description = + "Not Found. An attempt to connect to the database snapshot, enabled by the Use-Snapshot header, was unsuccessful (indicating the snapshot may have been removed)." + }); } } @@ -329,16 +330,16 @@ private IList CreateQueryParameters(bool isCompositeContext, bool inc if (_apiSettings.IsFeatureEnabled("ChangeQueries") && !isCompositeContext) { parameterList.Add( - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("MinChangeVersion")}); + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("MinChangeVersion") }); parameterList.Add( - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("MaxChangeVersion")}); + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("MaxChangeVersion") }); } if (includePagingParameters && _openApiMetadataPathsFactorySelectorStrategy.HasTotalCount) { parameterList.Add( - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("totalCount")}); + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("totalCount") }); } return parameterList; @@ -350,7 +351,7 @@ private IList CreateGetByExampleParameters(OpenApiMetadataPathsResour var parameterList = CreateQueryParameters(isCompositeContext, includePagingParameters) .Concat( openApiMetadataResource.DefaultGetByExampleParameters.Select( - p => new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference(p)})) + p => new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference(p) })) .ToList(); IEnumerableExtensions.ForEach( @@ -376,15 +377,49 @@ private IList CreateGetByExampleParameters(OpenApiMetadataPathsResour if (_apiSettings.IsFeatureEnabled(ApiFeature.ChangeQueries.GetConfigKeyName())) { - parameterList.Add(new Parameter + parameterList.Add( + new Parameter + { + name = "Use-Snapshot", + @in = "header", + description = "Indicates if the configured Snapshot should be used.", + type = "boolean", + required = false, + @default = false + }); + } + + // Try to get any queryable identification code properties for the resource + // Use the unfiltered root resource (if applicable) to avoid populating the IdentificationCodePropertiesProvider's cache with results based on a filtered resource. + if (_resourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + openApiMetadataResource.Resource.FilterContext.UnfilteredResourceClass?.ResourceRoot ?? + openApiMetadataResource.Resource, out List queryableIdentificationCodeProperties)) + { + //If this is a profile resource, do not include IdentificationCode query parameters unless the IdentificationCode collection is included in the filtered resource + if (openApiMetadataResource.IsProfileResource) { - name = "Use-Snapshot", - @in = "header", - description = "Indicates if the configured Snapshot should be used.", - type = "boolean", - required = false, - @default = false - }); + queryableIdentificationCodeProperties = queryableIdentificationCodeProperties.Where( + p => openApiMetadataResource.Resource.Collections.Select(c => c.ItemType.FullName) + .Contains(p.Parent.FullName)).ToList(); + } + + IEnumerableExtensions.ForEach( + queryableIdentificationCodeProperties, x => + { + parameterList.Add( + new Parameter + { + name = x.PropertyName.ToCamelCase(), + @in = "query", + description = x.Description, + type = OpenApiMetadataDocumentHelper.PropertyType(x), + format = x.PropertyType.ToOpenApiFormat(), + required = false, + maxLength = OpenApiMetadataDocumentHelper.GetMaxLength(x), + isDeprecated = OpenApiMetadataDocumentHelper.GetIsDeprecated(x), + deprecatedReasons = OpenApiMetadataDocumentHelper.GetDeprecatedReasons(x) + }); + }); } return parameterList; @@ -434,7 +469,9 @@ private string GetDescription(OpenApiMetadataPathsResource openApiMetadataResour } private bool? GetIsUpdatableCustomMetadataValue(OpenApiMetadataPathsResource openApiMetadataResource) - => openApiMetadataResource.Resource.Entity.Identifier.IsUpdatable ? (bool?) true : null; + => openApiMetadataResource.Resource.Entity.Identifier.IsUpdatable + ? (bool?)true + : null; private IList CreatePutParameters(OpenApiMetadataPathsResource openApiMetadataResource) { @@ -495,11 +532,11 @@ private Operation CreateTrackedChangesDeleteOperation(OpenApiMetadataPathsResour var parameters = new List { - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("offset")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("limit")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("MinChangeVersion")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("MaxChangeVersion")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("totalCount")} + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("offset") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("limit") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("MinChangeVersion") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("MaxChangeVersion") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("totalCount") } }; if (_apiSettings.IsFeatureEnabled(ApiFeature.ChangeQueries.GetConfigKeyName())) @@ -511,22 +548,24 @@ private Operation CreateTrackedChangesDeleteOperation(OpenApiMetadataPathsResour else { responses.Add( - "404", - new Response - { - description = - "Not Found. An attempt to connect to the database snapshot, enabled by the Use-Snapshot header, was unsuccessful (indicating the snapshot may have been removed)." - }); + "404", + new Response + { + description = + "Not Found. An attempt to connect to the database snapshot, enabled by the Use-Snapshot header, was unsuccessful (indicating the snapshot may have been removed)." + }); } - parameters.Add(new Parameter { - name = "Use-Snapshot", - @in = "header", - description = "Indicates if the configured Snapshot should be used.", - type = "boolean", - required = false, - @default = false - }); + parameters.Add( + new Parameter + { + name = "Use-Snapshot", + @in = "header", + description = "Indicates if the configured Snapshot should be used.", + type = "boolean", + required = false, + @default = false + }); } return new Operation @@ -537,13 +576,11 @@ private Operation CreateTrackedChangesDeleteOperation(OpenApiMetadataPathsResour .ToCamelCase() }, summary = "Retrieves deleted resources based on change version.", - description = "This operation is used to retrieve identifying information about resources that have been deleted.", + description = + "This operation is used to retrieve identifying information about resources that have been deleted.", operationId = $"deletes{openApiMetadataResource.Resource.PluralName}", deprecated = openApiMetadataResource.IsDeprecated, - consumes = new[] - { - "application/json" - }, + consumes = new[] { "application/json" }, parameters = parameters, responses = responses }; @@ -556,11 +593,11 @@ private Operation CreateTrackedChangesKeyChangeOperation(OpenApiMetadataPathsRes var parameters = new List { - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("offset")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("limit")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("MinChangeVersion")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("MaxChangeVersion")}, - new Parameter {@ref = OpenApiMetadataDocumentHelper.GetParameterReference("totalCount")} + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("offset") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("limit") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("MinChangeVersion") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("MaxChangeVersion") }, + new Parameter { @ref = OpenApiMetadataDocumentHelper.GetParameterReference("totalCount") } }; if (_apiSettings.IsFeatureEnabled(ApiFeature.ChangeQueries.GetConfigKeyName())) @@ -572,23 +609,24 @@ private Operation CreateTrackedChangesKeyChangeOperation(OpenApiMetadataPathsRes else { responses.Add( - "404", - new Response - { - description = - "Not Found. An attempt to connect to the database snapshot, enabled by the Use-Snapshot header, was unsuccessful (indicating the snapshot may have been removed)." - }); + "404", + new Response + { + description = + "Not Found. An attempt to connect to the database snapshot, enabled by the Use-Snapshot header, was unsuccessful (indicating the snapshot may have been removed)." + }); } - parameters.Add(new Parameter - { - name = "Use-Snapshot", - @in = "header", - description = "Indicates if the configured Snapshot should be used.", - type = "boolean", - required = false, - @default = false - }); + parameters.Add( + new Parameter + { + name = "Use-Snapshot", + @in = "header", + description = "Indicates if the configured Snapshot should be used.", + type = "boolean", + required = false, + @default = false + }); } return new Operation @@ -599,13 +637,11 @@ private Operation CreateTrackedChangesKeyChangeOperation(OpenApiMetadataPathsRes .ToCamelCase() }, summary = "Retrieves resources key changes based on change version.", - description = "This operation is used to retrieve identifying information about resources whose key values have been changed.", + description = + "This operation is used to retrieve identifying information about resources whose key values have been changed.", operationId = $"keyChanges{openApiMetadataResource.Resource.PluralName}", deprecated = openApiMetadataResource.IsDeprecated, - consumes = new[] - { - "application/json" - }, + consumes = new[] { "application/json" }, parameters = parameters, responses = responses }; @@ -643,13 +679,13 @@ private Operation CreatePostOperation(OpenApiMetadataPathsResource openApiMetada _contentTypeStrategy.GetOperationContentType(openApiMetadataResource, ContentTypeUsage.Writable) }, parameters = CreatePostParameters(openApiMetadataResource), - responses = responses + responses = responses }; } private IList CreatePostParameters(OpenApiMetadataPathsResource openApiMetadataResource) { - return new List {CreateBodyParameter(openApiMetadataResource)}; + return new List { CreateBodyParameter(openApiMetadataResource) }; } private Parameter CreateIfMatchParameter(string operationText) @@ -676,7 +712,7 @@ private Parameter CreateBodyParameter(OpenApiMetadataPathsResource openApiMetada $"The JSON representation of the \"{camelCaseName}\" resource to be created or updated.", @in = "body", required = true, - schema = new Schema {@ref = OpenApiMetadataDocumentHelper.GetDefinitionReference(referenceName)} + schema = new Schema { @ref = OpenApiMetadataDocumentHelper.GetDefinitionReference(referenceName) } }; } } diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Database/Querying/IdentificationCodeCriteriaApplicatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Database/Querying/IdentificationCodeCriteriaApplicatorTests.cs new file mode 100644 index 0000000000..70f2a4764a --- /dev/null +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Database/Querying/IdentificationCodeCriteriaApplicatorTests.cs @@ -0,0 +1,755 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; +using EdFi.Common.Configuration; +using EdFi.Ods.Common.Database.Querying; +using EdFi.Ods.Common.Database.Querying.Dialects; +using EdFi.Ods.Common.Descriptors; +using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Models.Domain; +using EdFi.Ods.Common.Models.Resource; +using EdFi.Ods.Common.Providers.Queries; +using EdFi.Ods.Common.Providers.Queries.Criteria; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; +using Test.Common; + +namespace EdFi.Ods.Tests.EdFi.Ods.Common.Database.Querying +{ + [TestFixture] + public class IdentificationCodeAggregateRootQueryCriteriaApplicatorTests + { + private IResourceModel _resourceModel; + private IResourceModelProvider _resourceModelProvider; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; + private readonly string identificationCodeTableAlias = + IdentificationCodeAggregateRootQueryCriteriaApplicator.IdentificationCodeEntityTableAlias(); + + [SetUp] + public void SetUp() + { + _resourceModelProvider = DomainModelDefinitionsProviderHelper.ResourceModelProvider; + _resourceModel = _resourceModelProvider.GetResourceModel(); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + public void + ApplyAdditionalParameters_NoCriteriaApplied_WhenResourceDoesNotIdentificationCodeAndNoAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + // Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeProperty = _resourceModel.GetResourceByFullName("edfi.calendar"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> (); + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + List identificationCodeProperties = null; + + var additionalParameters = new Dictionary(); + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeProperty, out identificationCodeProperties)).Returns(false); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeProperty.Entity, A.Fake(), additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.Count().ShouldBe(0) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + public void + ApplyAdditionalParameters_NoCriteriaApplied_WhenResourceDoesNotIdentificationCodeAndAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + // Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeProperty = _resourceModel.GetResourceByFullName("edfi.calendar"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> (); + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + List identificationCodeProperties = null; + + var additionalParameters = + new Dictionary() { { "IdentificationCode", "SomeIdentificationCodeValue" } }; + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeProperty, out identificationCodeProperties)).Returns(false); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeProperty.Entity, A.Fake(), additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.Count().ShouldBe(0) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + public void + ApplyAdditionalParameters_NoCriteriaApplied_WhenResourceHasIdentificationCodeAndNoAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + // Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeProperty = _resourceModel.GetResourceByFullName("edfi.course"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> + { + { + ("CourseIdentificationSystemDescriptor", "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"), 1 + } + }; + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + List identificationCodeProperties; + + var additionalParameters = new Dictionary(); + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeProperty, out identificationCodeProperties)).Returns(false); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeProperty.Entity, A.Fake(), additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.Count().ShouldBe(0) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + [Test] + public void + ApplyAdditionalParameters_CorrectCriteriaApplied_WhenResourceHasIdentificationCodeAndMatchingAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + //Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeProperty = _resourceModel.GetResourceByFullName("edfi.course"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> + { + { + ("CourseIdentificationSystemDescriptor", "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"), 1 + } + }; + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + List identificationCodeProperties = null; + + string identificationCodeParameterValue = "ALG-1"; + + var additionalParameters = + new Dictionary() { { "IdentificationCode", identificationCodeParameterValue } }; + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeProperty, out identificationCodeProperties)).Returns(true) + .AssignsOutAndRefParameters( + resourceWithIdentificationCodeProperty.CollectionByName["CourseIdentificationCodes"].ItemType.Properties + .Where( + property => property.PropertyName.Equals("IdentificationCode") || + !property.EntityProperty.IsPredefinedProperty()).ToList()); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeProperty.Entity, A.Fake(), additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM + INNER JOIN edfi.CourseIdentificationCode AS {identificationCodeTableAlias} + ON r.CourseCode = {identificationCodeTableAlias}.CourseCode + AND r.EducationOrganizationId = {identificationCodeTableAlias}.EducationOrganizationId + WHERE {identificationCodeTableAlias}.IdentificationCode = @p0".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.ShouldContain("p0"), + () => actualParameters.Get("@p0").ShouldBe(identificationCodeParameterValue) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + [Test] + public void + ApplyAdditionalParameters_CorrectCriteriaApplied_WhenResourceHasIdentificationCodeAndMatchingDescriptorTypeAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + //Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeProperty = _resourceModel.GetResourceByFullName("edfi.course"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> + { + { + ("CourseIdentificationSystemDescriptor", "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"), 1 + } + }; + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + List identificationCodeProperties = null; + + var descriptorParameterDescriptorName = "CourseIdentificationSystemDescriptor"; + var descriptorParameterDescriptorUri = "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"; + + var additionalParameters = new Dictionary() + { + { descriptorParameterDescriptorName, descriptorParameterDescriptorUri } + }; + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeProperty, out identificationCodeProperties)).Returns(true) + .AssignsOutAndRefParameters( + resourceWithIdentificationCodeProperty.CollectionByName["CourseIdentificationCodes"].ItemType.Properties + .Where( + property => property.PropertyName.Equals("IdentificationCode") || + !property.EntityProperty.IsPredefinedProperty()).ToList()); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeProperty.Entity, A.Fake(), additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM + INNER JOIN edfi.CourseIdentificationCode AS {identificationCodeTableAlias} + ON r.CourseCode = {identificationCodeTableAlias}.CourseCode + AND r.EducationOrganizationId = {identificationCodeTableAlias}.EducationOrganizationId + WHERE {identificationCodeTableAlias}.CourseIdentificationSystemDescriptorId = @p0" + .NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.ShouldContain("p0"), + () => actualParameters.Get("@p0").ShouldBe( + descriptorIdMappings[ + (descriptorParameterDescriptorName, descriptorParameterDescriptorUri)]) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + [Test] + public void + ApplyAdditionalParameters_CorrectCriteriaApplied_WhenResourceHasIdentificationCodeAndMatchingNonExistentDescriptorAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + //Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeCollection = _resourceModel.GetResourceByFullName("edfi.course"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> + { + { + ("CourseIdentificationSystemDescriptor", "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"), 1 + } + }; + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + var identificationCodePropertiesForResource = + IdentificationCodePropertiesForResourceWithAnIdentificationCodeCollection( + resourceWithIdentificationCodeCollection); + + List identificationCodeProperties = null; + + var descriptorParameterDescriptorName = "CourseIdentificationSystemDescriptor"; + var descriptorParameterDescriptorUri = "uri://ed-fi.org/CourseIdentificationSystemDescriptor#NonExistentCode"; + + var additionalParameters = new Dictionary() + { + { descriptorParameterDescriptorName, descriptorParameterDescriptorUri } + }; + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeCollection, out identificationCodeProperties)).Returns(true) + .AssignsOutAndRefParameters(identificationCodePropertiesForResource); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeCollection.Entity, A.Fake(), + additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM + INNER JOIN edfi.CourseIdentificationCode AS {identificationCodeTableAlias} + ON r.CourseCode = {identificationCodeTableAlias}.CourseCode + AND r.EducationOrganizationId = {identificationCodeTableAlias}.EducationOrganizationId + WHERE 1 = 0".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.Count().ShouldBe(0) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + [Test] + public void + ApplyAdditionalParameters_CorrectCriteriaApplied_WhenResourceHasIdentificationCodeAndMultipleAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + //Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeCollection = _resourceModel.GetResourceByFullName("edfi.course"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> + { + { + ("CourseIdentificationSystemDescriptor", "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"), 1 + } + }; + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + var identificationCodePropertiesForResource = + IdentificationCodePropertiesForResourceWithAnIdentificationCodeCollection( + resourceWithIdentificationCodeCollection); + + var descriptorParameterDescriptorName = "CourseIdentificationSystemDescriptor"; + var descriptorParameterDescriptorUri = "uri://ed-fi.org/CourseIdentificationSystemDescriptor#SEA"; + + var identificationCodeParameterValue = "ALG-2"; + + var additionalParameters = new Dictionary() + { + { descriptorParameterDescriptorName, descriptorParameterDescriptorUri }, + { "IdentificationCode", identificationCodeParameterValue } + }; + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + List identificationCodeProperties; + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeCollection, out identificationCodeProperties)).Returns(true) + .AssignsOutAndRefParameters(identificationCodePropertiesForResource); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeCollection.Entity, A.Fake(), + additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM + INNER JOIN edfi.CourseIdentificationCode AS {identificationCodeTableAlias} + ON r.CourseCode = {identificationCodeTableAlias}.CourseCode + AND r.EducationOrganizationId = {identificationCodeTableAlias}.EducationOrganizationId + WHERE {identificationCodeTableAlias}.CourseIdentificationSystemDescriptorId = @p0 + AND {identificationCodeTableAlias}.IdentificationCode = @p1".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.ShouldContain("p0"), + () => actualParameters.ParameterNames.ShouldContain("p1"), + () => actualParameters.Get("@p0").ShouldBe( + descriptorIdMappings[ + (descriptorParameterDescriptorName, descriptorParameterDescriptorUri)]), + () => actualParameters.Get("@p1").ShouldBe(identificationCodeParameterValue) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer)] + [TestCase(DatabaseEngineEnum.PostgreSql)] + [Test] + public void + ApplyAdditionalParameters_CorrectCriteriaApplied_WhenDerivedResourceHasIdentificationCodeAndMatchingAdditionalParametersSupplied( + DatabaseEngineEnum databaseEngineEnum) + { + //Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeCollection = _resourceModel.GetResourceByFullName("edfi.school"); + + var fakeDescriptorResolver = A.Fake(); + var descriptorIdMappings = + new Dictionary<(string descriptorName, string descriptorUri), int> (); + A.CallTo( + () => fakeDescriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => descriptorIdMappings.GetValueOrDefault((descriptorName, descriptorUri))); + + var identificationCodePropertiesForResource = + IdentificationCodePropertiesForResourceWithAnIdentificationCodeCollection( + resourceWithIdentificationCodeCollection); + + List identificationCodeProperties; + string identificationCodeParameterValue = "1000001"; + + var additionalParameters = + new Dictionary() { { "IdentificationCode", identificationCodeParameterValue } }; + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeCollection, out identificationCodeProperties)).Returns(true) + .AssignsOutAndRefParameters(identificationCodePropertiesForResource); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + fakeDescriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeCollection.Entity, A.Fake(), + additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + actualParameters.ShouldSatisfyAllConditions( + () => template.RawSql.NormalizeSql() + .ShouldBe( + $@"SELECT FROM + INNER JOIN edfi.EducationOrganizationIdentificationCode AS {identificationCodeTableAlias} + ON b.EducationOrganizationId = {identificationCodeTableAlias}.EducationOrganizationId + WHERE {identificationCodeTableAlias}.IdentificationCode = @p0".NormalizeSql()), + () => actualParameters.ShouldNotBeNull(), + () => actualParameters.ParameterNames.ShouldContain("p0"), + () => actualParameters.Get("@p0").ShouldBe(identificationCodeParameterValue) + ); + } + + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.course")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.course")] + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.school")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.school")] + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.educationorganization")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.educationorganization")] + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.staff")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.staff")] + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.coursetranscript")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.coursetranscript")] + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.studenteducationorganizationassociation")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.studenteducationorganizationassociation")] + [TestCase(DatabaseEngineEnum.SqlServer, "edfi.learningstandard")] + [TestCase(DatabaseEngineEnum.PostgreSql, "edfi.learningstandard")] + [Test] + public void + ApplyAdditionalParameters_CorrectCriteriaApplied_WhenResourceHasIdentificationCodeAndAllIdentificationCodeParametersAreSupplied( + DatabaseEngineEnum databaseEngineEnum, string resourceWithIdentificationCodeCollectionName) + { + //Arrange + var q = new QueryBuilder(GetDialectFor(databaseEngineEnum)); + + var resourceWithIdentificationCodeCollection = + _resourceModel.GetResourceByFullName(resourceWithIdentificationCodeCollectionName); + + var identificationCodePropertiesForResource = + IdentificationCodePropertiesForResourceWithAnIdentificationCodeCollection( + resourceWithIdentificationCodeCollection); + + var additionalParameters = new Dictionary(); + + Dictionary<(string descriptorName, string descriptorUri), int> + courseIdentificationSystemDescriptorToDescriptorIdMappings = new(); + + int descriptorId = 1; + + foreach (var property in identificationCodePropertiesForResource) + { + if (property.IsDescriptorUsage) + { + courseIdentificationSystemDescriptorToDescriptorIdMappings.Add( + (property.DescriptorName, $"uri://ed-fi.org/{property.DescriptorName}#ValidDescriptorCode"), + descriptorId++); + + additionalParameters.Add( + property.DescriptorName, $"uri://ed-fi.org/{property.DescriptorName}#ValidDescriptorCode"); + } + else + { + switch (property.PropertyType.DbType) + { + case DbType.Int32: + additionalParameters.Add(property.PropertyName, "1234"); + break; + case DbType.String: + additionalParameters.Add(property.PropertyName, "AdditionalParameterStringValue"); + break; + default: + throw new Exception("Expected DbType.Int32 or DbType.String but got " + property.PropertyType.DbType); + } + } + } + + IResourceIdentificationCodePropertiesProvider fakeResourceIdentificationCodePropertiesProvider = + A.Fake(); + + List identificationCodeProperties; + + A.CallTo( + () => fakeResourceIdentificationCodePropertiesProvider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeCollection, out identificationCodeProperties)).Returns(true) + .AssignsOutAndRefParameters(identificationCodePropertiesForResource); + + var descriptorResolver = A.Fake(); + + A.CallTo( + () => descriptorResolver.GetDescriptorId(A._, A._)).ReturnsLazily( + (string descriptorName, string descriptorUri) + => courseIdentificationSystemDescriptorToDescriptorIdMappings.GetValueOrDefault( + (descriptorName, descriptorUri))); + + var identificationCodeAggregateQueryCriteriaApplicator = new IdentificationCodeAggregateRootQueryCriteriaApplicator( + descriptorResolver, _resourceModelProvider, fakeResourceIdentificationCodePropertiesProvider, + global::EdFi.Common.Configuration.DatabaseEngine.TryParseEngine(databaseEngineEnum.ToString())); + + // Act + identificationCodeAggregateQueryCriteriaApplicator.ApplyAdditionalParameters( + q, resourceWithIdentificationCodeCollection.Entity, A.Fake(), + additionalParameters); + + var template = q.BuildTemplate(); + var actualParameters = template.Parameters as DynamicParameters; + + // Assert + + var identificationCodesEntity = identificationCodePropertiesForResource.First().EntityProperty.Entity; + var rawSql = template.RawSql.NormalizeSql(); + + // Assert that the generated SQL contains an INNER JOIN with the correct IdentificationCode table + rawSql.ShouldContain( + $"INNER JOIN {identificationCodesEntity.Schema}.{identificationCodesEntity.TableName(GetDatabaseEngineFor(databaseEngineEnum))} as {identificationCodeTableAlias} ON"); + + // Check the generated join criteria for the INNER JOIN of with the IdentificationCodes table + + // Get the equality criteria for the INNER JOIN from the SQL + var joinEqualityCriteriaInSql = rawSql.Split("ON")[1].Split("WHERE")[0].Split("AND").Select(s => s.Trim()).ToList(); + + // Get a list of the inherited identifying properties of the IdentificationCode entity + var joinProperties = identificationCodesEntity.Properties + .Where(p => p.IsIdentifying && p.IsFromParent).ToList(); + + // Calculate the equality criteria expected to be present in the join condition based on the IdentificationCode table's columns + var identificationCodesTableJoinColumns = joinProperties.Select( + p => p.ColumnName(GetDatabaseEngineFor(databaseEngineEnum), p.PropertyName)).ToList(); + + var expectedJoinEqualityCriteria = identificationCodesTableJoinColumns.Select( + c => $"{resourceWithIdentificationCodeCollection.Entity.RootTableAlias()}.{c} = {identificationCodeTableAlias}.{c}"); + + // Assert that the equality criteria in the join condition are unique + joinEqualityCriteriaInSql.ShouldBeUnique(); + + // Assert that the equality criteria in the join condition in the SQL exactly match the expected set + joinEqualityCriteriaInSql.Except(expectedJoinEqualityCriteria).ShouldBeEmpty(); + + // Check the equality criteria in the generated WHERE clause + + // Get the individual equality criteria from the where clause (excluding the parameter numbers) + var equalityCriteriaInWhereClause = rawSql.Split("WHERE")[1].Split("AND").Where(s => s.Contains('=')) + .Select(s => s.Substring(0, s.IndexOf("@p", StringComparison.Ordinal) + 2).Trim()).ToList(); + + // Calculate the expected to be in the where clause based on the queryable properties of the IdentificationCode + var expectedEqualityCriteriaInWhereClause = identificationCodePropertiesForResource.Select( + p => $"{identificationCodeTableAlias}.{p.EntityProperty.ColumnName(GetDatabaseEngineFor(databaseEngineEnum), p.PropertyName)} = @p") + .ToList(); + + // Assert that the equality criteria in the where clause are unique + equalityCriteriaInWhereClause.ShouldBeUnique(); + + // Assert that the equality criteria present in the sql where clause exactly match the expected set + equalityCriteriaInWhereClause.Except(expectedEqualityCriteriaInWhereClause).ShouldBeEmpty(); + + // Assert that the query parameters object should be non-null and have an item count equal to the number of queryable IdentificationCode properties + actualParameters.ShouldNotBeNull(); + actualParameters.ParameterNames.Count().ShouldBe(identificationCodePropertiesForResource.Count); + } + + public enum DatabaseEngineEnum + { + SqlServer, + PostgreSql, + } + + private static Dialect GetDialectFor(DatabaseEngineEnum databaseEngineEnum) + { + switch (databaseEngineEnum) + { + case DatabaseEngineEnum.SqlServer: + return new SqlServerDialect(); + case DatabaseEngineEnum.PostgreSql: + return new PostgreSqlDialect(); + default: + throw new NotSupportedException($"Unsupported database engine '{databaseEngineEnum.ToString()}'."); + } + } + + private static DatabaseEngine GetDatabaseEngineFor(DatabaseEngineEnum databaseEngineEnum) + { + switch (databaseEngineEnum) + { + case DatabaseEngineEnum.SqlServer: + return DatabaseEngine.SqlServer; + case DatabaseEngineEnum.PostgreSql: + return DatabaseEngine.Postgres; + default: + throw new NotSupportedException($"Unsupported database engine '{databaseEngineEnum.ToString()}'."); + } + } + + private static List IdentificationCodePropertiesForResourceWithAnIdentificationCodeCollection( + Resource resourceWithIdentificationCodeCollection) + { + return resourceWithIdentificationCodeCollection.Collections + .First(c => c.PropertyName.EndsWith("IdentificationCodes")).ItemType.Properties + .Where( + property => property.PropertyName.Equals("IdentificationCode") || + !property.EntityProperty.IsPredefinedProperty()).ToList(); + } + } +} diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Providers/ResourceIdentificationCodeProviderTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Providers/ResourceIdentificationCodeProviderTests.cs new file mode 100644 index 0000000000..14e873e376 --- /dev/null +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Providers/ResourceIdentificationCodeProviderTests.cs @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Models.Domain; +using EdFi.Ods.Common.Models.Resource; +using EdFi.Ods.Common.Providers.Queries; +using NUnit.Framework; +using Shouldly; +using Test.Common; + +namespace EdFi.Ods.Tests.EdFi.Ods.Common.Providers +{ + [TestFixture] + public class ResourceIdentificationCodePropertiesProviderTests + { + private IResourceIdentificationCodePropertiesProvider _provider; + private DomainModel _domainModel; + + [OneTimeSetUp] + public void Setup() + { + _provider = new ResourceIdentificationCodePropertiesProvider(); + _domainModel = DomainModelDefinitionsProviderHelper.DomainModelProvider.GetDomainModel(); + } + + [Test] + public void TryFindIdentificationCodes_ShouldReturnFalseAndNull_WhenResourceHasNoIdentificationCode() + { + // Arrange + Resource resourceWithoutIdentificationCode = _domainModel.ResourceModel.GetResourceByFullName("edfi.Calendar"); + + // Act + bool result = _provider.TryGetIdentificationCodeProperties( + resourceWithoutIdentificationCode, out List identificationCodeProperties); + + //Assert + result.ShouldBeFalse(); + identificationCodeProperties.ShouldBeNull(); + } + + [Test] + public void TryFindIdentificationCodes_ShouldReturnTrueAndCorrectIdentificationCodeProperties_WhenAResourceHasIdentificationCode() + { + // Arrange + _provider = new ResourceIdentificationCodePropertiesProvider(); + const string ResourceWithIdentificationCodeCollectionName = "edfi.Course"; + + Resource resourceWithIdentificationCodeCollection = _domainModel.ResourceModel.GetAllResources() + .First(r => r.FullName.Equals(ResourceWithIdentificationCodeCollectionName)); + + // Act + bool result = _provider.TryGetIdentificationCodeProperties( + resourceWithIdentificationCodeCollection, out List identificationCodeProperties); + + //Assert + result.ShouldBeTrue(); + identificationCodeProperties.Count.ShouldBe(4); + identificationCodeProperties.Select(p => p.PropertyName).ToList().ShouldBeEquivalentTo(new List + { + "CourseIdentificationSystemDescriptor", + "AssigningOrganizationIdentificationCode", + "CourseCatalogURL", + "IdentificationCode" + }); + } + + [Test] + public void TryFindIdentificationCodes_ShouldReturnTrueAndCorrectIdentificationCodeProperties_WhenADerivedResourceHasInheritedIdentificationCode() + { + // Arrange + _provider = new ResourceIdentificationCodePropertiesProvider(); + const string DerivedResourceName = "edfi.School"; + + var domainModel = DomainModelDefinitionsProviderHelper.DomainModelProvider.GetDomainModel(); + + Resource derivedResource = domainModel.ResourceModel.GetAllResources() + .First(r => r.FullName.Equals(DerivedResourceName)); + + // Act + bool result = _provider.TryGetIdentificationCodeProperties( + derivedResource, out List identificationCodeProperties); + + //Assert + result.ShouldBeTrue(); + identificationCodeProperties.Count.ShouldBe(2); + identificationCodeProperties.Select(p => p.PropertyName).ToList().ShouldBeEquivalentTo(new List + { + "EducationOrganizationIdentificationSystemDescriptor", + "IdentificationCode" + }); + } + } +} diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryTests.cs index 3372d000d8..964b14350b 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataDocumentFactoryTests.cs @@ -8,6 +8,7 @@ using EdFi.Ods.Common; using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Features.OpenApiMetadata.Dtos; using EdFi.Ods.Features.OpenApiMetadata.Factories; using EdFi.Ods.Features.OpenApiMetadata.Models; @@ -29,6 +30,8 @@ namespace EdFi.Ods.Tests.EdFi.Ods.Features.OpenApiMetadata.Factories public class OpenApiMetadataDocumentFactoryTests { protected static IOpenApiUpconversionProvider OpenApiV3UpconversionProvider = A.Fake(); + + private static readonly IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider = A.Fake(); protected static IResourceModelProvider ResourceModelProvider = DomainModelDefinitionsProviderHelper.ResourceModelProvider; @@ -108,6 +111,7 @@ protected override void Arrange() var defaultPageSizeLimitProvider = new DefaultPageSizeLimitProvider(GetConfiguration().GetValue("DefaultPageSizeLimit")); _openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSizeLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); } @@ -221,6 +225,7 @@ protected override void Arrange() _openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSizeLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); } diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactoryTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactoryTests.cs index c5061d6193..5e4abc5e28 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactoryTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Factories/OpenApiMetadataPathsFactoryTests.cs @@ -16,6 +16,7 @@ using EdFi.Ods.Common.Models; using EdFi.Ods.Common.Models.Resource; using EdFi.Ods.Common.Models.Validation; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Common.Utils.Profiles; using EdFi.Ods.Features.OpenApiMetadata.Dtos; using EdFi.Ods.Features.OpenApiMetadata.Factories; @@ -35,6 +36,8 @@ public class OpenApiMetadataPathsFactoryTests protected static IResourceModelProvider ResourceModelProvider = DomainModelDefinitionsProviderHelper.ResourceModelProvider; protected static ISchemaNameMapProvider SchemaNameMapProvider = DomainModelDefinitionsProviderHelper.SchemaNameMapProvider; + + private static readonly IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider = A.Fake(); private static ApiSettings CreateApiSettings() { @@ -82,7 +85,9 @@ protected override void Act() .ToList(); _actualPaths = OpenApiMetadataDocumentFactoryHelper.CreateOpenApiMetadataPathsFactory( - DomainModelDefinitionsProviderHelper.DefaultopenApiMetadataDocumentContext, new FakeOpenApiIdentityProvider(), CreateApiSettings()) + DomainModelDefinitionsProviderHelper.DefaultopenApiMetadataDocumentContext, new FakeOpenApiIdentityProvider(), + _resourceIdentificationCodePropertiesProvider, + CreateApiSettings()) .Create(openApiMetadataResources, false); } @@ -220,7 +225,7 @@ protected override void Act() appSettings.Features.Single(f => f.Name == "ChangeQueries").IsEnabled = false; _actualPaths = OpenApiMetadataDocumentFactoryHelper.CreateOpenApiMetadataPathsFactory( - DomainModelDefinitionsProviderHelper.DefaultopenApiMetadataDocumentContext, new FakeOpenApiIdentityProvider(), appSettings) + DomainModelDefinitionsProviderHelper.DefaultopenApiMetadataDocumentContext, new FakeOpenApiIdentityProvider(), _resourceIdentificationCodePropertiesProvider, appSettings) .Create(openApiMetadataResources, false); } @@ -316,7 +321,7 @@ protected override void Arrange() protected override void Act() { _actualPaths = OpenApiMetadataDocumentFactoryHelper - .CreateOpenApiMetadataPathsFactory(_openApiMetadataDocumentContext, new FakeOpenApiIdentityProvider(), CreateApiSettings()) + .CreateOpenApiMetadataPathsFactory(_openApiMetadataDocumentContext, new FakeOpenApiIdentityProvider(), _resourceIdentificationCodePropertiesProvider, CreateApiSettings()) .Create(_openApiMetadataResources, false); } diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/OpenApiMetadataSdkGenTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/OpenApiMetadataSdkGenTests.cs index 8d4b68f773..857e97f588 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/OpenApiMetadataSdkGenTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/OpenApiMetadataSdkGenTests.cs @@ -11,6 +11,7 @@ using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Conventions; using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Features.OpenApiMetadata.Dtos; using EdFi.Ods.Features.OpenApiMetadata.Factories; using EdFi.Ods.Features.OpenApiMetadata.Models; @@ -78,6 +79,7 @@ public class When_Generating_Extension_Only_As_Root_Aggregate_Document : TestFix { private OpenApiMetadataDocumentFactory _extensionOnlyOpenApiMetadataDocumentFactory; private SdkGenExtensionResourceStrategy _resourceStrategy; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; private string _actualMetadataText; private readonly string requestedExtensionPhysicalName = "gb"; private OpenApiMetadataDocument _actualMetadataObject; @@ -99,9 +101,12 @@ protected override void Arrange() var upconversionProvider = A.Fake(); A.CallTo(() => upconversionProvider.GetUpconvertedOpenApiJson(A._)).ReturnsLazily(x => x.Arguments.Get(0)); + _resourceIdentificationCodePropertiesProvider = A.Fake(); + _extensionOnlyOpenApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSieLimitProvider, upconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); _resourceStrategy = new SdkGenExtensionResourceStrategy(); @@ -218,6 +223,7 @@ protected override void Arrange() _extensionOnlyOpenApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSieLimitProvider, upconversionProvider, + Stub(), new FakeOpenApiIdentityProvider()); _resourceStrategy = new SdkGenExtensionResourceStrategy(); diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProviderTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProviderTests.cs index 6fcecb88e6..7af5a0f358 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProviderTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Providers/OpenApiMetadataCacheProviderTests.cs @@ -20,6 +20,7 @@ using EdFi.Ods.Common.Metadata.StreamProviders.Profiles; using EdFi.Ods.Common.Models; using EdFi.Ods.Common.Models.Validation; +using EdFi.Ods.Common.Providers.Queries; using EdFi.Ods.Composites.Test; using EdFi.Ods.Features.Composites; using EdFi.Ods.Features.Extensions; @@ -116,6 +117,8 @@ public class When_requesting_the_sdk_gen_section_from_the_cache : TestFixtureBas { private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; private List _actualMetadata; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; + protected override void Arrange() { @@ -128,6 +131,8 @@ protected override void Arrange() AssemblyLoader.EnsureLoaded(); var resourceModelProvider = Stub(); + + _resourceIdentificationCodePropertiesProvider = Stub(); var resourceModel = ResourceModelProvider.GetResourceModel(); @@ -147,6 +152,7 @@ protected override void Arrange() var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( apiSettings, defaultPageSieLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); var compositeOpenApiContentProvider = new CompositesOpenApiContentProvider( @@ -217,6 +223,8 @@ public class When_requesting_the_swagger_ui_section_from_the_cache : TestFixture private ICompositesMetadataProvider _compositesMetadataProvider; private IProfileResourceModelProvider _profileResourceModelProvider; private IProfileResourceNamesProvider _profileResourceNamesProvider; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; + private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; private List _actualMetadata; @@ -233,6 +241,8 @@ protected override void Arrange() _profileResourceNamesProvider = Stub(); + _resourceIdentificationCodePropertiesProvider = Stub(); + A.CallTo(() => _profileResourceNamesProvider.GetProfileResourceNames()) .Returns(new List()); @@ -247,6 +257,7 @@ protected override void Arrange() var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( apiSettings, defaultPageSieLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); var resourceModelProvider = Stub(); @@ -295,6 +306,7 @@ public class When_requesting_the_other_ui_section_from_the_cache : TestFixtureBa private ICompositesMetadataProvider _compositesMetadataProvider; private IProfileResourceModelProvider _profileResourceModelProvider; private IProfileResourceNamesProvider _profileResourceNamesProvider; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; private List _actualMetadata; @@ -307,6 +319,8 @@ protected override void Arrange() _profileResourceNamesProvider = Stub(); + _resourceIdentificationCodePropertiesProvider = Stub(); + A.CallTo(() => _compositesMetadataProvider.GetAllCategories()) .Returns(new List()); @@ -322,6 +336,7 @@ protected override void Arrange() var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSieLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); var openApiMetadataRouteInformation = new List(); @@ -382,6 +397,7 @@ public class When_requesting_the_profiles_section_from_the_cache : TestFixtureBa private IProfileResourceModelProvider _profileResourceModelProvider; private IProfileResourceNamesProvider _profileResourceNamesProvider; private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; private List _actualMetadata; protected override void Arrange() @@ -427,9 +443,12 @@ protected override void Arrange() var defaultPageSizeLimitProvider = new DefaultPageSizeLimitProvider(GetConfiguration().GetValue("DefaultPageSizeLimit")); + _resourceIdentificationCodePropertiesProvider = Stub(); + var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSizeLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); var resourceModelProvider = Stub(); @@ -495,6 +514,8 @@ public class When_requesting_the_composites_section_from_the_cache : TestFixture private CompositesMetadataProvider _compositesMetadataProvider; private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; private List _actualMetadata; + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; + protected override void Arrange() { @@ -527,9 +548,12 @@ protected override void Arrange() var defaultPageSieLimitProvider = new DefaultPageSizeLimitProvider(GetConfiguration().GetValue("DefaultPageSizeLimit")); + _resourceIdentificationCodePropertiesProvider = Stub(); + var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( apiSettings, defaultPageSieLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); var compositeOpenApiContentProvider = new CompositesOpenApiContentProvider( @@ -615,6 +639,9 @@ public class When_requesting_the_extensions_section_from_the_cache : TestFixture private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; private List _actualMetadata; + + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; + protected override void Arrange() { @@ -634,6 +661,8 @@ protected override void Arrange() var openApiMetadataRouteInformation = new List(); var _resourceModelProvider = Stub(); + + _resourceIdentificationCodePropertiesProvider = Stub(); var resourceModel = ResourceModelProvider.GetResourceModel(); @@ -651,6 +680,7 @@ protected override void Arrange() var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( apiSettings, defaultPageSieLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodePropertiesProvider, new FakeOpenApiIdentityProvider()); var compositeOpenApiContentProvider = new CompositesOpenApiContentProvider( @@ -718,13 +748,13 @@ public void Should_be_a_valid_swagger_document_for_each_extension_schema_in_the_ public class When_requesting_a_section_from_the_cache_for_which_no_route_was_registered : TestFixtureBase { private OpenApiMetadataCacheProvider _openApiMetadataCacheProvider; - + private IResourceIdentificationCodePropertiesProvider _resourceIdentificationCodePropertiesProvider; private List _actualMetadata; protected override void Arrange() { var _openAPIMetadataRouteInformation = Stub>(); - + var _resourceIdentificationCodeProperties = Stub(); var _openApiContentProviders = Stub>(); A.CallTo(() => OpenApiV3UpconversionProvider.GetUpconvertedOpenApiJson(A._)) @@ -736,6 +766,7 @@ protected override void Arrange() var openApiMetadataDocumentFactory = new OpenApiMetadataDocumentFactory( CreateApiSettings(), defaultPageSieLimitProvider, OpenApiV3UpconversionProvider, + _resourceIdentificationCodeProperties, new FakeOpenApiIdentityProvider()); var resourceModelProvider = Stub(); diff --git a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite GetByExample Tests.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite GetByExample Tests.postman_collection.json index 0198b46b19..f748a10df6 100644 --- a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite GetByExample Tests.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite GetByExample Tests.postman_collection.json @@ -1281,7 +1281,9 @@ "exec": [ "pm.environment.set('supplied:schoolId', 255901001);\r", "pm.environment.set('supplied:nameOfInstitution', 'Grand Bend Middle School');\r", - "pm.environment.set('supplied:shortNameOfInstitution', 'GBES');" + "pm.environment.set('supplied:shortNameOfInstitution', 'GBES');\r", + "pm.environment.set('supplied:identificationCode', \"255901001\");\r", + "pm.environment.set('supplied:educationOrganizationIdentificationSystemDescriptor', encodeURIComponent(\"uri://ed-fi.org/EducationOrganizationIdentificationSystemDescriptor#SEA\"));" ], "type": "text/javascript", "packages": {} @@ -1950,6 +1952,331 @@ "response": [] } ] + }, + { + "name": "When filtering by IdentificationCode", + "item": [ + { + "name": "Setup", + "item": [ + { + "name": "Create School with existing IdentificationCode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.environment.set(\"known:schoolWithDuplicateIdentificationCode_id\", pm.response.headers.one('Location').value.split(\"/\").pop());" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": " {\n \"localEducationAgencyReference\": {\n \"localEducationAgencyId\": 255901,\n \"link\": {\n \"rel\": \"LocalEducationAgency\",\n \"href\": \"/ed-fi/localEducationAgencies/97cb137b7004479893cf92f7d770bb81\"\n }\n },\n \"schoolId\": 255901999,\n \"nameOfInstitution\": \"Grand Bend High Alternate\",\n \"operationalStatusDescriptor\": \"uri://ed-fi.org/OperationalStatusDescriptor#Active\",\n \"shortNameOfInstitution\": \"GBHSA\",\n \"webSite\": \"http://www.GBISD.edu/GBHSA/\",\n \"administrativeFundingControlDescriptor\": \"uri://ed-fi.org/AdministrativeFundingControlDescriptor#Public School\",\n \"charterStatusDescriptor\": \"uri://ed-fi.org/CharterStatusDescriptor#Not a Charter School\",\n \"schoolTypeDescriptor\": \"uri://ed-fi.org/SchoolTypeDescriptor#Regular\",\n \"titleIPartASchoolDesignationDescriptor\": \"uri://ed-fi.org/TitleIPartASchoolDesignationDescriptor#Not A Title I School\",\n \"educationOrganizationCategories\": [\n {\n \"educationOrganizationCategoryDescriptor\": \"uri://ed-fi.org/EducationOrganizationCategoryDescriptor#School\"\n }\n ],\n \"identificationCodes\": [\n {\n \"educationOrganizationIdentificationSystemDescriptor\": \"uri://ed-fi.org/EducationOrganizationIdentificationSystemDescriptor#Federal\",\n \"identificationCode\": \"255901001\"\n }\n ],\n \"indicators\": [],\n \"institutionTelephones\": [],\n \"internationalAddresses\": [],\n \"schoolCategories\": [\n {\n \"schoolCategoryDescriptor\": \"uri://ed-fi.org/SchoolCategoryDescriptor#High School\"\n }\n ],\n \"gradeLevels\": [\n {\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Ninth grade\"\n },\n {\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Tenth grade\"\n },\n {\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Eleventh grade\"\n },\n {\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Twelfth grade\"\n }\n ]\n }", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Requests filtered by IdentificationCode properties", + "item": [ + { + "name": "When filtering by an existing IdentificationCode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const responseItems = pm.response.json();\r", + "\r", + "pm.test(\"Should return two expected resouces\", () => {\r", + " pm.expect(responseItems.count()).to.equal(2);\r", + " pm.expect(responseItems.map('id')).to.have.all.members([pm.environment.get('known:highSchool_id'),pm.environment.get('known:schoolWithDuplicateIdentificationCode_id')]);\r", + " pm.expect(responseItems[0].identificationCodes[0].identificationCode).to.equal(pm.environment.get('supplied:identificationCode'));\r", + " pm.expect(responseItems[1].identificationCodes[0].identificationCode).to.equal(pm.environment.get('supplied:identificationCode'));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools?identificationCode={{supplied:identificationCode}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ], + "query": [ + { + "key": "identificationCode", + "value": "{{supplied:identificationCode}}" + } + ] + } + }, + "response": [] + }, + { + "name": "When filtering by an existing IdentificationCode and IdentificationSystem descriptor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const responseItems = pm.response.json();\r", + "\r", + "pm.test(\"Should return one expected resouce\", () => {\r", + " pm.expect(responseItems.count()).to.equal(1);\r", + " pm.expect(responseItems[0].id).to.equals(pm.environment.get('known:highSchool_id'));\r", + " pm.expect(responseItems[0].identificationCodes[0].identificationCode).to.equals(pm.environment.get('supplied:identificationCode'));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools?identificationCode={{supplied:identificationCode}}&educationOrganizationIdentificationSystemDescriptor={{supplied:educationOrganizationIdentificationSystemDescriptor}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ], + "query": [ + { + "key": "identificationCode", + "value": "{{supplied:identificationCode}}" + }, + { + "key": "educationOrganizationIdentificationSystemDescriptor", + "value": "{{supplied:educationOrganizationIdentificationSystemDescriptor}}" + } + ] + } + }, + "response": [] + }, + { + "name": "When filtering by a non-existent IdentificationCode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "pm.test(\"Should return an empty response\", () => {\r", + " const responseItems = pm.response.json();\r", + "\r", + " pm.expect(responseItems.count()).to.equal(0);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/localEducationAgencies?identificationCode=non-existent", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "localEducationAgencies" + ], + "query": [ + { + "key": "identificationCode", + "value": "non-existent" + } + ] + } + }, + "response": [] + }, + { + "name": "When filtering by an existing educationOrganizationIdentificationSystemDescriptor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "const responseItems = pm.response.json();\r", + "\r", + "pm.test(\"Should return one expected resouce\", () => {\r", + " pm.expect(responseItems.count()).to.equal(1);\r", + " pm.expect(responseItems[0].id).to.equal(pm.environment.get('known:lea_id'));\r", + " pm.expect(encodeURIComponent(responseItems[0].identificationCodes[0].educationOrganizationIdentificationSystemDescriptor)).to.equal(pm.environment.get('supplied:educationOrganizationIdentificationSystemDescriptor'));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/localEducationAgencies?educationOrganizationIdentificationSystemDescriptor={{supplied:educationOrganizationIdentificationSystemDescriptor}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "localEducationAgencies" + ], + "query": [ + { + "key": "educationOrganizationIdentificationSystemDescriptor", + "value": "{{supplied:educationOrganizationIdentificationSystemDescriptor}}" + } + ] + } + }, + "response": [] + }, + { + "name": "When filtering by a non-existent educationOrganizationIdentificationSystemDescriptor", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {\r", + " pm.expect(pm.response.code).to.equal(200);\r", + "});\r", + "\r", + "pm.test(\"Should return an empty response\", () => {\r", + " const responseItems = pm.response.json();\r", + "\r", + " pm.expect(responseItems.count()).to.equal(0);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/localEducationAgencies?educationOrganizationIdentificationSystemDescriptor=uri://ed-fi.org/EducationOrganizationIdentificationSystemDescriptor%23non-existant", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "localEducationAgencies" + ], + "query": [ + { + "key": "educationOrganizationIdentificationSystemDescriptor", + "value": "uri://ed-fi.org/EducationOrganizationIdentificationSystemDescriptor#non-existant" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Teardown", + "item": [ + { + "name": "Delete School with duplicate IdentificationCode", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools/{{known:schoolWithDuplicateIdentificationCode_id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools", + "{{known:schoolWithDuplicateIdentificationCode_id}}" + ] + } + }, + "response": [] + } + ] + } + ] } ] },