using System.Net; using System.Text; using System.Text.Json; using dy.net.model.dto; using dy.net.model.entity; using dy.net.service; using dy.net.storage; using dy.net.Tests.TestInfrastructure; using Microsoft.AspNetCore.DataProtection; using SqlSugar; using MediaStorageType = dy.net.model.dto.StorageType; namespace dy.net.Tests; public sealed class OpenListDirectoryRepairServiceTests { [Fact] public async Task Preflight_OnlyIncludesExactTimestampDirectoriesAndKeepsCanonicalDirectory() { using var temporary = new TemporaryDirectory(); using var database = new SqlSugarClient(new ConnectionConfig { ConnectionString = $"DataSource={Path.Combine(temporary.Path, "repair.sqlite")}", DbType = DbType.Sqlite, InitKeyType = InitKeyType.Attribute, IsAutoCloseConnection = true }); database.CodeFirst.InitTables(); await database.Insertable(new AppConfig { Id = "config", StorageType = MediaStorageType.OpenList, FollowedTitleTemplate = string.Empty, FollowedTitleSeparator = string.Empty, FullFollowedTitleTemplate = string.Empty }).ExecuteCommandAsync(); var provider = DataProtectionProvider.Create(new DirectoryInfo(Path.Combine(temporary.Path, "keys")), builder => builder.SetApplicationName("dysync.net")); var legacy = new WebDavSettingsService(database, provider); var settingsService = new OpenListSettingsService(database, provider, legacy); var settings = await settingsService.BuildCandidateAsync(new OpenListTestRequest { Endpoint = "https://openlist.example.test", BasePath = "/归档", SourcePath = "/源挂载", LocalStagingPath = Path.Combine(temporary.Path, "staging"), UserName = "account", Password = "password" }); await settingsService.SaveAsync(settings, true, "tested"); using var factory = new RepairHttpClientFactory(); var service = new OpenListDirectoryRepairService(database, new DouyinCommonService(database), settingsService, new OpenListClient(factory)); var preflight = await service.PreflightAsync( new OpenListDirectoryRepairPreflightRequest { LogicalPath = "/collect/Kk" }, CancellationToken.None); Assert.True(preflight.CanStart); Assert.Equal("/collect/Kk", preflight.RequestedPath); Assert.Equal("/collect/KK", preflight.CanonicalPath); Assert.Equal(1, preflight.CandidateCount); var created = await service.CreateAsync(new CreateOpenListDirectoryRepairRequest { LogicalPath = preflight.RequestedPath, ConfigurationFingerprint = preflight.ConfigurationFingerprint }, CancellationToken.None); var items = await database.Queryable().ToListAsync(); Assert.Equal(1, created.Task.TotalCount); var item = Assert.Single(items); Assert.Equal("Kk_20260809_123456", item.DirectoryName); Assert.DoesNotContain(items, x => x.DirectoryName == "KK"); } private sealed class RepairHttpClientFactory : IHttpClientFactory, IDisposable { private readonly Handler _handler = new(); public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false); public void Dispose() => _handler.Dispose(); } private sealed class Handler : HttpMessageHandler { protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var api = request.RequestUri!.AbsolutePath; if (api.EndsWith("/api/auth/login", StringComparison.Ordinal)) return Json(new { code = 200, message = "success", data = new { token = "test-token" } }); var body = JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken)).RootElement; var path = body.GetProperty("path").GetString(); if (api.EndsWith("/api/fs/get", StringComparison.Ordinal)) { if (path == "/归档/collect/KK") return Json(new { code = 200, message = "success", data = new { name = "KK", size = 0, is_dir = true, hash_info = new { } } }); return Json(new { code = 404, message = "object not found", data = (object)null }); } Assert.EndsWith("/api/fs/list", api, StringComparison.Ordinal); var entries = path switch { "/" => new[] { Entry("归档", true) }, "/归档" => new[] { Entry("collect", true) }, "/归档/collect" => new[] { Entry("KK", true), Entry("Kk_20260809_123456", true), Entry("Kk_20260809_12345", true), Entry("Kk_20260809_123456_extra", true), Entry("KK_20260809_123456", true), Entry("Kkkk_20260809_123456", true), Entry("Kk_20260810_123456", false) }, _ => Array.Empty() }; return Json(new { code = 200, message = "success", data = new { content = entries, write = true } }); } private static object Entry(string name, bool directory) => new { name, size = 0, is_dir = directory }; private static HttpResponseMessage Json(object value) => new(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json") }; } }