using System.Threading; using System.Threading.Tasks; using FrameworkBLL.Promotion.Adapters; using FrameworkBLL.ReportViewer; using FrameworkDAL.CustomCode.Promotion; using FrameworkDAL.DTO.ReportViewer; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Promotion; using Moq; using Newtonsoft.Json; using Xunit; namespace FrameworkTests; /// /// ReportViewPromotionAdapter's real job is resolving cross-environment references via their /// natural keys (ReportId -> ReportCode, and — now that ReportHeaderPromotionAdapter exists — /// ReportHeaderId -> ReportHeaderCode too) and conservatively blocking the ones with no /// remapping support yet (Template/Page/Menu/ClientIds). Also covers that the "risky" raw fields /// genuinely reach ValidateForPromotionAsync (they're deliberately still present in the bundle /// payload precisely so this validator can see them, even though ImportPromotedAsync never maps /// them onto the destination row). /// public class ReportViewPromotionAdapterTests { private static LoginDTO Login() => new() { ClientId = -1, UserId = 7 }; private static ReportViewDTO CleanDto(int reportViewId = 42, int reportId = 100) => new() { ReportViewId = reportViewId, ReportId = reportId, ReportViewName = "Sales Summary", ReportViewType = 0, ClientIds = ",-1", TemplateId = -1, SecondTemplateId = -1, PageId = -1, ReportHeaderId = -1, ApplicableMenuIds = null, OrderByFields = string.Empty, DynamicFooter = string.Empty, ReportTitle = "NONE", ReportViewPivotFormat = string.Empty }; private static (Mock bll, Mock reportLookup, Mock headerLookup) Mocks() => (new Mock(), new Mock(), new Mock()); [Fact] public async Task Export_ResolvesReportCode_AndBundlesPortableFields() { var (bll, reportLookup, headerLookup) = Mocks(); bll.Setup(b => b.GetReportView(42, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(CleanDto())); reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny())) .ReturnsAsync("SALES_RPT"); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); var bundle = await adapter.ExportForPromotionAsync(42, Login(), CancellationToken.None); Assert.Equal("REPORTVIEW", bundle.EntityTypeCode); Assert.Contains("SALES_RPT", bundle.PayloadJson); Assert.Contains("Sales Summary", bundle.PayloadJson); } [Fact] public async Task Export_ReportHasNoCode_Throws() { var (bll, reportLookup, headerLookup) = Mocks(); bll.Setup(b => b.GetReportView(42, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(CleanDto())); reportLookup.Setup(l => l.GetReportCodeById(100, It.IsAny(), It.IsAny())) .ReturnsAsync((string?)null); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); await Assert.ThrowsAsync( () => adapter.ExportForPromotionAsync(42, Login(), CancellationToken.None)); } [Fact] public async Task Export_ReportHeaderSetButNoCode_Throws() { var dto = CleanDto(); dto.ReportHeaderId = 200; var (bll, reportLookup, headerLookup) = Mocks(); bll.Setup(b => b.GetReportView(dto.ReportViewId, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(dto)); reportLookup.Setup(l => l.GetReportCodeById(dto.ReportId, It.IsAny(), It.IsAny())) .ReturnsAsync("SALES_RPT"); headerLookup.Setup(l => l.GetReportHeaderCodeById(200, It.IsAny(), It.IsAny())) .ReturnsAsync((string?)null); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); await Assert.ThrowsAsync( () => adapter.ExportForPromotionAsync(dto.ReportViewId, Login(), CancellationToken.None)); } private static async Task BuildBundle( ReportViewDTO dto, string reportCode = "SALES_RPT", string? reportHeaderCode = null) { var (bll, reportLookup, headerLookup) = Mocks(); bll.Setup(b => b.GetReportView(dto.ReportViewId, It.IsAny(), It.IsAny())) .ReturnsAsync(JsonConvert.SerializeObject(dto)); reportLookup.Setup(l => l.GetReportCodeById(dto.ReportId, It.IsAny(), It.IsAny())) .ReturnsAsync(reportCode); if (dto.ReportHeaderId != 0 && dto.ReportHeaderId != -1) headerLookup.Setup(l => l.GetReportHeaderCodeById(dto.ReportHeaderId, It.IsAny(), It.IsAny())) .ReturnsAsync(reportHeaderCode); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); return await adapter.ExportForPromotionAsync(dto.ReportViewId, Login(), CancellationToken.None); } [Fact] public async Task Validate_CleanReportView_IsOk() { var bundle = await BuildBundle(CleanDto()); var (bll, reportLookup, headerLookup) = Mocks(); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None); Assert.True(result.IsPromotable); } [Fact] public async Task Validate_ReportHeaderSet_NoLongerBlocks() { var dto = CleanDto(); dto.ReportHeaderId = 200; var bundle = await BuildBundle(dto, reportHeaderCode: "STD_HEADER"); var (bll, reportLookup, headerLookup) = Mocks(); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None); Assert.True(result.IsPromotable); } [Theory] [InlineData("clientids")] [InlineData("templateid")] [InlineData("secondtemplateid")] [InlineData("pageid")] [InlineData("applicablemenuids")] public async Task Validate_RiskyFieldSet_Blocks(string whichField) { var dto = CleanDto(); switch (whichField) { case "clientids": dto.ClientIds = ",7,9"; break; case "templateid": dto.TemplateId = 55; break; case "secondtemplateid": dto.SecondTemplateId = 56; break; case "pageid": dto.PageId = 57; break; case "applicablemenuids": dto.ApplicableMenuIds = ",1,2,"; break; } var bundle = await BuildBundle(dto); var (bll, reportLookup, headerLookup) = Mocks(); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); var result = await adapter.ValidateForPromotionAsync(bundle, CancellationToken.None); Assert.False(result.IsPromotable); Assert.NotEmpty(result.BlockingReasons); } [Fact] public async Task Import_NewRow_ResolvesReportCodeAndSaves() { var bundle = await BuildBundle(CleanDto(), "SALES_RPT"); var (bll, reportLookup, headerLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny())) .ReturnsAsync(777); // Capture the state AS PASSED IN (before this same callback mutates it to simulate // SaveReportView assigning a new ID) — Moq's Verify re-inspects the live object later, so // asserting ReportViewId==0 there would actually observe the post-mutation value instead. int? reportIdAsPassedIn = null; byte? sourceTypeAsPassedIn = null; int? reportViewIdAsPassedIn = null; bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((d, _, _) => { reportIdAsPassedIn = d.ReportId; sourceTypeAsPassedIn = d.ReportViewSourceType; reportViewIdAsPassedIn = d.ReportViewId; d.ReportViewId = 901; // simulates SaveReportView assigning the new AutoNumber id }) .ReturnsAsync("Saved 901"); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); var newId = await adapter.ImportPromotedAsync(bundle, destinationSourceType: 3, existingDestinationId: null, Login(), CancellationToken.None); Assert.Equal(901, newId); Assert.Equal(777, reportIdAsPassedIn); Assert.Equal((byte)3, sourceTypeAsPassedIn); Assert.Equal(0, reportViewIdAsPassedIn); bll.Verify(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } // Regression for a real bug found live 2026-08-21 (first-ever real Promotion Import execution): // MREPORTVIEW.TEMPLATEID/PAGEID are NOT NULL with FKs to MFILE, which has a real dummy row at // FILEID=-1 but none at FILEID=0 — leaving the destination DTO at its own bare-0 default (as // it was before this fix) threw a genuine FK violation on every single import. [Fact] public async Task Import_StampsTemplateIdAndPageIdToTheirRealSentinel_NotTheDtoDefaultZero() { var dto = CleanDto(); var bundle = await BuildBundle(dto, "SALES_RPT", null); var (bll, reportLookup, headerLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny())) .ReturnsAsync(777); int? capturedTemplateId = null, capturedPageId = null; bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((d, _, _) => { capturedTemplateId = d.TemplateId; capturedPageId = d.PageId; }) .ReturnsAsync("Saved"); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); await adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None); Assert.Equal(-1, capturedTemplateId); Assert.Equal(-1, capturedPageId); } [Fact] public async Task Import_ReportHeaderCodeSet_ResolvesToLocalReportHeaderId() { var dto = CleanDto(); dto.ReportHeaderId = 200; var bundle = await BuildBundle(dto, "SALES_RPT", "STD_HEADER"); var (bll, reportLookup, headerLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny())) .ReturnsAsync(777); headerLookup.Setup(l => l.GetReportHeaderIdByCode("STD_HEADER", It.IsAny(), It.IsAny())) .ReturnsAsync(555); int? capturedReportHeaderId = null; bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((d, _, _) => capturedReportHeaderId = d.ReportHeaderId) .ReturnsAsync("Saved"); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); await adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None); Assert.Equal(555, capturedReportHeaderId); } [Fact] public async Task Import_NoReportHeaderCode_LeavesReportHeaderIdAtSentinel() { var bundle = await BuildBundle(CleanDto()); // ReportHeaderId == -1, no code in payload var (bll, reportLookup, headerLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny())) .ReturnsAsync(777); int? capturedReportHeaderId = null; bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((d, _, _) => capturedReportHeaderId = d.ReportHeaderId) .ReturnsAsync("Saved"); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); await adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None); Assert.Equal(-1, capturedReportHeaderId); headerLookup.Verify(l => l.GetReportHeaderIdByCode( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Import_UnknownReportCode_ThrowsBeforeSaving() { var bundle = await BuildBundle(CleanDto(), "MISSING_RPT"); var (bll, reportLookup, headerLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("MISSING_RPT", It.IsAny(), It.IsAny())) .ReturnsAsync((int?)null); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); await Assert.ThrowsAsync( () => adapter.ImportPromotedAsync(bundle, 3, null, Login(), CancellationToken.None)); bll.Verify(b => b.SaveReportView( It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Import_ExistingDestinationId_UpdatesRatherThanInsertsNew() { var bundle = await BuildBundle(CleanDto(), "SALES_RPT"); var (bll, reportLookup, headerLookup) = Mocks(); reportLookup.Setup(l => l.GetReportIdByCode("SALES_RPT", It.IsAny(), It.IsAny())) .ReturnsAsync(777); bll.Setup(b => b.SaveReportView(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("Updated"); var adapter = new ReportViewPromotionAdapter(bll.Object, reportLookup.Object, headerLookup.Object); var resultId = await adapter.ImportPromotedAsync(bundle, 3, existingDestinationId: 501, Login(), CancellationToken.None); Assert.Equal(501, resultId); bll.Verify(b => b.SaveReportView( It.Is(d => d.ReportViewId == 501), It.IsAny(), It.IsAny()), Times.Once); } }