Skip to content

Commit a0f9fd6

Browse files
committed
✨ Cover transport transaction state derivation with unit tests
The mapping from stored entries to dispatch state had no unit tests at all. Only the integration tests exercised it, so catching a regression required a database. Covers the six states produced by the factories, the hand-rolled shapes that external integrations and the core create, the invalid combinations that throw, and that the state follows the entries when they change after creation.
1 parent fbeebe6 commit a0f9fd6

1 file changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
namespace NServiceBus.Transport.SqlServer.UnitTests.Sending;
2+
3+
using System;
4+
using System.Data;
5+
using System.Data.Common;
6+
using System.Transactions;
7+
using NServiceBus.Transport.Sql.Shared;
8+
using NUnit.Framework;
9+
using Transport;
10+
using static NServiceBus.Transport.Sql.Shared.SqlTransportTransactionState;
11+
12+
[TestFixture]
13+
public class TransportTransactionStateTests
14+
{
15+
[Test]
16+
public void Empty_transaction_is_outside_handler()
17+
{
18+
var transportTransaction = new TransportTransaction();
19+
20+
Assert.That(transportTransaction.State, Is.SameAs(OutsideHandler.Instance));
21+
}
22+
23+
[Test]
24+
public void No_transaction_carries_the_receive_connection()
25+
{
26+
var connection = new FakeDbConnection();
27+
28+
var state = TransportTransactions.NoTransaction(connection).State;
29+
30+
Assert.That(state, Is.InstanceOf<NoTransaction>());
31+
Assert.That(((NoTransaction)state).Connection, Is.SameAs(connection));
32+
}
33+
34+
[Test]
35+
public void Receive_only_carries_no_data()
36+
{
37+
var connection = new FakeDbConnection();
38+
var transaction = new FakeDbTransaction(connection);
39+
40+
var transportTransaction = TransportTransactions.ReceiveOnly(connection, transaction);
41+
42+
// the dispatcher opens its own connection for this state, so there is nothing to carry
43+
Assert.That(transportTransaction.State, Is.SameAs(ReceiveOnly.Instance));
44+
}
45+
46+
[Test]
47+
public void Receive_only_still_writes_the_well_known_entries_for_downstream_components()
48+
{
49+
var connection = new FakeDbConnection();
50+
var transaction = new FakeDbTransaction(connection);
51+
52+
var transportTransaction = TransportTransactions.ReceiveOnly(connection, transaction);
53+
54+
Assert.Multiple(() =>
55+
{
56+
Assert.That(transportTransaction.TryGet(TransportTransactionKeys.ReceiveOnlyTransactionMode, out bool receiveOnly) && receiveOnly, Is.True);
57+
Assert.That(transportTransaction.TryGet(TransportTransactionKeys.SqlConnection, out DbConnection storedConnection) ? storedConnection : null, Is.SameAs(connection));
58+
Assert.That(transportTransaction.TryGet(TransportTransactionKeys.SqlTransaction, out DbTransaction storedTransaction) ? storedTransaction : null, Is.SameAs(transaction));
59+
});
60+
}
61+
62+
[Test]
63+
public void Sends_atomic_with_receive_carries_the_receive_connection_and_transaction()
64+
{
65+
var connection = new FakeDbConnection();
66+
var transaction = new FakeDbTransaction(connection);
67+
68+
var state = TransportTransactions.SendsAtomicWithReceive(connection, transaction).State;
69+
70+
Assert.That(state, Is.InstanceOf<SendsAtomicWithReceive>());
71+
72+
var sendsAtomicWithReceive = (SendsAtomicWithReceive)state;
73+
74+
Assert.Multiple(() =>
75+
{
76+
Assert.That(sendsAtomicWithReceive.Connection, Is.SameAs(connection));
77+
Assert.That(sendsAtomicWithReceive.NativeTransaction, Is.SameAs(transaction));
78+
});
79+
}
80+
81+
[Test]
82+
public void Transaction_scope_carries_no_data()
83+
{
84+
using var ambientTransaction = new CommittableTransaction();
85+
86+
var transportTransaction = TransportTransactions.TransactionScope(ambientTransaction);
87+
88+
// a new connection enlists in the ambient transaction automatically, so there is nothing to carry
89+
Assert.That(transportTransaction.State, Is.SameAs(AmbientTransaction.Instance));
90+
}
91+
92+
[Test]
93+
public void User_provided_connection_has_no_native_transaction()
94+
{
95+
var connection = new FakeDbConnection();
96+
97+
var state = TransportTransactions.UserProvided(connection).State;
98+
99+
Assert.That(state, Is.InstanceOf<UserProvided>());
100+
101+
var userProvided = (UserProvided)state;
102+
103+
Assert.Multiple(() =>
104+
{
105+
Assert.That(userProvided.Connection, Is.SameAs(connection));
106+
Assert.That(userProvided.NativeTransaction, Is.Null);
107+
});
108+
}
109+
110+
[Test]
111+
public void User_provided_transaction_falls_back_to_the_connection_it_was_created_on()
112+
{
113+
var connection = new FakeDbConnection();
114+
var transaction = new FakeDbTransaction(connection);
115+
116+
var state = TransportTransactions.UserProvided(transaction).State;
117+
118+
Assert.That(state, Is.InstanceOf<UserProvided>());
119+
120+
var userProvided = (UserProvided)state;
121+
122+
Assert.Multiple(() =>
123+
{
124+
Assert.That(userProvided.Connection, Is.SameAs(connection));
125+
Assert.That(userProvided.NativeTransaction, Is.SameAs(transaction));
126+
});
127+
}
128+
129+
[Test]
130+
public void User_provided_transaction_without_a_connection_throws()
131+
{
132+
var transportTransaction = TransportTransactions.UserProvided(new FakeDbTransaction(null));
133+
134+
var exception = Assert.Throws<Exception>(() => _ = transportTransaction.State);
135+
136+
Assert.That(exception.Message, Does.Contain("contains no SqlTransaction or SqlConnection"));
137+
}
138+
139+
[Test]
140+
public void Hand_rolled_transaction_with_only_a_connection_is_no_transaction()
141+
{
142+
var connection = new FakeDbConnection();
143+
144+
var transportTransaction = new TransportTransaction();
145+
transportTransaction.Set(TransportTransactionKeys.SqlConnection, connection);
146+
147+
var state = transportTransaction.State;
148+
149+
Assert.That(state, Is.InstanceOf<NoTransaction>());
150+
Assert.That(((NoTransaction)state).Connection, Is.SameAs(connection));
151+
}
152+
153+
[Test]
154+
public void Hand_rolled_transaction_with_a_connection_and_a_transaction_is_sends_atomic_with_receive()
155+
{
156+
var connection = new FakeDbConnection();
157+
var transaction = new FakeDbTransaction(connection);
158+
159+
var transportTransaction = new TransportTransaction();
160+
transportTransaction.Set(TransportTransactionKeys.SqlConnection, connection);
161+
transportTransaction.Set(TransportTransactionKeys.SqlTransaction, transaction);
162+
163+
Assert.That(transportTransaction.State, Is.InstanceOf<SendsAtomicWithReceive>());
164+
}
165+
166+
[Test]
167+
public void Hand_rolled_transaction_with_only_a_transaction_throws()
168+
{
169+
var transportTransaction = new TransportTransaction();
170+
transportTransaction.Set(TransportTransactionKeys.SqlTransaction, new FakeDbTransaction(new FakeDbConnection()));
171+
172+
Assert.Throws<Exception>(() => _ = transportTransaction.State);
173+
}
174+
175+
[Test]
176+
public void State_follows_the_entries_when_they_change_after_creation()
177+
{
178+
var connection = new FakeDbConnection();
179+
var transaction = new FakeDbTransaction(connection);
180+
181+
var transportTransaction = TransportTransactions.NoTransaction(connection);
182+
183+
Assert.That(transportTransaction.State, Is.InstanceOf<NoTransaction>());
184+
185+
// the state is derived from the entries rather than stored alongside them, so a downstream
186+
// component adding a transaction cannot leave the dispatcher acting on a stale state
187+
transportTransaction.Set(TransportTransactionKeys.SqlTransaction, transaction);
188+
189+
Assert.That(transportTransaction.State, Is.InstanceOf<SendsAtomicWithReceive>());
190+
}
191+
192+
class FakeDbConnection : DbConnection
193+
{
194+
public override string ConnectionString { get; set; }
195+
public override string Database => string.Empty;
196+
public override string DataSource => string.Empty;
197+
public override string ServerVersion => string.Empty;
198+
public override ConnectionState State => ConnectionState.Open;
199+
200+
public override void ChangeDatabase(string databaseName) => throw new NotSupportedException();
201+
public override void Close() => throw new NotSupportedException();
202+
public override void Open() => throw new NotSupportedException();
203+
protected override DbTransaction BeginDbTransaction(System.Data.IsolationLevel isolationLevel) => throw new NotSupportedException();
204+
protected override DbCommand CreateDbCommand() => throw new NotSupportedException();
205+
}
206+
207+
class FakeDbTransaction(DbConnection connection) : DbTransaction
208+
{
209+
public override System.Data.IsolationLevel IsolationLevel => System.Data.IsolationLevel.ReadCommitted;
210+
protected override DbConnection DbConnection { get; } = connection;
211+
212+
public override void Commit() => throw new NotSupportedException();
213+
public override void Rollback() => throw new NotSupportedException();
214+
}
215+
}

0 commit comments

Comments
 (0)