using AccountTracking.Api.Data; using AccountTracking.Api.Services; using Microsoft.EntityFrameworkCore; namespace AccountTracking.Api.Tests; public class CsvImportServiceTests { private static AppDbContext CreateDb() { var opts = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new AppDbContext(opts); } private static CsvImportService CreateService(AppDbContext db) => new(db); // Minimal valid CSV with two data rows private const string ValidCsv = """ Pohyby na účtu 216868554/0300 dne 19.03.2026 číslo účtu;datum zaúčtování;částka;měna;zůstatek;číslo protiúčtu;kód banky protiúčtu;jméno protistrany;adresa protistrany;konstantní symbol;variabilní symbol;specifický symbol;označení operace;název trálého příkazu;vlastní poznámka;zpráva;kategorie;původní transakce;kurz;E2E identifikace;reference plátce;původní plátce;konečný příjemce;ID transakce 216868554/0300;15.03.2026;-1 740,80;CZK;93 346,57;;;;;;205000001;;Transakce platební kartou;;;Zpráva 1;Potraviny;;;;;;;tx-001 216868554/0300;14.03.2026;-800,00;CZK;95 087,37;;;;;;205000002;;Transakce platební kartou;;;Zpráva 2;Restaurace;;;;;;;tx-002 """; private const string CsvMissingHeader = """ Pohyby na účtu Nějaky obsah bez správné hlavičky datum;částka """; private const string CsvMissingRequiredColumn = """ číslo účtu;datum zaúčtování;měna;zůstatek;ID transakce 216868554/0300;15.03.2026;CZK;93346,57;tx-001 """; [Fact] public async Task Import_ParsesRowsCorrectly() { using var db = CreateDb(); var svc = CreateService(db); var result = await svc.ImportAsync(ValidCsv, "test.csv"); Assert.True(result.IsSuccess); Assert.Equal(2, result.Value!.RecordsImported); Assert.Equal(0, result.Value.RecordsSkipped); var transactions = await db.Transactions.ToListAsync(); Assert.Equal(2, transactions.Count); var tx = transactions.First(t => t.TransactionId == "tx-001"); Assert.Equal(-1740.80m, tx.Amount); Assert.Equal(93346.57m, tx.Balance); Assert.Equal("Potraviny", tx.Category); Assert.Equal(new DateOnly(2026, 3, 15), tx.BookingDate); } [Fact] public async Task Import_SkipsDuplicateTransactionIds() { using var db = CreateDb(); var svc = CreateService(db); await svc.ImportAsync(ValidCsv, "first.csv"); var result = await svc.ImportAsync(ValidCsv, "second.csv"); Assert.True(result.IsSuccess); Assert.Equal(0, result.Value!.RecordsImported); Assert.Equal(2, result.Value.RecordsSkipped); Assert.Equal(2, await db.Transactions.CountAsync()); } [Fact] public async Task Import_FindsHeaderByScanning_NotByLineNumber() { // Header is on line 3 in ValidCsv — but we scan, not hardcode using var db = CreateDb(); var svc = CreateService(db); var result = await svc.ImportAsync(ValidCsv, "test.csv"); Assert.True(result.IsSuccess); } [Fact] public async Task Import_ReturnsError_WhenHeaderNotFound() { using var db = CreateDb(); var svc = CreateService(db); var result = await svc.ImportAsync(CsvMissingHeader, "bad.csv"); Assert.False(result.IsSuccess); Assert.Contains("hlavičku", result.Error, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Import_ReturnsError_WhenRequiredColumnMissing() { using var db = CreateDb(); var svc = CreateService(db); var result = await svc.ImportAsync(CsvMissingRequiredColumn, "bad.csv"); Assert.False(result.IsSuccess); Assert.Contains("částka", result.Error, StringComparison.OrdinalIgnoreCase); } [Theory] [InlineData("1 740,80", 1740.80)] [InlineData("-4 263,15", -4263.15)] [InlineData("-800,00", -800.00)] [InlineData("93 346,57", 93346.57)] public void ParseAmount_HandlesChechLocaleFormat(string input, decimal expected) { var result = CsvImportService.ParseCzechDecimal(input); Assert.Equal(expected, result); } }