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(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? uriResponses = null) : IApiManager { public Uri? LastUri { get; private set; } public Task 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 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 CheckHealthAsync(string endpointId, string input = "", CancellationToken cancellationToken = default) { return Task.FromResult(success ? ApiHealthStatus.Healthy : ApiHealthStatus.Unhealthy); } } }