324 lines
12 KiB
C#
324 lines
12 KiB
C#
using YMhut.Box.Core.Api;
|
|
using YMhut.Box.Core.App;
|
|
using YMhut.Box.Core.Media;
|
|
|
|
namespace YMhut.Box.Tests;
|
|
|
|
[TestClass]
|
|
public sealed class RemoteMediaCatalogTests
|
|
{
|
|
[TestMethod]
|
|
public void ParsesCurrentMediaTypesSnapshot()
|
|
{
|
|
var catalog = RemoteMediaCatalogParser.Parse(ReadRepoFile("server", "update", "public", "media-types.json"));
|
|
|
|
Assert.AreEqual("1.0.6", catalog.LayoutVersion);
|
|
Assert.AreEqual("grid", catalog.UiConfig.DefaultView);
|
|
Assert.IsGreaterThanOrEqualTo(2, catalog.Categories.Count);
|
|
|
|
var image = catalog.Categories.Single(category => category.Id == "image");
|
|
Assert.IsTrue(image.Enabled);
|
|
Assert.AreEqual("随机图片", image.DisplayName);
|
|
Assert.AreEqual(RemoteMediaKind.Image, image.Kind);
|
|
Assert.IsTrue(image.Layout.ShowPreview);
|
|
CollectionAssert.Contains(image.Sources.First(source => source.Id == "xjj").SupportedFormats.ToArray(), "jpg");
|
|
Assert.AreEqual(30, image.Sources.First(source => source.Id == "xjj").RefreshIntervalSeconds);
|
|
Assert.AreEqual("image", image.Sources.First(source => source.Id == "xjj").MediaType);
|
|
|
|
var video = catalog.Categories.Single(category => category.Id == "video");
|
|
Assert.AreEqual("随机视频", video.DisplayName);
|
|
Assert.AreEqual(RemoteMediaKind.Video, video.Kind);
|
|
Assert.IsFalse(video.Layout.AutoPlay);
|
|
CollectionAssert.Contains(video.Sources.First().SupportedFormats.ToArray(), "mp4");
|
|
Assert.AreEqual("video", video.Sources.First().MediaType);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ParsesLegacyMediaTypesSnapshot()
|
|
{
|
|
var catalog = RemoteMediaCatalogParser.Parse(ReadRepoFile("box-old", "server", "media-types.json"));
|
|
|
|
Assert.AreEqual("1.0.8", catalog.LayoutVersion);
|
|
Assert.IsGreaterThanOrEqualTo(2, catalog.Categories.Count);
|
|
Assert.IsTrue(catalog.Categories.Any(category => category.Id == "image" && category.Sources.Count >= 7));
|
|
Assert.IsTrue(catalog.Categories.Any(category => category.Id == "video" && category.Sources.Any(source => source.Id == "radom_xjj_mv")));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void AppliesDefaultsWhenOptionalFieldsAreMissing()
|
|
{
|
|
const string minimal = """
|
|
{
|
|
"categories": [
|
|
{
|
|
"id": "video",
|
|
"subcategories": [
|
|
{
|
|
"id": "demo",
|
|
"api_url": "https://example.test/media"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
|
|
var catalog = RemoteMediaCatalogParser.Parse(minimal);
|
|
var category = catalog.Categories.Single();
|
|
var source = category.Sources.Single();
|
|
|
|
Assert.IsTrue(category.Enabled);
|
|
Assert.AreEqual(RemoteMediaKind.Video, category.Kind);
|
|
Assert.AreEqual(1, category.Layout.Columns);
|
|
Assert.AreEqual("16:9", category.Layout.AspectRatio);
|
|
Assert.IsTrue(category.Layout.ShowPreview);
|
|
Assert.IsFalse(category.Layout.AutoPlay);
|
|
Assert.IsTrue(source.Downloadable);
|
|
Assert.AreEqual(60, source.RefreshIntervalSeconds);
|
|
CollectionAssert.AreEqual(new[] { "mp4", "webm" }, source.SupportedFormats.ToArray());
|
|
Assert.AreEqual("https://example.test/media", source.ThumbnailUrl);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ParsesUnifiedResolvedMediaFields()
|
|
{
|
|
const string content = """
|
|
{
|
|
"categories": [
|
|
{
|
|
"id": "image",
|
|
"subcategories": [
|
|
{
|
|
"id": "demo",
|
|
"api_url": "https://api.example.test/random",
|
|
"resolvedUrl": "https://cdn.example.test/media/demo.webp",
|
|
"resolvedKey": "data.cover",
|
|
"mediaType": "image",
|
|
"supported_formats": ["json", "webp"]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
|
|
var source = RemoteMediaCatalogParser.Parse(content).Categories.Single().Sources.Single();
|
|
|
|
Assert.AreEqual("https://api.example.test/random", source.ApiUrl);
|
|
Assert.AreEqual("https://cdn.example.test/media/demo.webp", source.ResolvedUrl);
|
|
Assert.AreEqual("data.cover", source.ResolvedKey);
|
|
Assert.AreEqual("image", source.MediaType);
|
|
Assert.AreEqual(source.ResolvedUrl, source.EffectiveApiUrl);
|
|
Assert.AreEqual(source.ApiUrl, source.RefreshApiUrl);
|
|
Assert.IsTrue(source.IsAvailable);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ParserPreservesSourceDescriptionForCards()
|
|
{
|
|
const string content = """
|
|
{
|
|
"categories": [
|
|
{
|
|
"id": "image",
|
|
"subcategories": [
|
|
{
|
|
"id": "demo",
|
|
"name": "Demo",
|
|
"description": "后台配置的子接口描述",
|
|
"api_url": "https://api.example.test/random"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
|
|
var source = RemoteMediaCatalogParser.Parse(content).Categories.Single().Sources.Single();
|
|
|
|
Assert.AreEqual("后台配置的子接口描述", source.Description);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void ExplicitMediaTypeWinsOverCategoryAndFormats()
|
|
{
|
|
const string content = """
|
|
{
|
|
"categories": [
|
|
{
|
|
"id": "mixed",
|
|
"type": "image",
|
|
"subcategories": [
|
|
{
|
|
"id": "json_picture",
|
|
"api_url": "https://api.example.test/random",
|
|
"mediaType": "image",
|
|
"supported_formats": ["json", "mp4"]
|
|
},
|
|
{
|
|
"id": "clip",
|
|
"api_url": "https://api.example.test/clip",
|
|
"type": "video",
|
|
"supported_formats": ["jpg"]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
|
|
var category = RemoteMediaCatalogParser.Parse(content).Categories.Single();
|
|
|
|
Assert.AreEqual(RemoteMediaKind.Image, category.Kind);
|
|
Assert.AreEqual(RemoteMediaKind.Image, category.Sources[0].Kind);
|
|
Assert.AreEqual(RemoteMediaKind.Video, category.Sources[1].Kind);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ServiceWritesReadsFallsBackAndClearsCache()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "ymhut-remote-media-" + Guid.NewGuid().ToString("N"));
|
|
try
|
|
{
|
|
var paths = new AppPaths(root);
|
|
paths.EnsureCreated();
|
|
var content = ReadRepoFile("server", "update", "public", "media-types.json");
|
|
var api = new FakeApiManager(content);
|
|
var service = new RemoteMediaCatalogService(paths, api);
|
|
|
|
var remote = await service.LoadAsync(forceRefresh: true);
|
|
|
|
Assert.AreEqual(RemoteMediaCatalogLoadSource.Remote, remote.Source);
|
|
Assert.IsNotNull(api.LastUri);
|
|
StringAssert.Contains(api.LastUri.Query, "_=");
|
|
Assert.IsTrue(File.Exists(Path.Combine(paths.Cache, "remote-media", "media-types.json")));
|
|
|
|
var fallback = new RemoteMediaCatalogService(paths, new FakeApiManager(string.Empty, success: false));
|
|
var cached = await fallback.LoadAsync();
|
|
|
|
Assert.AreEqual(RemoteMediaCatalogLoadSource.Cache, cached.Source);
|
|
Assert.IsFalse(string.IsNullOrWhiteSpace(cached.Warning));
|
|
|
|
await fallback.ClearCacheAsync();
|
|
Assert.IsFalse(Directory.Exists(Path.Combine(paths.Cache, "remote-media")));
|
|
Assert.IsTrue(Directory.Exists(paths.Cache));
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task ServicePrefersUnifiedBootstrapSources()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "ymhut-remote-media-" + Guid.NewGuid().ToString("N"));
|
|
try
|
|
{
|
|
var paths = new AppPaths(root);
|
|
paths.EnsureCreated();
|
|
const string bootstrap = """
|
|
{
|
|
"ok": true,
|
|
"sources": {
|
|
"layout_version": "2.0.0",
|
|
"categories": [
|
|
{
|
|
"id": "image",
|
|
"subcategories": [
|
|
{
|
|
"id": "demo",
|
|
"api_url": "https://api.example.test/random",
|
|
"resolvedUrl": "https://cdn.example.test/media/demo.webp",
|
|
"supported_formats": ["json", "webp"]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
""";
|
|
var api = new FakeApiManager(
|
|
string.Empty,
|
|
uriResponses: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
[RemoteMediaCatalogService.BootstrapUri.AbsolutePath] = bootstrap
|
|
});
|
|
var service = new RemoteMediaCatalogService(paths, api);
|
|
|
|
var result = await service.LoadAsync(forceRefresh: false);
|
|
|
|
Assert.AreEqual(RemoteMediaCatalogLoadSource.Remote, result.Source);
|
|
Assert.AreEqual(RemoteMediaCatalogService.BootstrapUri.AbsolutePath, api.LastUri?.AbsolutePath);
|
|
Assert.AreEqual("https://cdn.example.test/media/demo.webp", result.Catalog.Categories.Single().Sources.Single().EffectiveApiUrl);
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
{
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string ReadRepoFile(params string[] segments)
|
|
{
|
|
var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
|
|
while (directory is not null)
|
|
{
|
|
var candidate = Path.Combine(new[] { directory.FullName }.Concat(segments).ToArray());
|
|
if (File.Exists(candidate))
|
|
{
|
|
return File.ReadAllText(candidate);
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Unable to locate repository sample file.");
|
|
}
|
|
|
|
private sealed class FakeApiManager(string content, bool success = true, IReadOnlyDictionary<string, string>? uriResponses = null) : IApiManager
|
|
{
|
|
public Uri? LastUri { get; private set; }
|
|
|
|
public Task<ApiResponse> FetchAsync(string endpointId, string input = "", CancellationToken cancellationToken = default)
|
|
{
|
|
LastUri = RemoteMediaCatalogService.PrimaryConfigUri;
|
|
return Task.FromResult(new ApiResponse(
|
|
endpointId,
|
|
LastUri,
|
|
success,
|
|
success ? content : string.Empty,
|
|
success ? null : "offline",
|
|
DateTimeOffset.Now,
|
|
success ? 200 : 0));
|
|
}
|
|
|
|
public Task<ApiResponse> FetchUriAsync(string endpointId, Uri uri, string input = "", CancellationToken cancellationToken = default)
|
|
{
|
|
LastUri = uri;
|
|
var responseContent = uriResponses is not null && uriResponses.TryGetValue(uri.AbsolutePath, out var match)
|
|
? match
|
|
: content;
|
|
var responseSuccess = success || !string.IsNullOrWhiteSpace(responseContent);
|
|
return Task.FromResult(new ApiResponse(
|
|
endpointId,
|
|
uri,
|
|
responseSuccess,
|
|
responseSuccess ? responseContent : string.Empty,
|
|
responseSuccess ? null : "offline",
|
|
DateTimeOffset.Now,
|
|
responseSuccess ? 200 : 0));
|
|
}
|
|
|
|
public Task<ApiHealthStatus> CheckHealthAsync(string endpointId, string input = "", CancellationToken cancellationToken = default)
|
|
{
|
|
return Task.FromResult(success ? ApiHealthStatus.Healthy : ApiHealthStatus.Unhealthy);
|
|
}
|
|
}
|
|
}
|