Files
maraphon-app/tests/Marathon.Domain.Tests/Entities/BetTests.cs
T
alexei.dolgolyov 61114ea31b feat: implement Phase 1 — solution skeleton and domain model
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.
2026-05-05 01:20:28 +03:00

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.");
}
}