using FrameworkBLL.Engine; using Xunit; namespace FrameworkTests; /// /// Covers SyncQueryBuilder's identifier quoting, watermark-clause assembly, and the /// QUERYCONDITION injection guard (docs/Platform-Architecture-Integration-Reference.md ยง9.6) โ€” /// pure string-building logic, no database connection needed. Internal-only class, reached via /// [InternalsVisibleTo("FrameworkTests")] on FrameworkBLL (see Properties/AssemblyInfo.cs). /// public class SyncQueryBuilderTests { [Theory] [InlineData(DatabaseType.SqlServer, "[Col]")] [InlineData(DatabaseType.PostgreSQL, "\"Col\"")] [InlineData(DatabaseType.MySQL, "`Col`")] public void Quote_PerDialect_UsesCorrectDelimiters(DatabaseType dbType, string expected) { Assert.Equal(expected, SyncQueryBuilder.Quote("Col", dbType)); } [Fact] public void Quote_Oracle_UppercasesIdentifier() { Assert.Equal("\"COL\"", SyncQueryBuilder.Quote("col", DatabaseType.Oracle)); } [Fact] public void QuoteTable_WithSchema_ProducesSchemaDotTable() { Assert.Equal("[dbo].[MyTable]", SyncQueryBuilder.QuoteTable("dbo", "MyTable", DatabaseType.SqlServer)); } [Fact] public void QuoteTable_MySqlWithSchemaSupplied_StillProducesSchemaDotTable() { // QuoteTable itself has no MySQL-specific "no schema" branch โ€” the schema/no-schema // decision lives with the caller (DataSyncRunBLL passes an empty schema for MySQL // targets); QuoteTable just quotes whatever schema string it's given, per dialect. Assert.Equal("`public`.`MyTable`", SyncQueryBuilder.QuoteTable("public", "MyTable", DatabaseType.MySQL)); } [Fact] public void QuoteTable_EmptySchema_ReturnsJustTheTable() { Assert.Equal("[MyTable]", SyncQueryBuilder.QuoteTable("", "MyTable", DatabaseType.SqlServer)); } [Fact] public void BuildSelectSql_NoWatermarks_OmitsWhereClause() { var sql = SyncQueryBuilder.BuildSelectSql( "dbo", "MUOM", "UOMID", null, null, null, null, null, DatabaseType.SqlServer); Assert.DoesNotContain("WHERE", sql); Assert.Contains("ORDER BY [UOMID]", sql); Assert.Contains("OFFSET @ChunkOffset ROWS FETCH NEXT @ChunkSize ROWS ONLY", sql); } [Fact] public void BuildSelectSql_BothWatermarksSet_CombinesWithOrNotAnd() { // A row qualifies if it was EITHER created OR modified since the last run โ€” joining // with AND (the original, buggy behavior) meant a pre-existing row's old CreatedOn and // new ModifiedOn could never both be true at once, so no update to an existing row // could ever match. See SyncQueryBuilder.BuildSelectSql's own comment for the full // real-world bug this characterizes. var sql = SyncQueryBuilder.BuildSelectSql( "dbo", "MUOM", "UOMID", "CREATEDON", "MODIFIEDON", new DateTime(2026, 1, 1), new DateTime(2026, 1, 2), null, DatabaseType.SqlServer); Assert.Contains("[CREATEDON] >= @LastInsertedTill", sql); Assert.Contains("[MODIFIEDON] >= @LastUpdatedTill", sql); Assert.Contains("([CREATEDON] >= @LastInsertedTill OR [MODIFIEDON] >= @LastUpdatedTill)", sql); Assert.DoesNotContain(" AND ", sql); } [Fact] public void BuildSelectSql_OnlyInsertedWatermarkHasValue_OmitsUpdatedClause() { var sql = SyncQueryBuilder.BuildSelectSql( "dbo", "MUOM", "UOMID", "CREATEDON", "MODIFIEDON", new DateTime(2026, 1, 1), null, null, DatabaseType.SqlServer); Assert.Contains("[CREATEDON] >= @LastInsertedTill", sql); Assert.DoesNotContain("@LastUpdatedTill", sql); } [Fact] public void BuildSelectSql_MySql_UsesLimitOffsetSyntax() { var sql = SyncQueryBuilder.BuildSelectSql( "", "muom", "uomid", null, null, null, null, null, DatabaseType.MySQL); Assert.Contains("LIMIT @ChunkSize OFFSET @ChunkOffset", sql); } [Fact] public void BuildSelectSql_ValidExtraWhere_IsIncludedParenthesized() { var sql = SyncQueryBuilder.BuildSelectSql( "dbo", "MUOM", "UOMID", null, null, null, null, "STATUS = 1", DatabaseType.SqlServer); Assert.Contains("WHERE (STATUS = 1)", sql); } [Theory] [InlineData("STATUS = 1; DROP TABLE MUOM")] [InlineData("1=1 UNION SELECT * FROM MUSER")] [InlineData("EXEC sp_who")] public void BuildSelectSql_ExtraWhereContainsDisallowedKeyword_Throws(string maliciousCondition) { Assert.Throws(() => SyncQueryBuilder.BuildSelectSql( "dbo", "MUOM", "UOMID", null, null, null, null, maliciousCondition, DatabaseType.SqlServer)); } [Fact] public void BuildCdcSelectSql_ProducesExpectedCdcTableAndOperationFilter() { var sql = SyncQueryBuilder.BuildCdcSelectSql("MUOM", "UOMID", new DateTime(2026, 1, 1)); Assert.Contains("cdc.[dbo_MUOM_CT]", sql); Assert.Contains("__$operation = 1", sql); Assert.Contains("@SinceUtc", sql); } }