добавлены patch
This commit is contained in:
@@ -5,7 +5,6 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Api.Auth;
|
||||
|
||||
// Используется только когда Entra:TenantId/Entra:Audience не заданы (локальная разработка без реального Entra ID).
|
||||
public sealed class DevAuthHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
|
||||
@@ -35,6 +35,13 @@ public sealed class RequestTypesController(RequestTypeService service) : Control
|
||||
public async Task<IActionResult> Update(int id, UpdateRequestTypeRequest body, CancellationToken ct) =>
|
||||
(await service.UpdateAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpPatch("{id:int}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<RequestTypeDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Patch(int id, PatchRequestTypeRequest body, CancellationToken ct) =>
|
||||
(await service.PatchAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -36,6 +36,14 @@ public sealed class TemplateRouteStepsController(TemplateRouteStepService servic
|
||||
public async Task<IActionResult> Update(int routeId, int id, UpdateTemplateRouteStepRequest body, CancellationToken ct) =>
|
||||
(await service.UpdateAsync(routeId, id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpPatch("{id:int}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteStepDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Patch(int routeId, int id, PatchTemplateRouteStepRequest body, CancellationToken ct) =>
|
||||
(await service.PatchAsync(routeId, id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -45,6 +45,14 @@ public sealed class TemplateRoutesController(TemplateRouteService service) : Con
|
||||
public async Task<IActionResult> Update(int id, UpdateTemplateRouteRequest body, CancellationToken ct) =>
|
||||
(await service.UpdateAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpPatch("{id:int}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<TemplateRouteDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Patch(int id, PatchTemplateRouteRequest body, CancellationToken ct) =>
|
||||
(await service.PatchAsync(id, body, ct)).ToActionResult();
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ApiError), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -5,7 +5,6 @@ using Tnsu.ApprovalRoute.Api.Common;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Api.Filters;
|
||||
|
||||
// Валидирует аргументы action'ов, для которых зарегистрирован IValidator<T> — без валидации внутри сервисов.
|
||||
public sealed class ValidationActionFilter(IServiceProvider serviceProvider) : IAsyncActionFilter
|
||||
{
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
|
||||
@@ -47,7 +47,6 @@ builder.Services.AddOpenApi(options =>
|
||||
builder.Services.AddHealthChecks();
|
||||
builder.Services.AddControllers(options => options.Filters.Add<ValidationActionFilter>())
|
||||
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
||||
// Тот же конвертер для Http.Json — их читает генератор OpenAPI, чтобы enum в схеме были строками, а не числами.
|
||||
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
|
||||
|
||||
var app = builder.Build();
|
||||
@@ -56,8 +55,6 @@ if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
|
||||
// Scalar API reference UI — нативно понимает документ MapOpenApi (включая securitySchemes/security)
|
||||
// и корректно прикрепляет Bearer-токен к запросам.
|
||||
app.MapScalarApiReference(options =>
|
||||
{
|
||||
options
|
||||
|
||||
@@ -5,3 +5,5 @@ public sealed record RequestTypeDto(int Id, string Name, string PageUrl);
|
||||
public sealed record CreateRequestTypeRequest(string Name, string PageUrl);
|
||||
|
||||
public sealed record UpdateRequestTypeRequest(string Name, string PageUrl);
|
||||
|
||||
public sealed record PatchRequestTypeRequest(string? Name, string? PageUrl);
|
||||
|
||||
@@ -49,6 +49,19 @@ public sealed class RequestTypeService(ApprovalRouteDbContext db)
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<RequestTypeDto>> PatchAsync(int id, PatchRequestTypeRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.RequestTypes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<RequestTypeDto>(new NotFoundError("Тип заявки", id));
|
||||
|
||||
entity.Name = request.Name ?? entity.Name;
|
||||
entity.PageUrl = request.PageUrl ?? entity.PageUrl;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.RequestTypes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
|
||||
@@ -19,3 +19,14 @@ public sealed class UpdateRequestTypeRequestValidator : AbstractValidator<Update
|
||||
RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы обязателен.");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PatchRequestTypeRequestValidator : AbstractValidator<PatchRequestTypeRequest>
|
||||
{
|
||||
public PatchRequestTypeRequestValidator()
|
||||
{
|
||||
When(x => x.Name is not null, () =>
|
||||
RuleFor(x => x.Name).NotEmpty().WithMessage("Имя типа заявки не может быть пустым."));
|
||||
When(x => x.PageUrl is not null, () =>
|
||||
RuleFor(x => x.PageUrl).NotEmpty().WithMessage("Url страницы не может быть пустым."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,3 +31,12 @@ public sealed record UpdateTemplateRouteStepRequest(
|
||||
HeadType HeadType,
|
||||
RouteStepStatus Status,
|
||||
bool AllowReassignToOtherApprovers);
|
||||
|
||||
public sealed record PatchTemplateRouteStepRequest(
|
||||
int? OrderNo,
|
||||
string? ExecutorEmail,
|
||||
string? Position,
|
||||
ProjectRole? ProjectRole,
|
||||
HeadType? HeadType,
|
||||
RouteStepStatus? Status,
|
||||
bool? AllowReassignToOtherApprovers);
|
||||
|
||||
+44
-1
@@ -103,6 +103,50 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteStepDto>> PatchAsync(
|
||||
int routeId, int id, PatchTemplateRouteStepRequest r, CancellationToken cancellationToken)
|
||||
{
|
||||
var route = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == routeId, cancellationToken);
|
||||
if (route is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Маршрут", routeId));
|
||||
|
||||
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteStepDto>(new NotFoundError("Шаг маршрута", id));
|
||||
|
||||
var executorEmail = r.ExecutorEmail ?? entity.ExecutorEmail;
|
||||
var position = r.Position ?? entity.Position;
|
||||
var projectRole = r.ProjectRole ?? entity.ProjectRole;
|
||||
var headType = r.HeadType ?? entity.HeadType;
|
||||
var status = r.Status ?? entity.Status;
|
||||
|
||||
if (!route.ByProject && projectRole != ProjectRole.None)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Роль в проекте недопустима для шага маршрута, который не учитывает проект (by_project = false)."));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(executorEmail) && string.IsNullOrWhiteSpace(position) &&
|
||||
projectRole == ProjectRole.None && headType == HeadType.None)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ValidationError(
|
||||
"Должно быть заполнено хотя бы одно из полей: исполнитель, должность, роль в проекте или тип руководителя."));
|
||||
|
||||
var duplicate = await HasDuplicateAsync(
|
||||
route.Id, executorEmail, position, projectRole, headType, status, excludeId: entity.Id, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteStepDto>(new ConflictError(
|
||||
"Шаг с такими исполнителем, должностью, ролью в проекте, типом руководителя и статусом уже существует в этом маршруте."));
|
||||
|
||||
entity.OrderNo = r.OrderNo ?? entity.OrderNo;
|
||||
entity.ExecutorEmail = executorEmail;
|
||||
entity.Position = position;
|
||||
entity.ProjectRole = projectRole;
|
||||
entity.HeadType = headType;
|
||||
entity.Status = status;
|
||||
entity.AllowReassignToOtherApprovers = r.AllowReassignToOtherApprovers ?? entity.AllowReassignToOtherApprovers;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(await ToDtoWithWarningAsync(entity, route, cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(int routeId, int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRouteSteps.FirstOrDefaultAsync(x => x.Id == id && x.RouteId == routeId, cancellationToken);
|
||||
@@ -128,7 +172,6 @@ public sealed class TemplateRouteStepService(ApprovalRouteDbContext db)
|
||||
x.Status == status,
|
||||
cancellationToken);
|
||||
|
||||
// Для маршрута «по проекту» без единого шага с ролью в проекте DTO несёт предупреждение в поле Warning.
|
||||
private async Task<TemplateRouteStepDto> ToDtoWithWarningAsync(
|
||||
TemplateRouteStep entity, TemplateRoute route, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -25,3 +25,10 @@ public sealed record UpdateTemplateRouteRequest(
|
||||
Guid CompanyId,
|
||||
bool ByProject,
|
||||
bool RequiresControl);
|
||||
|
||||
public sealed record PatchTemplateRouteRequest(
|
||||
RouteType? RouteType,
|
||||
Guid? DepartmentId,
|
||||
Guid? CompanyId,
|
||||
bool? ByProject,
|
||||
bool? RequiresControl);
|
||||
|
||||
@@ -84,6 +84,31 @@ public sealed class TemplateRouteService(
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result<TemplateRouteDto>> PatchAsync(int id, PatchTemplateRouteRequest r, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (entity is null)
|
||||
return Result.Fail<TemplateRouteDto>(new NotFoundError("Маршрут", id));
|
||||
|
||||
var departmentId = r.DepartmentId ?? entity.DepartmentId;
|
||||
var companyId = r.CompanyId ?? entity.CompanyId;
|
||||
var byProject = r.ByProject ?? entity.ByProject;
|
||||
|
||||
var duplicate = await HasDuplicateAsync(entity.RequestTypeId, departmentId, companyId, byProject, excludeId: entity.Id, cancellationToken);
|
||||
if (duplicate)
|
||||
return Result.Fail<TemplateRouteDto>(new ConflictError(
|
||||
"Маршрут с таким типом заявки, отделом, компанией и признаком «по проекту» уже существует."));
|
||||
|
||||
entity.RouteType = r.RouteType ?? entity.RouteType;
|
||||
entity.DepartmentId = departmentId;
|
||||
entity.CompanyId = companyId;
|
||||
entity.ByProject = byProject;
|
||||
entity.RequiresControl = r.RequiresControl ?? entity.RequiresControl;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ToDto(entity));
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await db.TemplateRoutes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
@@ -162,8 +187,6 @@ public sealed class TemplateRouteService(
|
||||
|
||||
private static ResolvedRouteStepDto ResolveStep(TemplateRouteStep step, IReadOnlyList<ProjectExecutorInfo> projectExecutors)
|
||||
{
|
||||
// Для шагов с ролью в проекте фактический исполнитель (email) приходит из данных проекта (api.tnsu.kz);
|
||||
// должность берётся из шаблона, т.к. проект её не отдаёт. Для шагов без роли — статичный ExecutorEmail шаблона.
|
||||
if (step.ProjectRole != ProjectRole.None)
|
||||
{
|
||||
var projectExecutor = projectExecutors.FirstOrDefault(e => e.ProjectRole == step.ProjectRole);
|
||||
|
||||
@@ -19,6 +19,15 @@ public sealed class UpdateTemplateRouteRequestValidator : AbstractValidator<Upda
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PatchTemplateRouteRequestValidator : AbstractValidator<PatchTemplateRouteRequest>
|
||||
{
|
||||
public PatchTemplateRouteRequestValidator()
|
||||
{
|
||||
When(x => x.CompanyId is not null, () =>
|
||||
RuleFor(x => x.CompanyId!.Value).NotEqual(Guid.Empty).WithMessage("Компания обязательна."));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ResolveTemplateRouteRequestValidator : AbstractValidator<ResolveTemplateRouteRequest>
|
||||
{
|
||||
public ResolveTemplateRouteRequestValidator()
|
||||
|
||||
@@ -8,8 +8,6 @@ public class TemplateRoute
|
||||
public RouteType RouteType { get; set; } = RouteType.Sequential;
|
||||
public int RequestTypeId { get; set; }
|
||||
|
||||
// Id компании/отдела инициатора из 1С/AD (api-tnsu-portal /Employee/ByEmail) — без FK в этой БД.
|
||||
// Отдел опционален, компания обязательна.
|
||||
public Guid? DepartmentId { get; set; }
|
||||
public Guid CompanyId { get; set; }
|
||||
public bool ByProject { get; set; }
|
||||
|
||||
@@ -7,7 +7,6 @@ public class TemplateRouteStep
|
||||
public int Id { get; set; }
|
||||
public int OrderNo { get; set; }
|
||||
|
||||
// Внешняя ссылка на пользователя (1С/ZUP) — без локальной таблицы Users.
|
||||
public string ExecutorEmail { get; set; } = string.Empty;
|
||||
public string Position { get; set; } = string.Empty;
|
||||
public ProjectRole ProjectRole { get; set; } = ProjectRole.None;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
namespace Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
// Роли в проекте — соответствуют именованным полям проекта в api-tnsu-portal
|
||||
// (project_manager / planner / manager_hse / security).
|
||||
public enum ProjectRole
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -8,14 +8,11 @@ using Tnsu.ApprovalRoute.Domain.Enums;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
// Клиент api.tnsu.kz (api-tnsu-portal).
|
||||
public sealed class HttpTnsuPortalClient(
|
||||
HttpClient httpClient,
|
||||
TnsuPortalTokenProvider tokenProvider,
|
||||
IHttpContextAccessor httpContextAccessor) : ITnsuPortalClient
|
||||
{
|
||||
// Разные эндпоинты api-tnsu-portal отдают JSON в разном регистре (Dapper-результаты — PascalCase,
|
||||
// строго типизированные модели — camelCase), поэтому сопоставляем поля без учёта регистра.
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
public async Task<EmployeeDirectoryInfo?> GetEmployeeByEmailAsync(string email, CancellationToken cancellationToken)
|
||||
@@ -49,7 +46,6 @@ public sealed class HttpTnsuPortalClient(
|
||||
if (project is null)
|
||||
return [];
|
||||
|
||||
// Каждая роль проекта — email сотрудника в соответствующем поле (project_manager / planner / manager_hse / security).
|
||||
var executors = new List<ProjectExecutorInfo>();
|
||||
AddIfPresent(executors, ProjectRole.ProjectManager, project.ProjectManager);
|
||||
AddIfPresent(executors, ProjectRole.Planner, project.Planner);
|
||||
@@ -65,9 +61,6 @@ public sealed class HttpTnsuPortalClient(
|
||||
executors.Add(new ProjectExecutorInfo(role, email));
|
||||
}
|
||||
|
||||
// ВРЕМЕННО: для TnsuPortal ещё нет своего client-credentials приложения в Entra, поэтому прокидываем
|
||||
// токен текущего пользователя дальше — его aud совпадает с ClientId api-tnsu-portal, так что подходит.
|
||||
// Когда появится своё приложение — вернуть на tokenProvider.GetAccessTokenAsync(...).
|
||||
private async Task AuthorizeAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var incoming = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString();
|
||||
|
||||
@@ -4,7 +4,6 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
// Сервисный (client-credentials) токен для вызова api.tnsu.kz — не связан с токеном конечного пользователя.
|
||||
public sealed class TnsuPortalTokenProvider(HttpClient httpClient, IOptions<TnsuPortalOptions> options)
|
||||
{
|
||||
private string? _cachedToken;
|
||||
|
||||
+27
@@ -66,6 +66,33 @@ public sealed class RequestTypeServiceTests(PostgresDatabaseFixture fixture) : I
|
||||
result.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Патч типа заявки меняет только переданное поле, остальные остаются прежними")]
|
||||
public async Task PatchAsync_UpdatesOnlyProvidedFields()
|
||||
{
|
||||
// Arrange
|
||||
var created = await _service.CreateAsync(new CreateRequestTypeRequest("Original name", "/original"), CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
created.Value.Id, new PatchRequestTypeRequest(Name: "Patched name", PageUrl: null), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
patched.IsSuccess.ShouldBeTrue();
|
||||
patched.Value.Name.ShouldBe("Patched name");
|
||||
patched.Value.PageUrl.ShouldBe("/original");
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Патч несуществующего типа заявки возвращает неуспешный результат")]
|
||||
public async Task PatchAsync_UnknownId_ReturnsFailedResult()
|
||||
{
|
||||
// Action
|
||||
var result = await _service.PatchAsync(
|
||||
999999, new PatchRequestTypeRequest(Name: "X", PageUrl: null), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.IsFailed.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Удаление типа заявки убирает его из базы")]
|
||||
public async Task DeleteAsync_RemovesEntity()
|
||||
{
|
||||
|
||||
+23
@@ -106,6 +106,29 @@ public sealed class TemplateRouteServiceTests : IClassFixture<PostgresDatabaseFi
|
||||
updated.Value.RequiresControl.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Патч маршрута меняет только переданные поля, остальные остаются прежними")]
|
||||
public async Task PatchAsync_UpdatesOnlyProvidedFields()
|
||||
{
|
||||
// Arrange
|
||||
var requestTypeId = await CreateRequestTypeAsync("Patch route request");
|
||||
var created = await _service.CreateAsync(
|
||||
new CreateTemplateRouteRequest(RouteType.Sequential, requestTypeId, DepartmentId, CompanyId, ByProject: false, RequiresControl: false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteRequest(RouteType.Parallel, DepartmentId: null, CompanyId: null, ByProject: null, RequiresControl: true),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
patched.IsSuccess.ShouldBeTrue();
|
||||
patched.Value.RouteType.ShouldBe(RouteType.Parallel);
|
||||
patched.Value.RequiresControl.ShouldBeTrue();
|
||||
patched.Value.CompanyId.ShouldBe(CompanyId);
|
||||
patched.Value.DepartmentId.ShouldBe(DepartmentId);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Удаление маршрута убирает его из базы")]
|
||||
public async Task DeleteAsync_RemovesEntity()
|
||||
{
|
||||
|
||||
+24
@@ -181,6 +181,30 @@ public sealed class TemplateRouteStepServiceTests : IClassFixture<PostgresDataba
|
||||
updated.Value.AllowReassignToOtherApprovers.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Патч шага маршрута меняет только переданные поля, остальные остаются прежними")]
|
||||
public async Task PatchAsync_UpdatesOnlyProvidedFields()
|
||||
{
|
||||
// Arrange
|
||||
var routeId = await CreateRouteAsync("Route to patch step", byProject: false);
|
||||
var created = await _service.CreateAsync(
|
||||
routeId,
|
||||
new CreateTemplateRouteStepRequest(1, "a@b.kz", "Engineer", ProjectRole.None, HeadType.None, RouteStepStatus.Approval, false),
|
||||
CancellationToken.None);
|
||||
|
||||
// Action
|
||||
var patched = await _service.PatchAsync(
|
||||
routeId,
|
||||
created.Value.Id,
|
||||
new PatchTemplateRouteStepRequest(OrderNo: null, ExecutorEmail: "c@b.kz", Position: null, ProjectRole: null, HeadType: null, Status: RouteStepStatus.Execution, AllowReassignToOtherApprovers: null),
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
patched.IsSuccess.ShouldBeTrue();
|
||||
patched.Value.ExecutorEmail.ShouldBe("c@b.kz");
|
||||
patched.Value.Position.ShouldBe("Engineer");
|
||||
patched.Value.Status.ShouldBe(RouteStepStatus.Execution);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Удаление шага маршрута убирает его из базы")]
|
||||
public async Task DeleteAsync_RemovesStep()
|
||||
{
|
||||
|
||||
-2
@@ -5,8 +5,6 @@ using Xunit;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||
|
||||
// Отдельная БД на один xUnit-collection (= один тестовый класс) в общем инстансе Postgres.
|
||||
// Тесты внутри класса делят эту БД; разные классы (разные коллекции) получают разные БД.
|
||||
public sealed class PostgresDatabaseFixture : IAsyncLifetime
|
||||
{
|
||||
private readonly string _databaseName = $"tnsu_test_{Guid.NewGuid():N}";
|
||||
|
||||
-2
@@ -7,8 +7,6 @@ using Tnsu.ApprovalRoute.Infrastructure.TnsuPortal;
|
||||
|
||||
namespace Tnsu.ApprovalRoute.IntegrationTests.Infrastructure;
|
||||
|
||||
// Собирает такой же DI-контейнер, как реальное приложение (те же extension-методы, что в Program.cs),
|
||||
// но с тестовой БД, тестовым ICurrentUser и подменяемым (Moq) клиентом api.tnsu.kz.
|
||||
internal static class TestServiceProviderFactory
|
||||
{
|
||||
public static ServiceProvider Build(
|
||||
|
||||
Reference in New Issue
Block a user