using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using TrafagSalesExporter.Data; using TrafagSalesExporter.Models; using TrafagSalesExporter.Services; using TrafagSalesExporter.Services.DataSources; namespace TrafagSalesExporter.Tests; public sealed class SiteExportServiceTests : IDisposable { private readonly string _tempDirectory; public SiteExportServiceTests() { _tempDirectory = Path.Combine("C:\\TMP", $"trafag-site-export-tests-{Guid.NewGuid():N}"); Directory.CreateDirectory(_tempDirectory); } public void Dispose() { if (Directory.Exists(_tempDirectory)) Directory.Delete(_tempDirectory, recursive: true); } [Fact] public async Task ExportAsync_Uploads_AuditCsv_To_Same_SharePoint_Target_As_Excel() { await using var connection = new SqliteConnection("DataSource=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using (var db = new AppDbContext(options)) { await db.Database.EnsureCreatedAsync(); db.ExportSettings.Add(new ExportSettings { AuditCsvEnabled = true, LocalSiteExportFolder = _tempDirectory }); db.SharePointConfigs.Add(new SharePointConfig { TenantId = "tenant", ClientId = "client", ClientSecret = "secret", SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform", ExportFolder = "Import/Finance" }); db.SourceSystemDefinitions.Add(new SourceSystemDefinition { Code = "MANUAL_EXCEL", DisplayName = "Manual Excel", ConnectionKind = SourceSystemConnectionKinds.ManualExcel, IsActive = true }); await db.SaveChangesAsync(); } var sharePoint = new RecordingSharePointUploadService(); var service = new SiteExportService( new TestDbContextFactory(options), new FixedDataSourceAdapterResolver(new FixedDataSourceAdapter(new DataSourceFetchResult { Records = [ new SalesRecord { SourceSystem = "MANUAL_EXCEL", ExtractionDate = new DateTime(2026, 6, 11, 8, 0, 0, DateTimeKind.Utc), Tsc = "TRSE", Land = "Spanien", InvoiceNumber = "ES-1", SalesPriceValue = 100m, SalesCurrency = "EUR", InvoiceDate = new DateTime(2026, 6, 10), DocumentType = "Invoice" } ], SharePointUploadFolderOverride = "Import/Finance/Spanien", SharePointUploadLandOverride = string.Empty })), new FileWritingExcelExportService(), sharePoint, new NoopRecordTransformationService(), new NoopCentralSalesRecordService(), new ExportAuditCsvService(), new NoopAppEventLogService(), NullLogger.Instance); var result = await service.ExportAsync(new Site { Id = 7, TSC = "TRSE", Land = "Spanien", SourceSystem = "MANUAL_EXCEL", IsActive = true }); Assert.NotNull(result.FilePath); Assert.True(File.Exists(result.FilePath)); var auditCsv = Directory.GetFiles(_tempDirectory, "Sales_TRSE_*.csv").Single(); Assert.True(File.Exists(auditCsv)); Assert.Equal(2, sharePoint.Uploads.Count); Assert.EndsWith(".xlsx", sharePoint.Uploads[0].FileName, StringComparison.OrdinalIgnoreCase); Assert.EndsWith(".csv", sharePoint.Uploads[1].FileName, StringComparison.OrdinalIgnoreCase); Assert.All(sharePoint.Uploads, upload => { Assert.Equal("Import/Finance/Spanien", upload.ExportFolder); Assert.Equal(string.Empty, upload.Land); }); } private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions _options; public TestDbContextFactory(DbContextOptions options) { _options = options; } public AppDbContext CreateDbContext() => new(_options); public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => Task.FromResult(new AppDbContext(_options)); } private sealed class FixedDataSourceAdapterResolver : IDataSourceAdapterResolver { private readonly IDataSourceAdapter _adapter; public FixedDataSourceAdapterResolver(IDataSourceAdapter adapter) { _adapter = adapter; } public IDataSourceAdapter Resolve(string connectionKind) => _adapter; } private sealed class FixedDataSourceAdapter : IDataSourceAdapter { private readonly DataSourceFetchResult _result; public FixedDataSourceAdapter(DataSourceFetchResult result) { _result = result; } public string ConnectionKind => SourceSystemConnectionKinds.ManualExcel; public Task FetchAsync(DataSourceFetchContext context) => Task.FromResult(_result); } private sealed class FileWritingExcelExportService : IExcelExportService { public string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List records) { Directory.CreateDirectory(outputDirectory); var path = Path.Combine(outputDirectory, $"Sales_{tsc}_{fileDate:yyyy-MM-dd}.xlsx"); File.WriteAllText(path, "excel"); return path; } public string CreateConsolidatedExcelFile(string outputDirectory, DateTime fileDate, List records) => throw new NotSupportedException(); public string CreateGenericExcelFile(string outputDirectory, string filePrefix, DateTime fileDate, string worksheetName, IReadOnlyList> rows) => throw new NotSupportedException(); } private sealed class RecordingSharePointUploadService : ISharePointUploadService { public List Uploads { get; } = []; public Task UploadAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder, string land, string localFilePath, bool uploadTimestampedCopyIfLocked = false) { Uploads.Add(new UploadCall(exportFolder, land, Path.GetFileName(localFilePath))); return Task.CompletedTask; } public Task DownloadToTempFileAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string fileReference) => throw new NotSupportedException(); public Task ResolveLatestFileInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null) => throw new NotSupportedException(); public Task> ResolveManualImportFilesInFolderAsync(string tenantId, string clientId, string clientSecret, string siteUrl, string folderReference, string siteTsc, int? preferredYear = null) => throw new NotSupportedException(); public Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl) => Task.CompletedTask; } private sealed record UploadCall(string ExportFolder, string Land, string FileName); private sealed class NoopRecordTransformationService : IRecordTransformationService { public void Apply(List records, IEnumerable rules) { } } private sealed class NoopCentralSalesRecordService : ICentralSalesRecordService { public Task ReplaceForSiteAsync(Site site, IEnumerable records, Action? updateStatus = null) => Task.CompletedTask; public Task> GetAllAsync() => Task.FromResult(new List()); } private sealed class NoopAppEventLogService : IAppEventLogService { public Task WriteAsync(string category, string message, string level = "Info", int? siteId = null, string? land = null, string? details = null) => Task.CompletedTask; public Task WriteDebugAsync(string category, string message, int? siteId = null, string? land = null, string? details = null) => Task.CompletedTask; } }