From ee9bc84a986c1cb996891838069bbdf5a5481994 Mon Sep 17 00:00:00 2001 From: TheIrvin Date: Sat, 3 Oct 2026 09:56:41 -0500 Subject: [PATCH] test(conversion): cover service flow and history --- .../ConversionServiceTests.cs | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/MarkItDownWeb.Tests/ConversionServiceTests.cs diff --git a/tests/MarkItDownWeb.Tests/ConversionServiceTests.cs b/tests/MarkItDownWeb.Tests/ConversionServiceTests.cs new file mode 100644 index 0000000..bfaa3d6 --- /dev/null +++ b/tests/MarkItDownWeb.Tests/ConversionServiceTests.cs @@ -0,0 +1,156 @@ +using MarkItDownWeb.Application.Interfaces; +using MarkItDownWeb.Application.Services; +using MarkItDownWeb.Domain.Entities; +using MarkItDownWeb.Infrastructure.Persistence; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace MarkItDownWeb.Tests; + +public class ConversionServiceTests +{ + [Fact] + public async Task ConvertAsync_saves_markdown_and_exposes_it_in_history() + { + await using var database = await TestDatabase.CreateAsync(); + var storage = new FakeFileStorageService(); + var converter = new FakeMarkItDownService(_ => Task.FromResult("# Monthly report")); + var service = new ConversionService(converter, storage, database.Repository); + + await using var input = new MemoryStream([0x61, 0x2C, 0x62]); + var result = await service.ConvertAsync(input, "report.csv", input.Length); + + Assert.True(result.Success); + Assert.Equal("# Monthly report", result.Markdown); + Assert.Equal(1, converter.CallCount); + Assert.Empty(storage.TemporaryFiles); + + var history = await service.GetHistoryAsync("report", null); + var historyItem = Assert.Single(history); + Assert.Equal(result.ConversionId, historyItem.Id); + Assert.Equal("Completed", historyItem.Status); + + var reopened = await service.GetHistoryItemAsync(result.ConversionId); + Assert.Equal("# Monthly report", reopened.Markdown); + } + + [Theory] + [InlineData("report.exe", 10L)] + [InlineData("report.csv", 104_857_601L)] + public async Task ConvertAsync_rejects_unsupported_or_oversized_files_before_saving( + string fileName, + long fileSize) + { + await using var database = await TestDatabase.CreateAsync(); + var storage = new FakeFileStorageService(); + var converter = new FakeMarkItDownService(_ => Task.FromResult("unused")); + var service = new ConversionService(converter, storage, database.Repository); + + await using var input = new MemoryStream([0x61]); + + await Assert.ThrowsAsync( + () => service.ConvertAsync(input, fileName, fileSize)); + + Assert.Equal(0, converter.CallCount); + Assert.Empty(storage.TemporaryFiles); + Assert.Empty(await service.GetHistoryAsync(null, null)); + } + + [Fact] + public async Task ConvertAsync_records_failed_conversion_and_removes_temporary_file() + { + await using var database = await TestDatabase.CreateAsync(); + var storage = new FakeFileStorageService(); + var converter = new FakeMarkItDownService( + _ => Task.FromException(new InvalidOperationException("conversion failed"))); + var service = new ConversionService(converter, storage, database.Repository); + + await using var input = new MemoryStream([0x61]); + var result = await service.ConvertAsync(input, "report.html", input.Length); + + Assert.False(result.Success); + Assert.Equal("The file could not be converted.", result.ErrorMessage); + Assert.Empty(storage.TemporaryFiles); + + var history = await service.GetHistoryAsync(null, null); + var historyItem = Assert.Single(history); + Assert.Equal("Failed", historyItem.Status); + } + + private sealed class FakeMarkItDownService(Func> convert) : IMarkItDownService + { + public int CallCount { get; private set; } + + public Task ConvertFileAsync(string absoluteFilePath, CancellationToken ct = default) + { + CallCount++; + return convert(absoluteFilePath); + } + } + + private sealed class FakeFileStorageService : IFileStorageService + { + private readonly Dictionary _markdownFiles = new(); + + public HashSet TemporaryFiles { get; } = new(); + + public async Task SaveTemporaryAsync( + Stream fileStream, + string fileName, + CancellationToken ct = default) + { + await fileStream.CopyToAsync(Stream.Null, ct); + var path = Path.Combine("Uploads", $"{Guid.NewGuid()}{Path.GetExtension(fileName)}"); + TemporaryFiles.Add(path); + return path; + } + + public Task SaveMarkdownAsync(Guid conversionId, string markdown, CancellationToken ct = default) + { + var path = Path.Combine("Markdown", $"{conversionId}.md"); + _markdownFiles[path] = markdown; + return Task.FromResult(path); + } + + public Task ReadMarkdownAsync(string markdownFilePath, CancellationToken ct = default) + => Task.FromResult(_markdownFiles[markdownFilePath]); + + public void DeleteTemporary(string filePath) => TemporaryFiles.Remove(filePath); + } + + private sealed class TestDatabase : IAsyncDisposable + { + private readonly SqliteConnection _connection; + private readonly AppDbContext _context; + + private TestDatabase(SqliteConnection connection, AppDbContext context) + { + _connection = connection; + _context = context; + Repository = new ConversionRepository(context); + } + + public IConversionRepository Repository { get; } + + public static async Task CreateAsync() + { + var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + var context = new AppDbContext(options); + await context.Database.EnsureCreatedAsync(); + + return new TestDatabase(connection, context); + } + + public async ValueTask DisposeAsync() + { + await _context.DisposeAsync(); + await _connection.DisposeAsync(); + } + } +}