61114ea31b
Creates the 9-project .NET 8 solution (5 src + 4 test) with Marathon.Domain fully implemented: value objects (SportCode, EventId, OddsRate, OddsValue, BetScope hierarchy), enums (Side, BetType, OddsSource, AnomalyKind), and entities (Sport, Country, League, Event, Bet, OddsSnapshot, EventResult, Anomaly) with all invariants enforced in constructors. 96 domain tests pass (FluentAssertions + xUnit). Directory.Build.props and Directory.Packages.props centralise build settings and NuGet versions. Both Marathon.sln and Marathon.slnx are committed; dotnet build Marathon.sln succeeds with 0 warnings/errors.
171 lines
6.5 KiB
C#
171 lines
6.5 KiB
C#
using FluentAssertions;
|
|
using Marathon.Domain.Entities;
|
|
using Marathon.Domain.Enums;
|
|
using Marathon.Domain.ValueObjects;
|
|
|
|
namespace Marathon.Domain.Tests.Entities;
|
|
|
|
public sealed class BetTests
|
|
{
|
|
private static readonly BetScope MatchScope = Marathon.Domain.ValueObjects.MatchScope.Instance;
|
|
private static readonly OddsRate SampleRate = new(1.85m);
|
|
private static readonly OddsValue SampleValue = new(3.5m);
|
|
|
|
// ── Win bets ──────────────────────────────────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData(Side.Side1)]
|
|
[InlineData(Side.Side2)]
|
|
public void Constructor_CreatesWinBet_WhenSideIs1Or2AndValueIsNull(Side side)
|
|
{
|
|
var bet = new Bet(MatchScope, BetType.Win, side, null, SampleRate);
|
|
bet.Type.Should().Be(BetType.Win);
|
|
bet.Side.Should().Be(side);
|
|
bet.Value.Should().BeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Side.Draw)]
|
|
[InlineData(Side.Less)]
|
|
[InlineData(Side.More)]
|
|
public void Constructor_ThrowsArgumentException_WhenWinBetHasInvalidSide(Side side)
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Win, side, null, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("side");
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsArgumentException_WhenWinBetHasNonNullValue()
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Win, Side.Side1, SampleValue, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("value");
|
|
}
|
|
|
|
// ── Draw bets ─────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Constructor_CreatesDrawBet_WhenSideIsDrawAndValueIsNull()
|
|
{
|
|
var bet = new Bet(MatchScope, BetType.Draw, Side.Draw, null, SampleRate);
|
|
bet.Type.Should().Be(BetType.Draw);
|
|
bet.Side.Should().Be(Side.Draw);
|
|
bet.Value.Should().BeNull();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Side.Side1)]
|
|
[InlineData(Side.Side2)]
|
|
[InlineData(Side.Less)]
|
|
[InlineData(Side.More)]
|
|
public void Constructor_ThrowsArgumentException_WhenDrawBetHasInvalidSide(Side side)
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Draw, side, null, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("side");
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsArgumentException_WhenDrawBetHasNonNullValue()
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Draw, Side.Draw, SampleValue, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("value");
|
|
}
|
|
|
|
// ── WinFora bets ──────────────────────────────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData(Side.Side1)]
|
|
[InlineData(Side.Side2)]
|
|
public void Constructor_CreatesWinForaBet_WhenSideIs1Or2AndValueIsNonNull(Side side)
|
|
{
|
|
var bet = new Bet(MatchScope, BetType.WinFora, side, SampleValue, SampleRate);
|
|
bet.Type.Should().Be(BetType.WinFora);
|
|
bet.Value.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsArgumentException_WhenWinForaBetHasNullValue()
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.WinFora, Side.Side1, null, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("value");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Side.Draw)]
|
|
[InlineData(Side.Less)]
|
|
[InlineData(Side.More)]
|
|
public void Constructor_ThrowsArgumentException_WhenWinForaBetHasInvalidSide(Side side)
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.WinFora, side, SampleValue, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("side");
|
|
}
|
|
|
|
// ── Total bets ────────────────────────────────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData(Side.Less)]
|
|
[InlineData(Side.More)]
|
|
public void Constructor_CreatesTotalBet_WhenSideIsLessOrMoreAndValueIsNonNull(Side side)
|
|
{
|
|
var bet = new Bet(MatchScope, BetType.Total, side, SampleValue, SampleRate);
|
|
bet.Type.Should().Be(BetType.Total);
|
|
bet.Value.Should().NotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsArgumentException_WhenTotalBetHasNullValue()
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Total, Side.Less, null, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("value");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Side.Side1)]
|
|
[InlineData(Side.Side2)]
|
|
[InlineData(Side.Draw)]
|
|
public void Constructor_ThrowsArgumentException_WhenTotalBetHasInvalidSide(Side side)
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Total, side, SampleValue, SampleRate);
|
|
act.Should().Throw<ArgumentException>()
|
|
.WithParameterName("side");
|
|
}
|
|
|
|
// ── Null guards ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsArgumentNullException_WhenScopeIsNull()
|
|
{
|
|
var act = () => new Bet(null!, BetType.Win, Side.Side1, null, SampleRate);
|
|
act.Should().Throw<ArgumentNullException>()
|
|
.WithParameterName("scope");
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_ThrowsArgumentNullException_WhenRateIsNull()
|
|
{
|
|
var act = () => new Bet(MatchScope, BetType.Win, Side.Side1, null, null!);
|
|
act.Should().Throw<ArgumentNullException>()
|
|
.WithParameterName("rate");
|
|
}
|
|
|
|
// ── Immutability ──────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Bet_IsImmutable_NoSettablePublicProperties()
|
|
{
|
|
var betType = typeof(Bet);
|
|
var settableProperties = betType.GetProperties()
|
|
.Where(p => p.CanWrite && p.GetSetMethod(nonPublic: false) is not null)
|
|
.ToList();
|
|
|
|
settableProperties.Should().BeEmpty(
|
|
"Bet is a sealed record with only get-only properties — immutability is required.");
|
|
}
|
|
}
|