Skip to content

Commit 24219d9

Browse files
committed
Improve parameter validation and add unit tests
1 parent eac4f77 commit 24219d9

2 files changed

Lines changed: 308 additions & 15 deletions

File tree

src/BinanceBot.Market/Domain/MarketSymbol.cs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,44 @@ public record MarketSymbol
2828
/// <summary>
2929
/// Initializes a new instance of the <see cref="MarketSymbol"/> class.
3030
/// </summary>
31-
/// <param name="baseAsset"></param>
32-
/// <param name="quoteAsset"></param>
33-
/// <param name="contractType"></param>
34-
/// <exception cref="ArgumentNullException">Thrown when baseAsset or quoteAsset is null.</exception>
31+
/// <param name="baseAsset">The base asset of the trading pair (e.g., "BTC", "ETH")</param>
32+
/// <param name="quoteAsset">The quote asset of the trading pair (e.g., "USDT", "BTC")</param>
33+
/// <param name="contractType">The contract type (Spot or Futures)</param>
34+
/// <exception cref="ArgumentException">Thrown when baseAsset or quoteAsset is null, empty or whitespace.</exception>
3535
public MarketSymbol(string baseAsset, string quoteAsset, ContractType contractType)
3636
{
37-
BaseAsset = baseAsset ?? throw new ArgumentNullException(nameof(baseAsset));
38-
QuoteAsset = quoteAsset ?? throw new ArgumentNullException(nameof(quoteAsset));
37+
if (string.IsNullOrWhiteSpace(baseAsset))
38+
throw new ArgumentException("Base asset cannot be empty or whitespace", nameof(baseAsset));
39+
if (string.IsNullOrWhiteSpace(quoteAsset))
40+
throw new ArgumentException("Quote asset cannot be empty or whitespace", nameof(quoteAsset));
41+
42+
BaseAsset = baseAsset.Trim().ToUpperInvariant();
43+
QuoteAsset = quoteAsset.Trim().ToUpperInvariant();
3944
ContractType = contractType;
4045
}
4146

4247
/// <summary>
4348
/// Initializes a new instance of the <see cref="MarketSymbol"/> class from a pair string.
4449
/// </summary>
45-
/// <param name="pair">The trading pair in the format "BASE/QUOTE".</param>
50+
/// <param name="pair">The trading pair in the format "BASE/QUOTE" (e.g., "BTC/USDT", "ETH/BTC")</param>
4651
/// <param name="contractType">The contract type (Spot or Futures). Defaults to Spot.</param>
47-
/// <exception cref="ArgumentException">Thrown when the pair format is invalid.</exception>
52+
/// <exception cref="ArgumentException">Thrown when the pair is null, empty, or not in the correct format.</exception>
4853
public MarketSymbol(string pair, ContractType contractType = ContractType.Spot)
4954
{
5055
if (string.IsNullOrWhiteSpace(pair))
5156
throw new ArgumentException("Pair cannot be null or empty", nameof(pair));
5257

53-
var assets = pair.Split('/', StringSplitOptions.RemoveEmptyEntries);
58+
var assets = pair.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
5459
if (assets.Length != 2)
55-
throw new ArgumentException("Pair must be in the format 'BASE/QUOTE'", nameof(pair));
60+
throw new ArgumentException("Pair must be in the format 'BASE/QUOTE' (e.g., 'BTC/USDT')", nameof(pair));
61+
62+
if (string.IsNullOrWhiteSpace(assets[0]))
63+
throw new ArgumentException("Base asset cannot be empty", nameof(pair));
64+
if (string.IsNullOrWhiteSpace(assets[1]))
65+
throw new ArgumentException("Quote asset cannot be empty", nameof(pair));
5666

57-
BaseAsset = assets[0];
58-
QuoteAsset = assets[1];
67+
BaseAsset = assets[0].ToUpperInvariant();
68+
QuoteAsset = assets[1].ToUpperInvariant();
5969
ContractType = contractType;
6070
}
6171

@@ -70,14 +80,17 @@ public MarketSymbol(string pair, ContractType contractType = ContractType.Spot)
7080
public string QuoteAsset { get; init; }
7181

7282
/// <summary>
73-
/// The symbol name, can be used to overwrite the default formatted name
83+
/// The symbol name in Binance API format (e.g., "BTCUSDT")
7484
/// </summary>
7585
public string FullName => $"{BaseAsset}{QuoteAsset}";
7686

7787
/// <summary>
78-
/// The Contract type of the symbol
88+
/// The contract type of the symbol (Spot or Futures)
7989
/// </summary>
8090
public ContractType ContractType { get; init; }
8191

82-
override public string ToString() => $"{BaseAsset}/{QuoteAsset} ({ContractType})";
92+
/// <summary>
93+
/// Returns a string representation in the format "BASE/QUOTE (ContractType)" (e.g., "BTC/USDT (Spot)")
94+
/// </summary>
95+
public override string ToString() => $"{BaseAsset}/{QuoteAsset} ({ContractType})";
8396
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
using BinanceBot.Market.Domain;
2+
3+
namespace BinanceBot.Market.Tests.Domain;
4+
5+
public class MarketSymbolTests
6+
{
7+
#region Constructor Tests - Three Parameters
8+
9+
[Fact]
10+
public void Constructor_WithValidParameters_CreatesInstance()
11+
{
12+
// Arrange & Act
13+
var symbol = new MarketSymbol("BTC", "USDT", ContractType.Spot);
14+
15+
// Assert
16+
Assert.Equal("BTC", symbol.BaseAsset);
17+
Assert.Equal("USDT", symbol.QuoteAsset);
18+
Assert.Equal(ContractType.Spot, symbol.ContractType);
19+
Assert.Equal("BTCUSDT", symbol.FullName);
20+
Assert.Equal("BTC/USDT (Spot)", symbol.ToString());
21+
}
22+
23+
[Theory]
24+
[InlineData("btc", "usdt", "BTC", "USDT")]
25+
[InlineData("BTC", "USDT", "BTC", "USDT")]
26+
[InlineData("Eth", "BtC", "ETH", "BTC")]
27+
[InlineData(" BNB ", " BUSD ", "BNB", "BUSD")]
28+
public void Constructor_NormalizesToUpperCase(string baseInput, string quoteInput, string expectedBase, string expectedQuote)
29+
{
30+
// Act
31+
var symbol = new MarketSymbol(baseInput, quoteInput, ContractType.Spot);
32+
33+
// Assert
34+
Assert.Equal(expectedBase, symbol.BaseAsset);
35+
Assert.Equal(expectedQuote, symbol.QuoteAsset);
36+
}
37+
38+
[Fact]
39+
public void Constructor_WithFuturesContractType_CreatesInstance()
40+
{
41+
// Act
42+
var symbol = new MarketSymbol("ETH", "USDT", ContractType.Futures);
43+
44+
// Assert
45+
Assert.Equal(ContractType.Futures, symbol.ContractType);
46+
Assert.Equal("ETH/USDT (Futures)", symbol.ToString());
47+
}
48+
49+
[Fact]
50+
public void Constructor_WithNullBaseAsset_ThrowsArgumentNullException()
51+
{
52+
// Act & Assert
53+
var ex = Assert.Throws<ArgumentNullException>(() =>
54+
new MarketSymbol(null, "USDT", ContractType.Spot));
55+
Assert.Equal("baseAsset", ex.ParamName);
56+
}
57+
58+
[Fact]
59+
public void Constructor_WithNullQuoteAsset_ThrowsArgumentNullException()
60+
{
61+
// Act & Assert
62+
var ex = Assert.Throws<ArgumentNullException>(() =>
63+
new MarketSymbol("BTC", null, ContractType.Spot));
64+
Assert.Equal("quoteAsset", ex.ParamName);
65+
}
66+
67+
[Theory]
68+
[InlineData("")]
69+
[InlineData(" ")]
70+
[InlineData("\t")]
71+
public void Constructor_WithEmptyBaseAsset_ThrowsArgumentException(string emptyValue)
72+
{
73+
// Act & Assert
74+
var ex = Assert.Throws<ArgumentException>(() =>
75+
new MarketSymbol(emptyValue, "USDT", ContractType.Spot));
76+
Assert.Equal("baseAsset", ex.ParamName);
77+
Assert.Contains("empty or whitespace", ex.Message);
78+
}
79+
80+
[Theory]
81+
[InlineData("")]
82+
[InlineData(" ")]
83+
[InlineData("\t")]
84+
public void Constructor_WithEmptyQuoteAsset_ThrowsArgumentException(string emptyValue)
85+
{
86+
// Act & Assert
87+
var ex = Assert.Throws<ArgumentException>(() =>
88+
new MarketSymbol("BTC", emptyValue, ContractType.Spot));
89+
Assert.Equal("quoteAsset", ex.ParamName);
90+
Assert.Contains("empty or whitespace", ex.Message);
91+
}
92+
93+
#endregion
94+
95+
#region Constructor Tests - Pair String
96+
97+
[Theory]
98+
[InlineData("BTC/USDT", "BTC", "USDT")]
99+
[InlineData("ETH/BTC", "ETH", "BTC")]
100+
[InlineData("BNB/BUSD", "BNB", "BUSD")]
101+
[InlineData("DOGE/USDT", "DOGE", "USDT")]
102+
public void Constructor_WithValidPair_CreatesInstance(string pair, string expectedBase, string expectedQuote)
103+
{
104+
// Act
105+
var symbol = new MarketSymbol(pair);
106+
107+
// Assert
108+
Assert.Equal(expectedBase, symbol.BaseAsset);
109+
Assert.Equal(expectedQuote, symbol.QuoteAsset);
110+
Assert.Equal(ContractType.Spot, symbol.ContractType);
111+
}
112+
113+
[Theory]
114+
[InlineData("btc/usdt", "BTC", "USDT")]
115+
[InlineData("Eth/Btc", "ETH", "BTC")]
116+
[InlineData(" BNB / BUSD ", "BNB", "BUSD")]
117+
public void Constructor_WithPair_NormalizesToUpperCase(string pair, string expectedBase, string expectedQuote)
118+
{
119+
// Act
120+
var symbol = new MarketSymbol(pair);
121+
122+
// Assert
123+
Assert.Equal(expectedBase, symbol.BaseAsset);
124+
Assert.Equal(expectedQuote, symbol.QuoteAsset);
125+
}
126+
127+
[Fact]
128+
public void Constructor_WithPairAndFutures_CreatesInstance()
129+
{
130+
// Act
131+
var symbol = new MarketSymbol("BTC/USDT", ContractType.Futures);
132+
133+
// Assert
134+
Assert.Equal("BTC", symbol.BaseAsset);
135+
Assert.Equal("USDT", symbol.QuoteAsset);
136+
Assert.Equal(ContractType.Futures, symbol.ContractType);
137+
}
138+
139+
[Theory]
140+
[InlineData(null!)]
141+
[InlineData("")]
142+
[InlineData(" ")]
143+
public void Constructor_WithNullOrEmptyPair_ThrowsArgumentException(string? invalidPair)
144+
{
145+
// Act & Assert
146+
var ex = Assert.Throws<ArgumentException>(() =>
147+
new MarketSymbol(invalidPair));
148+
Assert.Equal("pair", ex.ParamName);
149+
Assert.Contains("null or empty", ex.Message);
150+
}
151+
152+
[Theory]
153+
[InlineData("BTCUSDT")]
154+
[InlineData("BTC")]
155+
[InlineData("BTC/USDT/EUR")]
156+
[InlineData("BTC-USDT")]
157+
public void Constructor_WithInvalidPairFormat_ThrowsArgumentException(string invalidPair)
158+
{
159+
// Act & Assert
160+
var ex = Assert.Throws<ArgumentException>(() =>
161+
new MarketSymbol(invalidPair));
162+
Assert.Equal("pair", ex.ParamName);
163+
Assert.Contains("BASE/QUOTE", ex.Message);
164+
}
165+
166+
[Theory]
167+
[InlineData("/USDT")]
168+
[InlineData("BTC/")]
169+
[InlineData(" / ")]
170+
public void Constructor_WithEmptyAssetInPair_ThrowsArgumentException(string invalidPair)
171+
{
172+
// Act & Assert
173+
Assert.Throws<ArgumentException>(() =>
174+
new MarketSymbol(invalidPair));
175+
}
176+
177+
#endregion
178+
179+
#region Property Tests
180+
181+
[Fact]
182+
public void FullName_ReturnsCorrectFormat()
183+
{
184+
// Arrange
185+
var symbol = new MarketSymbol("BTC", "USDT", ContractType.Spot);
186+
187+
// Act & Assert
188+
Assert.Equal("BTCUSDT", symbol.FullName);
189+
}
190+
191+
[Fact]
192+
public void ToString_ReturnsCorrectFormat()
193+
{
194+
// Arrange
195+
var spotSymbol = new MarketSymbol("BTC", "USDT", ContractType.Spot);
196+
var futuresSymbol = new MarketSymbol("ETH", "BTC", ContractType.Futures);
197+
198+
// Act & Assert
199+
Assert.Equal("BTC/USDT (Spot)", spotSymbol.ToString());
200+
Assert.Equal("ETH/BTC (Futures)", futuresSymbol.ToString());
201+
}
202+
203+
#endregion
204+
205+
#region Record Equality Tests
206+
207+
[Fact]
208+
public void Equals_WithSameValues_ReturnsTrue()
209+
{
210+
// Arrange
211+
var symbol1 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
212+
var symbol2 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
213+
214+
// Act & Assert
215+
Assert.Equal(symbol1, symbol2);
216+
Assert.True(symbol1 == symbol2);
217+
}
218+
219+
[Fact]
220+
public void Equals_WithDifferentBaseAsset_ReturnsFalse()
221+
{
222+
// Arrange
223+
var symbol1 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
224+
var symbol2 = new MarketSymbol("ETH", "USDT", ContractType.Spot);
225+
226+
// Act & Assert
227+
Assert.NotEqual(symbol1, symbol2);
228+
Assert.True(symbol1 != symbol2);
229+
}
230+
231+
[Fact]
232+
public void Equals_WithDifferentContractType_ReturnsFalse()
233+
{
234+
// Arrange
235+
var symbol1 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
236+
var symbol2 = new MarketSymbol("BTC", "USDT", ContractType.Futures);
237+
238+
// Act & Assert
239+
Assert.NotEqual(symbol1, symbol2);
240+
}
241+
242+
[Fact]
243+
public void GetHashCode_WithSameValues_ReturnsSameHashCode()
244+
{
245+
// Arrange
246+
var symbol1 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
247+
var symbol2 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
248+
249+
// Act & Assert
250+
Assert.Equal(symbol1.GetHashCode(), symbol2.GetHashCode());
251+
}
252+
253+
#endregion
254+
255+
#region Integration Tests
256+
257+
[Fact]
258+
public void BothConstructors_WithSameData_CreateEqualInstances()
259+
{
260+
// Arrange & Act
261+
var symbol1 = new MarketSymbol("BTC", "USDT", ContractType.Spot);
262+
var symbol2 = new MarketSymbol("BTC/USDT", ContractType.Spot);
263+
264+
// Assert
265+
Assert.Equal(symbol1, symbol2);
266+
}
267+
268+
[Fact]
269+
public void BothConstructors_WithCaseInsensitiveInput_CreateEqualInstances()
270+
{
271+
// Arrange & Act
272+
var symbol1 = new MarketSymbol("btc", "usdt", ContractType.Futures);
273+
var symbol2 = new MarketSymbol("BTC/USDT", ContractType.Futures);
274+
275+
// Assert
276+
Assert.Equal(symbol1, symbol2);
277+
}
278+
279+
#endregion
280+
}

0 commit comments

Comments
 (0)