191 lines
7.6 KiB
C#
191 lines
7.6 KiB
C#
using Duempelkas.App.Services;
|
|
using Duempelkas.Domain.Entities;
|
|
using Duempelkas.Domain.Enums;
|
|
using Duempelkas.Infrastructure.Persistence;
|
|
using Duempelkas.Infrastructure.Services;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
|
|
namespace Duempelkas.Tests;
|
|
|
|
public class DashboardPdfStatementServiceTests : IDisposable
|
|
{
|
|
private readonly FinanceDbContext _db;
|
|
private readonly PdfStatementService _pdfService;
|
|
private readonly string _connectionString = $"Data Source=duempelkas-dashboard-pdf-tests-{Guid.NewGuid():N};Mode=Memory;Cache=Shared";
|
|
|
|
public DashboardPdfStatementServiceTests()
|
|
{
|
|
var options = new DbContextOptionsBuilder<FinanceDbContext>()
|
|
.UseSqlite(_connectionString)
|
|
.Options;
|
|
|
|
_db = new FinanceDbContext(options);
|
|
_db.Database.OpenConnection();
|
|
_db.Database.EnsureCreated();
|
|
|
|
var dbFactory = new TestDbContextFactory(options);
|
|
var entryService = new EntryService(dbFactory);
|
|
var balanceQueryService = new BalanceQueryService(dbFactory);
|
|
var settingsService = new FixedSettingsService("Testverein");
|
|
|
|
_pdfService = new PdfStatementService(dbFactory, entryService, balanceQueryService, settingsService);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GenerateDashboardStatementAsync_WithBookingsAndTransfer_ReturnsPdf()
|
|
{
|
|
var barkasse = new Account { Name = "Barkasse" };
|
|
var girokonto = new Account { Name = "Girokonto" };
|
|
_db.Accounts.AddRange(barkasse, girokonto);
|
|
await _db.SaveChangesAsync();
|
|
|
|
_db.Entries.Add(new Entry
|
|
{
|
|
AccountId = barkasse.Id,
|
|
DisplayId = "2026-001",
|
|
Type = EntryType.Income,
|
|
Date = new DateTime(2026, 1, 12),
|
|
Title = "Einkuenfte Sommerfest",
|
|
Amount = 604.60m
|
|
});
|
|
|
|
var transferExpense = new Entry
|
|
{
|
|
AccountId = barkasse.Id,
|
|
DisplayId = "2026-002",
|
|
Type = EntryType.Expense,
|
|
Date = new DateTime(2026, 2, 16),
|
|
Title = "Einzahlung",
|
|
Amount = 600.00m
|
|
};
|
|
|
|
var transferIncome = new Entry
|
|
{
|
|
AccountId = girokonto.Id,
|
|
DisplayId = "2026-002",
|
|
Type = EntryType.Income,
|
|
Date = new DateTime(2026, 2, 16),
|
|
Title = "Einzahlung",
|
|
Amount = 600.00m
|
|
};
|
|
|
|
_db.Entries.AddRange(transferExpense, transferIncome);
|
|
await _db.SaveChangesAsync();
|
|
|
|
var transferLink = new TransferLink
|
|
{
|
|
SourceEntryId = transferExpense.Id,
|
|
TargetEntryId = transferIncome.Id,
|
|
Note = "Umbuchung"
|
|
};
|
|
|
|
_db.TransferLinks.Add(transferLink);
|
|
await _db.SaveChangesAsync();
|
|
|
|
transferExpense.TransferLinkId = transferLink.Id;
|
|
transferIncome.TransferLinkId = transferLink.Id;
|
|
await _db.SaveChangesAsync();
|
|
|
|
var pdf = await _pdfService.GenerateDashboardStatementAsync();
|
|
|
|
pdf.Should().NotBeNull();
|
|
pdf.Length.Should().BeGreaterThan(1000);
|
|
System.Text.Encoding.ASCII.GetString(pdf.Take(4).ToArray()).Should().Be("%PDF");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Exports_ExcludeDeleted_ExplainTransfers_AndMatchBalances()
|
|
{
|
|
var source = new Account { Name = "Barkasse", CarryoverBalance = 100m };
|
|
var target = new Account { Name = "Girokonto" };
|
|
_db.Accounts.AddRange(source, target);
|
|
await _db.SaveChangesAsync();
|
|
var options = new DbContextOptionsBuilder<FinanceDbContext>().UseSqlite(_connectionString).Options;
|
|
var factory = new TestDbContextFactory(options);
|
|
var entries = new EntryService(factory);
|
|
await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2026, 1, 1), "Mitgliedsbeitrag", 12.50m);
|
|
await entries.CreateEntryAsync(source.Id, EntryType.Expense, new(2026, 1, 2), "Porto", 4.20m);
|
|
await entries.CreateTransferAsync(source.Id, target.Id, new(2026, 1, 3), "Zur Bank", 50m);
|
|
var deleted = await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2026, 1, 4), "GELOESCHTE_TESTBUCHUNG", 999m);
|
|
await entries.DeleteEntryAsync(deleted.Id);
|
|
await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2027, 1, 1), "ZUKUNFT_TESTBUCHUNG", 1m);
|
|
var balance = await new BalanceQueryService(factory).GetAccountBalanceAsync(source.Id, 2026);
|
|
Assert.Equal(58.30m, balance.TotalBalance);
|
|
var single = await _pdfService.GenerateStatementAsync(source.Id, 2026);
|
|
var combined = await _pdfService.GenerateDashboardStatementAsync(2026);
|
|
foreach (var pdf in new[] { single, combined })
|
|
{
|
|
var text = Extract(pdf);
|
|
Assert.Contains("Mitgliedsbeitrag", text);
|
|
Assert.Contains("Porto", text);
|
|
Assert.Contains("Zur Bank", text);
|
|
Assert.DoesNotContain("GELOESCHTE_TESTBUCHUNG", text);
|
|
Assert.DoesNotContain("ZUKUNFT_TESTBUCHUNG", text);
|
|
Assert.Contains("58,30", text);
|
|
Assert.Contains("31.12.2026", text);
|
|
}
|
|
Assert.Contains("108,30", Extract(combined));
|
|
Assert.Contains("an Girokonto", Extract(single));
|
|
var all = Extract(await _pdfService.GenerateStatementAsync(source.Id, (int?)null));
|
|
Assert.Contains("Alle Jahre", all);
|
|
Assert.Contains("Anfangsbestand", all);
|
|
Assert.Contains("59,30", all);
|
|
var evidence = Environment.GetEnvironmentVariable("DUEMPELKAS_PDF_EVIDENCE");
|
|
if (evidence != null)
|
|
{
|
|
Directory.CreateDirectory(evidence);
|
|
await File.WriteAllBytesAsync(Path.Combine(evidence, "single.pdf"), single);
|
|
await File.WriteAllBytesAsync(Path.Combine(evidence, "combined.pdf"), combined);
|
|
// Additional pages exercise repeated headers and the final summary.
|
|
for (var i = 0; i < 90; i++)
|
|
await entries.CreateEntryAsync(source.Id, EntryType.Income, new(2026, 2, 1), $"Lesbarkeitsprüfung Buchung {i + 1}", 1m);
|
|
await File.WriteAllBytesAsync(Path.Combine(evidence, "multipage.pdf"), await _pdfService.GenerateStatementAsync(source.Id, 2026));
|
|
}
|
|
}
|
|
|
|
private static string Extract(byte[] pdf)
|
|
{
|
|
using var document = UglyToad.PdfPig.PdfDocument.Open(pdf);
|
|
return string.Join("\n", document.GetPages().Select(page => UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor.ContentOrderTextExtractor.GetText(page)));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_db.Database.CloseConnection();
|
|
_db.Dispose();
|
|
}
|
|
|
|
private sealed class TestDbContextFactory : IDbContextFactory<FinanceDbContext>
|
|
{
|
|
private readonly DbContextOptions<FinanceDbContext> _options;
|
|
|
|
public TestDbContextFactory(DbContextOptions<FinanceDbContext> options)
|
|
{
|
|
_options = options;
|
|
}
|
|
|
|
public FinanceDbContext CreateDbContext() => new(_options);
|
|
|
|
public Task<FinanceDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
|
=> Task.FromResult(new FinanceDbContext(_options));
|
|
}
|
|
|
|
private sealed class FixedSettingsService : ISettingsService
|
|
{
|
|
public Task<bool> GetShowDeletedEntriesAsync() => Task.FromResult(false);
|
|
public Task SetShowDeletedEntriesAsync(bool show) => Task.CompletedTask;
|
|
private readonly string? _clubName;
|
|
|
|
public FixedSettingsService(string? clubName)
|
|
{
|
|
_clubName = clubName;
|
|
}
|
|
|
|
public Task<string?> GetClubNameAsync() => Task.FromResult(_clubName);
|
|
|
|
public Task SetClubNameAsync(string? clubName) => Task.CompletedTask;
|
|
}
|
|
}
|