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 测试类,用于测试 IPageMetadataService 和 IPageRouteCatalogService 服务的功能。主要测试页面元数据(SEO信息)的管理和路由系统。
主要结构
SetUp 方法
[SetUp]
public async Task SetUpAsync()
- 每个测试前执行,清空数据库中的页面元数据和路由定义
- 初始化两个测试路由:
- Article/Index:带可选查询参数
tag - Article/Read:带路由参数
id,使用模板article/read/{id}.html
- Article/Index:带可选查询参数
核心测试用例
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:- 参数值区分大小写(
AbC≠abc) - 路由名称和参数名不区分大小写
- 参数值区分大小写(
6. 路由目录合并 (RouteCatalog_ShouldMergeEndpointVariantsAndParameterSources)
测试同一控制器/动作的多个路由定义合并:
mumble.html (pageIndex默认=1, tag可选)
mumble-{pageIndex}.html (pageIndex必需)
- 合并为一个路由定义,包含2个端点
- 参数源信息正确合并
- 旧路由标记为非活动状态
7. 向后兼容 (GetPageMetadataAsync_ShouldFallbackToLegacyRelations)
- 支持旧版关系数据格式
Relations = ["article", "read", "legacy-id"]
关键设计模式
- 继承策略:公共元数据 + 路由特定元数据 + 参数特定元数据
- 大小写策略:控制器/动作/参数名不敏感,参数值敏感
- 优先级规则:更具体的配置(带参数)优先于通用配置
- 验证机制:严格验证路由和参数的有效性
业务场景
典型的 SEO元数据管理系统,支持:
- 全站默认元数据
- 特定页面元数据
- 带参数的页面元数据(如特定文章)
- 灵活的继承和覆盖机制
AI 正在分析代码…
评论加载中...