Implements solution testing callback

Introduces a callback mechanism for the testing module to update solution status.

This change includes:
- New DTOs for tester state and error codes.
- A new API endpoint for receiving callbacks from the testing module.
- Validation for the callback request.
- Logic to update the solution status, including state, error code, and test progress.
- Generation and validation of a single-use callback token for security.
- Status composition based on tester state and results.

The previous `UpdateSolutionStatusRequest` endpoint and model are removed in favor of the new callback approach.
This commit is contained in:
2025-10-27 21:10:35 +03:00
parent c5708c1464
commit f4a24b840d
17 changed files with 1585 additions and 123 deletions

View File

@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace LiquidCode.Api.Submits.Dto;
/// <summary>
/// Состояние выполнения решения в модуле тестирования
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum TesterState
{
Waiting,
Compiling,
Testing,
Done
}
/// <summary>
/// Код ошибки, возвращаемый модулем тестирования
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum TesterErrorCode
{
None,
CompileError,
RuntimeError,
MemoryError,
TimeLimitError,
IncorrectAnswer,
UnknownError
}

View File

@@ -0,0 +1,32 @@
using FluentValidation;
namespace LiquidCode.Api.Submits.Requests;
/// <summary>
/// Валидатор для обратных вызовов от тестирующего модуля
/// </summary>
public sealed class TesterCallbackRequestValidator : AbstractValidator<TesterCallbackRequest>
{
public TesterCallbackRequestValidator()
{
RuleFor(x => x.SubmitId)
.GreaterThan(0)
.WithMessage("Submit ID must be greater than 0");
RuleFor(x => x.AmountOfTests)
.GreaterThanOrEqualTo(0)
.WithMessage("Amount of tests must be non-negative");
RuleFor(x => x.CurrentTest)
.GreaterThanOrEqualTo(0)
.WithMessage("Current test index must be non-negative")
.LessThanOrEqualTo(x => x.AmountOfTests)
.WithMessage("Current test cannot exceed total amount of tests")
.When(x => x.AmountOfTests > 0);
RuleFor(x => x.Message)
.MaximumLength(512)
.WithMessage("Message must not exceed 512 characters")
.When(x => !string.IsNullOrEmpty(x.Message));
}
}

View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
using LiquidCode.Api.Submits.Dto;
namespace LiquidCode.Api.Submits.Requests;
/// <summary>
/// Модель запроса обратного вызова от тестирующего модуля
/// </summary>
public sealed record TesterCallbackRequest(
[property: JsonPropertyName("SubmitId")] long SubmitId,
[property: JsonPropertyName("State")] TesterState State,
[property: JsonPropertyName("ErrorCode")] TesterErrorCode ErrorCode,
[property: JsonPropertyName("Message")] string? Message,
[property: JsonPropertyName("CurrentTest")] int CurrentTest,
[property: JsonPropertyName("AmountOfTests")] int AmountOfTests
);

View File

@@ -1,34 +0,0 @@
using FluentValidation;
namespace LiquidCode.Api.Submits.Requests;
/// <summary>
/// Валидатор для запросов обновления статуса решения
/// </summary>
public class UpdateSolutionStatusRequestValidator : AbstractValidator<UpdateSolutionStatusRequest>
{
public UpdateSolutionStatusRequestValidator()
{
RuleFor(x => x.SubmissionId)
.GreaterThan(0)
.WithMessage("Submission ID must be greater than 0");
RuleFor(x => x.VerdictCode)
.GreaterThanOrEqualTo(0)
.WithMessage("Verdict code must be non-negative")
.LessThanOrEqualTo(10)
.WithMessage("Verdict code must be between 0 and 10");
RuleFor(x => x.TestCase)
.GreaterThanOrEqualTo(0)
.WithMessage("Test case number must be non-negative")
.When(x => x.TestCase.HasValue);
RuleFor(x => x.TimeUsed)
.Length(1, 50)
.WithMessage("Time used must be between 1 and 50 characters")
.Matches(@"^\d+(\.\d+)?\s*m?s$")
.WithMessage("Time used must be in format like '100ms', '1.5s', '500'")
.When(x => !string.IsNullOrEmpty(x.TimeUsed));
}
}

View File

@@ -1,13 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace LiquidCode.Api.Submits.Requests;
/// <summary>
/// Модель запроса для обновления статуса решения (вызывается модулем тестирования)
/// </summary>
public record UpdateSolutionStatusRequest(
[Required] int SubmissionId,
[Required] int VerdictCode,
int? TestCase = null,
string? TimeUsed = null
);

View File

@@ -1,3 +1,4 @@
using LiquidCode.Api.Submits.Dto;
using LiquidCode.Infrastructure.Database.Entities;
namespace LiquidCode.Api.Submits.Responses;
@@ -12,7 +13,12 @@ public record SolutionResponse(
string LanguageVersion,
string SourceCode,
string Status,
DateTime Time
DateTime Time,
TesterState TesterState,
TesterErrorCode TesterErrorCode,
string? TesterMessage,
int CurrentTest,
int AmountOfTests
)
{
/// <summary>
@@ -25,6 +31,11 @@ public record SolutionResponse(
entity.LanguageVersion,
entity.SourceCode,
entity.Status,
entity.Time
entity.Time,
entity.TestingState,
entity.TestingErrorCode,
entity.TestingMessage,
entity.CurrentTest,
entity.AmountOfTests
);
}

View File

@@ -1,10 +1,16 @@
using System;
using System.Linq;
using LiquidCode.Api.Submits.Requests;
using LiquidCode.Api.Submits.Responses;
using LiquidCode.Domain.Services.Submits;
using LiquidCode.Infrastructure.External.TestingModule;
using LiquidCode.Shared.Constants;
using LiquidCode.Shared.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace LiquidCode.Api.Submits;
@@ -13,8 +19,18 @@ namespace LiquidCode.Api.Submits;
/// </summary>
[Route("submits")]
[ApiController]
public class SubmitController(ISubmitService submitService, TestingHttpClient testingClient) : ControllerBase
public class SubmitController(
ISubmitService submitService,
TestingHttpClient testingClient,
IConfiguration configuration,
ILogger<SubmitController> logger) : ControllerBase
{
private const string CallbackRouteName = "SubmitTesterCallback";
private readonly ISubmitService _submitService = submitService;
private readonly TestingHttpClient _testingClient = testingClient;
private readonly IConfiguration _configuration = configuration;
private readonly ILogger<SubmitController> _logger = logger;
/// <summary>
/// Отправляет решение для миссии
/// </summary>
@@ -28,7 +44,7 @@ public class SubmitController(ISubmitService submitService, TestingHttpClient te
if (!ModelState.IsValid)
return BadRequest(ModelState);
var solution = await submitService.SubmitSolutionAsync(
var solution = await _submitService.SubmitSolutionAsync(
request.MissionId,
userId,
request.SourceCode,
@@ -41,8 +57,32 @@ public class SubmitController(ISubmitService submitService, TestingHttpClient te
if (solution == null)
return BadRequest("Solution submission failed. Mission may not exist or language is not supported.");
// Отправить в модуль тестирования асинхронно (запустить и забыть)
_ = testingClient.PostData(solution.Id, request.MissionId, request.SourceCode, request.Language);
try
{
var callbackToken = solution.CallbackToken;
if (string.IsNullOrWhiteSpace(callbackToken))
throw new InvalidOperationException("Callback token is not generated.");
var missionKey = solution.Mission?.S3PrivateKey;
if (string.IsNullOrWhiteSpace(missionKey))
throw new InvalidOperationException("Mission package key is missing.");
var testerPayload = new SubmitForTesterModel(
solution.Id,
request.MissionId,
request.Language,
request.LanguageVersion,
request.SourceCode,
BuildPackageUrl(missionKey!),
BuildCallbackUrl(callbackToken));
await _testingClient.SubmitAsync(testerPayload, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to dispatch solution {SolutionId} to testing module", solution.Id);
return StatusCode(StatusCodes.Status502BadGateway, "Failed to dispatch solution to testing module.");
}
return Ok(SolutionResponse.FromEntity(solution));
}
@@ -57,7 +97,7 @@ public class SubmitController(ISubmitService submitService, TestingHttpClient te
if (!User.TryGetUserId(out var userId))
return Unauthorized("User ID not found in claims.");
var submissions = await submitService.GetUserSubmissionsAsync(userId, cancellationToken);
var submissions = await _submitService.GetUserSubmissionsAsync(userId, cancellationToken);
var result = submissions.Select(SubmissionResponse.FromEntity);
@@ -74,7 +114,7 @@ public class SubmitController(ISubmitService submitService, TestingHttpClient te
if (!User.TryGetUserId(out var userId))
return Unauthorized("User ID not found in claims.");
var submission = await submitService.GetSubmissionAsync(id, cancellationToken);
var submission = await _submitService.GetSubmissionAsync(id, cancellationToken);
if (submission == null || submission.User.Id != userId)
return NotFound("Submission not found or access denied.");
@@ -92,7 +132,7 @@ public class SubmitController(ISubmitService submitService, TestingHttpClient te
if (!User.TryGetUserId(out var userId))
return Unauthorized("User ID not found in claims.");
var submissions = await submitService.GetUserSubmissionsAsync(userId, cancellationToken);
var submissions = await _submitService.GetUserSubmissionsAsync(userId, cancellationToken);
var filtered = submissions
.Where(sub => sub.Solution.Mission.Id == missionId)
@@ -103,49 +143,71 @@ public class SubmitController(ISubmitService submitService, TestingHttpClient te
}
/// <summary>
/// Обновляет статус решения (вызывается модулем тестирования)
/// Получает обновление статуса решения от тестирующего модуля
/// </summary>
[HttpPost("update-status")]
public async Task<IActionResult> UpdateSolutionStatus([FromBody] UpdateSolutionStatusRequest request, CancellationToken cancellationToken)
[HttpPost("testing/callback/{token}", Name = CallbackRouteName)]
public async Task<IActionResult> ReceiveTesterCallback([FromRoute] string token, [FromBody] TesterCallbackRequest request, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var verdictMessage = FormatVerdictMessage(request.VerdictCode, request.TestCase);
if (string.IsNullOrWhiteSpace(token))
return BadRequest("Callback token is required.");
var result = await submitService.UpdateSolutionStatusAsync(request.SubmissionId, verdictMessage, cancellationToken);
if (result == null)
return NotFound("Solution not found.");
if (request.SubmitId <= 0 || request.SubmitId > int.MaxValue)
return BadRequest("SubmitId value is out of supported range.");
return Accepted(SolutionResponse.FromEntity(result));
var updateResult = await _submitService.UpdateTesterStatusAsync(
(int)request.SubmitId,
token,
request.State,
request.ErrorCode,
request.Message,
request.CurrentTest,
request.AmountOfTests,
cancellationToken);
return updateResult.Status switch
{
TesterCallbackUpdateStatus.Success when updateResult.Solution != null => Accepted(SolutionResponse.FromEntity(updateResult.Solution)),
TesterCallbackUpdateStatus.NotFound => NotFound("Solution not found."),
TesterCallbackUpdateStatus.TokenMismatch => StatusCode(StatusCodes.Status403Forbidden, "Invalid or expired callback token."),
TesterCallbackUpdateStatus.Error => StatusCode(StatusCodes.Status500InternalServerError, "Failed to update solution status."),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unexpected tester callback processing result.")
};
}
/// <summary>
/// Форматирует сообщение вердикта для статуса решения
/// </summary>
private static string FormatVerdictMessage(int verdictCode, int? testCase)
private string BuildPackageUrl(string s3Key)
{
var verdictMessages = new[]
{
"Accepted",
"Wrong answer",
"Time limit",
"Memory limit",
"Internal error",
"Runtime error",
"Compilation error"
};
if (string.IsNullOrWhiteSpace(s3Key))
throw new InvalidOperationException("Mission package key is not configured.");
if (verdictCode == -1)
return "Running";
var endpoint = _configuration[ConfigurationKeys.S3Endpoint] ??
throw new InvalidOperationException($"Configuration key '{ConfigurationKeys.S3Endpoint}' is not configured.");
var bucket = _configuration[ConfigurationKeys.S3PrivateBucket] ??
throw new InvalidOperationException($"Configuration key '{ConfigurationKeys.S3PrivateBucket}' is not configured.");
var message = verdictCode >= 0 && verdictCode < verdictMessages.Length
? verdictMessages[verdictCode]
: "Unknown verdict";
var encodedKey = string.Join('/', s3Key
.Split('/', StringSplitOptions.RemoveEmptyEntries)
.Select(Uri.EscapeDataString));
if (testCase.HasValue && verdictCode >= 1 && verdictCode <= 5)
message += $" #{testCase}";
return $"{endpoint.TrimEnd('/')}/{bucket}/{encodedKey}";
}
return message;
private string BuildCallbackUrl(string token)
{
if (string.IsNullOrWhiteSpace(token))
throw new InvalidOperationException("Callback token is not provided.");
if (!Request.Host.HasValue)
throw new InvalidOperationException("Unable to determine request host for callback URL generation.");
var scheme = string.IsNullOrWhiteSpace(Request.Scheme) ? Uri.UriSchemeHttps : Request.Scheme;
var link = Url.RouteUrl(CallbackRouteName, values: new { token }, protocol: scheme, host: Request.Host.Value);
if (string.IsNullOrWhiteSpace(link))
throw new InvalidOperationException("Unable to build callback URL.");
return link;
}
}

View File

@@ -1,3 +1,4 @@
using LiquidCode.Api.Submits.Dto;
using LiquidCode.Infrastructure.Database.Entities;
namespace LiquidCode.Domain.Services.Submits;
@@ -54,12 +55,43 @@ public interface ISubmitService
Task<IEnumerable<DbUserSubmission>> GetMissionSubmissionsAsync(int missionId, CancellationToken cancellationToken = default);
/// <summary>
/// Обновляет статус решения
/// Применяет обновление статуса решения от тестирующего модуля
/// </summary>
/// <param name="solutionId">ID решения</param>
/// <param name="status">Новый статус</param>
/// <param name="solutionId">Идентификатор решения</param>
/// <param name="callbackToken">Одноразовый токен обратного вызова</param>
/// <param name="state">Новое состояние выполнения</param>
/// <param name="errorCode">Информация об ошибке выполнения</param>
/// <param name="message">Сообщение от тестирующего модуля</param>
/// <param name="currentTest">Номер текущего теста</param>
/// <param name="amountOfTests">Общее количество тестов</param>
/// <param name="cancellationToken">Токен отмены</param>
/// <returns>Обновленное решение или null, если не найдено</returns>
Task<DbSolution?> UpdateSolutionStatusAsync(int solutionId, string status, CancellationToken cancellationToken = default);
/// <returns>Результат применения обновления</returns>
Task<TesterCallbackUpdateResult> UpdateTesterStatusAsync(
int solutionId,
string callbackToken,
TesterState state,
TesterErrorCode errorCode,
string? message,
int currentTest,
int amountOfTests,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Возможный исход применения обратного вызова тестирующего модуля
/// </summary>
public enum TesterCallbackUpdateStatus
{
Success,
NotFound,
TokenMismatch,
Error
}
/// <summary>
/// Результат применения обратного вызова тестирующего модуля
/// </summary>
public readonly record struct TesterCallbackUpdateResult(
TesterCallbackUpdateStatus Status,
DbSolution? Solution);

View File

@@ -1,6 +1,10 @@
using System;
using System.Linq;
using LiquidCode.Infrastructure.Database.Entities;
using System.Security.Cryptography;
using System.Text;
using LiquidCode.Domain.Interfaces.Repositories;
using LiquidCode.Infrastructure.Database.Entities;
using LiquidCode.Api.Submits.Dto;
using Microsoft.Extensions.Logging;
namespace LiquidCode.Domain.Services.Submits;
@@ -172,7 +176,13 @@ public class SubmitService : ISubmitService
Language = language,
LanguageVersion = languageVersion,
SourceCode = sourceCode,
Status = "submitted",
Status = ComposeStatus(TesterState.Waiting, TesterErrorCode.None, null, 0, 0),
TestingState = TesterState.Waiting,
TestingErrorCode = TesterErrorCode.None,
TestingMessage = null,
CurrentTest = 0,
AmountOfTests = 0,
CallbackToken = GenerateCallbackToken(),
Time = DateTime.UtcNow
};
@@ -237,7 +247,15 @@ public class SubmitService : ISubmitService
}
}
public async Task<DbSolution?> UpdateSolutionStatusAsync(int solutionId, string status, CancellationToken cancellationToken = default)
public async Task<TesterCallbackUpdateResult> UpdateTesterStatusAsync(
int solutionId,
string callbackToken,
TesterState state,
TesterErrorCode errorCode,
string? message,
int currentTest,
int amountOfTests,
CancellationToken cancellationToken = default)
{
try
{
@@ -245,20 +263,95 @@ public class SubmitService : ISubmitService
if (solution == null)
{
_logger.LogWarning("Solution not found: {SolutionId}", solutionId);
return null;
return new TesterCallbackUpdateResult(TesterCallbackUpdateStatus.NotFound, null);
}
solution.Status = status;
// TODO: Реализовать метод обновления в репозитории
if (string.IsNullOrWhiteSpace(solution.CallbackToken) || !IsTokenMatch(solution.CallbackToken, callbackToken))
{
_logger.LogWarning("Callback token mismatch for solution {SolutionId}", solutionId);
return new TesterCallbackUpdateResult(TesterCallbackUpdateStatus.TokenMismatch, null);
}
var normalizedAmount = Math.Max(amountOfTests, 0);
var normalizedCurrent = Math.Clamp(currentTest, 0, normalizedAmount > 0 ? normalizedAmount : int.MaxValue);
var trimmedMessage = string.IsNullOrWhiteSpace(message) ? null : message.Trim();
solution.TestingState = state;
solution.TestingErrorCode = errorCode;
solution.TestingMessage = trimmedMessage;
solution.CurrentTest = normalizedCurrent;
solution.AmountOfTests = normalizedAmount;
solution.Status = ComposeStatus(state, errorCode, trimmedMessage, normalizedCurrent, normalizedAmount);
solution.CallbackToken = null;
await _submitRepository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Solution status updated: SolutionId={SolutionId}, Status={Status}", solutionId, status);
return solution;
_logger.LogInformation(
"Solution tester status updated: SolutionId={SolutionId}, State={State}, ErrorCode={ErrorCode}, CurrentTest={CurrentTest}, TotalTests={TotalTests}",
solutionId,
state,
errorCode,
normalizedCurrent,
normalizedAmount);
return new TesterCallbackUpdateResult(TesterCallbackUpdateStatus.Success, solution);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating solution status: {SolutionId}", solutionId);
return null;
_logger.LogError(ex, "Error updating tester status: {SolutionId}", solutionId);
return new TesterCallbackUpdateResult(TesterCallbackUpdateStatus.Error, null);
}
}
private static string ComposeStatus(
TesterState state,
TesterErrorCode errorCode,
string? message,
int currentTest,
int amountOfTests)
{
var baseStatus = state switch
{
TesterState.Waiting => "Waiting",
TesterState.Compiling => "Compiling",
TesterState.Testing => amountOfTests > 0
? $"Testing {Math.Clamp(currentTest, 0, amountOfTests)}/{amountOfTests}"
: "Testing",
TesterState.Done => errorCode switch
{
TesterErrorCode.None => "Accepted",
TesterErrorCode.CompileError => "Compilation error",
TesterErrorCode.RuntimeError => "Runtime error",
TesterErrorCode.MemoryError => "Memory limit exceeded",
TesterErrorCode.TimeLimitError => "Time limit exceeded",
TesterErrorCode.IncorrectAnswer => "Wrong answer",
_ => "Unknown error"
},
_ => "Unknown state"
};
return string.IsNullOrWhiteSpace(message)
? baseStatus
: $"{baseStatus}: {message}";
}
private static string GenerateCallbackToken()
{
Span<byte> buffer = stackalloc byte[32];
RandomNumberGenerator.Fill(buffer);
return Convert.ToHexString(buffer).ToLowerInvariant();
}
private static bool IsTokenMatch(string storedToken, string providedToken)
{
if (string.IsNullOrWhiteSpace(storedToken) || string.IsNullOrWhiteSpace(providedToken))
return false;
var storedBytes = Encoding.UTF8.GetBytes(storedToken.Trim().ToLowerInvariant());
var providedBytes = Encoding.UTF8.GetBytes(providedToken.Trim().ToLowerInvariant());
if (storedBytes.Length != providedBytes.Length)
return false;
return CryptographicOperations.FixedTimeEquals(storedBytes, providedBytes);
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using LiquidCode.Api.Submits.Dto;
using Microsoft.EntityFrameworkCore;
namespace LiquidCode.Infrastructure.Database.Entities;
@@ -30,6 +31,20 @@ public class DbSolution : ITimestamped
[StringLength(32)]
[Required]
public string Status { get; set; } = null!;
public TesterState TestingState { get; set; } = TesterState.Waiting;
public TesterErrorCode TestingErrorCode { get; set; } = TesterErrorCode.None;
[StringLength(512)]
public string? TestingMessage { get; set; }
public int CurrentTest { get; set; }
public int AmountOfTests { get; set; }
[StringLength(128)]
public string? CallbackToken { get; set; }
public DateTime Time { get; init; }

View File

@@ -70,6 +70,7 @@ public class SubmitRepository : ISubmitRepository
public async Task<DbSolution?> GetSolutionAsync(int solutionId, CancellationToken cancellationToken = default) =>
await _dbContext.Solutions
.Include(s => s.Mission)
.FirstOrDefaultAsync(s => s.Id == solutionId, cancellationToken);
public async Task AddSolutionAsync(DbSolution solution, CancellationToken cancellationToken = default) =>

View File

@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace LiquidCode.Infrastructure.External.TestingModule;
/// <summary>
/// DTO, отправляемая во внешний тестирующий модуль
/// </summary>
public sealed record SubmitForTesterModel(
[property: JsonPropertyName("Id")] long Id,
[property: JsonPropertyName("MissionId")] long MissionId,
[property: JsonPropertyName("Language")] string Language,
[property: JsonPropertyName("LanguageVersion")] string LanguageVersion,
[property: JsonPropertyName("SourceCode")] string SourceCode,
[property: JsonPropertyName("PackageUrl")] string PackageUrl,
[property: JsonPropertyName("CallbackUrl")] string CallbackUrl
);

View File

@@ -1,31 +1,28 @@
using System.Text;
using System.Text.Json;
using NuGet.Protocol;
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
namespace LiquidCode.Infrastructure.External.TestingModule;
public class TestingHttpClient(string endpointUrl)
public class TestingHttpClient(string endpointUrl, ILogger<TestingHttpClient> logger)
{
private HttpClient _client = new()
private readonly HttpClient _client = new()
{
BaseAddress = new Uri(endpointUrl),
};
private readonly ILogger<TestingHttpClient> _logger = logger;
public async Task PostData(int id, int missionId, string sourceCode, string language)
public async Task SubmitAsync(SubmitForTesterModel payload, CancellationToken cancellationToken = default)
{
using StringContent jsonContent = new(
JsonSerializer.Serialize(new
{
id,
problemId = missionId,
sourceCode,
language
}),
Encoding.UTF8,
"application/json");
var response = await _client.PostAsJsonAsync("api/submit", payload, cancellationToken);
await _client.PostAsync(
"api/submit",
jsonContent);
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Testing module returned {StatusCode} for submit {SubmitId}: {Body}",
(int)response.StatusCode,
payload.Id,
string.IsNullOrWhiteSpace(content) ? "<empty>" : content);
response.EnsureSuccessStatusCode();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiquidCode.Migrations
{
/// <inheritdoc />
public partial class AddTesterStatusFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "amount_of_tests",
table: "solutions",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "callback_token",
table: "solutions",
type: "character varying(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "current_test",
table: "solutions",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "testing_error_code",
table: "solutions",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "testing_message",
table: "solutions",
type: "character varying(512)",
maxLength: 512,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "testing_state",
table: "solutions",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "amount_of_tests",
table: "solutions");
migrationBuilder.DropColumn(
name: "callback_token",
table: "solutions");
migrationBuilder.DropColumn(
name: "current_test",
table: "solutions");
migrationBuilder.DropColumn(
name: "testing_error_code",
table: "solutions");
migrationBuilder.DropColumn(
name: "testing_message",
table: "solutions");
migrationBuilder.DropColumn(
name: "testing_state",
table: "solutions");
}
}
}

View File

@@ -575,10 +575,23 @@ namespace LiquidCode.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AmountOfTests")
.HasColumnType("integer")
.HasColumnName("amount_of_tests");
b.Property<string>("CallbackToken")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasColumnName("callback_token");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<int>("CurrentTest")
.HasColumnType("integer")
.HasColumnName("current_test");
b.Property<string>("Language")
.IsRequired()
.HasMaxLength(16)
@@ -607,6 +620,19 @@ namespace LiquidCode.Migrations
.HasColumnType("character varying(32)")
.HasColumnName("status");
b.Property<int>("TestingErrorCode")
.HasColumnType("integer")
.HasColumnName("testing_error_code");
b.Property<string>("TestingMessage")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasColumnName("testing_message");
b.Property<int>("TestingState")
.HasColumnType("integer")
.HasColumnName("testing_state");
b.Property<DateTime>("Time")
.HasColumnType("timestamp with time zone")
.HasColumnName("time");

View File

@@ -1,4 +1,5 @@
using System.Text;
using System.Text.Json.Serialization;
using FluentValidation;
using FluentValidation.AspNetCore;
using LiquidCode;
@@ -21,6 +22,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Reflection;
using Microsoft.Extensions.Logging;
var builder = WebApplication.CreateBuilder(args);
@@ -68,7 +70,10 @@ if (builder.Configuration[ConfigurationKeys.MigrateOnlyFlag] == "1")
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
builder.Services.AddControllers();
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// Настроить разрешающую политику CORS, чтобы браузеры могли отправлять запросы с
// пользовательскими заголовками (например, Content-Type) и предварительный OPTIONS запрос
@@ -84,8 +89,13 @@ builder.Services.AddCors(options =>
});
builder.Services.AddS3Buckets(builder.Configuration);
builder.Services.AddSingleton(new TestingHttpClient(builder.Configuration[ConfigurationKeys.TestingModuleUrl] ??
throw new ArgumentNullException(ConfigurationKeys.TestingModuleUrl)));
builder.Services.AddSingleton<TestingHttpClient>(provider =>
{
var endpoint = builder.Configuration[ConfigurationKeys.TestingModuleUrl] ??
throw new ArgumentNullException(ConfigurationKeys.TestingModuleUrl);
var logger = provider.GetRequiredService<ILogger<TestingHttpClient>>();
return new TestingHttpClient(endpoint, logger);
});
// Добавить репозитории
builder.Services.AddScoped<IUserRepository, UserRepository>();