|
1 | 1 | using Microsoft.Data.SqlClient; |
2 | 2 | using Microsoft.EntityFrameworkCore; |
| 3 | +using System.Data; |
3 | 4 |
|
4 | 5 | namespace IntelliTect.Coalesce.AuditLogging.Tests; |
5 | 6 |
|
@@ -123,11 +124,148 @@ public async Task WithSqlServer_CreatesNewRecordForUnmergableChanges() |
123 | 124 | Assert.Equal(2, db.AuditLogs.Count(l => l.State == AuditEntryState.EntityModified)); |
124 | 125 | } |
125 | 126 |
|
| 127 | + [SkippableFact] |
| 128 | + public async Task WithSqlServer_StoredProceduresWork() |
| 129 | + { |
| 130 | + // Arrange - use stored procedures |
| 131 | + using TestDbContext db = BuildDbContextWithStoredProcedures(); |
| 132 | + |
| 133 | + var user = new AppUser { Name = "Bob", Title = "Intern" }; |
| 134 | + db.Add(user); |
| 135 | + await db.SaveChangesAsync(); |
| 136 | + |
| 137 | + // Act/Assert |
| 138 | + user.Title = "Manager"; |
| 139 | + await db.SaveChangesAsync(); |
| 140 | + |
| 141 | + Assert.Equal(2, db.AuditLogs.Count()); // Two records: EntityAdded, and EntityUpdated |
| 142 | + |
| 143 | + // Act/Assert - second change should merge like raw SQL |
| 144 | + user.Title = "CEO"; |
| 145 | + await db.SaveChangesAsync(); |
| 146 | + |
| 147 | + Assert.Equal(2, db.AuditLogs.Count()); // Two records: EntityAdded, and EntityUpdated |
| 148 | + |
| 149 | + // Assert - verify the merge happened correctly |
| 150 | + var entry = Assert.Single(db.AuditLogs.Include(c => c.Properties).Where(c => c.State == AuditEntryState.EntityModified)); |
| 151 | + |
| 152 | + var propChange = Assert.Single(entry.Properties!); |
| 153 | + Assert.Equal(nameof(AppUser.Title), propChange.PropertyName); |
| 154 | + Assert.Equal("Intern", propChange.OldValue); |
| 155 | + Assert.Equal("CEO", propChange.NewValue); |
| 156 | + } |
| 157 | + |
| 158 | + [SkippableFact] |
| 159 | + public async Task WithSqlServer_StoredProcedureIsCreated() |
| 160 | + { |
| 161 | + // Arrange |
| 162 | + using TestDbContext db = BuildDbContextWithStoredProcedures(); |
| 163 | + |
| 164 | + var user = new AppUser { Name = "Bob", Title = "Intern" }; |
| 165 | + db.Add(user); |
| 166 | + await db.SaveChangesAsync(); |
| 167 | + |
| 168 | + // Act - trigger merge operation to create stored procedure |
| 169 | + user.Title = "Manager"; |
| 170 | + await db.SaveChangesAsync(); |
| 171 | + |
| 172 | + // Assert - verify stored procedure was created |
| 173 | + using (var command = db.Database.GetDbConnection().CreateCommand()) |
| 174 | + { |
| 175 | + command.CommandText = "SELECT COUNT(*) FROM sys.procedures WHERE name LIKE 'AuditMerge_%'"; |
| 176 | + if (db.Database.GetDbConnection().State != ConnectionState.Open) |
| 177 | + await db.Database.GetDbConnection().OpenAsync(); |
| 178 | + var result = await command.ExecuteScalarAsync(); |
| 179 | + var procedureCount = Convert.ToInt32(result); |
| 180 | + Assert.True(procedureCount > 0, "Expected at least one AuditMerge stored procedure to be created"); |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + [SkippableFact] |
| 185 | + public async Task WithSqlServer_DifferentModelsCreateDifferentStoredProcedures() |
| 186 | + { |
| 187 | + // This test verifies that different entity models result in different stored procedures |
| 188 | + // due to the hash-based naming scheme |
| 189 | + |
| 190 | + // Arrange - Create two different contexts that would generate different SQL |
| 191 | + using TestDbContext db1 = BuildDbContextWithStoredProcedures(); |
| 192 | + |
| 193 | + var user1 = new AppUser { Name = "Bob" }; |
| 194 | + db1.Add(user1); |
| 195 | + await db1.SaveChangesAsync(); |
| 196 | + |
| 197 | + user1.Name = "Robert"; |
| 198 | + await db1.SaveChangesAsync(); // This should create a stored procedure |
| 199 | + |
| 200 | + // Get count of stored procedures after first context |
| 201 | + int initialCount; |
| 202 | + using (var command = db1.Database.GetDbConnection().CreateCommand()) |
| 203 | + { |
| 204 | + command.CommandText = "SELECT COUNT(*) FROM sys.procedures WHERE name LIKE 'AuditMerge_%'"; |
| 205 | + if (db1.Database.GetDbConnection().State != ConnectionState.Open) |
| 206 | + await db1.Database.GetDbConnection().OpenAsync(); |
| 207 | + var result = await command.ExecuteScalarAsync(); |
| 208 | + initialCount = Convert.ToInt32(result); |
| 209 | + } |
| 210 | + |
| 211 | + // The same model should reuse the same stored procedure |
| 212 | + using TestDbContext db2 = BuildDbContextWithStoredProcedures(); |
| 213 | + |
| 214 | + var user2 = new AppUser { Name = "Alice" }; |
| 215 | + db2.Add(user2); |
| 216 | + await db2.SaveChangesAsync(); |
| 217 | + |
| 218 | + user2.Name = "Alicia"; |
| 219 | + await db2.SaveChangesAsync(); // This should reuse the existing stored procedure |
| 220 | + |
| 221 | + int finalCount; |
| 222 | + using (var command = db2.Database.GetDbConnection().CreateCommand()) |
| 223 | + { |
| 224 | + command.CommandText = "SELECT COUNT(*) FROM sys.procedures WHERE name LIKE 'AuditMerge_%'"; |
| 225 | + if (db2.Database.GetDbConnection().State != ConnectionState.Open) |
| 226 | + await db2.Database.GetDbConnection().OpenAsync(); |
| 227 | + var result = await command.ExecuteScalarAsync(); |
| 228 | + finalCount = Convert.ToInt32(result); |
| 229 | + } |
| 230 | + |
| 231 | + // Should be the same count since the same model is used |
| 232 | + Assert.Equal(initialCount, finalCount); |
| 233 | + } |
| 234 | + |
126 | 235 | private static TestDbContext BuildDbContext() |
127 | 236 | { |
128 | 237 | var db = new TestDbContext(new DbContextOptionsBuilder<TestDbContext>() |
129 | 238 | .UseSqlServer(SqlServerConnString) |
130 | | - .UseCoalesceAuditLogging<TestAuditLog>() |
| 239 | + .UseCoalesceAuditLogging<TestAuditLog>(x => x.WithStoredProcedures(false)) |
| 240 | + .Options); |
| 241 | + |
| 242 | + try |
| 243 | + { |
| 244 | + db.Database.EnsureDeleted(); |
| 245 | + db.Database.EnsureCreated(); |
| 246 | + } |
| 247 | + catch (SqlException ex) when ( |
| 248 | + ex.Number == 53 |
| 249 | + || ex.Message.Contains("Could not open a connection to SQL Server") |
| 250 | + || ex.Message.Contains("The server was not found or was not accessible") |
| 251 | + ) |
| 252 | + { |
| 253 | + Skip.If(true, ex.Message); |
| 254 | + } |
| 255 | + catch (PlatformNotSupportedException ex) when ( |
| 256 | + ex.Message.Contains("LocalDB is not supported on this platform") |
| 257 | + ) |
| 258 | + { |
| 259 | + Skip.If(true, ex.Message); |
| 260 | + } |
| 261 | + return db; |
| 262 | + } |
| 263 | + |
| 264 | + private static TestDbContext BuildDbContextWithStoredProcedures() |
| 265 | + { |
| 266 | + var db = new TestDbContext(new DbContextOptionsBuilder<TestDbContext>() |
| 267 | + .UseSqlServer(SqlServerConnString) |
| 268 | + .UseCoalesceAuditLogging<TestAuditLog>(x => x.WithStoredProcedures()) |
131 | 269 | .Options); |
132 | 270 |
|
133 | 271 | try |
|
0 commit comments