diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslator.cs index 12b7fb07d..c5823c25c 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslator.cs @@ -13,8 +13,9 @@ namespace EdFi.Ods.Api.ExceptionHandling.Translators { public class NonUniqueObjectExceptionTranslator : IProblemDetailsExceptionTranslator { - private const string ExpectedExceptionPattern = - @"^a different object with the same identifier value was already associated with the session: (?\w*): (?\d*), (?\w*): (?\w*): (?\d*), of entity: (?\w*)"; + private static readonly Regex _expectedExceptionRegex = new( + @"^a different object with the same identifier value was already associated with the session: (?\w*): (?\d*), (?\w*): (?\w*): (?\d*), of entity: (?\w*)", + RegexOptions.Compiled); private readonly ILog _logger = LogManager.GetLogger(typeof(NonUniqueObjectExceptionTranslator)); @@ -22,7 +23,7 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) { if (ex is NonUniqueObjectException) { - var match = Regex.Match(ex.Message, ExpectedExceptionPattern); + var match = _expectedExceptionRegex.Match(ex.Message); if (match.Success) { @@ -34,9 +35,10 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) match.Groups["subject"].Value, match.Groups["entitySimple"].Value, match.Groups["property"].Value, - match.Groups["entityPropertyId"].Value)); + match.Groups["entityPropertyId"].Value), + ex); - problemDetails = new NonUniqueConflictException("A problem occurred while processing the request.", + problemDetails = new NonUniqueValuesException("A problem occurred while processing the request.", new[] { $"Two {match.Groups["subject"]} entities with the same identifier were associated with the session. See log for additional details." }); return true; diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslator.cs index 226402361..0ce60fe38 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslator.cs @@ -8,6 +8,8 @@ using System.Text.RegularExpressions; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Exceptions; +using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Models.Domain; using EdFi.Ods.Common.Security.Claims; using NHibernate.Exceptions; using Npgsql; @@ -17,21 +19,29 @@ namespace EdFi.Ods.Api.ExceptionHandling.Translators.Postgres public class PostgresDuplicateKeyExceptionTranslator : IProblemDetailsExceptionTranslator { private readonly IContextProvider _dataManagementResourceContextProvider; + private readonly IDomainModelProvider _domainModelProvider; - private static readonly Regex _expression = new(@"(?\d*): duplicate key value violates unique constraint ""(?.*?)"""); - private static readonly Regex _detailExpression = new(@"Key \((?.*?)\)=\((?.*?)\) (?already exists)."); + private static readonly Regex _detailExpression = new( + @"Key \((?.*?)\)=\((?.*?)\) (?already exists).", + RegexOptions.Compiled); - private const string GenericMessage = "The value(s) supplied for the resource are not unique."; - - private const string SimpleKeyMessageFormat = "The value supplied for property '{0}' of entity '{1}' is not unique."; - private const string CompositeKeyMessageFormat = "The values supplied for properties '{0}' of entity '{1}' are not unique."; + private const string DuplicatePrimaryKeyTableOnlyErrorFormat = + "A primary key conflict occurred when attempting to create or update a record in the '{0}' table."; + + private const string DuplicatePrimaryKeyErrorFormat = + "A primary key conflict occurred when attempting to create or update a record in the '{0}' table. The duplicate key is ({1}) = ({2})."; + + private const string SimpleKeyErrorMessageFormat = "The value supplied for property '{0}' of entity '{1}' is not unique."; + private const string CompositeKeyErrorMessageFormat = "The values supplied for properties '{0}' of entity '{1}' are not unique."; private const string PrimaryKeyNameSuffix = "_pk"; public PostgresDuplicateKeyExceptionTranslator( - IContextProvider dataManagementResourceContextProvider) + IContextProvider dataManagementResourceContextProvider, + IDomainModelProvider domainModelProvider) { _dataManagementResourceContextProvider = dataManagementResourceContextProvider; + _domainModelProvider = domainModelProvider; } public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) @@ -42,66 +52,110 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) if (exceptionToEvaluate is PostgresException postgresException) { - var match = _expression.Match(postgresException.Message); - - if (match.Success) + if (postgresException.SqlState == PostgresErrorCodes.UniqueViolation) { - var constraintName = match.Groups["ConstraintName"].ValueSpan; - - string message = GetMessageUsingRequestContext(constraintName) - ?? GetMessageUsingPostgresException(); - - problemDetails = new NonUniqueConflictException(message); + // Examples: + // ---------------------------------------- + // INSERT duplicate AK values + // [23505] ERROR: duplicate key value violates unique constraint "student_ui_studentuniqueid" Detail: Key (studentuniqueid)=(ABC) already exists. + + // INSERT duplicate PK values + // [23505] ERROR: duplicate key value violates unique constraint "educationorganization_pk" Detail: Key (educationorganizationid)=(123) already exists. + + // UPDATE PK to duplicate PK values + // [23505] ERROR: duplicate key value violates unique constraint "educationorganization_pk" Detail: Key (educationorganizationid)=(123) already exists. + // ---------------------------------------- + + problemDetails = + GetNonUniqueIdentityException() + ?? GetNonUniqueValuesException() + ?? new NonUniqueValuesException(NonUniqueValuesException.DefaultDetail); return true; - } - string GetMessageUsingRequestContext(ReadOnlySpan constraintName) - { - // Rely on PK suffix naming convention to identify PK constraint violation (which covers almost all scenarios for this violation) - if (constraintName.EndsWith(PrimaryKeyNameSuffix, StringComparison.OrdinalIgnoreCase)) + EdFiProblemDetailsExceptionBase GetNonUniqueIdentityException() { - var tableName = constraintName.Slice(0, constraintName.Length - PrimaryKeyNameSuffix.Length).ToString(); - - // Look for matching class in the request's targeted resource - if (_dataManagementResourceContextProvider.Get()?.Resource? - .ContainedItemTypeByName.TryGetValue(tableName, out var resourceClass) ?? false) + // Rely on PK suffix naming convention to identify PK constraint violation (which covers almost all scenarios for this violation) + if (postgresException.ConstraintName!.EndsWith(PrimaryKeyNameSuffix, StringComparison.OrdinalIgnoreCase)) { - var pkPropertyNames = resourceClass.IdentifyingProperties.Select(p => p.PropertyName).ToArray(); - - return string.Format( - (pkPropertyNames.Length > 1) - ? CompositeKeyMessageFormat - : SimpleKeyMessageFormat, - string.Join("', '", pkPropertyNames), - resourceClass.Name); + var tableName = postgresException.TableName; + + // If detail is available, use it. + if (!string.IsNullOrEmpty(postgresException.Detail) && !postgresException.Detail.StartsWith("Detail redacted")) + { + var exceptionInfo = new PostgresExceptionInfo(postgresException, _detailExpression); + + if (!string.IsNullOrEmpty(exceptionInfo.ColumnNames) && !string.IsNullOrEmpty(exceptionInfo.Values)) + { + string errorMessage = string.Format( + DuplicatePrimaryKeyErrorFormat, + exceptionInfo.TableName, + exceptionInfo.ColumnNames, + exceptionInfo.Values); + + return new NonUniqueIdentityException(NonUniqueIdentityException.DefaultDetail, [errorMessage]); + } + } + + // Look for matching class in the request's targeted resource + if (_dataManagementResourceContextProvider.Get()?.Resource? + .ContainedItemTypeByName.TryGetValue(tableName, out var resourceClass) ?? false) + { + var pkPropertyNames = resourceClass.IdentifyingProperties.Select(p => p.PropertyName).ToArray(); + + string errorMessage = string.Format( + (pkPropertyNames.Length > 1) + ? CompositeKeyErrorMessageFormat + : SimpleKeyErrorMessageFormat, + string.Join("', '", pkPropertyNames), + resourceClass.Name); + + return new NonUniqueIdentityException(NonUniqueIdentityException.DefaultDetail, [errorMessage]); + } + + // Look for matching entity from schema information (to provide normalized name in exception) + if (_domainModelProvider.GetDomainModel() + .EntityByFullName.TryGetValue( + new FullName(postgresException.SchemaName, postgresException.TableName), + out var entity)) + { + return new NonUniqueIdentityException( + NonUniqueIdentityException.DefaultDetail, + [ + string.Format(DuplicatePrimaryKeyTableOnlyErrorFormat, entity.Name) + ]); + } + + // Use the literal Postgres table name provided on the exception + return new NonUniqueIdentityException( + NonUniqueIdentityException.DefaultDetail, + [ + string.Format(DuplicatePrimaryKeyTableOnlyErrorFormat, tableName) + ]); } + + return null; } - return null; - } + EdFiProblemDetailsExceptionBase GetNonUniqueValuesException() + { + var exceptionInfo = new PostgresExceptionInfo(postgresException, _detailExpression); - string GetMessageUsingPostgresException() - { - string message; - var exceptionInfo = new PostgresExceptionInfo(postgresException, _detailExpression); + // Column names will only be available from Postgres if the "Include Error Detail=true" value is added to the connection string + if (exceptionInfo.ColumnNames.Length > 0 && exceptionInfo.ColumnNames != PostgresExceptionInfo.UnknownValue) + { + string detail = string.Format( + exceptionInfo.IsComposedKeyConstraint + ? CompositeKeyErrorMessageFormat + : SimpleKeyErrorMessageFormat, + exceptionInfo.ColumnNames, + exceptionInfo.TableName); + + return new NonUniqueValuesException(detail); + } - // Column names will only be available form Postgres if a special argument is added to the connection string - if (exceptionInfo.ColumnNames.Length > 0 && exceptionInfo.ColumnNames != PostgresExceptionInfo.UnknownValue) - { - message = string.Format( - exceptionInfo.IsComposedKeyConstraint - ? CompositeKeyMessageFormat - : SimpleKeyMessageFormat, - exceptionInfo.ColumnNames, - exceptionInfo.TableName); - } - else - { - message = GenericMessage; + return null; } - - return message; } } diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslator.cs index 03b0a5fa3..4486ab419 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslator.cs @@ -5,10 +5,13 @@ using System; using System.Linq; +using System.Text.RegularExpressions; using EdFi.Common.Configuration; using EdFi.Common.Extensions; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Exceptions; +using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Models.Domain; using EdFi.Ods.Common.Security.Claims; using NHibernate.Exceptions; using Npgsql; @@ -17,17 +20,11 @@ namespace EdFi.Ods.Api.ExceptionHandling.Translators.Postgres { public class PostgresForeignKeyExceptionTranslator : IProblemDetailsExceptionTranslator { - private readonly IContextProvider _dataManagementResourceContextProvider; - - private const string InsertOrUpdateMessageFormat = "The referenced '{0}' resource does not exist."; - private const string UpdateOrDeleteMessageFormat = "The operation cannot be performed because the resource is a dependency of the '{0}' resource."; + private readonly IDomainModelProvider _domainModelProvider; - private const string NoDetailsInsertOrUpdateMessage = "A referenced resource does not exist."; - private const string NoDetailsUpdateOrDeleteMessage = "The operation cannot be performed because the resource is a dependency of another resource."; - - public PostgresForeignKeyExceptionTranslator(IContextProvider dataManagementResourceContextProvider) + public PostgresForeignKeyExceptionTranslator(IDomainModelProvider domainModelProvider) { - _dataManagementResourceContextProvider = dataManagementResourceContextProvider; + _domainModelProvider = domainModelProvider; } public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) @@ -40,33 +37,42 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) { if (postgresException.SqlState == PostgresSqlStates.ForeignKeyViolation) { - var entity = _dataManagementResourceContextProvider.Get() - .Resource.Entity.Aggregate.Members.SingleOrDefault( - e => - e.Schema.EqualsIgnoreCase(postgresException.SchemaName) - && e.TableNameByDatabaseEngine[DatabaseEngine.Postgres].EqualsIgnoreCase(postgresException.TableName)); - - var association = entity?.IncomingAssociations.SingleOrDefault( - a => - a.Association.ConstraintByDatabaseEngine[DatabaseEngine.Postgres.Value] - .EqualsIgnoreCase(postgresException.ConstraintName)); - - if (association == null) + // Try to get the entity affected + if (_domainModelProvider.GetDomainModel() + .EntityByFullName.TryGetValue(new FullName(postgresException.SchemaName, postgresException.TableName), out var entity)) { - string noDetailsMessage = postgresException.MessageText.Contains("update or delete") - ? NoDetailsUpdateOrDeleteMessage - : NoDetailsInsertOrUpdateMessage; + // For "insert or update" violations, extract table name from the constraint name + // EXAMPLE: insert or update on table "school" violates foreign key constraint "fk_6cd2e3_localeducationagency" + if (postgresException.MessageText.Contains("insert or update")) + { + // Iterate the incoming associations looking for the offending constraint + var association = entity?.IncomingAssociations.SingleOrDefault( + a => + a.Association.ConstraintByDatabaseEngine[DatabaseEngine.Postgres.Value] + .EqualsIgnoreCase(postgresException.ConstraintName)); - problemDetails = new InvalidReferenceConflictException(noDetailsMessage); + if (association != null) + { + problemDetails = new UnresolvedReferenceException(association.OtherEntity.Name); - return true; - } + return true; + } + } + else if (postgresException.MessageText.Contains("update or delete")) + { + // NOTE: FK violations won't happen in the ODS for "update" because where key updates are allowed, cascade updates are applied. + // So this scenario will only happen with deletes where there are child aggregate/resources that must be removed first. + // In this case, the PostgreSQL exception identifies the dependent table (no translation is necesssary) + problemDetails = new DependentResourceItemExistsException(entity.Name); - string message = postgresException.MessageText.Contains("update or delete") - ? string.Format(UpdateOrDeleteMessageFormat, association.ThisEntity.Name) - : string.Format(InsertOrUpdateMessageFormat, association.OtherEntity.Name); + return true; + } + } - problemDetails = new InvalidReferenceConflictException(message); + // Unable to determine details, probably due to long table name munging for Postgres identities + problemDetails = postgresException.MessageText.Contains("update or delete") + ? new DependentResourceItemExistsException() + : new UnresolvedReferenceException(); return true; } diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresSqlStates.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresSqlStates.cs index e9e28ea39..087b9fbfd 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresSqlStates.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresSqlStates.cs @@ -9,6 +9,6 @@ namespace EdFi.Ods.Api.ExceptionHandling.Translators.Postgres; public static class PostgresSqlStates { - public const string ForeignKeyViolation = "23503"; - public const string UniqueViolation = "23505"; + public const string ForeignKeyViolation = Npgsql.PostgresErrorCodes.ForeignKeyViolation; + public const string UniqueViolation = Npgsql.PostgresErrorCodes.UniqueViolation; } diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerConstraintExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerConstraintExceptionTranslator.cs index a53dfbdcc..9a165379f 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerConstraintExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerConstraintExceptionTranslator.cs @@ -38,12 +38,9 @@ public class SqlServerConstraintExceptionTranslator : IProblemDetailsExceptionTr * */ - private static readonly Regex _expression = new Regex( + private static readonly Regex _expression = new( @"^The (?INSERT|UPDATE|DELETE) statement conflicted with the (?FOREIGN KEY|REFERENCE) constraint ""(?\w+)"".*?table ""[a-z]+\.(?\w+)""(?:, column '(?\w+)')?"); - private const string InsertOrUpdateMessageFormat = "The referenced '{0}' resource does not exist."; - private const string UpdateOrDeleteMessageFormat = "The operation cannot be performed because the resource is a dependency of the '{0}' resource."; - // ^The (?INSERT|UPDATE|DELETE) statement conflicted with the (?FOREIGN KEY|REFERENCE) constraint "(?\w+)".*?table "[a-z]+\.(?\w+)".*?(?: column '(?\w+)')? public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) { @@ -58,11 +55,9 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) if (match.Success) { - string errorMessageFormat = string.Empty; string statementType = match.Groups["StatementType"].Value; string constraintType = match.Groups["ConstraintType"].Value; string tableName = match.Groups["TableName"].Value; - string columnName = match.Groups["ColumnName"].Value; switch (statementType) { @@ -71,32 +66,22 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) if (constraintType == "FOREIGN KEY") { - errorMessageFormat = InsertOrUpdateMessageFormat; - break; + problemDetails = new UnresolvedReferenceException(tableName); + return true; } - // No explicit support for UPDATE/REFERENCE constraint yet - problemDetails = null; - return false; - + break; + case "DELETE": if (constraintType == "REFERENCE") { - errorMessageFormat = UpdateOrDeleteMessageFormat; - break; + problemDetails = new DependentResourceItemExistsException(tableName); + return true; } - // No explicit support for UPDATE/REFERENCE constraint yet - problemDetails = null; - return false; + break; } - - string errorMessage = string.Format(errorMessageFormat, tableName, columnName); - - problemDetails = new InvalidReferenceConflictException(errorMessage); - - return true; } } diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslator.cs index 817cd0d25..9ac12456b 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslator.cs @@ -16,11 +16,11 @@ namespace EdFi.Ods.Api.ExceptionHandling.Translators.SqlServer { public class SqlServerPrimaryKeyConstraintExceptionTranslator : IProblemDetailsExceptionTranslator { - private const string MessageFormat = - "A natural key conflict occurred when attempting to create a new resource '{0}' with a duplicate key. The duplicated columns and values are [{1}] {2}."; + private const string DuplicatePrimaryKeyErrorFormat = + "A primary key conflict occurred when attempting to create or update a record in the '{0}' table. The duplicate key is ({1}) = ({2})."; private static readonly Regex _matchPattern = new( - @"^Violation of PRIMARY KEY constraint '(?\w+)'\.\s+Cannot insert duplicate key in object '[a-z]+\.(?\w+)'\.\s+The duplicate key value is (?\(.*\))\.\s+The statement has been terminated\.\s*$"); + @"^Violation of PRIMARY KEY constraint '(?\w+)'\.\s+Cannot insert duplicate key in object '[a-z]+\.(?\w+)'\.\s+The duplicate key value is \((?.*)\)\.\s+The statement has been terminated\.\s*$"); private readonly IContextProvider _dataManagementResourceContextProvider; @@ -35,23 +35,26 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) ? ex.InnerException : ex; - if (exception is SqlException) + if (exception is SqlException sqlException) { - var match = _matchPattern.Match(exception.Message); + // TODO: Identify SqlState associated with this error to avoid unnecessary regex evaluation + + var match = _matchPattern.Match(sqlException.Message); if (match.Success) { var resourceEntity = _dataManagementResourceContextProvider.Get().Resource.Entity; - string values = match.Groups["Values"].Value; + string values = match.Groups["CsvValues"].Value; string columnNames = resourceEntity.BaseEntity == null ? string.Join(", ", resourceEntity.Identifier.Properties.Select(x => x.PropertyName)) : string.Join(", ", resourceEntity.BaseEntity.Identifier.Properties.Select(x => x.PropertyName)); - var message = string.Format(MessageFormat, resourceEntity.Name, columnNames, values); + var message = string.Format(DuplicatePrimaryKeyErrorFormat, resourceEntity.Name, columnNames, values); + + problemDetails = new NonUniqueIdentityException(NonUniqueIdentityException.DefaultDetail, [message]); - problemDetails = new NaturalKeyConflictException(message); return true; } } diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerUniqueIndexExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerUniqueIndexExceptionTranslator.cs index b3e7b8267..532843981 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerUniqueIndexExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerUniqueIndexExceptionTranslator.cs @@ -16,8 +16,9 @@ namespace EdFi.Ods.Api.ExceptionHandling.Translators.SqlServer { public class SqlServerUniqueIndexExceptionTranslator : IProblemDetailsExceptionTranslator { - private static readonly Regex _expression = new Regex( - @"^Cannot insert duplicate key row in object '[a-z]+\.(?\w+)' with unique index '(?\w+)'(?:\. The duplicate key value is (?\(.*\))\.)?|^Violation of UNIQUE KEY constraint '(?\w+)'. Cannot insert duplicate key in object '[a-z]+\.(?\w+)'."); + private static readonly Regex _expression = new( + @"^Cannot insert duplicate key row in object '[a-z]+\.(?\w+)' with unique index '(?\w+)'(?:\. The duplicate key value is (?\(.*\))\.)?|^Violation of UNIQUE KEY constraint '(?\w+)'. Cannot insert duplicate key in object '[a-z]+\.(?\w+)'.", + RegexOptions.Compiled); private const string SingleMessageFormat = "The value {0} supplied for property '{1}' of entity '{2}' is not unique."; private const string MultipleMessageFormat = "The values {0} supplied for properties '{1}' of entity '{2}' are not unique."; @@ -70,7 +71,7 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) message = string.Format(MultipleMessageFormat, values, columnNames, tableName); } - problemDetails = new NonUniqueConflictException(message); + problemDetails = new NonUniqueValuesException(message); return true; } } diff --git a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/StaleObjectStateExceptionTranslator.cs b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/StaleObjectStateExceptionTranslator.cs index 870fc240e..9e91812ff 100644 --- a/Application/EdFi.Ods.Api/ExceptionHandling/Translators/StaleObjectStateExceptionTranslator.cs +++ b/Application/EdFi.Ods.Api/ExceptionHandling/Translators/StaleObjectStateExceptionTranslator.cs @@ -20,9 +20,8 @@ public bool TryTranslate(Exception ex, out IEdFiProblemDetails problemDetails) { // This is probably a timing issue and is not expected under normal operations, so we'll log it. _logger.Error(ex); - - problemDetails = new NaturalKeyConflictException( - "A natural key conflict occurred when attempting to update a new resource with a duplicate key."); + + problemDetails = new ConcurrencyException(ex); return true; } diff --git a/Application/EdFi.Ods.Common/Exceptions/ConcurrencyException.cs b/Application/EdFi.Ods.Common/Exceptions/ConcurrencyException.cs index 047d311c6..755c3c1eb 100644 --- a/Application/EdFi.Ods.Common/Exceptions/ConcurrencyException.cs +++ b/Application/EdFi.Ods.Common/Exceptions/ConcurrencyException.cs @@ -3,31 +3,27 @@ // 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 Microsoft.AspNetCore.Http; namespace EdFi.Ods.Common.Exceptions { - public class ConcurrencyException : EdFiProblemDetailsExceptionBase + public class ConcurrencyException : ConflictException { // Fields containing override values for Problem Details - private const string TypePart = "multiuser-conflict"; - private const string TitleText = "Multiuser Conflict"; - private const int StatusValue = StatusCodes.Status412PreconditionFailed; + private const string TypePart = "multi-user-conflict"; + private const string TitleText = "Multi-User Conflict"; - private const string DetailText = "The resource was modified by another user while attempting to perform the current operation."; - private const string MessageText = "Resource modification by another consumer was detected (due to inclusion of If-Match request header by API client)."; - - public ConcurrencyException() - : base(DetailText, MessageText) { } + private const string DetailText = "The resource item was modified or deleted by another user while processing the request. Resending this request will either recreate the item, or introduce of copy with a different identifier."; + + public ConcurrencyException(Exception innerException) + : base(DetailText, innerException) { } // --------------------------- // Boilerplate for overrides // --------------------------- public override string Title { get => TitleText; } - public override int Status { get => StatusValue; } - protected override IEnumerable GetTypeParts() { foreach (var part in base.GetTypeParts()) diff --git a/Application/EdFi.Ods.Common/Exceptions/ConflictException.cs b/Application/EdFi.Ods.Common/Exceptions/ConflictException.cs index daffd9dd6..9fa306946 100644 --- a/Application/EdFi.Ods.Common/Exceptions/ConflictException.cs +++ b/Application/EdFi.Ods.Common/Exceptions/ConflictException.cs @@ -9,28 +9,18 @@ namespace EdFi.Ods.Common.Exceptions; -public class ConflictException : EdFiProblemDetailsExceptionBase +public abstract class ConflictException : EdFiProblemDetailsExceptionBase { // Fields containing override values for Problem Details private const string TypePart = "conflict"; private const string TitleText = "Resource Data Conflict"; - private const int StatusValue = StatusCodes.Status409Conflict; - - private const string DefaultDetail = "The requested change would result in data that conflicts with the existing data."; - public ConflictException() - : base(DefaultDetail, DefaultDetail) { } + private const int StatusValue = StatusCodes.Status409Conflict; - public ConflictException(string detail) + protected ConflictException(string detail) : base(detail, detail) { } - public ConflictException(string detail, string[] errors) - : base(detail, detail) - { - this.SetErrors(errors); - } - - public ConflictException(string detail, Exception inner) + protected ConflictException(string detail, Exception inner) : base(detail, detail, inner) { } // --------------------------- diff --git a/Application/EdFi.Ods.Common/Exceptions/DependentResourceItemExistsException.cs b/Application/EdFi.Ods.Common/Exceptions/DependentResourceItemExistsException.cs new file mode 100644 index 000000000..67621f65f --- /dev/null +++ b/Application/EdFi.Ods.Common/Exceptions/DependentResourceItemExistsException.cs @@ -0,0 +1,40 @@ +// 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; + +namespace EdFi.Ods.Common.Exceptions; + +public class DependentResourceItemExistsException : ConflictException +{ + // Fields containing override values for Problem Details + private const string TypePart = "dependent-item-exists"; + private const string TitleText = "Dependent Resource Item Exists"; + + private const string DefaultDetail = "The requested action cannot be performed because this resource item is referenced by another resource item."; + private const string DefaultDetailFormat = "The requested action cannot be performed because this resource item is referenced by an existing '{0}' resource item."; + + public DependentResourceItemExistsException() + : base(DefaultDetail) { } + + public DependentResourceItemExistsException(string resourceName) + : base(string.Format(DefaultDetailFormat, resourceName)) { } + + // --------------------------- + // Boilerplate for overrides + // --------------------------- + public override string Title { get => TitleText; } + + protected override IEnumerable GetTypeParts() + { + foreach (var part in base.GetTypeParts()) + { + yield return part; + } + + yield return TypePart; + } + // --------------------------- +} diff --git a/Application/EdFi.Ods.Common/Exceptions/EdFiProblemDetailsExceptionBase.cs b/Application/EdFi.Ods.Common/Exceptions/EdFiProblemDetailsExceptionBase.cs index 996d63fb7..905a486da 100644 --- a/Application/EdFi.Ods.Common/Exceptions/EdFiProblemDetailsExceptionBase.cs +++ b/Application/EdFi.Ods.Common/Exceptions/EdFiProblemDetailsExceptionBase.cs @@ -50,18 +50,24 @@ namespace EdFi.Ods.Common.Exceptions; ├──┤ ConflictException | 409 Conflict │ └───────────────────┘ │ △ - │ │ ┌─────────────────────────────┐ - │ ├──┤ NonUniqueConflictException | - │ │ └─────────────────────────────┘ + │ │ ┌──────────────────────┐ + │ ├──┤ ConcurrencyException | + │ │ └──────────────────────┘ │ │ ┌──────────────────────────────────────┐ - │ ├──┤ InvalidReferenceConflictException | + │ ├──┤ DependentResourceItemExistsException | │ │ └──────────────────────────────────────┘ - │ │ ┌──────────────────────────────────────┐ - │ └──┤ NaturalKeyConflictException | - │ └──────────────────────────────────────┘ - │ ┌──────────────────────┐ - ├──┤ ConcurrencyException | 412 Precondition Failed - │ └──────────────────────┘ + │ │ ┌────────────────────────────┐ + │ ├──┤ NonUniqueIdentityException | + │ │ └────────────────────────────┘ + │ │ ┌──────────────────────────┐ + │ ├──┤ NonUniqueValuesException | + │ │ └──────────────────────────┘ + │ │ ┌──────────────────────────────┐ + │ └──┤ UnresolvedReferenceException | + │ └──────────────────────────────┘ + │ ┌─────────────────────────┐ + ├──┤ OptimisticLockException | 412 Precondition Failed + │ └─────────────────────────┘ │ ┌──────────────────────────────┐ ├──┤ InternalServerErrorException | 500 Internal Server Error │ └──────────────────────────────┘ @@ -119,6 +125,13 @@ protected EdFiProblemDetailsExceptionBase(string detail, string message) Detail = detail; } + protected EdFiProblemDetailsExceptionBase(string detail, string[] errors) + : base(detail) + { + Detail = detail; + this.SetErrors(errors); + } + protected EdFiProblemDetailsExceptionBase(string detail, string message, Exception innerException) : base(message, innerException) { diff --git a/Application/EdFi.Ods.Common/Exceptions/NonUniqueConflictException.cs b/Application/EdFi.Ods.Common/Exceptions/NonUniqueIdentityException.cs similarity index 56% rename from Application/EdFi.Ods.Common/Exceptions/NonUniqueConflictException.cs rename to Application/EdFi.Ods.Common/Exceptions/NonUniqueIdentityException.cs index 94ffb623f..a135549dc 100644 --- a/Application/EdFi.Ods.Common/Exceptions/NonUniqueConflictException.cs +++ b/Application/EdFi.Ods.Common/Exceptions/NonUniqueIdentityException.cs @@ -3,38 +3,29 @@ // 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 Microsoft.AspNetCore.Http; namespace EdFi.Ods.Common.Exceptions; -public class NonUniqueConflictException : ConflictException +public class NonUniqueIdentityException : ConflictException { // Fields containing override values for Problem Details - private const string TypePart = "not-unique"; - private const string TitleText = "Resource Not Unique Conflict Due to Not-Unique"; - private const int StatusValue = StatusCodes.Status409Conflict; + private const string TypePart = "non-unique-identity"; + private const string TitleText = "Identifying Values Are Not Unique"; - public NonUniqueConflictException(string detail) - : base(detail) { } + public const string DefaultDetail = "The identifying value(s) of the resource item are the same as another resource item that already exists."; - public NonUniqueConflictException(string detail, Exception innerException) - : base(detail, innerException) { } - - - public NonUniqueConflictException(string detail, string[] errors) - : base(detail) + public NonUniqueIdentityException(string detail, string[] errors) + : base(detail) { this.SetErrors(errors); } + // --------------------------- // Boilerplate for overrides // --------------------------- public override string Title { get => TitleText; } - public override int Status { get => StatusValue; } - protected override IEnumerable GetTypeParts() { foreach (var part in base.GetTypeParts()) diff --git a/Application/EdFi.Ods.Common/Exceptions/InvalidReferenceConflictException.cs b/Application/EdFi.Ods.Common/Exceptions/NonUniqueValuesException.cs similarity index 53% rename from Application/EdFi.Ods.Common/Exceptions/InvalidReferenceConflictException.cs rename to Application/EdFi.Ods.Common/Exceptions/NonUniqueValuesException.cs index 58ff75bd6..380083a24 100644 --- a/Application/EdFi.Ods.Common/Exceptions/InvalidReferenceConflictException.cs +++ b/Application/EdFi.Ods.Common/Exceptions/NonUniqueValuesException.cs @@ -3,27 +3,29 @@ // 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 Microsoft.AspNetCore.Http; namespace EdFi.Ods.Common.Exceptions; -public class InvalidReferenceConflictException : ConflictException +public class NonUniqueValuesException : ConflictException { // Fields containing override values for Problem Details - private const string TypePart = "invalid-reference"; - private const string TitleText = "Resource Not Unique Conflict due to invalid-reference"; - private const int StatusValue = StatusCodes.Status409Conflict; + private const string TypePart = "non-unique-values"; + private const string TitleText = "Non-Unique Values"; - public InvalidReferenceConflictException(string detail) + public const string DefaultDetail = + "A value (or values) in the resource item must be unique, but another resource item with these values already exists."; + + public NonUniqueValuesException(string detail) : base(detail) { } - public InvalidReferenceConflictException(string detail, Exception innerException) - : base(detail, innerException) { } - - - public InvalidReferenceConflictException(string detail, string[] errors) + /// + /// Initializes the exception using the supplied detail and errors array (used exclusively for surfacing the NHibernate + /// NonUniqueObjectException exception as a Problem Details response). + /// + /// + /// + public NonUniqueValuesException(string detail, string[] errors) : base(detail) { this.SetErrors(errors); @@ -34,8 +36,6 @@ public InvalidReferenceConflictException(string detail, string[] errors) // --------------------------- public override string Title { get => TitleText; } - public override int Status { get => StatusValue; } - protected override IEnumerable GetTypeParts() { foreach (var part in base.GetTypeParts()) diff --git a/Application/EdFi.Ods.Common/Exceptions/NotModifiedException.cs b/Application/EdFi.Ods.Common/Exceptions/NotModifiedException.cs index 1b889434c..21c5375ad 100644 --- a/Application/EdFi.Ods.Common/Exceptions/NotModifiedException.cs +++ b/Application/EdFi.Ods.Common/Exceptions/NotModifiedException.cs @@ -14,6 +14,7 @@ public class NotModifiedException : EdFiProblemDetailsExceptionBase // Fields containing override values for Problem Details private const string TypePart = "not-modified"; private const string TitleText = "Not Modified"; + private const int StatusValue = StatusCodes.Status304NotModified; private const string DefaultDetail = "The specified resource has not changed since it was last retrieved."; @@ -21,12 +22,6 @@ public class NotModifiedException : EdFiProblemDetailsExceptionBase public NotModifiedException() : base(DefaultDetail, DefaultDetail) { } - public NotModifiedException(string message) - : base(DefaultDetail, message) { } - - public NotModifiedException(string message, Exception inner) - : base(DefaultDetail, message, inner) { } - // --------------------------- // Boilerplate for overrides // --------------------------- diff --git a/Application/EdFi.Ods.Common/Exceptions/OptimisticLockException.cs b/Application/EdFi.Ods.Common/Exceptions/OptimisticLockException.cs new file mode 100644 index 000000000..7f6580ea5 --- /dev/null +++ b/Application/EdFi.Ods.Common/Exceptions/OptimisticLockException.cs @@ -0,0 +1,46 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace EdFi.Ods.Common.Exceptions; + +public class OptimisticLockException : EdFiProblemDetailsExceptionBase +{ + // Fields containing override values for Problem Details + private const string TypePart = "optimistic-lock-failed"; + private const string TitleText = "Optimistic Lock Failed"; + + private const int StatusValue = StatusCodes.Status412PreconditionFailed; + + private const string DetailText = "The resource item has been modified by another user."; + private const string ErrorText = "The resource item's etag value does not match what was specified in the 'If-Match' request header indicating that it has been modified by another client since it was last retrieved."; + + /// + /// Initializes a new instance of the class using the default text and error messages + /// indicating an optimistic locking evaluation detected changes from another API client. + /// + public OptimisticLockException() + : base(DetailText, [ErrorText]) { } + + // --------------------------- + // Boilerplate for overrides + // --------------------------- + public override string Title { get => TitleText; } + + public override int Status { get => StatusValue; } + + protected override IEnumerable GetTypeParts() + { + foreach (var part in base.GetTypeParts()) + { + yield return part; + } + + yield return TypePart; + } + // --------------------------- +} diff --git a/Application/EdFi.Ods.Common/Exceptions/NaturalKeyConflictException.cs b/Application/EdFi.Ods.Common/Exceptions/UnresolvedReferenceException.cs similarity index 56% rename from Application/EdFi.Ods.Common/Exceptions/NaturalKeyConflictException.cs rename to Application/EdFi.Ods.Common/Exceptions/UnresolvedReferenceException.cs index 84d312261..a160a414d 100644 --- a/Application/EdFi.Ods.Common/Exceptions/NaturalKeyConflictException.cs +++ b/Application/EdFi.Ods.Common/Exceptions/UnresolvedReferenceException.cs @@ -5,36 +5,31 @@ using System; using System.Collections.Generic; +using EdFi.Ods.Common.Extensions; using Microsoft.AspNetCore.Http; namespace EdFi.Ods.Common.Exceptions; -public class NaturalKeyConflictException : ConflictException +public class UnresolvedReferenceException : ConflictException { // Fields containing override values for Problem Details - private const string TypePart = "natural-key"; - private const string TitleText = "Resource Not Unique Conflict due to natural-key"; - private const int StatusValue = StatusCodes.Status409Conflict; + private const string TypePart = "unresolved-reference"; + private const string TitleText = "Unresolved Reference"; - public NaturalKeyConflictException(string detail) - : base(detail) { } + private const string DefaultDetail = "A referenced item does not exist."; + private const string DefaultDetailFormat = "The referenced '{0}' item does not exist."; - public NaturalKeyConflictException(string detail, Exception innerException) - : base(detail, innerException) { } + public UnresolvedReferenceException() + : base(DefaultDetail) { } + public UnresolvedReferenceException(string resourceName) + : base(string.Format(DefaultDetailFormat, resourceName)) { } - public NaturalKeyConflictException(string detail, string[] errors) - : base(detail) - { - this.SetErrors(errors); - } // --------------------------- // Boilerplate for overrides // --------------------------- public override string Title { get => TitleText; } - public override int Status { get => StatusValue; } - protected override IEnumerable GetTypeParts() { foreach (var part in base.GetTypeParts()) diff --git a/Application/EdFi.Ods.Common/Infrastructure/Repositories/NHibernateRepositoryDeleteOperationBase.cs b/Application/EdFi.Ods.Common/Infrastructure/Repositories/NHibernateRepositoryDeleteOperationBase.cs index 641765b68..6966a0da2 100644 --- a/Application/EdFi.Ods.Common/Infrastructure/Repositories/NHibernateRepositoryDeleteOperationBase.cs +++ b/Application/EdFi.Ods.Common/Infrastructure/Repositories/NHibernateRepositoryDeleteOperationBase.cs @@ -56,7 +56,7 @@ protected async Task DeleteAsync(TEntity persistedEntity, string etag, Cancellat if (!persistedEntity.LastModifiedDate.Equals(lastModifiedDate)) { - throw new ConcurrencyException(); + throw new OptimisticLockException(); } } diff --git a/Application/EdFi.Ods.Common/Infrastructure/Repositories/UpsertEntity.cs b/Application/EdFi.Ods.Common/Infrastructure/Repositories/UpsertEntity.cs index c4ccaa104..fa7957f6a 100644 --- a/Application/EdFi.Ods.Common/Infrastructure/Repositories/UpsertEntity.cs +++ b/Application/EdFi.Ods.Common/Infrastructure/Repositories/UpsertEntity.cs @@ -100,7 +100,7 @@ public async Task> UpsertAsync(TEntity entity, bool { if (!persistedEntity.LastModifiedDate.Equals(entity.LastModifiedDate)) { - throw new ConcurrencyException(); + throw new OptimisticLockException(); } } diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslatorTests.cs index faca8d1fa..4501349a6 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslatorTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/NonUniqueObjectExceptionTranslatorTests.cs @@ -30,7 +30,7 @@ public void TryTranslate_WithNonUniqueObjectException_ShouldReturnTrueAndSetProb // Assert result.ShouldBeTrue(); problemDetails.ShouldNotBeNull(); - problemDetails.ShouldBeOfType(); + problemDetails.ShouldBeOfType(); problemDetails.Detail.ShouldBe("A problem occurred while processing the request."); problemDetails.Errors.ShouldContain( diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslatorTests.cs index 7756654da..47714bad1 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslatorTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresDuplicateKeyExceptionTranslatorTests.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using EdFi.Ods.Api.ExceptionHandling.Translators.Postgres; -using EdFi.Ods.Api.Models; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Exceptions; using EdFi.Ods.Common.Models; @@ -18,15 +17,15 @@ using EdFi.Ods.Common.Models.Resource; using EdFi.Ods.Common.Security.Claims; using EdFi.Ods.Tests._Builders; -using EdFi.Ods.Tests.TestExtension; using EdFi.TestFixture; using FakeItEasy; using NHibernate.Exceptions; using NUnit.Framework; using Shouldly; using Test.Common; +using static EdFi.Ods.Tests._Helpers.DomainModelHelper; -namespace EdFi.Ods.Tests.EdFi.Ods.Common.ExceptionHandling +namespace EdFi.Ods.Tests.EdFi.Ods.Api.ExceptionHandling.Translators.Postgres { [SuppressMessage("ReSharper", "InconsistentNaming")] [TestFixture] @@ -40,16 +39,18 @@ public class When_a_regular_exception_is_thrown : TestFixtureBase private bool result; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { exception = new Exception(); _contextProvider = Stub>(); + _domainModelProvider = Stub(); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); result = translator.TryTranslate(exception, out actualError); } @@ -73,16 +74,18 @@ public class When_a_generic_ADO_exception_is_thrown_without_an_inner_exception private bool wasHandled; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { exception = new GenericADOException(GenericSqlExceptionMessage, null); _contextProvider = Stub>(); + _domainModelProvider = Stub(); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -99,13 +102,14 @@ public void Should_RestError_be_null() } } - public class When_a_generic_ADO_exception_is_thrown_with_an_inner_exception_with_the_wrong_message + public class When_a_generic_ADO_exception_is_thrown_with_an_inner_exception_with_the_wrong_sql_state : TestFixtureBase { private Exception exception; private bool wasHandled; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { @@ -114,17 +118,18 @@ protected override void Arrange() exception = NHibernateExceptionBuilder.CreateWrappedPostgresException( GenericSqlExceptionMessage, slightlyWrongMessage, - PostgresSqlStates.UniqueViolation, - null, - null, + Npgsql.PostgresErrorCodes.CheckViolation, // Something we don't translate + "edfi", + "studentschoolassociation", null); _contextProvider = Stub>(); + _domainModelProvider = Stub(); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -147,6 +152,7 @@ public class When_an_insert_or_update_conflicts_with_the_unique_index_on_a_singl private bool wasHandled; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { @@ -156,18 +162,21 @@ protected override void Arrange() GenericSqlExceptionMessage, message, PostgresSqlStates.UniqueViolation, - null, - null, - null); + "edfi", + "educationorganization", + "something_pk"); _contextProvider = Stub>(); var resource = PrepareTestResource(isCompositePrimaryKey: false); A.CallTo(() => _contextProvider.Get()).Returns(new DataManagementResourceContext(resource)); + + var domainModel = this.LoadDomainModel("StudentSchoolAssociation"); + _domainModelProvider = new SuppliedDomainModelProvider(domainModel); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -183,8 +192,9 @@ public void Should_RestError_show_single_value_message() AssertHelper.All( () => actualError.ShouldNotBeNull(), () => actualError.Status.ShouldBe(409), - () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique")), - () => actualError.Detail.ShouldBe("The value supplied for property 'Property1' of entity 'Something' is not unique.") + () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-identity")), + () => actualError.Detail.ShouldBe("The identifying value(s) of the resource item are the same as another resource item that already exists."), + () => actualError.Errors.Single().ShouldBe("A primary key conflict occurred when attempting to create or update a record in the 'EducationOrganization' table.") ); } } @@ -195,6 +205,7 @@ public class When_an_insert_or_update_conflicts_with_the_unique_index_on_a_compo private bool wasHandled; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { @@ -204,18 +215,21 @@ protected override void Arrange() GenericSqlExceptionMessage, message, PostgresSqlStates.UniqueViolation, - null, - null, - null); + "edfi", + "studentschoolassociation", + "something_pk"); _contextProvider = Stub>(); var resource = PrepareTestResource(isCompositePrimaryKey: true); A.CallTo(() => _contextProvider.Get()).Returns(new DataManagementResourceContext(resource)); + + var domainModel = this.LoadDomainModel("StudentSchoolAssociation"); + _domainModelProvider = new SuppliedDomainModelProvider(domainModel); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -231,8 +245,9 @@ public void Should_RestError_show_multiple_values_message() AssertHelper.All( () => actualError.ShouldNotBeNull(), () => actualError.Status.ShouldBe(409), - () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique")), - () => actualError.Detail.ShouldBe("The values supplied for properties 'Property1', 'Property2' of entity 'Something' are not unique.") + () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-identity")), + () => actualError.Detail.ShouldBe("The identifying value(s) of the resource item are the same as another resource item that already exists."), + () => actualError.Errors.Single().ShouldBe("A primary key conflict occurred when attempting to create or update a record in the 'StudentSchoolAssociation' table.") ); } } @@ -243,6 +258,7 @@ public class When_an_insert_or_update_conflicts_with_a_non_pk_unique_index_and_d private bool wasHandled; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { @@ -254,14 +270,15 @@ protected override void Arrange() PostgresSqlStates.UniqueViolation, null, null, - null); + "ux_something_property1"); _contextProvider = Stub>(); + _domainModelProvider = Stub(); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -277,8 +294,8 @@ public void Should_RestError_show_unknown_value_message() AssertHelper.All( () => actualError.ShouldNotBeNull(), () => actualError.Status.ShouldBe(409), - () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique")), - () => actualError.Detail.ShouldBe("The value(s) supplied for the resource are not unique.") + () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-values")), + () => actualError.Detail.ShouldBe("A value (or values) in the resource item must be unique, but another resource item with these values already exists.") ); } } @@ -289,6 +306,7 @@ public class When_an_insert_or_update_conflicts_with_a_non_pk_unique_index_and_d private bool wasHandled; private IEdFiProblemDetails actualError; private IContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; protected override void Arrange() { @@ -303,15 +321,16 @@ protected override void Arrange() PostgresSqlStates.UniqueViolation, null, "something", - null, + "ux_something_property1", details); _contextProvider = Stub>(); + _domainModelProvider = Stub(); } protected override void Act() { - var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider); + var translator = new PostgresDuplicateKeyExceptionTranslator(_contextProvider, _domainModelProvider); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -327,7 +346,7 @@ public void Should_RestError_show_unknown_value_message() AssertHelper.All( () => actualError.ShouldNotBeNull(), () => actualError.Status.ShouldBe(409), - () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique")), + () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-values")), () => actualError.Detail.ShouldBe("The values supplied for properties 'property1, property2, property3' of entity 'something' are not unique.") ); } diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslatorTests.cs index b0c261a57..a2f5f8692 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslatorTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/Postgres/PostgresForeignKeyExceptionTranslatorTests.cs @@ -9,6 +9,8 @@ using EdFi.Ods.Api.Models; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Exceptions; +using EdFi.Ods.Common.Models; +using EdFi.Ods.Common.Models.Domain; using EdFi.Ods.Common.Models.Resource; using EdFi.Ods.Common.Security.Claims; using EdFi.Ods.Tests._Builders; @@ -39,8 +41,7 @@ protected override void Arrange() protected override void Act() { - var translator = - new PostgresForeignKeyExceptionTranslator(Stub>()); + var translator = new PostgresForeignKeyExceptionTranslator(Stub()); result = translator.TryTranslate(exception, out actualError); } @@ -72,8 +73,7 @@ protected override void Arrange() protected override void Act() { - var translator = - new PostgresForeignKeyExceptionTranslator(Stub>()); + var translator = new PostgresForeignKeyExceptionTranslator(Stub()); wasHandled = translator.TryTranslate(exception, out actualError); } @@ -96,8 +96,9 @@ public class When_an_insert_or_update_violated_a_foreign_key_constraint : TestFi private Exception _exception; private bool _wasHandled; private IEdFiProblemDetails _actualError; - private ContextProvider _contextProvider; - + private IDomainModelProvider _domainModelProvider; + // private ContextProvider _contextProvider; + /* Severity: ERROR InvariantSeverity: ERROR @@ -115,10 +116,12 @@ public class When_an_insert_or_update_violated_a_foreign_key_constraint : TestFi protected override void Arrange() { var domainModel = this.LoadDomainModel("StudentSchoolAssociation"); - var resourceModel = new ResourceModel(domainModel); - var resource = resourceModel.GetResourceByApiCollectionName("ed-fi", "studentSchoolAssociations"); - _contextProvider = new ContextProvider(new HashtableContextStorage()); - _contextProvider.Set(new DataManagementResourceContext(resource)); + _domainModelProvider = new DomainModelHelper.SuppliedDomainModelProvider(domainModel); + + // var resourceModel = new ResourceModel(domainModel); + // var resource = resourceModel.GetResourceByApiCollectionName("ed-fi", "studentSchoolAssociations"); + // _contextProvider = new ContextProvider(new HashtableContextStorage()); + // _contextProvider.Set(new DataManagementResourceContext(resource)); const string message = "insert or update on table \"studentschoolassociation\" violates foreign key constraint \"fk_857b52_student\""; @@ -133,7 +136,7 @@ protected override void Arrange() protected override void Act() { - var translator = new PostgresForeignKeyExceptionTranslator(_contextProvider); + var translator = new PostgresForeignKeyExceptionTranslator(_domainModelProvider); _wasHandled = translator.TryTranslate(_exception, out _actualError); } @@ -149,8 +152,8 @@ public void Should_RestError_show_simple_constraint_message() AssertHelper.All( () => _actualError.ShouldNotBeNull(), () => _actualError.Status.ShouldBe(409), - () => _actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference")), - () => _actualError.Detail.ShouldBe("The referenced 'School' resource does not exist.") + () => _actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:unresolved-reference")), + () => _actualError.Detail.ShouldBe("The referenced 'School' item does not exist.") ); } } @@ -160,7 +163,8 @@ public class When_an_update_or_delete_violates_a_foreign_key_constraint : TestFi private Exception _exception; private bool wasHandled; private IEdFiProblemDetails actualError; - private ContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; + // private ContextProvider _contextProvider; /* Severity: ERROR @@ -180,10 +184,12 @@ protected override void Arrange() { //Arrange var domainModel = this.LoadDomainModel("StudentSchoolAssociation"); - var resourceModel = new ResourceModel(domainModel); - var resource = resourceModel.GetResourceByApiCollectionName("ed-fi", "studentSchoolAssociations"); - _contextProvider = new ContextProvider(new HashtableContextStorage()); - _contextProvider.Set(new DataManagementResourceContext(resource)); + _domainModelProvider = new DomainModelHelper.SuppliedDomainModelProvider(domainModel); + + // var resourceModel = new ResourceModel(domainModel); + // var resource = resourceModel.GetResourceByApiCollectionName("ed-fi", "studentSchoolAssociations"); + // _contextProvider = new ContextProvider(new HashtableContextStorage()); + // _contextProvider.Set(new DataManagementResourceContext(resource)); const string message = "update or delete on table \"school\" violates foreign key constraint \"fk_857b52_school\" on table \"studentschoolassociation\""; @@ -198,7 +204,7 @@ protected override void Arrange() protected override void Act() { - var translator = new PostgresForeignKeyExceptionTranslator(_contextProvider); + var translator = new PostgresForeignKeyExceptionTranslator(_domainModelProvider); wasHandled = translator.TryTranslate(_exception, out actualError); } @@ -214,8 +220,8 @@ public void Should_return_a_409_Conflict_error_with_a_message_identifying_the_de AssertHelper.All( () => actualError.ShouldNotBeNull(), () => actualError.Status.ShouldBe(409), - () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference")), - () => actualError.Detail.ShouldBe("The operation cannot be performed because the resource is a dependency of the 'StudentSchoolAssociation' resource.") + () => actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:dependent-item-exists")), + () => actualError.Detail.ShouldBe("The requested action cannot be performed because this resource item is referenced by an existing 'StudentSchoolAssociation' resource item.") ); } } @@ -225,7 +231,8 @@ public class When_an_update_or_delete_conflicts_with_a_foreign_key_constraint_th private Exception _exception; private bool wasHandled; private IEdFiProblemDetails actualError; - private ContextProvider _contextProvider; + private IDomainModelProvider _domainModelProvider; + // private ContextProvider _contextProvider; /* Severity: ERROR @@ -245,10 +252,12 @@ protected override void Arrange() { //Arrange var domainModel = this.LoadDomainModel("StudentSchoolAssociation"); - var resourceModel = new ResourceModel(domainModel); - var resource = resourceModel.GetResourceByApiCollectionName("ed-fi", "studentSchoolAssociations"); - _contextProvider = new ContextProvider(new HashtableContextStorage()); - _contextProvider.Set(new DataManagementResourceContext(resource)); + _domainModelProvider = new DomainModelHelper.SuppliedDomainModelProvider(domainModel); + + // var resourceModel = new ResourceModel(domainModel); + // var resource = resourceModel.GetResourceByApiCollectionName("ed-fi", "studentSchoolAssociations"); + // _contextProvider = new ContextProvider(new HashtableContextStorage()); + // _contextProvider.Set(new DataManagementResourceContext(resource)); const string message = "update or delete on table \"school\" violates foreign key constraint \"fk_857b52_school\" on table \"studentschoolassociation\""; @@ -263,7 +272,7 @@ protected override void Arrange() protected override void Act() { - var translator = new PostgresForeignKeyExceptionTranslator(_contextProvider); + var translator = new PostgresForeignKeyExceptionTranslator(_domainModelProvider); wasHandled = translator.TryTranslate(_exception, out actualError); } @@ -279,9 +288,9 @@ public void Should_return_a_409_Conflict_error_with_a_message_indicating_a_depen actualError.ShouldSatisfyAllConditions( e => e.ShouldNotBeNull(), e => e.Status.ShouldBe(409), - e => e.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference")), + e => e.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:dependent-item-exists")), e => e.Detail.ShouldBe( - "The operation cannot be performed because the resource is a dependency of another resource.")); + "The requested action cannot be performed because this resource item is referenced by an existing 'StudentSchoolAssociation' resource item.")); } } } diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/ExceptionTranslatorFixtures.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/ExceptionTranslatorFixtures.cs index 8a64957f9..58ad6ade1 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/ExceptionTranslatorFixtures.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/ExceptionTranslatorFixtures.cs @@ -42,7 +42,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:unresolved-reference"))); } [Test] @@ -50,7 +50,7 @@ public virtual void Should_translate_the_message_to_indicate_that_a_related_resource_does_not_have_the_value_specified_in_the_current_request_but_does_not_provide_column_level_details() { Assert.That( - _actualError.Detail, Is.EqualTo("The referenced 'AddressType' resource does not exist.")); + _actualError.Detail, Is.EqualTo("The referenced 'AddressType' item does not exist.")); } } @@ -76,7 +76,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:unresolved-reference"))); } [Test] @@ -85,7 +85,7 @@ public virtual void { Assert.That( _actualError.Detail, - Is.EqualTo("The referenced 'LimitedEnglishProficiencyType' resource does not exist.")); + Is.EqualTo("The referenced 'LimitedEnglishProficiencyType' item does not exist.")); } } @@ -111,7 +111,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:dependent-item-exists"))); } [Test] @@ -121,7 +121,7 @@ public virtual void Assert.That( _actualError.Detail, Is.EqualTo( - "The operation cannot be performed because the resource is a dependency of the 'DisciplineAction' resource.")); + "The requested action cannot be performed because this resource item is referenced by an existing 'DisciplineAction' resource item.")); } } @@ -147,7 +147,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:invalid-reference"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:dependent-item-exists"))); } [Test] @@ -157,7 +157,7 @@ public virtual void Assert.That( _actualError.Detail, Is.EqualTo( - "The operation cannot be performed because the resource is a dependency of the 'CourseTranscript' resource.")); + "The requested action cannot be performed because this resource item is referenced by an existing 'CourseTranscript' resource item.")); } } @@ -196,7 +196,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-values"))); } [Test] @@ -246,7 +246,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-values"))); } [Test] @@ -294,7 +294,7 @@ protected override void Act() public virtual void Should_respond_with_a_409_Conflict() { Assert.That(_actualError.Status, Is.EqualTo((int) HttpStatusCode.Conflict)); - Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:not-unique"))); + Assert.That(_actualError.Type, Is.EqualTo(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-values"))); } [Test] diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslatorTests.cs index fb83403b9..6036e1647 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslatorTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/SqlServerPrimaryKeyConstraintExceptionTranslatorTests.cs @@ -4,8 +4,10 @@ // See the LICENSE and NOTICES files in the project root for more information. using System; +using System.Linq; using EdFi.Ods.Api.ExceptionHandling.Translators.SqlServer; using EdFi.Ods.Api.Models; +using EdFi.Ods.Api.Security.Authentication; using EdFi.Ods.Common.Context; using EdFi.Ods.Common.Exceptions; using EdFi.Ods.Common.Models.Resource; @@ -139,13 +141,17 @@ public void Should_handle_this_exception() public void Should_set_a_reasonable_message() { actualError.Detail.ShouldBe( - "A natural key conflict occurred when attempting to create a new resource 'StudentProgramAssociation' with a duplicate key. The duplicated columns and values are [BeginDate, EducationOrganizationId, ProgramEducationOrganizationId, ProgramName, ProgramTypeDescriptorId, StudentUSI] (2021-08-30, 255901, 255901, Career and Technical Education, 1921, 1)."); + "The identifying value(s) of the resource item are the same as another resource item that already exists."); + + actualError.Errors.Length.ShouldBe(1); + actualError.Errors.Single().ShouldBe( + "A primary key conflict occurred when attempting to create or update a record in the 'StudentProgramAssociation' table. The duplicate key is (BeginDate, EducationOrganizationId, ProgramEducationOrganizationId, ProgramName, ProgramTypeDescriptorId, StudentUSI) = (2021-08-30, 255901, 255901, Career and Technical Education, 1921, 1)."); } [Test] public void Should_set_the_exception_type_to_conflict() { - actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:natural-key")); + actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-identity")); } [Test] @@ -193,13 +199,17 @@ public void Should_handle_this_exception() public void Should_set_a_reasonable_message() { actualError.Detail.ShouldBe( - "A natural key conflict occurred when attempting to create a new resource 'Session' with a duplicate key. The duplicated columns and values are [SchoolId, SchoolYear, SessionName] (900007, 9, 2014)."); + "The identifying value(s) of the resource item are the same as another resource item that already exists."); + + actualError.Errors.Length.ShouldBe(1); + actualError.Errors.Single().ShouldBe( + "A primary key conflict occurred when attempting to create or update a record in the 'Session' table. The duplicate key is (SchoolId, SchoolYear, SessionName) = (900007, 9, 2014)."); } [Test] public void Should_set_the_exception_type_to_conflict() { - actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:natural-key")); + actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:non-unique-identity")); } [Test] diff --git a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/StaleObjectStateExceptionTranslatorTests.cs b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/StaleObjectStateExceptionTranslatorTests.cs index 0d0dc4e49..67378fe97 100644 --- a/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/StaleObjectStateExceptionTranslatorTests.cs +++ b/Application/EdFi.Ods.Tests/EdFi.Ods.Api/ExceptionHandling/Translators/SqlServer/StaleObjectStateExceptionTranslatorTests.cs @@ -128,13 +128,13 @@ public void Should_handle_this_exception() public void Should_set_a_reasonable_message() { actualError.Detail.ShouldBe( - "A natural key conflict occurred when attempting to update a new resource with a duplicate key."); + "The resource item was modified or deleted by another user while processing the request. Resending this request will either recreate the item, or introduce of copy with a different identifier."); } [Test] public void Should_set_the_exception_type_to_conflict() { - actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:natural-key")); + actualError.Type.ShouldBe(string.Join(':', EdFiProblemDetailsExceptionBase.BaseTypePrefix, "conflict:multi-user-conflict")); } [Test] diff --git a/Application/EdFi.Ods.Tests/_Helpers/DomainModelHelper.cs b/Application/EdFi.Ods.Tests/_Helpers/DomainModelHelper.cs index 93072e925..1863fc0b0 100644 --- a/Application/EdFi.Ods.Tests/_Helpers/DomainModelHelper.cs +++ b/Application/EdFi.Ods.Tests/_Helpers/DomainModelHelper.cs @@ -35,7 +35,7 @@ public static DomainModel LoadDomainModel(this object obj, string modelName) public static IDomainModelProvider GetDomainModelProvider(this object obj, string modelName) { - return new DomainModelProvider(LoadDomainModel(obj, modelName)); + return new SuppliedDomainModelProvider(LoadDomainModel(obj, modelName)); } public static IResourceModelProvider GetResourceModelProvider(this object obj, string modelName) @@ -43,11 +43,11 @@ public static IResourceModelProvider GetResourceModelProvider(this object obj, s return new ResourceModelProvider(GetDomainModelProvider(obj, modelName)); } - private class DomainModelProvider : IDomainModelProvider + public class SuppliedDomainModelProvider : IDomainModelProvider { private readonly DomainModel _domainModel; - public DomainModelProvider(DomainModel domainModel) + public SuppliedDomainModelProvider(DomainModel domainModel) { _domainModel = domainModel; } diff --git a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests Multiple Key-Secrets.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests Multiple Key-Secrets.postman_collection.json index 3921c1d54..5160b625d 100644 --- a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests Multiple Key-Secrets.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests Multiple Key-Secrets.postman_collection.json @@ -5919,10 +5919,13 @@ "", "pm.test(\"Should return a problem details result for invalid-reference conflict\", () => {", " pm.expect(pm.response.code).equal(problemDetails.status);", - " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:conflict:invalid-reference\");", - "});" + " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:conflict:dependent-item-exists\");", + " pm.expect(problemDetails.title).to.equal(\"Dependent Resource Item Exists\");", + "});", + "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -5931,7 +5934,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], diff --git a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests.postman_collection.json index e53efca36..1ef1032ce 100644 --- a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite AuthorizationTests.postman_collection.json @@ -5259,17 +5259,27 @@ "listen": "test", "script": { "exec": [ - "pm.test(\"Response is 409\", () => {", - " pm.expect(pm.response.code).to.equal(409) ", - " });", + "pm.test(\"Status code is 409\", () => {", + " pm.expect(pm.response.code).to.equal(409);", + "});", + "", "const problemDetails = pm.response.json();", "", - "pm.test(\"Should return a problem details result for invalid-reference conflict\", () => {", - " pm.expect(pm.response.code).equal(problemDetails.status);", - " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:conflict:invalid-reference\");", + "pm.test(\"Should return a problem details result for dependent item exists\", () => {", + " pm.expect(problemDetails).to.deep.include({", + " \"type\": \"urn:ed-fi:api:conflict:dependent-item-exists\",", + " \"title\": \"Dependent Resource Item Exists\",", + " \"status\": 409", + " });", + "", + " const parentOrContact = pm.environment.get(\"ParentOrContactProperName\");", + " const expectedDetail = `The requested action cannot be performed because this resource item is referenced by an existing 'Student${parentOrContact}Association' resource item.`;", + "", + " pm.expect(problemDetails.detail).to.equal(expectedDetail);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -5278,7 +5288,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], diff --git a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json index a412cb9ca..4ac95b085 100644 --- a/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json +++ b/Postman Test Suite/Ed-Fi ODS-API Integration Test Suite ResponseTests.postman_collection.json @@ -5861,62 +5861,279 @@ "name": "api_should_fail_with_409_code", "item": [ { - "name": "api_should_fail_with_409_conflict", - "event": [ + "name": "Reference to non-existing Person", + "item": [ { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is 409\", () => {\r", - " pm.expect(pm.response.code).to.equal(409);\r", - "});\r", - "\r", - "const problemDetails = pm.response.json();\r", - "\r", - "pm.test(\"Should return a problem details result\", () => {\r", - " pm.expect(pm.response.code).equal(problemDetails.status);\r", - " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:conflict:invalid-reference\");\r", - " pm.expect(problemDetails.detail).to.equal(\"The referenced 'Student' resource does not exist.\");\r", - "});\r", - "\r", - "pm.test(\"Should return a message indicating that access was denied, student was not found.\", () => {\r", - " pm.expect(problemDetails.detail).to.equal(\"The referenced 'Student' resource does not exist.\");\r", - " // NOTE: This was previously handled by a \"SecurityConflictException\" in C#, and provided an authorization related 409 message.\r", - " // Previous expected message: \"Access to the resource item could not be authorized because the 'Student' was not found.\"\r", - "});\r", - "" + "name": "api_should_fail_with_409_conflict", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 409\", () => {\r", + " pm.expect(pm.response.code).to.equal(409);\r", + "});\r", + "\r", + "const problemDetails = pm.response.json();\r", + "\r", + "pm.test(\"Should return a problem details result\", () => {\r", + " pm.expect(pm.response.code).equal(problemDetails.status);\r", + " pm.expect(problemDetails.detail).to.equal(\"The referenced 'Student' item does not exist.\");\r", + " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:conflict:unresolved-reference\");\r", + " pm.expect(problemDetails.title).to.equal(\"Unresolved Reference\");\r", + "});\r", + "\r", + "pm.test(\"Should return a message indicating that access was denied, student was not found.\", () => {\r", + " pm.expect(problemDetails.detail).to.equal(\"The referenced 'Student' item does not exist.\");\r", + " // NOTE: This was previously handled by a \"SecurityConflictException\" in C#, and provided an authorization related 409 message.\r", + " // Previous expected message: \"Access to the resource item could not be authorized because the 'Student' was not found.\"\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } ], - "type": "text/javascript" - } + "body": { + "mode": "raw", + "raw": "{\r\n \"schoolReference\": {\r\n \"schoolId\": \"255901001\"\r\n },\r\n \"studentReference\": {\r\n \"studentUniqueId\": \"999999\"\r\n },\r\n \"entryDate\": \"2021-08-23\",\r\n \"entryGradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Ninth grade\"\r\n}" + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/studentSchoolAssociations", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "studentSchoolAssociations" + ] + } + }, + "response": [] } - ], - "request": { - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "body": { - "mode": "raw", - "raw": "{ \r\n \"schoolReference\": {\r\n \"schoolId\": \"255901001\"\r\n },\r\n \"studentReference\": {\r\n \"studentUniqueId\": \"999999\"\r\n },\r\n \"entryDate\": \"2021-08-23\",\r\n \"entryGradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#Ninth grade\"\r\n }" + ] + }, + { + "name": "Reference to Non-Existing Item on Create/Update", + "item": [ + { + "name": "Create School with reference to non-existing LEA", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 409\", () => {\r", + " pm.expect(pm.response.code).to.equal(409);\r", + "});\r", + "\r", + "const problemDetails = pm.response.json();\r", + "\r", + "pm.test(\"Problem Details response indicates referenced LocalEducationAgency item does not exist.\", () => {\r", + " pm.expect(problemDetails).to.deep.include({\r", + " \"detail\": \"The referenced 'LocalEducationAgency' item does not exist.\",\r", + " \"type\": \"urn:ed-fi:api:conflict:unresolved-reference\",\r", + " \"title\": \"Unresolved Reference\",\r", + " \"status\": 409,\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": " {\r\n \"localEducationAgencyReference\": {\r\n \"localEducationAgencyId\": 99999999\r\n },\r\n \"schoolId\": 88888888,\r\n \"nameOfInstitution\": \"UNRESOLVED REFERENCE TEST\",\r\n \"addresses\": [],\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://ed-fi.org/EducationOrganizationCategoryDescriptor#School\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#First grade\"\r\n }\r\n ]\r\n }" + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] }, - "url": { - "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/studentSchoolAssociations", - "host": [ - "{{ApiBaseUrl}}" + { + "name": "Create School with reference to existing LEA", + "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:school:id', pm.response.headers.one('Location').value.split(\"/\").pop());\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } ], - "path": [ - "data", - "v3", - "ed-fi", - "studentSchoolAssociations" - ] + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": " {\r\n \"localEducationAgencyReference\": {\r\n \"localEducationAgencyId\": 255901\r\n },\r\n \"schoolId\": 88888888,\r\n \"nameOfInstitution\": \"UNRESOLVED REFERENCE UPDATE TEST\",\r\n \"addresses\": [],\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://ed-fi.org/EducationOrganizationCategoryDescriptor#School\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#First grade\"\r\n }\r\n ]\r\n }" + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] + }, + { + "name": "Update School with reference to non-existing LEA", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 409\", () => {\r", + " pm.expect(pm.response.code).to.equal(409);\r", + "});\r", + "\r", + "const problemDetails = pm.response.json();\r", + "\r", + "pm.test(\"Problem Details response indicates referenced LocalEducationAgency item does not exist.\", () => {\r", + " pm.expect(problemDetails).to.deep.include({\r", + " \"detail\": \"The referenced 'LocalEducationAgency' item does not exist.\",\r", + " \"type\": \"urn:ed-fi:api:conflict:unresolved-reference\",\r", + " \"title\": \"Unresolved Reference\",\r", + " \"status\": 409,\r", + " });\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": " {\r\n \"localEducationAgencyReference\": {\r\n \"localEducationAgencyId\": 99999999\r\n },\r\n \"schoolId\": 88888888,\r\n \"nameOfInstitution\": \"UNRESOLVED REFERENCE UPDATE TEST\",\r\n \"addresses\": [],\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://ed-fi.org/EducationOrganizationCategoryDescriptor#School\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#First grade\"\r\n }\r\n ]\r\n }" + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools" + ] + } + }, + "response": [] + }, + { + "name": "Delete 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", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": " {\r\n \"localEducationAgencyReference\": {\r\n \"localEducationAgencyId\": 255901\r\n },\r\n \"schoolId\": 88888888,\r\n \"nameOfInstitution\": \"UNRESOLVED REFERENCE UPDATE TEST\",\r\n \"addresses\": [],\r\n \"educationOrganizationCategories\": [\r\n {\r\n \"educationOrganizationCategoryDescriptor\": \"uri://ed-fi.org/EducationOrganizationCategoryDescriptor#School\"\r\n }\r\n ],\r\n \"gradeLevels\": [\r\n {\r\n \"gradeLevelDescriptor\": \"uri://ed-fi.org/GradeLevelDescriptor#First grade\"\r\n }\r\n ]\r\n }" + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/schools/{{known:school:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "schools", + "{{known:school:id}}" + ] + } + }, + "response": [] } - }, - "response": [] + ] } ] } @@ -7022,15 +7239,17 @@ "", "pm.test(\"Should return a problem details result\", () => {", " pm.expect(pm.response.code).equal(problemDetails.status);", - " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:multiuser-conflict\");", + " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:optimistic-lock-failed\");", + " pm.expect(problemDetails.title).to.equal(\"Optimistic Lock Failed\");", "});", "", - "pm.test(\"Should return detail indicating that Resource was modified by another consumer.\", () => {", - " pm.expect(problemDetails.detail).to.equal(\"The resource was modified by another user while attempting to perform the current operation.\");", + "pm.test(\"Should return detail indicating that Resource item was modified by another user.\", () => {", + " pm.expect(problemDetails.detail).to.equal(\"The resource item has been modified by another user.\");", "});", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -7039,7 +7258,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -8209,54 +8429,34 @@ "name": "when_deleting_a_resource_would_break_referential_integrity", "item": [ { - "name": "api_should_fail_with_409_code", + "name": "Setup", "item": [ { - "name": "Initialize Student Data for Delete Request", + "name": "Create Student", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 201\", () => {", - " pm.expect(pm.response.code).to.equal(201);", + " pm.expect(pm.response.code).to.equal(201);", "});", "", - "", - "const scenarioId = pm.environment.get('scenarioId');", - "pm.environment.set('known:'+scenarioId+':studentGuid',pm.response.headers.one('Location').value.split(\"/\").pop());", - "pm.environment.set('known:'+scenarioId+':studentUniqueId',pm.environment.get('supplied:'+scenarioId+':studentUniqueId'));", - "", - "", - " " + "pm.environment.set('known:student:id', pm.response.headers.one('Location').value.split(\"/\").pop());", + "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { "listen": "prerequest", "script": { "exec": [ - "const uuid = require('uuid');", - "function newGuid() { return uuid.v4().toString().replace(/[^a-zA-Z0-9 ]/g,\"\"); }", - "function createScenarioId() { return newGuid().substring(0,5); }", - "pm.environment.set('scenarioId', createScenarioId());", - "", - "const scenarioId = pm.environment.get('scenarioId');", - "", - "pm.environment.set('supplied:'+scenarioId+':studentUniqueId', newGuid());", - "pm.environment.set('supplied:'+scenarioId+':lastSurname', newGuid());", - "pm.environment.set('supplied:'+scenarioId+':firstName', newGuid());", - "", - "", - "const moment = require('moment');", - "let birthDate=new Date();", - "birthDate = birthDate.addYears(-20);", - "birthDate= moment(birthDate).format(\"YYYY-MM-DD\");", - "pm.environment.set('supplied:'+scenarioId+':birthDate',birthDate);", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -8272,7 +8472,7 @@ ], "body": { "mode": "raw", - "raw": "{\r\n \"studentUniqueId\": \"{{supplied:{{scenarioId}}:studentUniqueId}}\",\r\n \"birthDate\":\"{{supplied:{{scenarioId}}:birthDate}}\",\r\n \"firstName\": \"{{supplied:{{scenarioId}}:firstName}}\",\r\n \"lastSurname\": \"{{supplied:{{scenarioId}}:lastSurname}}\"\r\n}" + "raw": "{\r\n \"studentUniqueId\": \"DELETE-CONFLICT-TEST\",\r\n \"birthDate\": \"2010-01-01\",\r\n \"firstName\": \"John\",\r\n \"lastSurname\": \"Doe\"\r\n}" }, "url": { "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/students", @@ -8290,22 +8490,16 @@ "response": [] }, { - "name": "Initialize StudentSchoolAssociations Data for Delete Request", + "name": "Create StudentSchoolAssociation", "event": [ { "listen": "prerequest", "script": { "exec": [ - "const moment = require('moment');", - "const scenarioId = pm.environment.get('scenarioId');", - "let entryDate=new Date();", - "entryDate = entryDate.addMonths(-10);", - "entryDate= moment(entryDate).format(\"YYYY-MM-DD\");", - "pm.environment.set('supplied:'+scenarioId+':entryDate',entryDate);", - "pm.environment.set('supplied:'+scenarioId+':entryGradeLevelDescriptor',\"uri://ed-fi.org/GradeLevelDescriptor#Fourth grade\");", - " " + "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -8315,9 +8509,12 @@ "pm.test(\"Status code is 201\", () => {", " pm.expect(pm.response.code).to.equal(201);", "});", + "", + "pm.environment.set('known:studentSchoolAssociation:id', pm.response.headers.one('Location').value.split(\"/\").pop());", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -8333,7 +8530,7 @@ ], "body": { "mode": "raw", - "raw": "{ \r\n \"schoolReference\":{ \r\n \"schoolId\":\"{{known:schoolId}}\"\r\n },\r\n \"studentReference\":{ \r\n \"studentUniqueId\":\"{{known:{{scenarioId}}:studentUniqueId}}\"\r\n },\r\n \"entryDate\":\"{{supplied:{{scenarioId}}:entryDate}}\",\r\n \"entryGradeLevelDescriptor\":\"{{supplied:{{scenarioId}}:entryGradeLevelDescriptor}}\"\r\n \r\n}" + "raw": "{ \r\n \"schoolReference\":{ \r\n \"schoolId\":\"255901001\"\r\n },\r\n \"studentReference\":{ \r\n \"studentUniqueId\":\"DELETE-CONFLICT-TEST\"\r\n },\r\n \"entryDate\":\"2020-02-02\",\r\n \"entryGradeLevelDescriptor\":\"uri://ed-fi.org/GradeLevelDescriptor#Fourth grade\"\r\n}" }, "url": { "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/StudentSchoolAssociations", @@ -8350,26 +8547,36 @@ "description": "This api post method adds new academicWeeks for particular school .\nThis test method will throw WeekIdentifier is required error when WeekIdentifier is not passed" }, "response": [] - }, + } + ] + }, + { + "name": "Attempt to delete item with dependencies", + "item": [ { - "name": "api_should_fail_with_409_code", + "name": "Delete Student fails with 409", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 409\", () => {", - " pm.expect(pm.response.code).to.equal(409);", + " pm.expect(pm.response.code).to.equal(409);", "});", - "const problemDetails = pm.response.json();", "", - "pm.test(\"Should return a problem details result for invalid-reference conflict\", () => {", - " pm.expect(pm.response.code).equal(problemDetails.status);", - " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:conflict:invalid-reference\");", + "const problemDetails = pm.response.json();", "", + "pm.test(\"Should return a problem details result for dependent item exists\", () => {", + " pm.expect(problemDetails).to.deep.include({", + " \"detail\": \"The requested action cannot be performed because this resource item is referenced by an existing 'StudentSchoolAssociation' resource item.\",", + " \"type\": \"urn:ed-fi:api:conflict:dependent-item-exists\",", + " \"title\": \"Dependent Resource Item Exists\",", + " \"status\": 409", + " });", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -8378,7 +8585,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -8386,7 +8594,7 @@ "method": "DELETE", "header": [], "url": { - "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/students/{{known:{{scenarioId}}:studentGuid}}", + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/students/{{known:student:id}}", "host": [ "{{ApiBaseUrl}}" ], @@ -8395,7 +8603,116 @@ "v3", "ed-fi", "students", - "{{known:{{scenarioId}}:studentGuid}}" + "{{known:student:id}}" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Teardown", + "item": [ + { + "name": "Delete StudentSchoolAssociation", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {", + " pm.expect(pm.response.code).to.equal(204);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/studentSchoolAssociations/{{known:studentSchoolAssociation:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "studentSchoolAssociations", + "{{known:studentSchoolAssociation:id}}" + ] + }, + "description": "This api post method adds new academicWeeks for particular school .\nThis test method will throw WeekIdentifier is required error when WeekIdentifier is not passed" + }, + "response": [] + }, + { + "name": "Delete Student", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 204\", () => {", + " pm.expect(pm.response.code).to.equal(204);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{ApiBaseUrl}}/data/v3/ed-fi/students/{{known:student:id}}", + "host": [ + "{{ApiBaseUrl}}" + ], + "path": [ + "data", + "v3", + "ed-fi", + "students", + "{{known:student:id}}" ] } }, @@ -8853,15 +9170,17 @@ "", "pm.test(\"Should return a problem details result\", () => {", " pm.expect(pm.response.code).equal(problemDetails.status);", - " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:multiuser-conflict\");", + " pm.expect(problemDetails.type).to.equal(\"urn:ed-fi:api:optimistic-lock-failed\");", + " pm.expect(problemDetails.title).to.equal(\"Optimistic Lock Failed\");", "});", "", - "pm.test(\"Should return detail indicating that resource was modified by another user.\", () => {", - " pm.expect(problemDetails.detail).to.equal(\"The resource was modified by another user while attempting to perform the current operation.\");", + "pm.test(\"Should return detail indicating that Resource item was modified by another user.\", () => {", + " pm.expect(problemDetails.detail).to.equal(\"The resource item has been modified by another user.\");", "});", "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } }, { @@ -8870,7 +9189,8 @@ "exec": [ "" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ],