Skip to content

Commit 132fdee

Browse files
committed
SQL tests
1 parent 8f90f80 commit 132fdee

6 files changed

Lines changed: 409 additions & 8 deletions

File tree

src/test/Dime.Repositories.Sql.EntityFramework.IntegrationTests/Helpers/SqlServerFixture.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,31 @@ RETURNS TABLE
7373
FROM [Blogs]
7474
);");
7575

76+
// Stored procedures for the StoredProcedureRepository integration tests.
77+
await ctx.Database.ExecuteSqlRawAsync(@"
78+
CREATE PROCEDURE dbo.pInsertBlog
79+
@url NVARCHAR(200),
80+
@description NVARCHAR(200) = NULL
81+
AS
82+
BEGIN
83+
SET NOCOUNT OFF;
84+
INSERT INTO [Blogs] ([Url], [Description]) VALUES (@url, @description);
85+
END");
86+
87+
await ctx.Database.ExecuteSqlRawAsync(@"
88+
CREATE PROCEDURE dbo.pGetBlogsByUrlPrefix
89+
@prefix NVARCHAR(50)
90+
AS
91+
BEGIN
92+
SELECT [BlogId], [Url], [Description]
93+
FROM [Blogs]
94+
WHERE [Url] LIKE @prefix + '%';
95+
END");
96+
97+
// Unique index to drive a 2601 (duplicate key in unique index) SqlException.
98+
await ctx.Database.ExecuteSqlRawAsync(
99+
"CREATE UNIQUE INDEX UX_Tags_Name ON [Tags]([Name]);");
100+
76101
// Seed deterministic data.
77102
Blog cats = new() { Url = "http://sample.com/cats", Description = "Feline blog" };
78103
Blog catfish = new() { Url = "http://sample.com/catfish", Description = null };

src/test/Dime.Repositories.Sql.EntityFramework.IntegrationTests/IntegrationTests.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,7 @@ public async Task Bulk_SplitTrue_EmitsMoreDataCommandsThanSingle()
349349
int splitDataCommands = splitSql.Count(s => s.Contains("Executed DbCommand") && !s.Contains("COUNT(*)"));
350350
int singleDataCommands = singleSql.Count(s => s.Contains("Executed DbCommand") && !s.Contains("COUNT(*)"));
351351

352-
Assert.IsGreaterThan(singleDataCommands, splitDataCommands,
353-
$"Split mode should issue more data commands than single mode on a bulk query. Split={splitDataCommands}, Single={singleDataCommands}");
352+
Assert.IsGreaterThan(singleDataCommands, splitDataCommands, $"Split mode should issue more data commands than single mode on a bulk query. Split={splitDataCommands}, Single={singleDataCommands}");
354353
}
355354

356355
// 14. Bulk corpus: TVF returns the full universe (deterministic + bulk).
@@ -389,8 +388,7 @@ public async Task Bulk_WhereOnNavCollection_FiltersDistinctRoots()
389388
// Total is the count of distinct roots matching the predicate, not the JOIN cardinality.
390389
Assert.IsGreaterThan(0, result.Total);
391390
Assert.IsLessThanOrEqualTo(SqlServerFixture.BulkSummary.BlogCount, result.Total);
392-
Assert.IsTrue(result.Data.All(b => b.Posts != null && b.Posts.Count > 0),
393-
"Every returned blog should have at least one Post (split-query must populate the included collection).");
391+
Assert.IsTrue(result.Data.All(b => b.Posts != null && b.Posts.Count > 0), "Every returned blog should have at least one Post (split-query must populate the included collection).");
394392
}
395393

396394
// 10. splitQuery true vs false produce equivalent shape/data.
@@ -427,10 +425,8 @@ public async Task FindAllAsync_SplitQueryTrue_WithMultipleIncludes_OnSqlServer_E
427425
{
428426
Assert.AreEqual(singleList[i].BlogId, splitList[i].BlogId);
429427
Assert.AreEqual(singleList[i].Url, splitList[i].Url);
430-
Assert.AreEqual(singleList[i].Posts?.Count ?? 0, splitList[i].Posts?.Count ?? 0,
431-
$"Posts count mismatch for blog {singleList[i].BlogId}");
432-
Assert.AreEqual(singleList[i].Tags?.Count ?? 0, splitList[i].Tags?.Count ?? 0,
433-
$"Tags count mismatch for blog {singleList[i].BlogId}");
428+
Assert.AreEqual(singleList[i].Posts?.Count ?? 0, splitList[i].Posts?.Count ?? 0, $"Posts count mismatch for blog {singleList[i].BlogId}");
429+
Assert.AreEqual(singleList[i].Tags?.Count ?? 0, splitList[i].Tags?.Count ?? 0, $"Tags count mismatch for blog {singleList[i].BlogId}");
434430
}
435431
}
436432
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
7+
namespace Dime.Repositories.Sql.EntityFramework.IntegrationTests
8+
{
9+
/// <summary>
10+
/// Covers the DbUpdateException branches in EfRepository.SaveChanges / SaveChangesAsync
11+
/// that map SqlException numbers to the typed repository exceptions.
12+
/// </summary>
13+
[TestClass]
14+
public class SaveChangesExceptionTests
15+
{
16+
private static EfRepository<TEntity, BloggingContext> NewRepo<TEntity>() where TEntity : class, new()
17+
=> new(SqlServerFixture.CreateContext());
18+
19+
[TestMethod]
20+
public void Create_ForeignKeyViolation_ShouldThrowMappedException()
21+
{
22+
// Inserting a Post that references a non-existent Blog forces SQL Server error 547
23+
// (FK violation), which the repository maps via the DbUpdateException → SqlException path.
24+
using EfRepository<Post, BloggingContext> repo = NewRepo<Post>();
25+
26+
try
27+
{
28+
repo.Create(new Post { Title = "orphan", Content = "x", BlogId = int.MaxValue });
29+
Assert.Fail("Expected an exception due to the FK violation.");
30+
}
31+
catch (Exception ex) when (ex is ConstraintViolationException || ex is DbUpdateException || ex is DatabaseAccessException)
32+
{
33+
// Each of those branches is exercised regardless of how EF wraps the SqlException.
34+
}
35+
}
36+
37+
[TestMethod]
38+
public async Task CreateAsync_ForeignKeyViolation_ShouldThrowMappedException()
39+
{
40+
using EfRepository<Post, BloggingContext> repo = NewRepo<Post>();
41+
42+
try
43+
{
44+
await repo.CreateAsync(new Post { Title = "orphan-async", Content = "x", BlogId = int.MaxValue });
45+
Assert.Fail("Expected an exception due to the FK violation.");
46+
}
47+
catch (Exception ex) when (ex is ConstraintViolationException || ex is DbUpdateException || ex is DatabaseAccessException)
48+
{
49+
}
50+
}
51+
52+
[TestMethod]
53+
public void Create_DuplicateUniqueIndex_ShouldThrowMappedException()
54+
{
55+
// Tags table has a unique index on Name (created by the fixture).
56+
// First insert seeds the value; second insert raises SQL Server error 2601.
57+
using EfRepository<Tag, BloggingContext> repo = NewRepo<Tag>();
58+
59+
// Use a Blog from the deterministic seed.
60+
using BloggingContext setupCtx = SqlServerFixture.CreateContext();
61+
int blogId = setupCtx.Blogs.Where(b => b.Url == "http://sample.com/dogs").Select(b => b.BlogId).First();
62+
63+
string uniqueName = $"dup-tag-{System.Guid.NewGuid()}";
64+
repo.Create(new Tag { Name = uniqueName, BlogId = blogId });
65+
66+
using EfRepository<Tag, BloggingContext> repo2 = NewRepo<Tag>();
67+
try
68+
{
69+
repo2.Create(new Tag { Name = uniqueName, BlogId = blogId });
70+
Assert.Fail("Expected an exception due to the duplicate unique index.");
71+
}
72+
catch (Exception ex) when (ex is ConstraintViolationException || ex is DbUpdateException || ex is DatabaseAccessException)
73+
{
74+
}
75+
}
76+
77+
[TestMethod]
78+
public async Task CreateAsync_DuplicateUniqueIndex_ShouldThrowMappedException()
79+
{
80+
using EfRepository<Tag, BloggingContext> repo = NewRepo<Tag>();
81+
82+
using BloggingContext setupCtx = SqlServerFixture.CreateContext();
83+
int blogId = setupCtx.Blogs.Where(b => b.Url == "http://sample.com/dogs").Select(b => b.BlogId).First();
84+
85+
string uniqueName = $"dup-tag-async-{System.Guid.NewGuid()}";
86+
await repo.CreateAsync(new Tag { Name = uniqueName, BlogId = blogId });
87+
88+
using EfRepository<Tag, BloggingContext> repo2 = NewRepo<Tag>();
89+
try
90+
{
91+
await repo2.CreateAsync(new Tag { Name = uniqueName, BlogId = blogId });
92+
Assert.Fail("Expected an exception due to the duplicate unique index.");
93+
}
94+
catch (Exception ex) when (ex is ConstraintViolationException || ex is DbUpdateException || ex is DatabaseAccessException)
95+
{
96+
}
97+
}
98+
}
99+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System.Collections.Generic;
2+
using System.Data.Common;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.Data.SqlClient;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Dime.Repositories.Sql.EntityFramework.IntegrationTests
9+
{
10+
[TestClass]
11+
public class StoredProcedureTests
12+
{
13+
private static EfRepository<Blog, BloggingContext> NewRepo()
14+
=> new(SqlServerFixture.CreateContext());
15+
16+
[TestMethod]
17+
public void ExecuteStoredProcedure_NoSchemaOverload_ShouldExecuteAndReturnRowCount()
18+
{
19+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
20+
21+
DbParameter[] parameters =
22+
[
23+
new SqlParameter("@url", $"http://sp-test-noschema/{System.Guid.NewGuid()}"),
24+
new SqlParameter("@description", "sp insert no-schema")
25+
];
26+
27+
int result = repo.ExecuteStoredProcedure("dbo.pInsertBlog", parameters);
28+
29+
Assert.AreEqual(1, result, "INSERT should report one affected row.");
30+
}
31+
32+
[TestMethod]
33+
public void ExecuteStoredProcedure_WithSchemaOverload_ShouldExecuteAndReturnRowCount()
34+
{
35+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
36+
37+
DbParameter[] parameters =
38+
[
39+
new SqlParameter("@url", $"http://sp-test-schema/{System.Guid.NewGuid()}"),
40+
new SqlParameter("@description", "sp insert schema")
41+
];
42+
43+
int result = repo.ExecuteStoredProcedure("pInsertBlog", "dbo", parameters);
44+
45+
Assert.AreEqual(1, result);
46+
}
47+
48+
[TestMethod]
49+
public void ExecuteStoredProcedure_Generic_ShouldMapResultsToType()
50+
{
51+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
52+
53+
DbParameter[] parameters = [new SqlParameter("@prefix", "http://sample.com/")];
54+
IEnumerable<BlogDto> result = repo.ExecuteStoredProcedure<BlogDto>(
55+
"pGetBlogsByUrlPrefix", "dbo", parameters);
56+
57+
List<BlogDto> list = [.. result];
58+
Assert.HasCount(3, list, "Three deterministic blogs match the http://sample.com/ prefix.");
59+
CollectionAssert.AreEquivalent(
60+
new[] { "http://sample.com/cats", "http://sample.com/catfish", "http://sample.com/dogs" },
61+
list.Select(b => b.Url).ToList());
62+
}
63+
64+
[TestMethod]
65+
public async Task ExecuteStoredProcedureAsync_Generic_ShouldMapResultsToType()
66+
{
67+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
68+
69+
DbParameter[] parameters = [new SqlParameter("@prefix", "http://sample.com/")];
70+
IEnumerable<BlogDto> result = await repo.ExecuteStoredProcedureAsync<BlogDto>(
71+
"pGetBlogsByUrlPrefix", "dbo", parameters);
72+
73+
List<BlogDto> list = [.. result];
74+
Assert.HasCount(3, list);
75+
}
76+
77+
[TestMethod]
78+
public async Task ExecuteStoredProcedureAsync_NoSchemaOverload_ShouldExecuteAndReturnRowCount()
79+
{
80+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
81+
82+
DbParameter[] parameters =
83+
[
84+
new SqlParameter("@url", $"http://sp-async-noschema/{System.Guid.NewGuid()}"),
85+
new SqlParameter("@description", "async no-schema")
86+
];
87+
88+
int result = await repo.ExecuteStoredProcedureAsync("dbo.pInsertBlog", parameters);
89+
90+
Assert.AreEqual(1, result);
91+
}
92+
93+
[TestMethod]
94+
public async Task ExecuteStoredProcedureAsync_WithSchemaOverload_ShouldExecuteAndReturnRowCount()
95+
{
96+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
97+
98+
DbParameter[] parameters =
99+
[
100+
new SqlParameter("@url", $"http://sp-async-schema/{System.Guid.NewGuid()}"),
101+
new SqlParameter("@description", "async schema")
102+
];
103+
104+
int result = await repo.ExecuteStoredProcedureAsync("pInsertBlog", "dbo", parameters);
105+
106+
Assert.AreEqual(1, result);
107+
}
108+
109+
[TestMethod]
110+
public void GetStoredProcedureSchema_ShouldReturnParameterMetadata()
111+
{
112+
using EfRepository<Blog, BloggingContext> repo = NewRepo();
113+
114+
List<SqlParameter> parameters = [.. repo.GetStoredProcedureSchema("pInsertBlog")];
115+
116+
// pInsertBlog declares @url and @description (DeriveParameters also adds @RETURN_VALUE).
117+
Assert.IsTrue(parameters.Any(p => p.ParameterName.Contains("url", System.StringComparison.OrdinalIgnoreCase)),
118+
$"Expected @url parameter. Got: [{string.Join(",", parameters.Select(p => p.ParameterName))}]");
119+
Assert.IsTrue(parameters.Any(p => p.ParameterName.Contains("description", System.StringComparison.OrdinalIgnoreCase)),
120+
$"Expected @description parameter. Got: [{string.Join(",", parameters.Select(p => p.ParameterName))}]");
121+
}
122+
}
123+
}

0 commit comments

Comments
 (0)