← Назад к вопросам

Web API: Реализация Rate Limiting

2.2 Middle🔥 111 комментариев
#ASP.NET и Web API

Условие

Реализуйте middleware для ограничения количества запросов (rate limiting) в ASP.NET Core Web API.

Требования:

  1. Ограничение: 100 запросов в минуту на IP-адрес
  2. При превышении лимита возвращать HTTP 429 Too Many Requests
  3. Добавить заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset

Интерфейс:

public class RateLimitingMiddleware { // TODO: реализовать }

public interface IRateLimitService { bool IsAllowed(string clientId); RateLimitInfo GetInfo(string clientId); }

Дополнительно:

  • Как сделать rate limiting распределённым (для нескольких инстансов приложения)?
  • Какие алгоритмы rate limiting существуют (Token Bucket, Sliding Window)?

Критерии оценки:

  • Корректность подсчёта запросов
  • Потокобезопасность
  • Понимание распределённых систем (Redis)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Структуры данных и моки

Перед написанием тестов нужно определить необходимые структуры:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

// Enum для статуса заказа
public enum OrderStatus
{
    Pending = 0,
    Paid = 1,
    Shipped = 2,
    Delivered = 3,
    Cancelled = 4
}

// Модель заказа
public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string CustomerEmail { get; set; }
    public List<OrderItem> Items { get; set; } = new();
    public OrderStatus Status { get; set; }
}

public class OrderItem
{
    public int Id { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

// Результат обработки заказа
public class OrderResult
{
    public bool Success { get; set; }
    public int? OrderId { get; set; }
    public string ErrorMessage { get; set; }

    public static OrderResult Success(int orderId) =>
        new() { Success = true, OrderId = orderId };

    public static OrderResult PaymentFailed(string error) =>
        new() { Success = false, ErrorMessage = error };
}

// Результат платежа
public class PaymentResult
{
    public bool Success { get; set; }
    public string Error { get; set; }
    public string TransactionId { get; set; }
}

// Исключение для пустого заказа
public class InvalidOrderException : Exception
{
    public InvalidOrderException(string message) : base(message) { }
}

// Интерфейсы зависимостей
public interface IOrderRepository
{
    Task SaveAsync(Order order);
    Task<Order> GetByIdAsync(int orderId);
}

public interface IPaymentGateway
{
    Task<PaymentResult> ChargeAsync(int customerId, decimal amount);
}

public interface IEmailService
{
    Task SendOrderConfirmationAsync(string email, int orderId);
    Task SendOrderFailureAsync(string email, string reason);
}

// Сервис обработки заказов
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IPaymentGateway _paymentGateway;
    private readonly IEmailService _emailService;

    public OrderService(
        IOrderRepository repository,
        IPaymentGateway paymentGateway,
        IEmailService emailService)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _paymentGateway = paymentGateway ?? throw new ArgumentNullException(nameof(paymentGateway));
        _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
    }

    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        if (order.Items.Count == 0)
            throw new InvalidOrderException("Order has no items");

        var total = order.Items.Sum(i => i.Price * i.Quantity);

        var paymentResult = await _paymentGateway.ChargeAsync(order.CustomerId, total);
        if (!paymentResult.Success)
            return OrderResult.PaymentFailed(paymentResult.Error);

        order.Status = OrderStatus.Paid;
        await _repository.SaveAsync(order);

        await _emailService.SendOrderConfirmationAsync(order.CustomerEmail, order.Id);

        return OrderResult.Success(order.Id);
    }
}

Комплексные Unit-тесты (xUnit + Moq)

using Xunit;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class OrderServiceTests
{
    // ===== Helper Method для Arrange =====
    private OrderService CreateOrderService(
        IOrderRepository repository = null,
        IPaymentGateway paymentGateway = null,
        IEmailService emailService = null)
    {
        repository ??= new Mock<IOrderRepository>().Object;
        paymentGateway ??= new Mock<IPaymentGateway>().Object;
        emailService ??= new Mock<IEmailService>().Object;

        return new OrderService(repository, paymentGateway, emailService);
    }

    private Order CreateValidOrder(int id = 1, int customerId = 100, int itemCount = 2)
    {
        var order = new Order
        {
            Id = id,
            CustomerId = customerId,
            CustomerEmail = "customer@example.com",
            Status = OrderStatus.Pending,
            Items = new List<OrderItem>()
        };

        for (int i = 0; i < itemCount; i++)
        {
            order.Items.Add(new OrderItem
            {
                Id = i + 1,
                Price = 50m,
                Quantity = 1
            });
        }

        return order;
    }

    // ===== TEST 1: Успешная обработка заказа =====
    [Fact]
    public async Task ProcessOrderAsync_WithValidOrder_ReturnsSuccess()
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var mockPaymentGateway = new Mock<IPaymentGateway>();
        var mockEmailService = new Mock<IEmailService>();

        var paymentResult = new PaymentResult { Success = true, TransactionId = "TXN123" };
        mockPaymentGateway
            .Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
            .ReturnsAsync(paymentResult);

        var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
        var order = CreateValidOrder(id: 1, customerId: 100);

        // Act
        var result = await service.ProcessOrderAsync(order);

        // Assert
        Assert.True(result.Success);
        Assert.Equal(1, result.OrderId);
        Assert.Null(result.ErrorMessage);

        // Проверяем, что зависимости были вызваны правильно
        mockPaymentGateway.Verify(
            pg => pg.ChargeAsync(100, 100m),
            Times.Once);

        mockRepository.Verify(
            r => r.SaveAsync(It.Is<Order>(o => o.Status == OrderStatus.Paid)),
            Times.Once);

        mockEmailService.Verify(
            es => es.SendOrderConfirmationAsync("customer@example.com", 1),
            Times.Once);
    }

    // ===== TEST 2: Заказ без товаров выбрасывает исключение =====
    [Fact]
    public async Task ProcessOrderAsync_WithEmptyItems_ThrowsInvalidOrderException()
    {
        // Arrange
        var service = CreateOrderService();
        var order = new Order
        {
            Id = 1,
            CustomerId = 100,
            CustomerEmail = "customer@example.com",
            Items = new List<OrderItem>()
        };

        // Act & Assert
        var exception = await Assert.ThrowsAsync<InvalidOrderException>(
            () => service.ProcessOrderAsync(order));

        Assert.Equal("Order has no items", exception.Message);
    }

    // ===== TEST 3: Ошибка платежа =====
    [Fact]
    public async Task ProcessOrderAsync_WhenPaymentFails_ReturnsFailureResult()
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var mockPaymentGateway = new Mock<IPaymentGateway>();
        var mockEmailService = new Mock<IEmailService>();

        var paymentResult = new PaymentResult { Success = false, Error = "Card declined" };
        mockPaymentGateway
            .Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
            .ReturnsAsync(paymentResult);

        var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
        var order = CreateValidOrder();

        // Act
        var result = await service.ProcessOrderAsync(order);

        // Assert
        Assert.False(result.Success);
        Assert.Null(result.OrderId);
        Assert.Equal("Card declined", result.ErrorMessage);

        // Email не должен быть отправлен
        mockEmailService.Verify(
            es => es.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<int>()),
            Times.Never);
    }

    // ===== TEST 4: Email отправляется только после успешной оплаты =====
    [Fact]
    public async Task ProcessOrderAsync_SendsEmailOnlyAfterSuccessfulPayment()
    {
        // Arrange
        var callOrder = new List<string>();

        var mockRepository = new Mock<IOrderRepository>();
        mockRepository
            .Setup(r => r.SaveAsync(It.IsAny<Order>()))
            .Callback(() => callOrder.Add("SaveAsync"))
            .Returns(Task.CompletedTask);

        var mockPaymentGateway = new Mock<IPaymentGateway>();
        mockPaymentGateway
            .Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
            .Callback(() => callOrder.Add("ChargeAsync"))
            .ReturnsAsync(new PaymentResult { Success = true, TransactionId = "TXN123" });

        var mockEmailService = new Mock<IEmailService>();
        mockEmailService
            .Setup(es => es.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<int>()))
            .Callback(() => callOrder.Add("SendEmailAsync"))
            .Returns(Task.CompletedTask);

        var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
        var order = CreateValidOrder();

        // Act
        await service.ProcessOrderAsync(order);

        // Assert
        Assert.Equal(new[] { "ChargeAsync", "SaveAsync", "SendEmailAsync" }, callOrder);
    }

    // ===== TEST 5: Email НЕ отправляется при ошибке платежа =====
    [Fact]
    public async Task ProcessOrderAsync_DoesNotSendEmail_WhenPaymentFails()
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var mockPaymentGateway = new Mock<IPaymentGateway>();
        var mockEmailService = new Mock<IEmailService>();

        mockPaymentGateway
            .Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
            .ReturnsAsync(new PaymentResult { Success = false, Error = "Insufficient funds" });

        var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
        var order = CreateValidOrder();

        // Act
        await service.ProcessOrderAsync(order);

        // Assert
        mockEmailService.Verify(
            es => es.SendOrderConfirmationAsync(It.IsAny<string>(), It.IsAny<int>()),
            Times.Never);
    }

    // ===== TEST 6: Корректно рассчитывается сумма =====
    [Theory]
    [InlineData(1, 50, 50)]
    [InlineData(2, 100, 200)]
    [InlineData(3, 25, 75)]
    public async Task ProcessOrderAsync_CalculatesTotalCorrectly(int quantity, decimal price, decimal expectedTotal)
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var mockPaymentGateway = new Mock<IPaymentGateway>();
        var mockEmailService = new Mock<IEmailService>();

        mockPaymentGateway
            .Setup(pg => pg.ChargeAsync(It.IsAny<int>(), It.IsAny<decimal>()))
            .ReturnsAsync(new PaymentResult { Success = true });

        var service = new OrderService(mockRepository.Object, mockPaymentGateway.Object, mockEmailService.Object);
        var order = new Order
        {
            Id = 1,
            CustomerId = 100,
            CustomerEmail = "test@example.com",
            Items = new List<OrderItem>
            {
                new OrderItem { Id = 1, Price = price, Quantity = quantity }
            }
        };

        // Act
        await service.ProcessOrderAsync(order);

        // Assert
        mockPaymentGateway.Verify(
            pg => pg.ChargeAsync(100, expectedTotal),
            Times.Once);
    }
}

Выводы

Best Practices:

  1. Arrange-Act-Assert — чёткая структура теста
  2. Один тест = один сценарий — не объединяй проверки
  3. Моки для зависимостей — изолируй тестируемый код
  4. Verify для проверки — убедись, что методы вызваны правильно
  5. Theory tests — для множественных данных
  6. Helper методы — для создания тестовых объектов

Покрытие основных сценариев:

  • Успешная обработка
  • Ошибки валидации
  • Ошибки платежа
  • Проверка порядка вызовов
  • Расчёты сумм