Files
douyin/tests/dy.net.Tests/WebDavMediaStorageTests.cs

255 lines
12 KiB
C#

using System.Net;
using System.Text;
using dy.net.model.dto;
using dy.net.Tests.TestInfrastructure;
namespace dy.net.Tests;
public class WebDavMediaStorageTests
{
private const string UserName = "test-user";
private const string Password = "test-password";
[Fact]
public async Task WriteReadAndDelete_UseBasicAuthEncodedPathsAndAtomicMove()
{
var handler = new InMemoryWebDavHandler(UserName, Password);
using var host = await CreateHostAsync(handler, "/媒体 库");
var path = "/作者 名/视频 #1.mp4";
var bytes = Encoding.UTF8.GetBytes("abcdefghij");
await using (var source = new MemoryStream(bytes, false))
await host.Storage.WriteAsync(path, source, bytes.Length, "video/mp4");
const string expectedServerPath = "/dav/媒体 库/作者 名/视频 #1.mp4";
Assert.True(await host.Storage.ExistsAsync(path));
Assert.Equal(bytes.Length, await host.Storage.GetLengthAsync(path));
Assert.Equal(bytes, handler.Files[expectedServerPath]);
Assert.DoesNotContain(handler.Files.Keys, x => x.Contains(".part-", StringComparison.Ordinal));
var put = Assert.Single(handler.Requests, x => x.Method == "PUT");
Assert.Contains(".part-", put.DecodedPath);
Assert.Contains("%E5%AA%92%E4%BD%93%20%E5%BA%93", put.RawUri);
Assert.Contains("%23", put.RawUri);
var move = Assert.Single(handler.Requests, x => x.Method == "MOVE");
Assert.EndsWith("/视频 #1.mp4", Uri.UnescapeDataString(new Uri(move.Destination).AbsolutePath));
Assert.All(handler.Requests, request => Assert.Equal(handler.ExpectedAuthorization, request.Authorization));
await using (var range = await host.Storage.OpenReadAsync(path, 2, 4))
{
Assert.Equal(206, range.StatusCode);
Assert.Equal("bytes 2-4/10", range.ContentRange);
using var reader = new StreamReader(range.Stream, Encoding.UTF8);
Assert.Equal("cde", await reader.ReadToEndAsync());
}
await host.Storage.DeleteAsync(path);
Assert.False(await host.Storage.ExistsAsync(path));
Assert.DoesNotContain(expectedServerPath, handler.Files.Keys);
}
[Fact]
public async Task WriteAsync_WhenMoveFails_CleansTemporaryUpload()
{
var handler = new InMemoryWebDavHandler(UserName, Password) { FailNextMove = true };
using var host = await CreateHostAsync(handler);
var bytes = Encoding.UTF8.GetBytes("upload that must be cleaned");
await using var source = new MemoryStream(bytes, false);
await Assert.ThrowsAsync<HttpRequestException>(() =>
host.Storage.WriteAsync("/failed/video.mp4", source, bytes.Length, "video/mp4"));
Assert.Empty(handler.Files);
Assert.Contains(handler.Requests, x => x.Method == "DELETE" && x.DecodedPath.Contains(".part-", StringComparison.Ordinal));
}
[Fact]
public async Task WriteAsync_WhenExistingDirectoriesReturnConflict_VerifiesAndReusesCollections()
{
var handler = new InMemoryWebDavHandler(UserName, Password)
{
ExistingDirectoryStatusCode = HttpStatusCode.Conflict
};
using var host = await CreateHostAsync(handler);
var first = Encoding.UTF8.GetBytes("first object");
var second = Encoding.UTF8.GetBytes("second object");
await using (var source = new MemoryStream(first, false))
await host.Storage.WriteAsync("/same-parent/first.mp4", source, first.Length, "video/mp4");
await using (var source = new MemoryStream(second, false))
await host.Storage.WriteAsync("/same-parent/second.mp4", source, second.Length, "video/mp4");
Assert.Equal(first, handler.Files["/dav/dysync-test/same-parent/first.mp4"]);
Assert.Equal(second, handler.Files["/dav/dysync-test/same-parent/second.mp4"]);
Assert.Contains(handler.Requests, request => request.Method == "PROPFIND"
&& request.DecodedPath == "/dav/dysync-test/same-parent");
}
[Fact]
public async Task WriteAsync_WhenMovedMetadataIsStale_UsesRangeReadToVerifyObject()
{
var handler = new InMemoryWebDavHandler(UserName, Password)
{
StaleMetadataReadsAfterMove = 1
};
using var host = await CreateHostAsync(handler);
var bytes = Encoding.UTF8.GetBytes("moved object is already readable");
await using (var source = new MemoryStream(bytes, false))
await host.Storage.WriteAsync("/eventual/video.mp4", source, bytes.Length, "video/mp4");
Assert.Equal(bytes.Length, await host.Storage.GetLengthAsync("/eventual/video.mp4"));
Assert.Contains(handler.Requests, request => request.Method == "GET" && request.Range == "bytes=0-0");
Assert.DoesNotContain(handler.Files.Keys, path => path.Contains(".part-", StringComparison.Ordinal));
}
[Fact]
public async Task WriteAsync_WhenMovedObjectIsBrieflyInvisible_RetriesVerification()
{
var handler = new InMemoryWebDavHandler(UserName, Password)
{
// The first HEAD and ranged GET both observe stale state. The next delayed
// verification sees the object and must complete without uploading again.
StaleAllReadsAfterMove = 2
};
using var host = await CreateHostAsync(handler);
var bytes = Encoding.UTF8.GetBytes("eventually visible object");
await using (var source = new MemoryStream(bytes, false))
await host.Storage.WriteAsync("/eventual/retry.mp4", source, bytes.Length, "video/mp4");
Assert.Equal(bytes, handler.Files["/dav/dysync-test/eventual/retry.mp4"]);
Assert.Single(handler.Requests, request => request.Method == "PUT");
Assert.Single(handler.Requests, request => request.Method == "MOVE");
}
[Fact]
public async Task WriteAsync_WhenMoveVisibilityExceedsOldRetryWindow_UsesExtendedVerification()
{
var handler = new InMemoryWebDavHandler(UserName, Password)
{
// Six HEAD + Range verification rounds all see stale state. The seventh
// round represents visibility beyond the 0.2.12 retry window.
StaleAllReadsAfterMove = 12
};
using var host = await CreateHostAsync(handler, finalVisibilityRetryDelays:
Enumerable.Repeat(TimeSpan.Zero, 7).ToArray());
var bytes = Encoding.UTF8.GetBytes("visible after the old retry window");
await using (var source = new MemoryStream(bytes, false))
await host.Storage.WriteAsync("/eventual/extended.mp4", source, bytes.Length, "video/mp4");
Assert.Equal(bytes, handler.Files["/dav/dysync-test/eventual/extended.mp4"]);
Assert.Single(handler.Requests, request => request.Method == "PUT");
Assert.Single(handler.Requests, request => request.Method == "MOVE");
Assert.True(handler.Requests.Count(request => request.Method is "HEAD" or "GET") >= 13);
}
[Fact]
public async Task WriteAsync_WhenOverwriteBrieflyReturnsOldLength_WaitsForNewObject()
{
var handler = new InMemoryWebDavHandler(UserName, Password);
using var host = await CreateHostAsync(handler, finalVisibilityRetryDelays:
Enumerable.Repeat(TimeSpan.Zero, 3).ToArray());
var path = "/eventual/overwrite.mp4";
var oldBytes = Encoding.UTF8.GetBytes("old-content");
await using (var oldSource = new MemoryStream(oldBytes, false))
await host.Storage.WriteAsync(path, oldSource, oldBytes.Length, "video/mp4");
handler.StaleLengthReadsAfterMove = 4;
var newBytes = Encoding.UTF8.GetBytes("new-content-with-a-different-length");
await using (var newSource = new MemoryStream(newBytes, false))
await host.Storage.WriteAsync(path, newSource, newBytes.Length, "video/mp4");
Assert.Equal(newBytes, handler.Files["/dav/dysync-test/eventual/overwrite.mp4"]);
Assert.Contains(handler.Requests, request => request.Method == "GET"
&& request.Range == "bytes=0-0"
&& request.CacheControl?.Contains("no-cache", StringComparison.OrdinalIgnoreCase) == true);
}
[Fact]
public async Task BoundStorage_KeepsOneTaskOnItsOriginalTargetAfterGlobalConfigChanges()
{
var handler = new InMemoryWebDavHandler(UserName, Password);
using var host = await CreateHostAsync(handler, "/original-target");
var bound = host.Storage.Bind(host.Settings);
var changed = await host.SettingsService.BuildCandidateAsync(new WebDavTestRequest
{
Endpoint = host.Settings.Endpoint,
BasePath = "/changed-target",
UserName = UserName,
Password = Password
});
await host.SettingsService.SaveAsync(changed, true, "changed");
var bytes = Encoding.UTF8.GetBytes("task snapshot");
await using (var source = new MemoryStream(bytes, false))
await bound.WriteAsync("/video.mp4", source, bytes.Length, "video/mp4");
Assert.Contains("/dav/original-target/video.mp4", handler.Files.Keys);
Assert.DoesNotContain("/dav/changed-target/video.mp4", handler.Files.Keys);
}
[Fact]
public async Task ProbeAsync_ExercisesRequiredWebDavCapabilitiesAndCleansProbeData()
{
var handler = new InMemoryWebDavHandler(UserName, Password);
using var host = await CreateHostAsync(handler);
var result = await host.Storage.ProbeAsync(host.Settings);
Assert.True(result.Success, result.Message);
Assert.Empty(handler.Files);
Assert.Contains(handler.Requests, x => x.Method == "MKCOL");
Assert.Contains(handler.Requests, x => x.Method == "PUT");
Assert.Contains(handler.Requests, x => x.Method == "HEAD");
Assert.Contains(handler.Requests, x => x.Method == "MOVE");
Assert.Contains(handler.Requests, x => x.Method == "GET" && x.Range == "bytes=0-0");
Assert.Contains(handler.Requests, x => x.Method == "DELETE");
Assert.All(handler.Requests, request => Assert.Equal(handler.ExpectedAuthorization, request.Authorization));
}
[Fact]
public async Task ProbeAsync_RejectsServerThatIgnoresRangeRequests()
{
var handler = new InMemoryWebDavHandler(UserName, Password) { SupportsRange = false };
using var host = await CreateHostAsync(handler);
var result = await host.Storage.ProbeAsync(host.Settings);
Assert.False(result.Success);
Assert.Contains("206", result.Message);
Assert.Empty(handler.Files);
}
[Fact]
public async Task ProbeAsync_RejectsPartialResponseWithoutContentRange()
{
var handler = new InMemoryWebDavHandler(UserName, Password) { IncludeContentRange = false };
using var host = await CreateHostAsync(handler);
var result = await host.Storage.ProbeAsync(host.Settings);
Assert.False(result.Success);
Assert.Contains("Content-Range", result.Message);
Assert.Empty(handler.Files);
}
private static async Task<WebDavTestHost> CreateHostAsync(
InMemoryWebDavHandler handler,
string basePath = "/dysync-test",
IReadOnlyList<TimeSpan> finalVisibilityRetryDelays = null)
{
var factory = new StubHttpClientFactory(handler);
return await WebDavTestHost.CreateAsync(factory, factory, new WebDavTestRequest
{
Endpoint = "https://dav.example.test/dav",
BasePath = basePath,
UserName = UserName,
Password = Password
},
finalVisibilityRetryDelays == null ? null : new[] { TimeSpan.Zero },
finalVisibilityRetryDelays);
}
}