Skip to content

Commit 4ea20ba

Browse files
Gregory NikolaishviliGregory Nikolaishvili
authored andcommitted
More unit tests
1 parent f873dc9 commit 4ea20ba

8 files changed

Lines changed: 347 additions & 9 deletions

File tree

Directory.Packages.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
<PackageVersion Include="System.Text.Json" Version="9.0.15" />
2727
</ItemGroup>
2828
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
29-
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
30-
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.26" />
29+
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
30+
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="10.0.8" />
3131
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
3232
<PackageVersion Include="System.Text.Json" Version="8.0.6" />
3333
</ItemGroup>
34-
</Project>
34+
</Project>

Tests/OrmTests/AltaSoft.Storm.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<ItemGroup>
1313

1414
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
15-
<PackageReference Include="FluentAssertions" Version="[7.2.2]" />
15+
<PackageReference Include="FluentAssertions" Version="8.8.0" />
1616

1717
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
1818

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using AltaSoft.Storm.Exceptions;
4+
using AltaSoft.Storm.TestModels;
5+
using AltaSoft.Storm.TestModels.VeryBadNamespace;
6+
using FluentAssertions;
7+
using Microsoft.Data.SqlClient;
8+
using Xunit;
9+
10+
namespace AltaSoft.Storm.Tests;
11+
12+
public class BatchTransactionRollbackTests : IClassFixture<DatabaseFixture>
13+
{
14+
private readonly DatabaseFixture _fixture;
15+
16+
public BatchTransactionRollbackTests(DatabaseFixture fixture)
17+
{
18+
_fixture = fixture;
19+
}
20+
21+
[Fact]
22+
public async Task Batch_WithPrimaryKeyViolation_ShouldThrowAndRollbackAllCommands()
23+
{
24+
var account1 = NewAccount(100_001, "US12345678901234560001");
25+
var account2 = NewAccount(100_001, "US12345678901234560002");
26+
var account3 = NewAccount(100_002, "US12345678901234560003");
27+
28+
using (var _ = new StormTransactionScope())
29+
{
30+
await using var context = new TestStormContext(_fixture.ConnectionString);
31+
await using var batch = context.CreateBatch();
32+
33+
batch.Add(context.InsertIntoAccount().Values(account1));
34+
batch.Add(context.InsertIntoAccount().Values(account2));
35+
batch.Add(context.InsertIntoAccount().Values(account3));
36+
37+
var act = async () => await batch.ExecuteAsync(CancellationToken.None);
38+
await act.Should().ThrowAsync<StormPrimaryKeyViolationException>();
39+
}
40+
41+
await using var verifyContext = new TestStormContext(_fixture.ConnectionString);
42+
43+
var inserted1 = await verifyContext.SelectFromAccount(account1.IbanAccount, account1.Ccy).GetAsync();
44+
var inserted2 = await verifyContext.SelectFromAccount(account2.IbanAccount, account2.Ccy).GetAsync();
45+
var inserted3 = await verifyContext.SelectFromAccount(account3.IbanAccount, account3.Ccy).GetAsync();
46+
47+
inserted1.Should().BeNull();
48+
inserted2.Should().BeNull();
49+
inserted3.Should().BeNull();
50+
}
51+
52+
[Fact]
53+
public async Task Batch_WithUsersPrimaryKeyViolation_ShouldThrowAndRollbackAllCommands()
54+
{
55+
var user1 = DatabaseHelper.NewUser(100_100);
56+
var user2 = DatabaseHelper.NewUser(100_100); // same PK (UserId + BranchId)
57+
var user3 = DatabaseHelper.NewUser(100_101); // New PK, should not cause violation if executed alone
58+
59+
60+
using (var _ = new StormTransactionScope())
61+
{
62+
await using var context = new TestStormContext(_fixture.ConnectionString);
63+
await using var batch = context.CreateBatch();
64+
65+
batch.Add(context.InsertIntoUsersTable().Values(user1));
66+
batch.Add(context.InsertIntoUsersTable().Values(user2));
67+
batch.Add(context.InsertIntoUsersTable().Values(user3));
68+
69+
var act = async () => await batch.ExecuteAsync(CancellationToken.None);
70+
await act.Should().ThrowAsync<StormPrimaryKeyViolationException>();
71+
}
72+
73+
await using var verifyContext = new TestStormContext(_fixture.ConnectionString);
74+
75+
var inserted1 = await verifyContext.SelectFromUsersTable(user1.UserId, user1.BranchId).GetAsync();
76+
var inserted2 = await verifyContext.SelectFromUsersTable(user2.UserId, user2.BranchId).GetAsync();
77+
var inserted3 = await verifyContext.SelectFromUsersTable(user3.UserId, user3.BranchId).GetAsync();
78+
79+
inserted1.Should().BeNull();
80+
inserted2.Should().BeNull();
81+
inserted3.Should().BeNull();
82+
}
83+
84+
[Fact]
85+
public async Task Batch_WithNotNullViolation_HigherSeverity_ShouldThrowAndRollbackAllCommands()
86+
{
87+
var user1 = DatabaseHelper.NewUser(100_200);
88+
var user2 = DatabaseHelper.NewUser(100_201);
89+
var user3 = DatabaseHelper.NewUser(100_202);
90+
user2.LoginName = null!; // force NOT NULL violation (SqlException class 16)
91+
92+
SqlException? thrown;
93+
94+
using (var _ = new StormTransactionScope())
95+
{
96+
await using var context = new TestStormContext(_fixture.ConnectionString);
97+
await using var batch = context.CreateBatch();
98+
99+
batch.Add(context.InsertIntoUsersTable().Values(user1));
100+
batch.Add(context.InsertIntoUsersTable().Values(user2));
101+
batch.Add(context.InsertIntoUsersTable().Values(user3));
102+
103+
var act = async () => await batch.ExecuteAsync(CancellationToken.None);
104+
thrown = (await act.Should().ThrowAsync<SqlException>()).Which;
105+
}
106+
107+
thrown.Should().NotBeNull();
108+
thrown.Class.Should().BeGreaterThanOrEqualTo(16);
109+
110+
await using var verifyContext = new TestStormContext(_fixture.ConnectionString);
111+
112+
var inserted1 = await verifyContext.SelectFromUsersTable(user1.UserId, user1.BranchId).GetAsync();
113+
var inserted2 = await verifyContext.SelectFromUsersTable(user2.UserId, user2.BranchId).GetAsync();
114+
var inserted3 = await verifyContext.SelectFromUsersTable(user3.UserId, user3.BranchId).GetAsync();
115+
116+
inserted1.Should().BeNull();
117+
inserted2.Should().BeNull();
118+
inserted3.Should().BeNull();
119+
}
120+
121+
122+
[Fact]
123+
public async Task Batch_WithCommandTimeout_ShouldThrowAndRollbackAllCommands()
124+
{
125+
const int lockedUserId = 1;
126+
const short branchId = 7;
127+
128+
await using var lockerConnection = new SqlConnection(_fixture.ConnectionString);
129+
await lockerConnection.OpenAsync();
130+
await using var lockerTransaction = (SqlTransaction)await lockerConnection.BeginTransactionAsync();
131+
132+
await using (var lockCmd = lockerConnection.CreateCommand())
133+
{
134+
lockCmd.Transaction = lockerTransaction;
135+
lockCmd.CommandText = "UPDATE dbo.Users SET FullName = FullName WHERE Id = @id AND BranchId = @branchId";
136+
lockCmd.Parameters.AddWithValue("@id", lockedUserId);
137+
lockCmd.Parameters.AddWithValue("@branchId", branchId);
138+
await lockCmd.ExecuteNonQueryAsync();
139+
}
140+
141+
var account = NewAccount(100_300, "US12345678901234560300");
142+
143+
SqlException? thrown;
144+
145+
using (var _ = new StormTransactionScope())
146+
{
147+
await using var context = new TestStormContext(_fixture.ConnectionString);
148+
await using var batch = context.CreateBatch();
149+
150+
var insertCmd = context.InsertIntoAccount().Values(account);
151+
var blockedUpdateCmd = context.UpdateUsersTable(lockedUserId, branchId)
152+
.WithCommandTimeOut(1)
153+
.Set(x => x.FullName, "WillTimeout");
154+
155+
batch.Add(insertCmd);
156+
batch.Add(blockedUpdateCmd);
157+
158+
var act = async () => await batch.ExecuteAsync(CancellationToken.None);
159+
thrown = (await act.Should().ThrowAsync<SqlException>()).Which;
160+
}
161+
162+
thrown.Should().NotBeNull();
163+
thrown.Number.Should().Be(-2);
164+
165+
await lockerTransaction.RollbackAsync();
166+
167+
await using var verifyContext = new TestStormContext(_fixture.ConnectionString);
168+
var inserted = await verifyContext.SelectFromAccount(account.IbanAccount, account.Ccy).GetAsync();
169+
inserted.Should().BeNull();
170+
}
171+
172+
private static Account NewAccount(int id, string iban)
173+
{
174+
return new Account
175+
{
176+
Id = id,
177+
RelatedCustomerId = new CustomerId(1),
178+
Ccy = "USD",
179+
IbanAccount = iban,
180+
BbanAccount = 1234567890123456,
181+
BranchId = 1,
182+
Type = 1,
183+
Name = $"Test Account {id}",
184+
CanDebit = true,
185+
CanCredit = true
186+
};
187+
}
188+
}

Tests/OrmTests/TransactionScopeTests.cs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,4 +488,125 @@ public async Task Parallel_JoinScopes_DoNotLeakAmbientToMainContext()
488488

489489
await outer.CompleteAsync(CancellationToken.None);
490490
}
491+
492+
[Fact]
493+
public async Task OuterScope_WithProcedureAndStandaloneContexts_PersistsStandaloneChangesOnly()
494+
{
495+
const int transactionalUserId = 310_001;
496+
const int standaloneUser1Id = 310_002;
497+
const int standaloneUser2Id = 310_003;
498+
const int standaloneUser3Id = 310_004;
499+
const short branchId = 7;
500+
501+
var transactionalUser = DatabaseHelper.NewUser(transactionalUserId);
502+
var standaloneUser1 = DatabaseHelper.NewUser(standaloneUser1Id);
503+
var standaloneUser2 = DatabaseHelper.NewUser(standaloneUser2Id);
504+
var standaloneUser3 = DatabaseHelper.NewUser(standaloneUser3Id);
505+
506+
using (var outer = new StormTransactionScope())
507+
{
508+
Assert.True(outer.IsRoot);
509+
Assert.Equal(1, outer.Ambient.TransactionCount);
510+
Assert.Same(outer, StormTransactionScope.Current);
511+
512+
await using var context1 = new TestStormContext(_fixture.ConnectionString, standalone: false);
513+
514+
Assert.False(context1.IsStandalone);
515+
Assert.True(context1.IsInTransactionScope);
516+
517+
var (outerConnection, outerTransaction) = await context1.EnsureConnectionAsync(CancellationToken.None);
518+
Assert.Same(outer.Ambient.Connection, outerConnection);
519+
Assert.Same(outer.Ambient.Transaction, outerTransaction);
520+
Assert.NotNull(outerTransaction);
521+
522+
var procResult = await context1.ExecuteInputOutputProc(1, 0).ExecuteAsync();
523+
Assert.NotNull(procResult);
524+
Assert.Equal(1, procResult.ReturnValue);
525+
Assert.Equal(1, procResult.ResultValue);
526+
Assert.Equal(77, procResult.Io);
527+
528+
await context1.InsertIntoUsersTable().Values(transactionalUser).GoAsync();
529+
530+
var transactionalVisibleInsideOuter = await context1.SelectFromUsersTable(transactionalUserId, branchId).GetAsync();
531+
Assert.NotNull(transactionalVisibleInsideOuter);
532+
533+
await using (var standaloneContext1 = new TestStormContext(_fixture.ConnectionString, standalone: true))
534+
{
535+
Assert.True(standaloneContext1.IsStandalone);
536+
Assert.False(standaloneContext1.IsInTransactionScope);
537+
538+
var (standaloneConnection1, standaloneTransaction1) = await standaloneContext1.EnsureConnectionAsync(CancellationToken.None);
539+
Assert.NotNull(standaloneConnection1);
540+
Assert.Null(standaloneTransaction1);
541+
Assert.NotSame(outerConnection, standaloneConnection1);
542+
543+
await standaloneContext1.InsertIntoUsersTable().Values(standaloneUser1).GoAsync();
544+
545+
await standaloneContext1.UpdateUsersTable(standaloneUser1.UserId, standaloneUser1.BranchId)
546+
.Set(x => x.FullName, "Standalone1_Updated")
547+
.GoAsync();
548+
}
549+
550+
await using (var outsideReadContext = new TestStormContext(_fixture.ConnectionString, standalone: true))
551+
{
552+
var standaloneVisibleOutside = await outsideReadContext.SelectFromUsersTable(standaloneUser1Id, branchId).GetAsync();
553+
554+
Assert.NotNull(standaloneVisibleOutside);
555+
Assert.Equal("Standalone1_Updated", standaloneVisibleOutside!.FullName);
556+
}
557+
558+
using (var inner = new StormTransactionScope(StormTransactionScopeOption.CreateNew))
559+
{
560+
Assert.Same(inner, StormTransactionScope.Current);
561+
Assert.NotSame(outer.Ambient, inner.Ambient);
562+
Assert.Same(outer.Ambient, inner.Ambient.Previous);
563+
Assert.Equal(1, outer.Ambient.TransactionCount);
564+
Assert.Equal(1, inner.Ambient.TransactionCount);
565+
566+
await using var context2 = new TestStormContext(_fixture.ConnectionString, standalone: true);
567+
568+
Assert.True(context2.IsStandalone);
569+
Assert.False(context2.IsInTransactionScope);
570+
571+
var (standaloneConnection2, standaloneTransaction2) = await context2.EnsureConnectionAsync(CancellationToken.None);
572+
Assert.NotNull(standaloneConnection2);
573+
Assert.Null(standaloneTransaction2);
574+
Assert.NotSame(outerConnection, standaloneConnection2);
575+
576+
await context2.InsertIntoUsersTable().Values(standaloneUser2).GoAsync();
577+
await context2.InsertIntoUsersTable().Values(standaloneUser3).GoAsync();
578+
await context2.UpdateUsersTable(standaloneUser2.UserId, standaloneUser2.BranchId)
579+
.Set(x => x.FullName, "Standalone2_Updated")
580+
.GoAsync();
581+
582+
await inner.CompleteAsync(CancellationToken.None);
583+
584+
Assert.True(inner.IsCompleted);
585+
Assert.Equal(0, inner.Ambient.TransactionCount);
586+
}
587+
588+
Assert.Same(outer, StormTransactionScope.Current);
589+
Assert.False(outer.IsCompleted);
590+
Assert.Equal(1, outer.Ambient.TransactionCount);
591+
592+
// Intentionally do not complete outer scope so ambient transactional changes are rolled back.
593+
}
594+
595+
await using var verifyContext = new TestStormContext(_fixture.ConnectionString);
596+
597+
var transactionalInserted = await verifyContext.SelectFromUsersTable(transactionalUserId, branchId).GetAsync();
598+
var standaloneInserted1 = await verifyContext.SelectFromUsersTable(standaloneUser1Id, branchId).GetAsync();
599+
var standaloneInserted2 = await verifyContext.SelectFromUsersTable(standaloneUser2Id, branchId).GetAsync();
600+
var standaloneInserted3 = await verifyContext.SelectFromUsersTable(standaloneUser3Id, branchId).GetAsync();
601+
602+
Assert.Null(transactionalInserted);
603+
604+
Assert.NotNull(standaloneInserted1);
605+
Assert.Equal("Standalone1_Updated", standaloneInserted1!.FullName);
606+
607+
Assert.NotNull(standaloneInserted2);
608+
Assert.Equal("Standalone2_Updated", standaloneInserted2!.FullName);
609+
610+
Assert.NotNull(standaloneInserted3);
611+
}
491612
}

Tests/TestModels/TestStormContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ public TestStormContext(string connectionString) : base(connectionString)
1010
{
1111
}
1212

13+
public TestStormContext(string connectionString, bool standalone) : base(connectionString, standalone)
14+
{
15+
}
16+
1317
public TestStormContext() : base()
1418
{
1519
}

Tests/TrackingListTests/AltaSoft.Storm.TrackingListTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="FluentAssertions" Version="[7.2.2]" />
17+
<PackageReference Include="FluentAssertions" Version="8.8.0" />
1818
<PackageReference Include="FluentAssertions.Analyzers" Version="0.34.1">
1919
<PrivateAssets>all</PrivateAssets>
2020
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/AltaSoft.Storm.MsSql/AltaSoft.Storm.MsSql.csproj

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>$(DefaultTargetFrameworks)</TargetFrameworks>
@@ -54,14 +54,26 @@
5454
<Pack>true</Pack>
5555
<PackagePath>lib\net8.0\</PackagePath>
5656
</None>
57+
<None Include="..\AltaSoft.Storm\bin\$(Configuration)\net8.0\AltaSoft.Storm.xml">
58+
<Pack>true</Pack>
59+
<PackagePath>lib\net8.0\</PackagePath>
60+
</None>
5761
<None Include="..\AltaSoft.Storm\bin\$(Configuration)\net9.0\AltaSoft.Storm.dll">
5862
<Pack>true</Pack>
5963
<PackagePath>lib\net9.0\</PackagePath>
6064
</None>
65+
<None Include="..\AltaSoft.Storm\bin\$(Configuration)\net9.0\AltaSoft.Storm.xml">
66+
<Pack>true</Pack>
67+
<PackagePath>lib\net9.0\</PackagePath>
68+
</None>
6169
<None Include="..\AltaSoft.Storm\bin\$(Configuration)\net10.0\AltaSoft.Storm.dll">
6270
<Pack>true</Pack>
6371
<PackagePath>lib\net10.0\</PackagePath>
6472
</None>
73+
<None Include="..\AltaSoft.Storm\bin\$(Configuration)\net10.0\AltaSoft.Storm.xml">
74+
<Pack>true</Pack>
75+
<PackagePath>lib\net10.0\</PackagePath>
76+
</None>
6577

6678
<!-- Include analyzers assembly -->
6779
<!--

0 commit comments

Comments
 (0)