using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Dpz.Core.Public.Entity;
using Dpz.Core.Public.Entity.Seo;
using Dpz.Core.Public.ViewModel;

namespace Dpz.Core.ServiceTest;

[TestFixture]
public class PageMetadataServiceTest : Basic
{
    [SetUp]
    public async Task SetUpAsync()
    {
        await PageMetadataRepository.DeleteAsync(x => true);
        await PageRouteDefinitionRepository.DeleteAsync(x => true);

        var routeCatalogService = GetService<IPageRouteCatalogService>();
        await routeCatalogService.SaveScannedRoutesAsync([
            new VmSavePageRouteDefinition
            {
                Controller = "Article",
                Action = "Index",
                Parameters =
                [
                    new VmPageRouteParameterDefinition
                    {
                        Name = "tag",
                        TypeName = "String",
                        Source = PageRouteParameterSource.Query,
                        IsOptional = true,
                    },
                ],
            },
            new VmSavePageRouteDefinition
            {
                Controller = "Article",
                Action = "Read",
                Endpoints =
                [
                    new VmPageRouteEndpointDefinition
                    {
                        RouteName = "Article",
                        Template = "article/read/{id}.html",
                        HttpMethods = ["GET"],
                    },
                ],
                Parameters =
                [
                    new VmPageRouteParameterDefinition
                    {
                        Name = "id",
                        TypeName = "String",
                        Source = PageRouteParameterSource.Route,
                    },
                ],
            },
        ]);
    }

    [Test]
    public async Task SavePageMetadataTest()
    {
        var service = GetService<IPageMetadataService>();
        var pageMetadata = new VmPageMetadata
        {
            Description = "the index description",
            Keywords = ["article", "news", "blog"],
            Metas = new Dictionary<string, string>
            {
                { "author", "pengqian" },
                { "revised", $"apang ,{DateTime.Now}" },
            },
            Route = new PageMetadataRoute { Controller = "Article", Action = "Index" },
        };
        await service.SavePageMetadataAsync(pageMetadata);

        var dbPageMetadata = await service.GetPageMetadataByRouteAsync(pageMetadata.Route);
        Assert.That(dbPageMetadata, Is.Not.Null);

        Console.WriteLine(
            JsonSerializer.Serialize(
                dbPageMetadata,
                new JsonSerializerOptions { WriteIndented = true }
            )
        );
    }

    [Test]
    public async Task SavePublicMetadata_ShouldNotRequireRoute()
    {
        var service = GetService<IPageMetadataService>();
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                IsPublicMetadata = true,
                ApplyToUnconfiguredPages = true,
                Keywords = ["core"],
                Description = "site description",
                Metas = new Dictionary<string, string> { ["author"] = "pengqian" },
            }
        );

        var metadata = await service.GetPublicMetadataAsync();

        Assert.That(metadata, Is.Not.Null);
        Assert.That(metadata!.IsPublicMetadata, Is.True);
        Assert.That(metadata.ApplyToUnconfiguredPages, Is.True);
    }

    [Test]
    public void SavePublicMetadata_WithRoute_ShouldThrow()
    {
        var service = GetService<IPageMetadataService>();
        var metadata = new VmPageMetadata
        {
            IsPublicMetadata = true,
            Route = new PageMetadataRoute { Controller = "Article", Action = "Index" },
        };

        Assert.ThrowsAsync<ArgumentException>(async () =>
            await service.SavePageMetadataAsync(metadata)
        );
    }

    [Test]
    public void SavePageMetadata_WithUnknownRoute_ShouldThrow()
    {
        var service = GetService<IPageMetadataService>();
        var metadata = new VmPageMetadata
        {
            Route = new PageMetadataRoute { Controller = "Unknown", Action = "Index" },
        };

        Assert.ThrowsAsync<ArgumentException>(async () =>
            await service.SavePageMetadataAsync(metadata)
        );
    }

    [Test]
    public void SavePageMetadata_WithUnlistedParameter_ShouldThrow()
    {
        var service = GetService<IPageMetadataService>();
        var metadata = new VmPageMetadata
        {
            Route = new PageMetadataRoute
            {
                Controller = "Article",
                Action = "Index",
                Parameters = new Dictionary<string, string> { ["pageSize"] = "50" },
            },
        };

        Assert.ThrowsAsync<ArgumentException>(async () =>
            await service.SavePageMetadataAsync(metadata)
        );
    }

    [Test]
    public void SavePageMetadata_WithEmptyParameterValue_ShouldThrow()
    {
        var service = GetService<IPageMetadataService>();
        var metadata = new VmPageMetadata
        {
            Route = new PageMetadataRoute
            {
                Controller = "Article",
                Action = "Read",
                Parameters = new Dictionary<string, string> { ["id"] = " " },
            },
        };

        Assert.ThrowsAsync<ArgumentException>(async () =>
            await service.SavePageMetadataAsync(metadata)
        );
    }

    [Test]
    public async Task ResolvePageMetadata_ShouldMergePublicAndRouteMetadata()
    {
        var service = GetService<IPageMetadataService>();
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                IsPublicMetadata = true,
                ApplyToUnconfiguredPages = true,
                Keywords = ["core", "article"],
                Description = "site description",
                Metas = new Dictionary<string, string>
                {
                    ["author"] = "pengqian",
                    ["theme"] = "site",
                },
            }
        );
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                Route = new PageMetadataRoute { Controller = "Article", Action = "Index" },
                Keywords = ["article", "index"],
                Description = "article index",
                Metas = new Dictionary<string, string> { ["theme"] = "article" },
            }
        );

        var metadata = await service.ResolvePageMetadataAsync(
            new PageMetadataRoute { Controller = "Article", Action = "Index" }
        );

        Assert.That(metadata, Is.Not.Null);
        Assert.That(metadata!.Description, Is.EqualTo("article index"));
        Assert.That(metadata.Keywords, Is.EquivalentTo(new[] { "core", "article", "index" }));
        Assert.That(metadata.Metas["author"], Is.EqualTo("pengqian"));
        Assert.That(metadata.Metas["theme"], Is.EqualTo("article"));
    }

    [Test]
    public async Task ResolvePageMetadata_ShouldRespectIgnoreInheritance()
    {
        var service = GetService<IPageMetadataService>();
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                IsPublicMetadata = true,
                ApplyToUnconfiguredPages = true,
                Keywords = ["core"],
                Description = "site description",
            }
        );
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                Route = new PageMetadataRoute { Controller = "Article", Action = "Index" },
                InheritanceMode = PageMetadataInheritanceMode.Ignore,
                Keywords = ["index"],
            }
        );

        var metadata = await service.ResolvePageMetadataAsync(
            new PageMetadataRoute { Controller = "Article", Action = "Index" }
        );

        Assert.That(metadata, Is.Not.Null);
        Assert.That(metadata!.Keywords, Is.EquivalentTo(new[] { "index" }));
        Assert.That(metadata.Description, Is.Null);
    }

    [Test]
    public async Task ResolvePageMetadata_ShouldUsePublicForUnconfiguredPage()
    {
        var service = GetService<IPageMetadataService>();
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                IsPublicMetadata = true,
                ApplyToUnconfiguredPages = true,
                Keywords = ["core"],
                Description = "site description",
            }
        );

        var metadata = await service.ResolvePageMetadataAsync(
            new PageMetadataRoute { Controller = "Article", Action = "Index" }
        );

        Assert.That(metadata, Is.Not.Null);
        Assert.That(metadata!.Description, Is.EqualTo("site description"));
    }

    [Test]
    public async Task GetPageMetadataByRoute_ShouldPreferMoreSpecificParameters()
    {
        var service = GetService<IPageMetadataService>();
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                Route = new PageMetadataRoute { Controller = "Article", Action = "Read" },
                Description = "article read",
            }
        );
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                Route = new PageMetadataRoute
                {
                    Controller = "Article",
                    Action = "Read",
                    Parameters = new Dictionary<string, string> { ["id"] = "abc" },
                },
                Description = "specific article",
            }
        );

        var metadata = await service.GetPageMetadataByRouteAsync(
            new PageMetadataRoute
            {
                Controller = "Article",
                Action = "Read",
                Parameters = new Dictionary<string, string> { ["id"] = "abc" },
            }
        );

        Assert.That(metadata, Is.Not.Null);
        Assert.That(metadata!.Description, Is.EqualTo("specific article"));
    }

    [Test]
    public async Task GetPageMetadataByRoute_ShouldPreserveParameterValueCase()
    {
        var service = GetService<IPageMetadataService>();
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                Route = new PageMetadataRoute { Controller = "Article", Action = "Read" },
                Description = "generic article",
            }
        );
        await service.SavePageMetadataAsync(
            new VmPageMetadata
            {
                Route = new PageMetadataRoute
                {
                    Controller = "Article",
                    Action = "Read",
                    Parameters = new Dictionary<string, string> { ["id"] = "AbC" },
                },
                Description = "case-sensitive article",
            }
        );

        var exactMetadata = await service.GetPageMetadataByRouteAsync(
            new PageMetadataRoute
            {
                Controller = "article",
                Action = "read",
                Parameters = new Dictionary<string, string> { ["ID"] = "AbC" },
            }
        );
        var differentCaseMetadata = await service.GetPageMetadataByRouteAsync(
            new PageMetadataRoute
            {
                Controller = "article",
                Action = "read",
                Parameters = new Dictionary<string, string> { ["id"] = "abc" },
            }
        );

        Assert.That(exactMetadata!.Description, Is.EqualTo("case-sensitive article"));
        Assert.That(differentCaseMetadata!.Description, Is.EqualTo("generic article"));
    }

    [Test]
    public async Task RouteCatalog_ShouldMergeEndpointVariantsAndParameterSources()
    {
        var routeCatalogService = GetService<IPageRouteCatalogService>();
        await routeCatalogService.SaveScannedRoutesAsync([
            new VmSavePageRouteDefinition
            {
                Controller = "Mumble",
                Action = "Index",
                HttpMethods = ["GET"],
                Endpoints =
                [
                    new VmPageRouteEndpointDefinition
                    {
                        RouteName = "mumble",
                        Template = "mumble.html",
                        HttpMethods = ["GET"],
                        Defaults = new Dictionary<string, string> { ["pageIndex"] = "1" },
                    },
                ],
                Parameters =
                [
                    new VmPageRouteParameterDefinition
                    {
                        Name = "pageIndex",
                        TypeName = "Int32",
                        Source = PageRouteParameterSource.Route,
                        IsOptional = true,
                    },
                    new VmPageRouteParameterDefinition
                    {
                        Name = "tag",
                        TypeName = "String",
                        Source = PageRouteParameterSource.Query,
                        IsOptional = true,
                    },
                ],
            },
            new VmSavePageRouteDefinition
            {
                Controller = "Mumble",
                Action = "Index",
                HttpMethods = ["GET"],
                Endpoints =
                [
                    new VmPageRouteEndpointDefinition
                    {
                        RouteName = "mumble-page",
                        Template = "mumble-{pageIndex}.html",
                        HttpMethods = ["GET"],
                    },
                ],
                Parameters =
                [
                    new VmPageRouteParameterDefinition
                    {
                        Name = "pageIndex",
                        TypeName = "Int32",
                        Source = PageRouteParameterSource.Route,
                    },
                ],
            },
        ]);

        var routes = await routeCatalogService.GetRoutesAsync();
        var route = routes.Single(x => x.Controller == "mumble" && x.Action == "index");
        var pageIndex = route.Parameters.Single(x => x.Name == "pageindex");

        Assert.That(route.Endpoints, Has.Count.EqualTo(2));
        Assert.That(
            route.Endpoints.Select(x => x.Template),
            Is.EquivalentTo(new[] { "mumble.html", "mumble-{pageIndex}.html" })
        );
        Assert.That(pageIndex.Source, Is.EqualTo(PageRouteParameterSource.Route));
        Assert.That(
            route.Parameters.Single(x => x.Name == "tag").Source,
            Is.EqualTo(PageRouteParameterSource.Query)
        );
        var allRoutes = await routeCatalogService.GetRoutesAsync(activeOnly: false);
        Assert.That(allRoutes.Where(x => x.Controller == "article").All(x => !x.IsActive), Is.True);
    }

    [Test]
    public async Task GetPageMetadataAsync_ShouldFallbackToLegacyRelations()
    {
        var legacy = new PageMetadata
        {
            Description = "legacy",
            Relations = ["article", "read", "legacy-id"],
        };
        await PageMetadataRepository.InsertAsync(legacy);

        var service = GetService<IPageMetadataService>();
        var metadata = await service.GetPageMetadataAsync(["Article", "Read", "legacy-id"]);

        Assert.That(metadata, Is.Not.Null);
        Assert.That(metadata!.Description, Is.EqualTo("legacy"));
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码分析

这是一个 NUnit 测试类,用于测试 IPageMetadataServiceIPageRouteCatalogService 服务的功能。主要测试页面元数据(SEO信息)的管理和路由系统。

主要结构

SetUp 方法

[SetUp]
public async Task SetUpAsync()
  • 每个测试前执行,清空数据库中的页面元数据和路由定义
  • 初始化两个测试路由:
    • Article/Index:带可选查询参数 tag
    • Article/Read:带路由参数 id,使用模板 article/read/{id}.html

核心测试用例

1. 基础保存和查询 (SavePageMetadataTest)

  • 测试保存页面元数据(描述、关键词、自定义meta标签)
  • 验证能通过路由信息查询到保存的元数据

2. 公共元数据 (3个测试)

  • SavePublicMetadata_ShouldNotRequireRoute:公共元数据不需要关联具体路由
  • SavePublicMetadata_WithRoute_ShouldThrow:公共元数据不能同时指定路由(应抛异常)
  • 公共元数据可应用到未配置的页面

3. 数据验证 (3个测试)

  • SavePageMetadata_WithUnknownRoute_ShouldThrow:未知路由应抛异常
  • SavePageMetadata_WithUnlistedParameter_ShouldThrow:使用未定义的参数应抛异常
  • SavePageMetadata_WithEmptyParameterValue_ShouldThrow:空参数值应抛异常

4. 元数据继承与合并 (3个测试)

  • ResolvePageMetadata_ShouldMergePublicAndRouteMetadata

    • 合并公共元数据和路由特定元数据
    • 关键词去重合并
    • 路由特定值覆盖公共值
  • ResolvePageMetadata_ShouldRespectIgnoreInheritance

    • 支持 IgnoreInheritance 模式,不继承公共元数据
  • ResolvePageMetadata_ShouldUsePublicForUnconfiguredPage

    • 未配置页面使用公共元数据

5. 参数匹配优先级 (2个测试)

  • GetPageMetadataByRoute_ShouldPreferMoreSpecificParameters

    • 带参数的路由优先于无参数的路由
  • GetPageMetadataByRoute_ShouldPreserveParameterValueCase

    • 参数值区分大小写AbCabc
    • 路由名称和参数名不区分大小写

6. 路由目录合并 (RouteCatalog_ShouldMergeEndpointVariantsAndParameterSources)

测试同一控制器/动作的多个路由定义合并:

mumble.html (pageIndex默认=1, tag可选)
mumble-{pageIndex}.html (pageIndex必需)
  • 合并为一个路由定义,包含2个端点
  • 参数源信息正确合并
  • 旧路由标记为非活动状态

7. 向后兼容 (GetPageMetadataAsync_ShouldFallbackToLegacyRelations)

  • 支持旧版关系数据格式 Relations = ["article", "read", "legacy-id"]

关键设计模式

  1. 继承策略:公共元数据 + 路由特定元数据 + 参数特定元数据
  2. 大小写策略:控制器/动作/参数名不敏感,参数值敏感
  3. 优先级规则:更具体的配置(带参数)优先于通用配置
  4. 验证机制:严格验证路由和参数的有效性

业务场景

典型的 SEO元数据管理系统,支持:

  • 全站默认元数据
  • 特定页面元数据
  • 带参数的页面元数据(如特定文章)
  • 灵活的继承和覆盖机制
评论加载中...