using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using Dpz.Core.Entity.Base.Image;
using Dpz.Core.EnumLibrary;
using Dpz.Core.Service.Mediator.Features.Article.Commands;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using Moq;

namespace Dpz.Core.ServiceTest;

[TestFixture]
public class TryDeleteItHomeArticleCommandTest
{
    [Test]
    public async Task Handle_WhenArticleDoesNotExist_TreatsAsSuccess()
    {
        var id = ObjectId.GenerateNewId();
        var repository = new Mock<IRepository<Article>>();
        repository
            .Setup(x => x.FindAsync(id, It.IsAny<CancellationToken>()))
            .ReturnsAsync((Article?)null);

        var handler = CreateHandler(repository);

        await handler.Handle(
            new TryDeleteItHomeArticleCommand { Id = id.ToString() },
            CancellationToken.None
        );

        repository.Verify(
            x =>
                x.DeleteAsync(
                    It.IsAny<Expression<Func<Article, bool>>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Never
        );
        repository.Verify(
            x =>
                x.UpdateAsync(
                    It.IsAny<Expression<Func<Article, bool>>>(),
                    It.IsAny<UpdateDefinition<Article>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Never
        );
    }

    [Test]
    public async Task Handle_WhenArticleIsNotItHome_ClearsImageMetadata()
    {
        var article = CreateArticle(["Other"]);
        var repository = new Mock<IRepository<Article>>();
        repository
            .Setup(x => x.FindAsync(article.Id, It.IsAny<CancellationToken>()))
            .ReturnsAsync(article);

        var handler = CreateHandler(repository);

        await handler.Handle(
            new TryDeleteItHomeArticleCommand { Id = article.Id.ToString() },
            CancellationToken.None
        );

        repository.Verify(
            x =>
                x.UpdateAsync(
                    It.IsAny<Expression<Func<Article, bool>>>(),
                    It.IsAny<UpdateDefinition<Article>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Once
        );
        repository.Verify(
            x =>
                x.DeleteAsync(
                    It.IsAny<Expression<Func<Article, bool>>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Never
        );
    }

    [Test]
    public async Task Handle_WhenArticleIsItHome_DeletesArticle()
    {
        var article = CreateArticle(["ItHome"]);
        var repository = new Mock<IRepository<Article>>();
        repository
            .Setup(x => x.FindAsync(article.Id, It.IsAny<CancellationToken>()))
            .ReturnsAsync(article);

        var handler = CreateHandler(repository);

        await handler.Handle(
            new TryDeleteItHomeArticleCommand { Id = article.Id.ToString() },
            CancellationToken.None
        );

        repository.Verify(
            x =>
                x.DeleteAsync(
                    It.IsAny<Expression<Func<Article, bool>>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Once
        );
        repository.Verify(
            x =>
                x.UpdateAsync(
                    It.IsAny<Expression<Func<Article, bool>>>(),
                    It.IsAny<UpdateDefinition<Article>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Never
        );
    }

    private static TryDeleteItHomeArticleCommandHandler CreateHandler(
        Mock<IRepository<Article>> repository
    )
    {
        return new TryDeleteItHomeArticleCommandHandler(
            repository.Object,
            Mock.Of<ILogger<TryDeleteItHomeArticleCommandHandler>>()
        );
    }

    private static Article CreateArticle(List<string> tags)
    {
        return new Article
        {
            Id = ObjectId.GenerateNewId(),
            Title = "title",
            Markdown = "markdown",
            Author = new UserInfo
            {
                Id = "user",
                Name = "user",
                Avatar = "avatar",
                Sex = Sex.Other,
                Key = "key",
                Permissions = null,
            },
            Tags = tags,
            Images =
            [
                new ImageMetadata
                {
                    Url = "https://cdn.example.com/a.jpg",
                    Width = 100,
                    Height = 100,
                },
            ],
            MainImageMetadata = new ImageMetadata
            {
                Url = "https://cdn.example.com/a.jpg",
                Width = 100,
                Height = 100,
            },
        };
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 NUnit 单元测试类,用于测试 TryDeleteItHomeArticleCommandHandler 的功能。该处理程序负责处理删除"IT之家"文章的命令。

整体结构

测试类使用 Moq 框架来模拟依赖项,测试了处理程序在不同场景下的行为。

三个测试用例

1. Handle_WhenArticleDoesNotExist_TreatsAsSuccess

测试场景:文章不存在时的处理

- 模拟数据库查询返回 null(文章不存在)
- 执行删除命令
- 验证:既没有调用删除操作,也没有调用更新操作

预期行为:将不存在的文章视为成功处理(幂等性设计)

2. Handle_WhenArticleIsNotItHome_ClearsImageMetadata

测试场景:文章存在但标签不是"ItHome"

- 创建一个标签为 "Other" 的文章
- 模拟数据库返回该文章
- 执行删除命令
- 验证:调用了一次更新操作,没有调用删除操作

预期行为:不删除文章,只清理图片元数据(通过更新操作)

3. Handle_WhenArticleIsItHome_DeletesArticle

测试场景:文章标签是"ItHome"

- 创建一个标签为 "ItHome" 的文章
- 模拟数据库返回该文章
- 执行删除命令
- 验证:调用了一次删除操作,没有调用更新操作

预期行为:直接删除文章

辅助方法

CreateHandler

创建被测试的处理程序实例,注入模拟的仓储和日志记录器。

CreateArticle

创建测试用的 Article 对象,包含:

  • 随机生成的 ObjectId
  • 标题、Markdown 内容
  • 用户信息(作者)
  • 可自定义的标签列表
  • 图片元数据(包含 URL、宽度、高度)

测试目的

这些测试确保 TryDeleteItHomeArticleCommand 处理程序能够:

  1. 安全处理不存在的文章(避免异常)
  2. 根据文章来源区别处理
    • IT之家文章:直接删除
    • 其他来源文章:仅清理元数据
  3. 正确调用底层仓储方法(通过 Mock 验证)

这是典型的 行为驱动测试(Behavior-Driven Testing),关注业务逻辑的正确性。

评论加载中...