using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using TeleWave.Infrastructure.Media;
using Xunit;
namespace TeleWave.Application.Tests.Media;
///
/// Уборка перевалочного каталога: в uploads/ не должно оставаться файлов, на которые никто
/// не сослался — это гигабайты на диске, невидимые в админке.
///
public class UploadsCleanupTests : IDisposable
{
private readonly string _root = Path.Combine(
Path.GetTempPath(),
"telewave-tests",
Guid.NewGuid().ToString("N")
);
private MediaPathResolver Paths() =>
new(Options.Create(new StorageOptions { RootPath = _root }));
[Fact]
public async Task DeletesPartialUpload_WhenStreamFails()
{
var paths = Paths();
paths.EnsureDirectories();
var storage = new FileSystemMediaStorage(paths);
await Assert.ThrowsAsync(() =>
storage.SaveUploadAsync(new FailingStream(), ".mkv", CancellationToken.None)
);
Assert.Empty(Directory.GetFiles(paths.UploadsDir));
}
[Fact]
public void Sweep_DeletesStaleFilesOnly()
{
var paths = Paths();
paths.EnsureDirectories();
var stale = Path.Combine(paths.UploadsDir, "stale.mkv");
var fresh = Path.Combine(paths.UploadsDir, "fresh.mkv");
// Идущую заливку от брошенного файла отличает именно время последней записи.
WriteAged(stale, TimeSpan.FromHours(25));
WriteAged(fresh, TimeSpan.Zero);
UploadsCleaner.Sweep(paths, TimeSpan.FromHours(24), NullLogger.Instance);
Assert.False(File.Exists(stale));
Assert.True(File.Exists(fresh));
}
[Fact]
public void Sweep_SurvivesMissingDirectory()
{
// Каталога может не быть на самом первом старте — это не повод падать.
UploadsCleaner.Sweep(Paths(), TimeSpan.FromHours(24), NullLogger.Instance);
}
///
/// Кладёт файл и «состаривает» его. Отметку перепроверяем: Windows дописывает время последней
/// записи при закрытии handle и может затереть выставленное сразу после записи.
///
private static void WriteAged(string path, TimeSpan age)
{
File.WriteAllText(path, "x");
if (age == TimeSpan.Zero)
return;
var stamp = DateTime.UtcNow - age;
for (var attempt = 0; attempt < 20; attempt++)
{
File.SetLastWriteTimeUtc(path, stamp);
if ((File.GetLastWriteTimeUtc(path) - stamp).Duration() < TimeSpan.FromSeconds(1))
return;
Thread.Sleep(20);
}
Assert.Fail("Не удалось выставить файлу время последней записи.");
}
public void Dispose()
{
GC.SuppressFinalize(this);
if (Directory.Exists(_root))
Directory.Delete(_root, recursive: true);
}
/// Поток, обрывающийся на середине, — имитация отвалившегося клиента.
private sealed class FailingStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count) =>
throw new IOException("Соединение разорвано.");
public override ValueTask ReadAsync(
Memory buffer,
CancellationToken cancellationToken = default
) => throw new IOException("Соединение разорвано.");
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
}
}