using GB5Shared.DTO.Framework.SchemaIntrospection;
using Moq;
using SwBLL.Provisioning;
using Xunit;
namespace SwTests;
///
/// Covers ReferenceDataScriptGenerator (tracker §36.2) — the generic, registry-driven replacement
/// for hand-authoring a reference/standard-data DdlScript per table. All tests mock
/// ITargetDbExecutor directly, so none touch a real database; GetServerColumnsAsync/
/// ExecuteQueryAsync stand in for live schema introspection and row retrieval.
///
public class ReferenceDataScriptGeneratorTests
{
private static (ReferenceDataScriptGenerator Svc, Mock TargetDbExecutor) BuildService()
{
var targetDbExecutor = new Mock();
return (new ReferenceDataScriptGenerator(targetDbExecutor.Object), targetDbExecutor);
}
private static SchemaTableColumnDTO Col(string name, string dataType, int ordinal, bool isPk = false, string tableName = "MPAYMENTTERM") => new()
{
SchemaName = "dbo", TableName = tableName, ColumnName = name, DataType = dataType,
OrdinalPosition = ordinal, IsPrimaryKey = isPk, IsNullable = "NO",
};
// ── Tenant-scoped table (has TENANTID) ───────────────────────────────────────────────────
[Fact]
public async Task GenerateSeedScriptAsync_TableHasTenantIdColumn_SubstitutesTokenNotSourceValue()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List
{
Col("CURRENCYID", "int", 1, isPk: true, tableName: "MCURRENCY"),
Col("CURRENCYCODE", "nvarchar", 2, tableName: "MCURRENCY"),
Col("TENANTID", "int", 3, tableName: "MCURRENCY"),
};
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
targetDbExecutor.Setup(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MCURRENCY", It.IsAny>(), default))
.ReturnsAsync(new List>
{
new() { ["CURRENCYID"] = -1800000000, ["CURRENCYCODE"] = "INR", ["TENANTID"] = -1800000000 },
});
var sql = await svc.GenerateSeedScriptAsync("src-conn", 0, "MCURRENCY", null, default);
Assert.Contains("{{TENANTID}}", sql);
Assert.DoesNotContain("-1800000000, N'INR', -1800000000", sql); // the source's own literal TenantId must never leak through
Assert.Contains("(-1800000000, N'INR', {{TENANTID}})", sql);
}
// ── Global table (no TENANTID) ───────────────────────────────────────────────────────────
[Fact]
public async Task GenerateSeedScriptAsync_TableHasNoTenantIdColumn_NoSubstitution()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List
{
Col("PAYMENTTERMID", "int", 1, isPk: true),
Col("PAYMENTTERMCODE", "nvarchar", 2),
};
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
targetDbExecutor.Setup(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MPAYMENTTERM", It.IsAny>(), default))
.ReturnsAsync(new List>
{
new() { ["PAYMENTTERMID"] = -1900000000, ["PAYMENTTERMCODE"] = "IMMEDIATE" },
});
var sql = await svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", null, default);
Assert.DoesNotContain("{{TENANTID}}", sql);
Assert.Contains("(-1900000000, N'IMMEDIATE')", sql);
}
// ── Override columns (audit/provenance) always overridden, never copied from source ─────
[Fact]
public async Task GenerateSeedScriptAsync_OverrideColumns_AlwaysUseFixedValuesNotSourceRow()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List
{
Col("PAYMENTTERMID", "int", 1, isPk: true),
Col("VERSION", "smallint", 2),
Col("CREATEDBYID", "int", 3),
Col("CREATEDON", "datetime", 4),
Col("MODIFIEDBYID", "int", 5),
Col("MODIFIEDON", "datetime", 6),
Col("SOURCETYPE", "tinyint", 7),
};
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
// Source row deliberately carries "wrong" historical values (a real user id, a real
// historical timestamp, a real edit-count, SOURCETYPE=3) that must never appear in the
// generated script — confirmed live against GB5DEMO's real MCURRENCY rows, which carry
// genuinely varying VERSION values (2/3/4) reflecting that source database's own edit
// history, meaningless for a freshly-delivered row in a different database.
targetDbExecutor.Setup(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MPAYMENTTERM", It.IsAny>(), default))
.ReturnsAsync(new List>
{
new() { ["PAYMENTTERMID"] = 1, ["VERSION"] = (short)7, ["CREATEDBYID"] = 12345, ["CREATEDON"] = new DateTime(2019, 1, 1),
["MODIFIEDBYID"] = 67890, ["MODIFIEDON"] = new DateTime(2019, 1, 1), ["SOURCETYPE"] = (byte)3 },
});
var sql = await svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", null, default);
Assert.Contains("(1, 0, -1, GETDATE(), -1, GETDATE(), 1)", sql);
Assert.DoesNotContain("12345", sql);
Assert.DoesNotContain("67890", sql);
Assert.DoesNotContain("2019-01-01", sql);
Assert.DoesNotContain(", 7,", sql);
}
// ── Idempotency guard shape ───────────────────────────────────────────────────────────────
[Fact]
public async Task GenerateSeedScriptAsync_EmitsWhereNotExistsGuardOnPrimaryKey()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List { Col("PAYMENTTERMID", "int", 1, isPk: true) };
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
targetDbExecutor.Setup(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MPAYMENTTERM", It.IsAny>(), default))
.ReturnsAsync(new List> { new() { ["PAYMENTTERMID"] = 1 } });
var sql = await svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", null, default);
Assert.Contains("WHERE NOT EXISTS (SELECT 1 FROM MPAYMENTTERM x WHERE x.PAYMENTTERMID = v.PAYMENTTERMID);", sql);
}
// ── Row filter — passed through, validated ───────────────────────────────────────────────
[Fact]
public async Task GenerateSeedScriptAsync_RowFilterSupplied_AppliedToSelect()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List { Col("PAYMENTTERMID", "int", 1, isPk: true) };
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
targetDbExecutor.Setup(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MPAYMENTTERM WHERE SOURCETYPE IN (1,2,3)", It.IsAny>(), default))
.ReturnsAsync(new List> { new() { ["PAYMENTTERMID"] = 1 } });
var sql = await svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", "SOURCETYPE IN (1,2,3)", default);
Assert.Contains("(1)", sql);
targetDbExecutor.Verify(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MPAYMENTTERM WHERE SOURCETYPE IN (1,2,3)", It.IsAny>(), default), Times.Once);
}
[Fact]
public async Task GenerateSeedScriptAsync_RowFilterContainsDisallowedKeyword_ThrowsWithoutQuerying()
{
var (svc, targetDbExecutor) = BuildService();
await Assert.ThrowsAsync(() =>
svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", "1=1; DROP TABLE MPAYMENTTERM", default));
targetDbExecutor.Verify(t => t.GetServerColumnsAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
// ── Guard clauses ─────────────────────────────────────────────────────────────────────────
[Fact]
public async Task GenerateSeedScriptAsync_TableNotFound_Throws()
{
var (svc, targetDbExecutor) = BuildService();
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(new List());
await Assert.ThrowsAsync(() =>
svc.GenerateSeedScriptAsync("src-conn", 0, "MNOTATABLE", null, default));
}
[Fact]
public async Task GenerateSeedScriptAsync_NoRowsMatchFilter_Throws()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List { Col("PAYMENTTERMID", "int", 1, isPk: true) };
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
targetDbExecutor.Setup(t => t.ExecuteQueryAsync("src-conn", "SELECT * FROM MPAYMENTTERM", It.IsAny>(), default))
.ReturnsAsync(new List>());
await Assert.ThrowsAsync(() =>
svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", null, default));
}
[Fact]
public async Task GenerateSeedScriptAsync_NoSingleColumnPrimaryKey_Throws()
{
var (svc, targetDbExecutor) = BuildService();
var columns = new List { Col("PAYMENTTERMCODE", "nvarchar", 1, isPk: false) };
targetDbExecutor.Setup(t => t.GetServerColumnsAsync("src-conn", 0, default)).ReturnsAsync(columns);
await Assert.ThrowsAsync(() =>
svc.GenerateSeedScriptAsync("src-conn", 0, "MPAYMENTTERM", null, default));
}
}