diff --git a/.github/workflows/Cleanup Caches by a branch.yml b/.github/workflows/Cleanup Caches by a branch.yml index 57ce83669f..ebc6394637 100644 --- a/.github/workflows/Cleanup Caches by a branch.yml +++ b/.github/workflows/Cleanup Caches by a branch.yml @@ -6,7 +6,7 @@ name: Cleanup Caches by a branch on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] types: [ closed ] env: diff --git a/.github/workflows/Lib edFi.admin.dataaccess pullrequest.yml b/.github/workflows/Lib edFi.admin.dataaccess pullrequest.yml index 1df0825f09..0ec4084dfe 100644 --- a/.github/workflows/Lib edFi.admin.dataaccess pullrequest.yml +++ b/.github/workflows/Lib edFi.admin.dataaccess pullrequest.yml @@ -2,7 +2,7 @@ name: Lib EdFi.Admin.DataAccess Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Lib edFi.common pullrequest.yml b/.github/workflows/Lib edFi.common pullrequest.yml index bca8d84a1d..bbba3a4a2d 100644 --- a/.github/workflows/Lib edFi.common pullrequest.yml +++ b/.github/workflows/Lib edFi.common pullrequest.yml @@ -2,7 +2,7 @@ name: Lib EdFi.Common Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Lib edFi.loadtools pullrequest.yml b/.github/workflows/Lib edFi.loadtools pullrequest.yml index 09ef22e081..ed70d7c611 100644 --- a/.github/workflows/Lib edFi.loadtools pullrequest.yml +++ b/.github/workflows/Lib edFi.loadtools pullrequest.yml @@ -2,7 +2,7 @@ name: Lib EdFi.LoadTools Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Lib edFi.ods.api pullrequest .yml b/.github/workflows/Lib edFi.ods.api pullrequest .yml index 2d7665b399..46613f264b 100644 --- a/.github/workflows/Lib edFi.ods.api pullrequest .yml +++ b/.github/workflows/Lib edFi.ods.api pullrequest .yml @@ -2,7 +2,7 @@ name: Lib EdFi.Ods.Api Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Lib edFi.ods.common pullrequest.yml b/.github/workflows/Lib edFi.ods.common pullrequest.yml index 6699d6fc57..d6239ce2c4 100644 --- a/.github/workflows/Lib edFi.ods.common pullrequest.yml +++ b/.github/workflows/Lib edFi.ods.common pullrequest.yml @@ -2,7 +2,7 @@ name: Lib EdFi.Ods.Common Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Lib edFi.ods.standard pullrequest.yml b/.github/workflows/Lib edFi.ods.standard pullrequest.yml index caac299808..d7408185d9 100644 --- a/.github/workflows/Lib edFi.ods.standard pullrequest.yml +++ b/.github/workflows/Lib edFi.ods.standard pullrequest.yml @@ -2,7 +2,7 @@ name: Lib EdFi.Ods.Standard Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Lib edFi.security.dataaccess pullrequest.yml b/.github/workflows/Lib edFi.security.dataaccess pullrequest.yml index 86428f146f..45a652f6dd 100644 --- a/.github/workflows/Lib edFi.security.dataaccess pullrequest.yml +++ b/.github/workflows/Lib edFi.security.dataaccess pullrequest.yml @@ -2,7 +2,7 @@ name: Lib EdFi.Security.DataAccess Pull request build and test on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] env: INFORMATIONAL_VERSION: "7.1" diff --git a/.github/workflows/Pkg EdFi.Database.Admin.yml b/.github/workflows/Pkg EdFi.Database.Admin.yml index 4f5223f5e6..18c755cee2 100644 --- a/.github/workflows/Pkg EdFi.Database.Admin.yml +++ b/.github/workflows/Pkg EdFi.Database.Admin.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Database.Admin on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] paths: - '**.sql' workflow_dispatch: diff --git a/.github/workflows/Pkg EdFi.Database.Security.yml b/.github/workflows/Pkg EdFi.Database.Security.yml index 56db5a99da..f4a2f9ec73 100644 --- a/.github/workflows/Pkg EdFi.Database.Security.yml +++ b/.github/workflows/Pkg EdFi.Database.Security.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Database.Security on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] paths: - '**.sql' workflow_dispatch: diff --git a/.github/workflows/Pkg EdFi.Ods.CodeGen.yml b/.github/workflows/Pkg EdFi.Ods.CodeGen.yml index b6b1a269f6..a24e0f9091 100644 --- a/.github/workflows/Pkg EdFi.Ods.CodeGen.yml +++ b/.github/workflows/Pkg EdFi.Ods.CodeGen.yml @@ -2,10 +2,10 @@ name: Pkg EdFi.Ods.CodeGen on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.PostgreSQL.yml b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.PostgreSQL.yml index de7616dd70..614009ca1f 100644 --- a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.PostgreSQL.yml +++ b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.PostgreSQL.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Minimal.Template.PostgreSQL on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.PostgreSQL.yml b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.PostgreSQL.yml index 4b085534b5..6734dd6c2a 100644 --- a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.PostgreSQL.yml +++ b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.PostgreSQL.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Minimal.Template.TPDM.PostgreSQL on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.yml b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.yml index 97e4657d61..1be2fdf0a1 100644 --- a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.yml +++ b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.TPDM.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Minimal.Template.TPDM on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.yml b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.yml index 99d3601766..40000dbcc5 100644 --- a/.github/workflows/Pkg EdFi.Ods.Minimal.Template.yml +++ b/.github/workflows/Pkg EdFi.Ods.Minimal.Template.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Minimal.Template on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Populated.Template.PostgreSQL.yml b/.github/workflows/Pkg EdFi.Ods.Populated.Template.PostgreSQL.yml index e140982ecc..f10d7761b6 100644 --- a/.github/workflows/Pkg EdFi.Ods.Populated.Template.PostgreSQL.yml +++ b/.github/workflows/Pkg EdFi.Ods.Populated.Template.PostgreSQL.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Populated.Template.PostgreSQL on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.PostgreSQL.yml b/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.PostgreSQL.yml index bf1c83f0d3..0033ce7412 100644 --- a/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.PostgreSQL.yml +++ b/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.PostgreSQL.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Populated.Template.TPDM.PostgreSQL on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.yml b/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.yml index b6cb4b5060..903e1573fe 100644 --- a/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.yml +++ b/.github/workflows/Pkg EdFi.Ods.Populated.Template.TPDM.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Populated.Template.TPDM on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Pkg EdFi.Ods.Populated.Template.yml b/.github/workflows/Pkg EdFi.Ods.Populated.Template.yml index 6da119846c..5139c1ac93 100644 --- a/.github/workflows/Pkg EdFi.Ods.Populated.Template.yml +++ b/.github/workflows/Pkg EdFi.Ods.Populated.Template.yml @@ -8,7 +8,7 @@ name: Pkg EdFi.Ods.Populated.Template on: push: branches: - [main, 'ODS-*',b-v*-patch*] + [main, b-v*-patch*] workflow_dispatch: inputs: distinct_id: diff --git a/.github/workflows/Trgr InitDev workflows in Implementation repo.yml b/.github/workflows/Trgr InitDev workflows in Implementation repo.yml index 84d7588eac..e834e20f1c 100644 --- a/.github/workflows/Trgr InitDev workflows in Implementation repo.yml +++ b/.github/workflows/Trgr InitDev workflows in Implementation repo.yml @@ -2,7 +2,7 @@ name: Trgr InitDev workflows in Implementation repo on: pull_request: - branches: [main, 'ODS-*',b-v*-patch*] + branches: [main, b-v*-patch*] workflow_dispatch: env: diff --git a/Application/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJob.cs b/Application/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJob.cs index 7730d2f06b..dddcc6b945 100644 --- a/Application/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJob.cs +++ b/Application/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJob.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using EdFi.Admin.DataAccess.Authentication; using EdFi.Common; -using EdFi.Ods.Api.Middleware; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using log4net; using Quartz; diff --git a/Application/EdFi.Ods.Api/Jobs/TenantSpecificJobBase.cs b/Application/EdFi.Ods.Api/Jobs/TenantSpecificJobBase.cs index 2131df12a4..eaac5c347d 100644 --- a/Application/EdFi.Ods.Api/Jobs/TenantSpecificJobBase.cs +++ b/Application/EdFi.Ods.Api/Jobs/TenantSpecificJobBase.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using EdFi.Ods.Api.Middleware; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using log4net; using Quartz; diff --git a/Application/EdFi.Ods.Api/Middleware/ITenantConfigurationProvider.cs b/Application/EdFi.Ods.Api/Middleware/ITenantConfigurationProvider.cs index 51179d595c..4c347070dd 100644 --- a/Application/EdFi.Ods.Api/Middleware/ITenantConfigurationProvider.cs +++ b/Application/EdFi.Ods.Api/Middleware/ITenantConfigurationProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Autofac.Extras.DynamicProxy; using Castle.DynamicProxy; +using EdFi.Ods.Common.Configuration; namespace EdFi.Ods.Api.Middleware; diff --git a/Application/EdFi.Ods.Api/Middleware/TenantIdentificationMiddleware.cs b/Application/EdFi.Ods.Api/Middleware/TenantIdentificationMiddleware.cs index 89296e369a..72a805995f 100644 --- a/Application/EdFi.Ods.Api/Middleware/TenantIdentificationMiddleware.cs +++ b/Application/EdFi.Ods.Api/Middleware/TenantIdentificationMiddleware.cs @@ -4,6 +4,8 @@ // See the LICENSE and NOTICES files in the project root for more information. using System.Threading.Tasks; +using EdFi.Ods.Api.Extensions; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using log4net; using Microsoft.AspNetCore.Http; diff --git a/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs b/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs index 640449bdb8..f54ffb4f41 100644 --- a/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs +++ b/Application/EdFi.Ods.Api/Serialization/ProfilesAwareContractResolver.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Reflection; using EdFi.Ods.Common; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Models; using EdFi.Ods.Common.Models.Domain; @@ -37,6 +38,7 @@ public class ProfilesAwareContractResolver : DefaultContractResolver private readonly ConcurrentDictionary _fullNameByType = new(); private readonly IContextProvider _profileContentTypeContextProvider; + private readonly IContextProvider _tenantConfigurationContextProvider; private readonly IProfileResourceModelProvider _profileResourceModelProvider; private readonly string _resourcesNamespacePrefix = $"{Namespaces.Resources.BaseNamespace}."; private readonly ISchemaNameMapProvider _schemaNameMapProvider; @@ -46,6 +48,7 @@ public class ProfilesAwareContractResolver : DefaultContractResolver public ProfilesAwareContractResolver( IContextProvider profileContentTypeContextProvider, IContextProvider dataManagementResourceContextProvider, + IContextProvider tenantConfigurationContextProvider, IProfileResourceModelProvider profileResourceModelProvider, ISchemaNameMapProvider schemaNameMapProvider) { @@ -55,6 +58,8 @@ public ProfilesAwareContractResolver( _profileResourceModelProvider = profileResourceModelProvider ?? throw new ArgumentNullException(nameof(profileResourceModelProvider)); + _tenantConfigurationContextProvider = tenantConfigurationContextProvider; + _schemaNameMapProvider = schemaNameMapProvider ?? throw new ArgumentNullException(nameof(schemaNameMapProvider)); _profileContentTypeContextProvider = profileContentTypeContextProvider @@ -79,11 +84,14 @@ public override JsonContract ResolveContract(Type type) var resourceClassFullName = GetFullNameFromResourceTypeNamespace(type); + var tenantName = _tenantConfigurationContextProvider?.Get()?.TenantIdentifier; + var mappingContractKey = new MappingContractKey( resourceClassFullName, profileContentTypeContext.ProfileName, _dataManagementResourceContextProvider.Get().Resource.FullName, - profileContentTypeContext.ContentTypeUsage); + profileContentTypeContext.ContentTypeUsage, + tenantName); var contract = _contractByKey.GetOrAdd(mappingContractKey, static (k, args) => diff --git a/Application/EdFi.Ods.Api/Middleware/TenantConfiguration.cs b/Application/EdFi.Ods.Common/Configuration/TenantConfiguration.cs similarity index 98% rename from Application/EdFi.Ods.Api/Middleware/TenantConfiguration.cs rename to Application/EdFi.Ods.Common/Configuration/TenantConfiguration.cs index 865d4b72af..5a080c2890 100644 --- a/Application/EdFi.Ods.Api/Middleware/TenantConfiguration.cs +++ b/Application/EdFi.Ods.Common/Configuration/TenantConfiguration.cs @@ -8,7 +8,7 @@ using EdFi.Ods.Common.Extensions; using Standart.Hash.xxHash; -namespace EdFi.Ods.Api.Middleware; +namespace EdFi.Ods.Common.Configuration; /// /// Contains details related to the tenant configuration and associated database connection strings. diff --git a/Application/EdFi.Ods.Common/Metadata/Profiles/EmbeddedResourceProfileDefinitionsProvider.cs b/Application/EdFi.Ods.Common/Metadata/Profiles/EmbeddedResourceProfileDefinitionsProvider.cs index 606c71bdfe..93f050cbe3 100644 --- a/Application/EdFi.Ods.Common/Metadata/Profiles/EmbeddedResourceProfileDefinitionsProvider.cs +++ b/Application/EdFi.Ods.Common/Metadata/Profiles/EmbeddedResourceProfileDefinitionsProvider.cs @@ -80,7 +80,23 @@ private XDocument[] LazyInitializeXDocuments() private IDictionary LazyInitializeProfileDefinitions() { - return _allDocs.Value.SelectMany(x => x.Descendants("Profile")) + var profileDefinitions = _allDocs.Value.SelectMany(x => x.Descendants("Profile")).ToArray(); + + var profilesWithDuplicateDefinitions = profileDefinitions + .GroupBy(x => x.AttributeValue("name"), StringComparer.OrdinalIgnoreCase) + .Where(x => x.Count() > 1) + .SelectMany(x => profileDefinitions.Where(y => y.AttributeValue("name").Equals(x.Key, StringComparison.OrdinalIgnoreCase))) + .ToArray(); + + if (profilesWithDuplicateDefinitions.Any()) + { + var duplicateProfileNames = string.Join(", ", profilesWithDuplicateDefinitions.Select(x => x.AttributeValue("name")).Distinct(StringComparer.OrdinalIgnoreCase)); + + _logger.Error( + $"The following profile names were not loaded from plugin assemblies because multiple XML definitions were provided through embedded resources: {duplicateProfileNames}"); + } + + return profileDefinitions.Except(profilesWithDuplicateDefinitions) .ToDictionary(x => x.AttributeValue("name"), x => x, StringComparer.InvariantCultureIgnoreCase); } } diff --git a/Application/EdFi.Ods.Common/Metadata/Profiles/IProfileMetadataProvider.cs b/Application/EdFi.Ods.Common/Metadata/Profiles/IProfileMetadataProvider.cs index 39af538e6d..b2d21d9708 100644 --- a/Application/EdFi.Ods.Common/Metadata/Profiles/IProfileMetadataProvider.cs +++ b/Application/EdFi.Ods.Common/Metadata/Profiles/IProfileMetadataProvider.cs @@ -21,7 +21,7 @@ public interface IProfileMetadataProvider /// /// Collection of valid profile definitions, organized by name. /// - IReadOnlyDictionary ProfileDefinitionsByName { get; } + IReadOnlyDictionary GetProfileDefinitionsByName(); /// /// Gets the validation results for all profile metadata that has been loaded (or attempted to be loaded). diff --git a/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataProvider.cs b/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataProvider.cs index 687a53406a..1db4eb3760 100644 --- a/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataProvider.cs +++ b/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataProvider.cs @@ -29,8 +29,27 @@ public ProfileMetadataProvider(IProfileDefinitionsProvider[] profileDefinitionsP /// /// Collection of valid profile definitions, organized by name. /// - public IReadOnlyDictionary ProfileDefinitionsByName => - _profileDefinitionsProviders.SelectMany(x => x.GetProfileDefinitions()).ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); + public IReadOnlyDictionary GetProfileDefinitionsByName() + { + var profileDefinitions = _profileDefinitionsProviders.SelectMany(x => x.GetProfileDefinitions()).ToArray(); + + var profilesWithDuplicateDefinitions = profileDefinitions + .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) + .Where(x => x.Count() > 1) + .SelectMany(x => profileDefinitions.Where(y => y.Key.Equals(x.Key, StringComparison.OrdinalIgnoreCase))) + .ToArray(); + + if (profilesWithDuplicateDefinitions.Any()) + { + var duplicateProfileNames = string.Join(", ", profilesWithDuplicateDefinitions.Select(x => x.Key).Distinct(StringComparer.OrdinalIgnoreCase)); + + _logger.Error( + $"The following profile names were not loaded because multiple XML definitions were provided: {duplicateProfileNames}"); + } + + return profileDefinitions.Except(profilesWithDuplicateDefinitions).ToDictionary( + x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase); + } /// public MetadataValidationResult[] GetValidationResults() diff --git a/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataValidator.cs b/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataValidator.cs index a7b4c32788..e7750bbb82 100644 --- a/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataValidator.cs +++ b/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileMetadataValidator.cs @@ -52,24 +52,6 @@ public ValidationResult Validate(XDocument profilesDefinition) { return new ValidationResult(validationFailures); } - - // Find duplicate profiles names - var duplicateProfileNames = profilesDefinition.XPathSelectElements("//Profile") - .Select(e => e.AttributeValue("name")) - .GroupBy(n => n, n => n) - .Where(x => x.Count() > 1) - .Select(x => x.Key) - .OrderBy(n => n) - .ToArray(); - - if (duplicateProfileNames.Any()) - { - validationFailures.Add(new ValidationFailure( - string.Empty, - $"Duplicate profile name(s) encountered: '{string.Join("', '", duplicateProfileNames)}'")); - - return new ValidationResult(validationFailures); - } // Resource model validation var resourceModel = _resourceModelProvider.GetResourceModel(); diff --git a/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileResourceNamesProvider.cs b/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileResourceNamesProvider.cs index 4bc918df2f..d5d64ed854 100644 --- a/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileResourceNamesProvider.cs +++ b/Application/EdFi.Ods.Common/Metadata/Profiles/ProfileResourceNamesProvider.cs @@ -29,7 +29,7 @@ public List GetProfileResourceNames() _logger.Debug("Initializing Profile and resource names..."); } - return _profileMetadataProvider.ProfileDefinitionsByName.Values.SelectMany(CreateNameTuples).ToList(); + return _profileMetadataProvider.GetProfileDefinitionsByName().Values.SelectMany(CreateNameTuples).ToList(); IEnumerable CreateNameTuples(XElement profileElt) { diff --git a/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs b/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs index d287bd8d3e..f3469334d3 100644 --- a/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs +++ b/Application/EdFi.Ods.Common/Models/MappingContractProvider.cs @@ -9,6 +9,7 @@ using EdFi.Common; using EdFi.Common.Extensions; using EdFi.Common.Inflection; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Conventions; using EdFi.Ods.Common.Exceptions; @@ -24,6 +25,7 @@ public class MappingContractProvider : IMappingContractProvider { private readonly IContextProvider _dataManagementResourceContextProvider; private readonly IContextProvider _profileContentTypeContextProvider; + private readonly IContextProvider _tenantConfigurationContextProvider; private readonly IProfileResourceModelProvider _profileResourceModelProvider; private readonly ISchemaNameMapProvider _schemaNameMapProvider; private readonly ILog _logger = LogManager.GetLogger(typeof(MappingContractProvider)); @@ -34,6 +36,7 @@ private readonly ConcurrentDictionary public MappingContractProvider( IContextProvider profileContentTypeContextProvider, IContextProvider dataManagementResourceContextProvider, + IContextProvider tenantConfigurationContextProvider, IProfileResourceModelProvider profileResourceModelProvider, ISchemaNameMapProvider schemaNameMapProvider) { @@ -42,6 +45,9 @@ public MappingContractProvider( _profileContentTypeContextProvider = profileContentTypeContextProvider ?? throw new ArgumentNullException(nameof(profileContentTypeContextProvider)); + + _tenantConfigurationContextProvider = tenantConfigurationContextProvider + ?? throw new ArgumentNullException(nameof(tenantConfigurationContextProvider)); _profileResourceModelProvider = profileResourceModelProvider ?? throw new ArgumentNullException(nameof(profileResourceModelProvider)); @@ -68,11 +74,15 @@ public IMappingContract GetMappingContract(FullName resourceClassFullName) "The resource in the profile-based content type does not match the resource targeted by the request."); } + var tenantConfigurationContext = _tenantConfigurationContextProvider.Get(); + var mappingContractKey = new MappingContractKey( resourceClassFullName, profileContentTypeContext.ProfileName, dataManagementResourceContext.Resource.FullName, - profileContentTypeContext.ContentTypeUsage); + profileContentTypeContext.ContentTypeUsage, + tenantConfigurationContext?.TenantIdentifier + ); return GetOrCreateMappingContract(mappingContractKey); } @@ -84,7 +94,7 @@ private IMappingContract GetOrCreateMappingContract(MappingContractKey mappingCo { return null; } - + var mappingContract = _mappingContractByKey.GetOrAdd( mappingContractKey, static (key, @this) => @@ -244,11 +254,13 @@ public MappingContractKey(FullName resourceClassName) /// The name of the profile. /// The of the resource in context (e.g. the School or LocalEducationAgency for a resource class of EducationOrganizationAddress). /// The usage of the profile (readable or writable). + /// (Optional) In a multi-tenant environment, a unique identifier of the tenant for the current context. public MappingContractKey( FullName resourceClassName, string profileName, FullName profileResourceName, - ContentTypeUsage contentTypeUsage) + ContentTypeUsage contentTypeUsage, + string tenantIdentifier = null) { ResourceClassName = resourceClassName; @@ -262,6 +274,8 @@ public MappingContractKey( } ContentTypeUsage = contentTypeUsage; + + TenantIdentifier = tenantIdentifier ?? ""; } public FullName ResourceClassName { get; } @@ -271,6 +285,8 @@ public MappingContractKey( public FullName ProfileResourceName { get; } public ContentTypeUsage ContentTypeUsage { get; } + + public string TenantIdentifier { get; } public bool Equals(MappingContractKey other) { @@ -287,7 +303,8 @@ public bool Equals(MappingContractKey other) return ResourceClassName.Equals(other.ResourceClassName) && string.Equals(ProfileName, other.ProfileName) && ProfileResourceName.Equals(other.ProfileResourceName) - && ContentTypeUsage == other.ContentTypeUsage; + && ContentTypeUsage == other.ContentTypeUsage + && string.Equals(TenantIdentifier,other.TenantIdentifier); } public override bool Equals(object obj) @@ -324,6 +341,11 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ ProfileResourceName.GetHashCode(); hashCode = (hashCode * 397) ^ (int)ContentTypeUsage; + if (!string.IsNullOrEmpty(TenantIdentifier)) + { + hashCode = (hashCode * 397) ^ TenantIdentifier.GetHashCode(); + } + return hashCode; } } diff --git a/Application/EdFi.Ods.Common/Models/ProfileResourceModelProvider.cs b/Application/EdFi.Ods.Common/Models/ProfileResourceModelProvider.cs index b861fb95c5..fbb50d55fb 100644 --- a/Application/EdFi.Ods.Common/Models/ProfileResourceModelProvider.cs +++ b/Application/EdFi.Ods.Common/Models/ProfileResourceModelProvider.cs @@ -41,7 +41,7 @@ public ProfileResourceModel GetProfileResourceModel(string profileName) { return new ProfileResourceModel( _resourceModel.Value, - _profileMetadataProvider.ProfileDefinitionsByName.GetValueOrThrow(profileName, "Unable to find profile '{0}'."), + _profileMetadataProvider.GetProfileDefinitionsByName().GetValueOrThrow(profileName, "Unable to find profile '{0}'."), _profileValidationReporter); } } diff --git a/Application/EdFi.Ods.Features/Container/Modules/MultitenancyModule.cs b/Application/EdFi.Ods.Features/Container/Modules/MultitenancyModule.cs index e5e996c5a8..506e73f44c 100644 --- a/Application/EdFi.Ods.Features/Container/Modules/MultitenancyModule.cs +++ b/Application/EdFi.Ods.Features/Container/Modules/MultitenancyModule.cs @@ -16,9 +16,11 @@ using EdFi.Ods.Common.Configuration.Sections; using EdFi.Ods.Common.Constants; using EdFi.Ods.Common.Container; +using EdFi.Ods.Common.Profiles; using EdFi.Ods.Features.MultiTenancy; using EdFi.Security.DataAccess.Providers; using log4net; +using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -93,10 +95,12 @@ public override void ApplyConfigurationSpecificRegistrations(ContainerBuilder bu ctx => { var apiSettings = ctx.Resolve(); + var mediator = ctx.Resolve(); return (ICacheProvider) new ExpiringConcurrentDictionaryCacheProvider( "Profile Metadata", - TimeSpan.FromSeconds(apiSettings.Caching.Profiles.AbsoluteExpirationSeconds)); + TimeSpan.FromSeconds(apiSettings.Caching.Profiles.AbsoluteExpirationSeconds), + () => mediator.Publish(new ProfileMetadataCacheExpired())); }) .SingleInstance(); diff --git a/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs b/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs index 698704dca1..0734f5895c 100644 --- a/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs +++ b/Application/EdFi.Ods.Features/Container/Modules/ProfilesModule.cs @@ -28,8 +28,13 @@ namespace EdFi.Ods.Features.Container.Modules { public class ProfilesModule : ConditionalModule { + private readonly ApiSettings _apiSettings; + public ProfilesModule(ApiSettings apiSettings) - : base(apiSettings, nameof(ProfilesModule)) { } + : base(apiSettings, nameof(ProfilesModule)) + { + _apiSettings = apiSettings; + } public override bool IsSelected() => IsFeatureEnabled(ApiFeature.Profiles); @@ -53,20 +58,24 @@ public override void ApplyConfigurationSpecificRegistrations(ContainerBuilder bu .EnableInterfaceInterceptors() .SingleInstance(); - builder.RegisterType() - .Named("cache-profile-metadata") - .WithParameter( - ctx => - { - var mediator = ctx.Resolve(); - - return (ICacheProvider)new ExpiringConcurrentDictionaryCacheProvider( - "Profile Metadata", - TimeSpan.FromSeconds(ApiSettings.Caching.Profiles.AbsoluteExpirationSeconds), - () => mediator.Publish(new ProfileMetadataCacheExpired())); - }) - .SingleInstance(); - + //When MultiTenancy is enabled, the CachingInterceptor is registered as a ContextualCachingInterceptor in the MultiTenancyModule + if (!IsFeatureEnabled(ApiFeature.MultiTenancy)) + { + builder.RegisterType() + .Named(InterceptorCacheKeys.ProfileMetadata) + .WithParameter( + ctx => + { + var mediator = ctx.Resolve(); + + return (ICacheProvider)new ExpiringConcurrentDictionaryCacheProvider( + "Profile Metadata", + TimeSpan.FromSeconds(_apiSettings.Caching.Profiles.AbsoluteExpirationSeconds), + () => mediator.Publish(new ProfileMetadataCacheExpired())); + }) + .SingleInstance(); + } + builder.RegisterType() .As() .SingleInstance(); diff --git a/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantAdminDatabaseConnectionStringProvider.cs b/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantAdminDatabaseConnectionStringProvider.cs index 7bba3c3e9c..4e71125057 100644 --- a/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantAdminDatabaseConnectionStringProvider.cs +++ b/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantAdminDatabaseConnectionStringProvider.cs @@ -6,7 +6,7 @@ using System; using EdFi.Admin.DataAccess.Providers; using EdFi.Common.Database; -using EdFi.Ods.Api.Middleware; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using log4net; diff --git a/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantOdsInstanceHashIdGenerator.cs b/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantOdsInstanceHashIdGenerator.cs index 8aff157731..97e8d08ef0 100644 --- a/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantOdsInstanceHashIdGenerator.cs +++ b/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantOdsInstanceHashIdGenerator.cs @@ -4,8 +4,8 @@ // See the LICENSE and NOTICES files in the project root for more information. using EdFi.Ods.Api.Configuration; -using EdFi.Ods.Api.Middleware; using EdFi.Ods.Common.Caching; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; namespace EdFi.Ods.Features.MultiTenancy; diff --git a/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantSecurityDatabaseConnectionStringProvider.cs b/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantSecurityDatabaseConnectionStringProvider.cs index d30396bb6c..9832159150 100644 --- a/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantSecurityDatabaseConnectionStringProvider.cs +++ b/Application/EdFi.Ods.Features/MultiTenancy/MultiTenantSecurityDatabaseConnectionStringProvider.cs @@ -6,6 +6,7 @@ using System; using EdFi.Common.Database; using EdFi.Ods.Api.Middleware; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using EdFi.Security.DataAccess.Providers; using log4net; diff --git a/Application/EdFi.Ods.Features/MultiTenancy/TenantConfigurationProvider.cs b/Application/EdFi.Ods.Features/MultiTenancy/TenantConfigurationProvider.cs index 3fa22155f2..d8b0c440d9 100644 --- a/Application/EdFi.Ods.Features/MultiTenancy/TenantConfigurationProvider.cs +++ b/Application/EdFi.Ods.Features/MultiTenancy/TenantConfigurationProvider.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using EdFi.Ods.Api.Middleware; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Configuration.Sections; using Microsoft.Extensions.Options; diff --git a/Application/EdFi.Ods.Features/Profiles/AdminDatabaseProfileDefinitionsProvider.cs b/Application/EdFi.Ods.Features/Profiles/AdminDatabaseProfileDefinitionsProvider.cs index 6d27a41b53..5cc2654a4f 100644 --- a/Application/EdFi.Ods.Features/Profiles/AdminDatabaseProfileDefinitionsProvider.cs +++ b/Application/EdFi.Ods.Features/Profiles/AdminDatabaseProfileDefinitionsProvider.cs @@ -63,6 +63,14 @@ IDictionary IProfileDefinitionsProvider.GetProfileDefinitions( var validationDoc = new XDocument(profilesElement); var validationResult = _profileMetadataValidator.Validate(validationDoc); + + var profileNameFromXmlDefinition = profileDefinition.AttributeValue("name"); + + if(!profile.ProfileName.Equals(profileNameFromXmlDefinition, StringComparison.OrdinalIgnoreCase)) + { + _logger.Error($"A profile could not be loaded because the profile name '{profile.ProfileName}' in the database does not match the profile name '{profileNameFromXmlDefinition}' in its XML definition."); + continue; + } if (!validationResult.IsValid) { @@ -76,7 +84,7 @@ IDictionary IProfileDefinitionsProvider.GetProfileDefinitions( continue; } - profileDefinitionByName.Add(profileDefinition.AttributeValue("name"), profileDefinition); + profileDefinitionByName.Add(profile.ProfileName, profileDefinition); } catch (XmlException) { diff --git a/Application/EdFi.Ods.Features/Profiles/AdminProfileNamesPublisherJob.cs b/Application/EdFi.Ods.Features/Profiles/AdminProfileNamesPublisherJob.cs index 0435aa5977..d3774e7a0e 100644 --- a/Application/EdFi.Ods.Features/Profiles/AdminProfileNamesPublisherJob.cs +++ b/Application/EdFi.Ods.Features/Profiles/AdminProfileNamesPublisherJob.cs @@ -6,8 +6,8 @@ using System; using System.Threading.Tasks; using EdFi.Ods.Api.Jobs; -using EdFi.Ods.Api.Middleware; using EdFi.Ods.Api.Security.Profiles; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using log4net; using Quartz; diff --git a/Application/EdFi.Ods.Features/Profiles/ProfileContentTypeContextMiddleware.cs b/Application/EdFi.Ods.Features/Profiles/ProfileContentTypeContextMiddleware.cs index cc648a5cbd..44689dab9e 100644 --- a/Application/EdFi.Ods.Features/Profiles/ProfileContentTypeContextMiddleware.cs +++ b/Application/EdFi.Ods.Features/Profiles/ProfileContentTypeContextMiddleware.cs @@ -36,6 +36,8 @@ public class ProfileContentTypeContextMiddleware "The profile usage segment in the profile-based '{0}' header was not recognized."; private const string NonExistingProfileFormat = "The profile specified by the content type in the '{0}' header is not supported by this host."; + private const string MisconfiguredProfileFormat = + "The profile specified by the content type in the '{0}' header is configured but invalid."; private const int ResourceNameFacet = 0; private const int ProfileNameFacet = 1; @@ -161,7 +163,18 @@ await WriteResponse( // Validate that the Profile exists string profileName = profileContentTypeFacets[ProfileNameFacet].Value; - if (!_profileMetadataProvider.ProfileDefinitionsByName.ContainsKey(profileName)) + if (_profileMetadataProvider.GetValidationResults().Any(x => x.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase))) + { + await WriteResponse( + response, + StatusCodes.Status406NotAcceptable, + headerName, + MisconfiguredProfileFormat); + + return (false, null); + } + + if (!_profileMetadataProvider.GetProfileDefinitionsByName().ContainsKey(profileName)) { await WriteResponse( response, diff --git a/Application/EdFi.Ods.Profiles.Test/ProfilesTenant1.xml b/Application/EdFi.Ods.Profiles.Test/ProfilesTenant1.xml new file mode 100644 index 0000000000..6f6a1de83f --- /dev/null +++ b/Application/EdFi.Ods.Profiles.Test/ProfilesTenant1.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Application/EdFi.Ods.Profiles.Test/ProfilesTenant2.xml b/Application/EdFi.Ods.Profiles.Test/ProfilesTenant2.xml new file mode 100644 index 0000000000..35e1056038 --- /dev/null +++ b/Application/EdFi.Ods.Profiles.Test/ProfilesTenant2.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJobTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJobTests.cs index f911550668..769d3d2f50 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJobTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/Jobs/DeleteExpiredTokensJobTests.cs @@ -5,6 +5,7 @@ using EdFi.Admin.DataAccess.Authentication; using EdFi.Ods.Api.Jobs; using EdFi.Ods.Api.Middleware; +using EdFi.Ods.Common.Configuration; using EdFi.Ods.Common.Context; using FakeItEasy; using NUnit.Framework; diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/ProfileResourceModelProviderTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/ProfileResourceModelProviderTests.cs index d06f2b27e0..37d1d72bd4 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/ProfileResourceModelProviderTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Common/Models/ProfileResourceModelProviderTests.cs @@ -114,7 +114,7 @@ protected override void Arrange() { resourceModelProvider = A.Fake(); profileMetadaProvider = A.Fake(); - A.CallTo(() => profileMetadaProvider.ProfileDefinitionsByName) + A.CallTo(() => profileMetadaProvider.GetProfileDefinitionsByName()) .Returns(new Dictionary() { {"Profile1", diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/FactoryStrategies/OpenApiMetadataProfilePathsFactoryStrategyTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/FactoryStrategies/OpenApiMetadataProfilePathsFactoryStrategyTests.cs index b64f3b77ab..73f837eba1 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/FactoryStrategies/OpenApiMetadataProfilePathsFactoryStrategyTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/FactoryStrategies/OpenApiMetadataProfilePathsFactoryStrategyTests.cs @@ -71,7 +71,7 @@ private class TestProfileResourceNamesProvider : IProfileMetadataProvider "; - public IReadOnlyDictionary ProfileDefinitionsByName + public IReadOnlyDictionary GetProfileDefinitionsByName() => new Dictionary { { TestProfileName, XElement.Parse(_profileDefinition) } }; public MetadataValidationResult[] GetValidationResults() => Array.Empty(); @@ -125,7 +125,7 @@ protected override void Arrange() var profileResourceModel = new ProfileResourceModel( _resourceModelProvider.GetResourceModel(), - _testProfileResourceNamesProvider.ProfileDefinitionsByName.GetValueOrThrow(TestProfileName, "Unable to find profile '{0}'."), + _testProfileResourceNamesProvider.GetProfileDefinitionsByName().GetValueOrThrow(TestProfileName, "Unable to find profile '{0}'."), profileValidationReporter); _openApiMetadataDocumentContext = diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/ResourceStrategies/OpenApiProfileStrategyTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/ResourceStrategies/OpenApiProfileStrategyTests.cs index 3acc3c5a5f..77c29b5205 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/ResourceStrategies/OpenApiProfileStrategyTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Features/OpenApiMetadata/Strageties/ResourceStrategies/OpenApiProfileStrategyTests.cs @@ -71,7 +71,7 @@ private class TestProfileResourceNamesProvider : IProfileMetadataProvider "; - public IReadOnlyDictionary ProfileDefinitionsByName + public IReadOnlyDictionary GetProfileDefinitionsByName() => new Dictionary { { TestProfileName, XElement.Parse(_profileDefinition) } }; public MetadataValidationResult[] GetValidationResults() => Array.Empty(); @@ -132,7 +132,7 @@ protected override void Arrange() var profileResourceModel = new ProfileResourceModel( _resourceModelProvider.GetResourceModel(), - _testProfileResourceNamesProvider.ProfileDefinitionsByName.GetValueOrThrow(TestProfileName, "Unable to find profile '{0}'."), + _testProfileResourceNamesProvider.GetProfileDefinitionsByName().GetValueOrThrow(TestProfileName, "Unable to find profile '{0}'."), profileValidationReporter); _openApiMetadataDocumentContext = diff --git a/Artifacts/MsSql/Structure/Admin/0177-Add-Unique-Constraint-Profiles.sql b/Artifacts/MsSql/Structure/Admin/0177-Add-Unique-Constraint-Profiles.sql new file mode 100644 index 0000000000..2289bda126 --- /dev/null +++ b/Artifacts/MsSql/Structure/Admin/0177-Add-Unique-Constraint-Profiles.sql @@ -0,0 +1,12 @@ +-- 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. + +ALTER TABLE dbo.Profiles +ALTER COLUMN ProfileName NVARCHAR(500) NOT NULL +GO + +ALTER TABLE dbo.Profiles DROP CONSTRAINT IF EXISTS UQ_Profiles_ProfileName; +ALTER TABLE dbo.Profiles ADD CONSTRAINT UQ_Profiles_ProfileName UNIQUE (ProfileName); +GO diff --git a/Artifacts/PgSql/Structure/Admin/0177-Add-Unique-Constraint-Profiles.sql b/Artifacts/PgSql/Structure/Admin/0177-Add-Unique-Constraint-Profiles.sql new file mode 100644 index 0000000000..b22b3f18b3 --- /dev/null +++ b/Artifacts/PgSql/Structure/Admin/0177-Add-Unique-Constraint-Profiles.sql @@ -0,0 +1,43 @@ +-- 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. +DO $$ +BEGIN + +DROP VIEW IF EXISTS dbo.apiclientidentityrawdetails; + +ALTER TABLE IF EXISTS dbo.profiles +ALTER COLUMN profilename TYPE character varying(500) COLLATE pg_catalog."default"; + +ALTER TABLE dbo.profiles DROP CONSTRAINT IF EXISTS profiles_profilename_key; +ALTER TABLE dbo.profiles ADD CONSTRAINT profiles_profilename_key UNIQUE (profilename); + +CREATE OR REPLACE VIEW dbo.apiclientidentityrawdetails + AS + SELECT ac.key, + ac.usesandbox, + ac.studentidentificationsystemdescriptor, + aeo.educationorganizationid, + app.claimsetname, + vnp.namespaceprefix, + p.profilename, + ac.creatorownershiptokenid_ownershiptokenid AS creatorownershiptokenid, + acot.ownershiptoken_ownershiptokenid AS ownershiptokenid, + ac.apiclientid, + ac.secret, + ac.secretishashed + FROM dbo.apiclients ac + JOIN dbo.applications app ON app.applicationid = ac.application_applicationid + LEFT JOIN dbo.vendors v ON v.vendorid = app.vendor_vendorid + LEFT JOIN dbo.vendornamespaceprefixes vnp ON v.vendorid = vnp.vendor_vendorid + LEFT JOIN dbo.apiclientapplicationeducationorganizations acaeo ON acaeo.apiclient_apiclientid = ac.apiclientid + LEFT JOIN dbo.applicationeducationorganizations aeo ON aeo.applicationeducationorganizationid = acaeo.applicationedorg_applicationedorgid + LEFT JOIN dbo.profileapplications ap ON ap.application_applicationid = app.applicationid + LEFT JOIN dbo.profiles p ON p.profileid = ap.profile_profileid + LEFT JOIN dbo.apiclientownershiptokens acot ON ac.apiclientid = acot.apiclient_apiclientid; + +ALTER TABLE dbo.apiclientidentityrawdetails + OWNER TO postgres; + +END $$; diff --git a/Postman Test Suite/Multitenancy/Ed-Fi ODS-API Multi-Tenancy.postman_collection.json b/Postman Test Suite/Multitenancy/Ed-Fi ODS-API Multi-Tenancy.postman_collection.json index d4eea5e7e1..f784ede1e8 100644 --- a/Postman Test Suite/Multitenancy/Ed-Fi ODS-API Multi-Tenancy.postman_collection.json +++ b/Postman Test Suite/Multitenancy/Ed-Fi ODS-API Multi-Tenancy.postman_collection.json @@ -1690,6 +1690,723 @@ ] } ] + }, + { + "name": "Profiles", + "item": [ + { + "name": "Setup", + "item": [ + { + "name": "Create Tenant1 School", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {\r", + " pm.expect(pm.response.code).to.equal(201);\r", + "});\r", + "\r", + "pm.environment.set(\"known:tenant1:schoolId\", pm.response.headers.one(\"Location\").value.split(\"/\").pop());\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.environment.set(\"supplied:tenant1:schoolId\", pm.variables.replaceIn(\"12{{$randomInt}}{{$randomInt}}\"));", + "pm.environment.set(\"supplied:tenant1:tenantSpecificTestProfileName\", \"tenant-one-test-profile\");", + "pm.environment.set(\"supplied:tenant1:nonIncludedPropertyName\", \"schoolTypeDescriptor\");", + "pm.environment.set(\"supplied:tenant1:nonIncludedPropertyValue\", \"uri://ed-fi.org/SchoolTypeDescriptor#Regular\");", + "pm.environment.set(\"supplied:tenant1:includedPropertyName\", \"administrativeFundingControlDescriptor\");", + "pm.environment.set(\"supplied:tenant1:includedPropertyValue\", \"uri://ed-fi.org/AdministrativeFundingControlDescriptor#Public School\");", + "pm.environment.set(\"supplied:tenant1:testProfileWithCommonName\", \"multitenancy-profile-same-name\");", + "pm.environment.set(\"supplied:tenant1:commonNameProfileIncludedPropertyName\", \"operationalStatusDescriptor\");", + "pm.environment.set(\"supplied:tenant1:commonNameProfileIncludedPropertyValue\", \"uri://ed-fi.org/OperationalStatusDescriptor#Active\");", + "pm.environment.set(\"supplied:tenant1:commonNameProfileNonIncludedPropertyName\", \"charterStatusDescriptor\");", + "pm.environment.set(\"supplied:tenant1:commonNameProfileNonIncludedPropertyValue\", \"uri://ed-fi.org/charterStatusDescriptor#Open Enrollment\");" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant1}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"schoolId\": {{supplied:tenant1:schoolId}},\r\n \"nameOfInstitution\" : \"Tenant1 School\",\r\n \"{{supplied:tenant1:commonNameProfileIncludedPropertyName}}\": \"{{supplied:tenant1:commonNameProfileIncludedPropertyValue}}\",\r\n \"{{supplied:tenant1:commonNameProfileNonIncludedPropertyName}}\": \"{{supplied:tenant1:commonNameProfileNonIncludedPropertyValue}}\",\r\n \"{{supplied:tenant1:nonIncludedPropertyName}}\": \"{{supplied:tenant1:nonIncludedPropertyValue}}\",\r\n \"{{supplied:tenant1:includedPropertyName}}\": \"{{supplied:tenant1:includedPropertyValue}}\",\r\n \"addresses\": [\r\n {\r\n \"addressTypeDescriptor\": \"uri://ed-fi.org/AddressTypeDescriptor#Physical\",\r\n \"city\": \"Austin\",\r\n \"postalCode\": \"78712\",\r\n \"stateAbbreviationDescriptor\": \"uri://ed-fi.org/StateAbbreviationDescriptor#TX\",\r\n \"streetNumberName\": \"1912 Speedway Stop D5000\",\r\n \"nameOfCounty\": \"Travis\"\r\n }\r\n ],\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://tpdm.ed-fi.org/EducationOrganizationCategoryDescriptor#University\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Postsecondary\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/Tenant1/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant1", + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] + }, + { + "name": "Create Tenant2 School", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 201\", () => {\r", + " pm.expect(pm.response.code).to.equal(201);\r", + "});\r", + "\r", + "pm.environment.set(\"known:tenant2:schoolId\", pm.response.headers.one(\"Location\").value.split(\"/\").pop());\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.environment.set(\"supplied:tenant2:schoolId\", pm.variables.replaceIn(\"12{{$randomInt}}{{$randomInt}}\"));", + "pm.environment.set(\"supplied:tenant2:tenantSpecificTestProfileName\", \"tenant-two-test-profile\");", + "pm.environment.set(\"supplied:tenant2:nonIncludedPropertyName\", \"administrativeFundingControlDescriptor\");", + "pm.environment.set(\"supplied:tenant2:nonIncludedPropertyValue\", \"uri://ed-fi.org/AdministrativeFundingControlDescriptor#Public School\");", + "pm.environment.set(\"supplied:tenant2:includedPropertyName\", \"schoolTypeDescriptor\");", + "pm.environment.set(\"supplied:tenant2:includedPropertyValue\", \"uri://ed-fi.org/SchoolTypeDescriptor#Regular\");", + "pm.environment.set(\"supplied:tenant2:testProfileWithCommonName\", \"multitenancy-profile-same-name\");", + "pm.environment.set(\"supplied:tenant2:commonNameProfileIncludedPropertyName\", \"charterStatusDescriptor\");", + "pm.environment.set(\"supplied:tenant2:commonNameProfileIncludedPropertyValue\", \"uri://ed-fi.org/CharterStatusDescriptor#Not a Charter School\");", + "pm.environment.set(\"supplied:tenant2:commonNameProfileNonIncludedPropertyName\", \"operationalStatusDescriptor\");", + "pm.environment.set(\"supplied:tenant2:commonNameProfileNonIncludedPropertyValue\", \"uri://ed-fi.org/OperationalStatusDescriptor#Active\");" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant2}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"schoolId\": {{supplied:tenant2:schoolId}},\r\n \"nameOfInstitution\" : \"Tenant2 School\",\r\n \"{{supplied:tenant2:commonNameProfileIncludedPropertyName}}\": \"{{supplied:tenant2:commonNameProfileIncludedPropertyValue}}\",\r\n \"{{supplied:tenant2:commonNameProfileNonIncludedPropertyName}}\": \"{{supplied:tenant2:commonNameProfileNonIncludedPropertyValue}}\",\r\n \"{{supplied:tenant2:nonIncludedPropertyName}}\": \"{{supplied:tenant2:nonIncludedPropertyValue}}\",\r\n \"{{supplied:tenant2:includedPropertyName}}\": \"{{supplied:tenant2:includedPropertyValue}}\",\r\n \"addresses\": [\r\n {\r\n \"addressTypeDescriptor\": \"uri://ed-fi.org/AddressTypeDescriptor#Physical\",\r\n \"city\": \"Austin\",\r\n \"postalCode\": \"78712\",\r\n \"stateAbbreviationDescriptor\": \"uri://ed-fi.org/StateAbbreviationDescriptor#TX\",\r\n \"streetNumberName\": \"1912 Speedway Stop D5000\",\r\n \"nameOfCounty\": \"Travis\"\r\n }\r\n ],\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://tpdm.ed-fi.org/EducationOrganizationCategoryDescriptor#University\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Postsecondary\"\r\n }\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{ApiBaseUrl}}/Tenant2/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant2", + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Tenant1 Requests", + "item": [ + { + "name": "Get School from Tenant1 using a Tenant1 Profile", + "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(\"Response Content-Type header should match profile usage\",\r", + " () => {\r", + " pm.expect(pm.response).to.have.header(\"Content-Type\");\r", + " pm.expect(pm.response.headers.one(\"Content-Type\").value).to.contain(pm.variables.replaceIn(\"application/vnd.ed-fi.school.{{supplied:tenant1:tenantSpecificTestProfileName}}.readable+json\"));\r", + " });\r", + "\r", + "\r", + "pm.test(\"Response should not contain a property not included in the Tenant1 test profile\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.not.have.property(pm.variables.replaceIn(\"{{supplied:tenant1:nonIncludedPropertyName}}\"));\r", + "});\r", + "\r", + "pm.test(\"The value of the property included in the Tenant1 test profile should match the expected value for Tenant1\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.have.property(pm.variables.replaceIn(\"{{supplied:tenant1:includedPropertyName}}\"));\r", + " pm.expect(responseData[pm.variables.replaceIn(\"{{supplied:tenant1:includedPropertyName}}\")]).to.equal(pm.variables.replaceIn(\"{{supplied:tenant1:includedPropertyValue}}\"));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant1}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.ed-fi.school.{{supplied:tenant1:tenantSpecificTestProfileName}}.readable+json", + "type": "text" + } + ], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant1/data/v3/ed-fi/schools/{{known:tenant1:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant1", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant1:schoolId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get School from Tenant1 using a Tenant2 Profile", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 406\", () => {\r", + " pm.expect(pm.response.code).to.equal(406);\r", + "});\r", + "\r", + "pm.test(\"Error response should contain the expected content\", () => {\r", + " var jsonData = pm.response.json();\r", + " console.log(jsonData);\r", + " pm.expect(jsonData.message).to.eql(\"The profile specified by the content type in the 'Accept' header is not supported by this host.\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant1}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.ed-fi.school.{{supplied:tenant2:tenantSpecificTestProfileName}}.readable+json", + "type": "text" + } + ], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant1/data/v3/ed-fi/schools/{{known:tenant1:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant1", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant1:schoolId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get School from Tenant1 using a profile with common name", + "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(\"Response Content-Type header should match profile usage\",\r", + " () => {\r", + " pm.expect(pm.response).to.have.header(\"Content-Type\");\r", + " pm.expect(pm.response.headers.one(\"Content-Type\").value).to.contain(pm.variables.replaceIn(\"application/vnd.ed-fi.school.{{supplied:tenant1:testProfileWithCommonName}}.readable+json\"));\r", + " });\r", + "\r", + "\r", + "pm.test(\"Response should not contain a property not included in the Tenant1 test profile\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.not.have.property(pm.variables.replaceIn(\"{{supplied:tenant1:commonNameProfileNonIncludedPropertyName}}\"));\r", + "});\r", + "\r", + "pm.test(\"The value of the property included in the Tenant1 test profile should match the expected value for Tenant1\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.have.property(pm.variables.replaceIn(\"{{supplied:tenant1:commonNameProfileIncludedPropertyName}}\"));\r", + " pm.expect(responseData[pm.variables.replaceIn(\"{{supplied:tenant1:commonNameProfileIncludedPropertyName}}\")]).to.equal(pm.variables.replaceIn(\"{{supplied:tenant1:commonNameProfileIncludedPropertyValue}}\"));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant1}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.ed-fi.school.{{supplied:tenant1:testProfileWithCommonName}}.readable+json", + "type": "text" + } + ], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant1/data/v3/ed-fi/schools/{{known:tenant1:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant1", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant1:schoolId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Tenant2 Requests", + "item": [ + { + "name": "Get School from Tenant2 using a Tenant2 Profile", + "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(\"Response Content-Type header should match profile usage\",\r", + " () => {\r", + " pm.expect(pm.response).to.have.header(\"Content-Type\");\r", + " pm.expect(pm.response.headers.one(\"Content-Type\").value).to.contain(pm.variables.replaceIn(\"application/vnd.ed-fi.school.{{supplied:tenant2:tenantSpecificTestProfileName}}.readable+json\"));\r", + " });\r", + "\r", + "pm.test(\"Response should not contain a property not included in the Tenant2 test profile\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.not.have.property(pm.variables.replaceIn(\"{{supplied:tenant2:nonIncludedPropertyName}}\"));\r", + "});\r", + "\r", + "pm.test(\"The value of the property included in the Tenant2 test profile should match the expected value for Tenant2\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.have.property(pm.variables.replaceIn(\"{{supplied:tenant2:includedPropertyName}}\"));\r", + " pm.expect(responseData[pm.variables.replaceIn(\"{{supplied:tenant2:includedPropertyName}}\")]).to.equal(pm.variables.replaceIn(\"{{supplied:tenant2:includedPropertyValue}}\"));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant2}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.ed-fi.school.{{supplied:tenant2:tenantSpecificTestProfileName}}.readable+json", + "type": "text" + } + ], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant2/data/v3/ed-fi/schools/{{known:tenant2:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant2", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant2:schoolId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get School from Tenant2 using a Tenant1 Profile", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 406\", () => {\r", + " pm.expect(pm.response.code).to.equal(406);\r", + "});\r", + "\r", + "pm.test(\"Error response should contain the expected content\", () => {\r", + " var jsonData = pm.response.json();\r", + " console.log(jsonData);\r", + " pm.expect(jsonData.message).to.eql(\"The profile specified by the content type in the 'Accept' header is not supported by this host.\");\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant2}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.ed-fi.school.{{supplied:tenant1:tenantSpecificTestProfileName}}.readable+json", + "type": "text" + } + ], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant2/data/v3/ed-fi/schools/{{known:tenant2:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant2", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant2:schoolId}}" + ] + } + }, + "response": [] + }, + { + "name": "Get School from Tenant2 using a profile with common name", + "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(\"Response Content-Type header should match profile usage\",\r", + " () => {\r", + " pm.expect(pm.response).to.have.header(\"Content-Type\");\r", + " pm.expect(pm.response.headers.one(\"Content-Type\").value).to.contain(pm.variables.replaceIn(\"application/vnd.ed-fi.school.{{supplied:tenant2:testProfileWithCommonName}}.readable+json\"));\r", + " });\r", + "\r", + "pm.test(\"Response should not contain a property not included in the Tenant2 test profile\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.not.have.property(pm.variables.replaceIn(\"{{supplied:tenant2:commonNameProfileNonIncludedPropertyName}}\"));\r", + "});\r", + "\r", + "pm.test(\"The value of the property included in the Tenant2 test profile should match the expected value for Tenant2\", () => {\r", + " var responseData = pm.response.json();\r", + " pm.expect(responseData).to.have.property(pm.variables.replaceIn(\"{{supplied:tenant2:commonNameProfileIncludedPropertyName}}\"));\r", + " pm.expect(responseData[pm.variables.replaceIn(\"{{supplied:tenant2:commonNameProfileIncludedPropertyName}}\")]).to.equal(pm.variables.replaceIn(\"{{supplied:tenant2:commonNameProfileIncludedPropertyValue}}\"));\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant2}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/vnd.ed-fi.school.{{supplied:tenant2:testProfileWithCommonName}}.readable+json", + "type": "text" + } + ], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant2/data/v3/ed-fi/schools/{{known:tenant2:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant2", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant2:schoolId}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Teardown", + "item": [ + { + "name": "Delete Tenant1 School", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {\r", + " pm.expect(pm.response.code).to.equal(204);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant1}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant1/data/v3/ed-fi/schools/{{known:tenant1:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant1", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant1:schoolId}}" + ] + } + }, + "response": [] + }, + { + "name": "Delete Tenant2 School", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 404\", () => {\r", + " pm.expect(pm.response.code).to.equal(404);\r", + "});\r", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{AccessToken_Tenant1}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/Tenant1/data/v3/ed-fi/schools/{{known:tenant1:schoolId}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "Tenant1", + "data", + "v3", + "ed-fi", + "schools", + "{{known:tenant1:schoolId}}" + ] + } + }, + "response": [] + }, + { + "name": "Clean up Environment Variables", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "// Remove all environment variables that start with \"known:\" or \"supplied:\"\r", + "_.chain(_.keys(pm.environment.toObject()))\r", + " .filter(x => _.startsWith(x, 'known:') || _.startsWith(x, 'supplied:'))\r", + " .each(k => pm.environment.unset(k)).value();\r", + " " + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}", + "host": [ + "{{ApiBaseUrl}}" + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ] } ], "auth": { diff --git a/Utilities/CodeGeneration/EdFi.Ods.CodeGen.Tests/IntegrationTests/Profiles/ProfileMetadataValidatorTests.cs b/Utilities/CodeGeneration/EdFi.Ods.CodeGen.Tests/IntegrationTests/Profiles/ProfileMetadataValidatorTests.cs index ae8bf214cf..daa51bce15 100644 --- a/Utilities/CodeGeneration/EdFi.Ods.CodeGen.Tests/IntegrationTests/Profiles/ProfileMetadataValidatorTests.cs +++ b/Utilities/CodeGeneration/EdFi.Ods.CodeGen.Tests/IntegrationTests/Profiles/ProfileMetadataValidatorTests.cs @@ -30,14 +30,6 @@ protected override void Act() var profilesMetadataValidator = new ProfileMetadataValidator(resourceModelProvider); _actualValidationResult = profilesMetadataValidator.Validate(xDoc); } - - [Test] - public void Should_include_validation_failure_message_indicating_that_a_duplicate_profile_name_was_found() - { - _actualValidationResult.ShouldSatisfyAllConditions( - () => _actualValidationResult.IsValid.ShouldBeFalse(), - () => _actualValidationResult.Errors.First().ErrorMessage.ShouldContain("Duplicate profile name(s) encountered: 'The-Duplicated-Profile-Name'")); - } } public class When_invalid_resource_found_in_profiles_xml : TestFixtureBase diff --git a/tests/EdFi.Ods.Features.UnitTests/Profiles/ProfileMetadataDatabaseProviderTests.cs b/tests/EdFi.Ods.Features.UnitTests/Profiles/ProfileMetadataDatabaseProviderTests.cs index aaea57c97d..015dc5b269 100644 --- a/tests/EdFi.Ods.Features.UnitTests/Profiles/ProfileMetadataDatabaseProviderTests.cs +++ b/tests/EdFi.Ods.Features.UnitTests/Profiles/ProfileMetadataDatabaseProviderTests.cs @@ -23,14 +23,22 @@ public class AdminDatabaseProfileDefinitionsProviderTests public class WhenProfileDefinitionIsValid : TestFixtureBase { private const string ValidProfileName = "Sample-Profile-Resource-WriteOnly"; + private const string SecondValidProfileName = "Second-Profile-Resource-WriteOnly"; + private const string SecondValidProfileAlteredName = "Second-Profile-Resource-WriteOnly-Altered"; private IProfileDefinitionsProvider? _adminDatabaseProfileDefinitionsProvider; public IDictionary? _profileDefinitions; private readonly Profile ValidProfile = new() { - ProfileName = "Valid-Profile", + ProfileName = ValidProfileName, ProfileDefinition = $"" }; + + private readonly Profile ProfileWithNamesMismatched = new() + { + ProfileName = SecondValidProfileName, + ProfileDefinition = $"" + }; private readonly Profile InvalidProfile = new() { @@ -45,7 +53,7 @@ public class WhenProfileDefinitionIsValid : TestFixtureBase protected override void Arrange() { - var profiles = new[] { ValidProfile, InvalidProfile, EmptyProfile }.AsQueryable(); + var profiles = new[] { ValidProfile, ProfileWithNamesMismatched, InvalidProfile, EmptyProfile }.AsQueryable(); var userContext = Stub(); @@ -100,6 +108,14 @@ public void Should_not_contain_invalid_profiles(string profileName) { _profileDefinitions!.ShouldNotContainKey(profileName); } + + [Test] + public void Should_not_contain_profile_with_name_mismatch() + { + _profileDefinitions!.ShouldNotContainKey(SecondValidProfileName); + _profileDefinitions!.ShouldNotContainKey(SecondValidProfileAlteredName); + } + } } }