Files
SchotzIAD/backend/SchotzIadKmTracker.Tests/PdfExport/PdfServiceIntegrationTests.cs
T
MadJax 0e14a8f806 Exports PDF : total mensuel à fin de mois et mise en avant de la régularisation
Report mensuel :
- « Total parcouru sur l'année » devient « Total annuel (à fin de mois) »
  et affiche le cumul jusqu'au mois de restitution inclus
  (Total_Annuel_Partiel, via ITotauxService)
- la tranche de barème (coefficient, base, TOTAL IK du mois) est
  déterminée sur ce même cumul, cohérent avec le Panneau_Totaux du site
  et stable même si le PDF est édité plus tard

Report annuel :
- ligne « Formule réelle » centrée (comme la régularisation)
- ligne « Régularisation (réel − payé) » agrandie (14 pt, gras) pour la
  mettre en avant comme action finale

Test d'intégration PDF mis à jour (injection d'ITotauxService). Specs
requirements/design-requirements/design synchronisées (Exigence 7).
2026-10-13 07:08:12 +02:00

172 lines
6.5 KiB
C#

using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using SchotzIadKmTracker.Api.Data;
using SchotzIadKmTracker.Api.Models.DTOs;
using SchotzIadKmTracker.Api.Models.Entities;
using SchotzIadKmTracker.Api.Repositories;
using SchotzIadKmTracker.Api.Services;
using Xunit;
namespace SchotzIadKmTracker.Tests.PdfExport;
/// <summary>
/// Tests d'intégration de <see cref="PdfService"/> (vérification manuelle demandée après les
/// changements de nommage de fichier, logo, précision des distances et restructuration des
/// en-têtes mensuel/annuel). Branche les services réels sur une base SQLite en mémoire ;
/// seul <see cref="IBANClient"/> est mocké (jamais appelé ici, aucune adresse de domicile
/// à géocoder dans les scénarios testés).
/// </summary>
public class PdfServiceIntegrationTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly AppDbContext _context;
private readonly PdfService _pdfService;
public PdfServiceIntegrationTests()
{
// Licence QuestPDF (usage communautaire, gratuit) — requise avant toute génération de
// PDF ; configurée normalement au démarrage de l'application (Program.cs), reproduite
// ici car ce test instancie PdfService directement sans démarrer le host.
QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community;
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>().UseSqlite(_connection).Options;
_context = new AppDbContext(options);
_context.Database.EnsureCreated();
var precisionDistanceService = new PrecisionDistanceService(_context);
var baremeService = new BaremeService(_context);
var resumeService = new ResumeService(_context, precisionDistanceService);
var montantRegleService = new MontantRegleService(_context, baremeService, precisionDistanceService);
var remboursementService = new RemboursementService(
_context, baremeService, montantRegleService, precisionDistanceService);
var vehiculeRepository = new VehiculeRepository(_context);
var vehiculeService = new VehiculeService(vehiculeRepository, _context);
var totauxService = new TotauxService(_context, vehiculeService, precisionDistanceService);
var adresseCacheRepository = new AdresseCacheRepository(_context);
var adresseCacheService = new AdresseCacheService(adresseCacheRepository, _context);
var banClientMock = new Mock<IBANClient>();
var profilService = new ProfilService(
_context, banClientMock.Object, adresseCacheService, NullLogger<ProfilService>.Instance);
_pdfService = new PdfService(
resumeService,
profilService,
vehiculeService,
baremeService,
montantRegleService,
remboursementService,
precisionDistanceService,
totauxService,
_context);
}
/// <summary>Prépare un profil, un véhicule, un trajet et une tranche de barème de test.</summary>
private async Task<int> PreparerScenarioAsync(string matricule, string plaque, int precision)
{
_context.Profil.Add(new Profil
{
Id = 1,
Nom = "Dupont",
Prenom = "Jean",
Matricule = matricule,
AdresseDomicile = null
});
var vehicule = new Vehicule
{
Denomination = "Véhicule de test",
Plaque = plaque,
PuissanceFiscale = 5,
DateEffet = new DateOnly(2020, 1, 1)
};
_context.Vehicules.Add(vehicule);
_context.TranchesBareme.Add(new TrancheBareme
{
Annee = 2026,
PuissanceFiscale = 5,
KmMin = 0m,
KmMax = null,
Coefficient = 0.350m,
Base = 0m
});
await _context.SaveChangesAsync();
_context.Trajets.Add(new Trajet
{
Date = new DateOnly(2026, 3, 15),
AdresseDepart = "Départ test",
AdresseArrivee = "Arrivée test",
Distance = 12.3456m,
AllerRetour = false,
Position = 1m,
VehiculeId = vehicule.Id,
PuissanceFiscale = 5
});
await _context.SaveChangesAsync();
await precisionServiceDefinir(precision);
return vehicule.Id;
}
private async Task precisionServiceDefinir(int precision)
{
var service = new PrecisionDistanceService(_context);
await service.DefinirAsync(precision, CancellationToken.None);
}
[Fact]
public async Task GenererRapportMensuelAsync_produit_un_pdf_et_un_nom_de_fichier_conforme()
{
var vehiculeId = await PreparerScenarioAsync("AB1234", "AB-123-CD", precision: 0);
var resultat = await _pdfService.GenererRapportMensuelAsync(2026, 3, vehiculeId, CancellationToken.None);
resultat.Contenu.Should().NotBeEmpty();
// En-tête PDF standard : les octets générés doivent être un vrai document PDF.
System.Text.Encoding.ASCII.GetString(resultat.Contenu, 0, 4).Should().Be("%PDF");
resultat.NomFichier.Should().Be("202603_Trajets_AB1234_AB123CD.pdf");
}
[Fact]
public async Task GenererRapportAnnuelAsync_produit_un_pdf_et_un_nom_de_fichier_conforme()
{
var vehiculeId = await PreparerScenarioAsync("XY9876", "ZZ-999-ZZ", precision: 2);
var resultat = await _pdfService.GenererRapportAnnuelAsync(2026, vehiculeId, CancellationToken.None);
resultat.Contenu.Should().NotBeEmpty();
System.Text.Encoding.ASCII.GetString(resultat.Contenu, 0, 4).Should().Be("%PDF");
resultat.NomFichier.Should().Be("2026_Trajets_XY9876_ZZ999ZZ.pdf");
}
[Fact]
public async Task GenererRapportMensuelAsync_omet_les_segments_matricule_et_plaque_absents()
{
// Matricule vide (chaîne vide) : le segment doit être omis du nom de fichier.
var vehiculeId = await PreparerScenarioAsync(string.Empty, "CD-456-EF", precision: 1);
var resultat = await _pdfService.GenererRapportMensuelAsync(2026, 3, vehiculeId, CancellationToken.None);
resultat.NomFichier.Should().Be("202603_Trajets_CD456EF.pdf");
}
public void Dispose()
{
_context.Dispose();
_connection.Dispose();
}
}