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.
This commit is contained in:
2026-05-05 01:20:28 +03:00
parent e4b03f42ef
commit 61114ea31b
60 changed files with 1845 additions and 19 deletions
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Marathon.Application\Marathon.Application.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,8 @@
// Phase 4 will add real tests to this project.
namespace Marathon.Application.Tests;
public sealed class PlaceholderTest
{
[Fact]
public void Placeholder_AlwaysPasses() => Assert.True(true);
}
@@ -0,0 +1,68 @@
using FluentAssertions;
using Marathon.Domain.Entities;
using Marathon.Domain.Enums;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.Entities;
public sealed class AnomalyTests
{
private static readonly Guid SampleId = Guid.NewGuid();
private static readonly EventId SampleEventId = new("26456117");
[Fact]
public void Constructor_CreatesAnomaly_WhenAllParametersAreValid()
{
var anomaly = new Anomaly(
SampleId,
SampleEventId,
DateTimeOffset.UtcNow,
AnomalyKind.SuspensionFlip,
0.85m,
"{}");
anomaly.Id.Should().Be(SampleId);
anomaly.Score.Should().Be(0.85m);
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenIdIsEmptyGuid()
{
var act = () => new Anomaly(Guid.Empty, SampleEventId, DateTimeOffset.UtcNow,
AnomalyKind.SuspensionFlip, 0.5m, "{}");
act.Should().Throw<ArgumentException>().WithParameterName("Id");
}
[Theory]
[InlineData(-0.01)]
[InlineData(1.01)]
[InlineData(2.0)]
[InlineData(-1.0)]
public void Constructor_ThrowsArgumentOutOfRangeException_WhenScoreIsOutOfRange(double score)
{
var act = () => new Anomaly(SampleId, SampleEventId, DateTimeOffset.UtcNow,
AnomalyKind.SuspensionFlip, (decimal)score, "{}");
act.Should().Throw<ArgumentOutOfRangeException>().WithParameterName("Score");
}
[Theory]
[InlineData(0.0)]
[InlineData(0.5)]
[InlineData(1.0)]
public void Constructor_CreatesAnomaly_WhenScoreIsInValidRange(double score)
{
var act = () => new Anomaly(SampleId, SampleEventId, DateTimeOffset.UtcNow,
AnomalyKind.SuspensionFlip, (decimal)score, "{}");
act.Should().NotThrow();
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Constructor_ThrowsArgumentException_WhenEvidenceJsonIsEmptyOrWhitespace(string evidence)
{
var act = () => new Anomaly(SampleId, SampleEventId, DateTimeOffset.UtcNow,
AnomalyKind.SuspensionFlip, 0.5m, evidence);
act.Should().Throw<ArgumentException>().WithParameterName("EvidenceJson");
}
}
@@ -0,0 +1,170 @@
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.");
}
}
@@ -0,0 +1,130 @@
using FluentAssertions;
using Marathon.Domain.Entities;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.Entities;
public sealed class EventTests
{
private static readonly EventId SampleId = new("26456117");
private static readonly SportCode SampleSport = new(6);
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
private static readonly DateTimeOffset ValidScheduledAt =
new(2026, 5, 10, 18, 0, 0, MoscowOffset);
private static Event CreateValidEvent(DateTimeOffset? scheduledAt = null) =>
new(
SampleId,
SampleSport,
"RU",
"nba-league-1",
"Play-Offs",
scheduledAt ?? ValidScheduledAt,
"Арсенал",
"Атлетико");
[Fact]
public void Constructor_CreatesEvent_WhenAllParametersAreValid()
{
var evt = CreateValidEvent();
evt.Id.Should().Be(SampleId);
evt.Sport.Should().Be(SampleSport);
evt.ScheduledAt.Offset.Should().Be(MoscowOffset);
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenScheduledAtIsNotMoscowTime()
{
// UTC+0 — wrong offset
var utcTime = new DateTimeOffset(2026, 5, 10, 15, 0, 0, TimeSpan.Zero);
var act = () => CreateValidEvent(utcTime);
act.Should().Throw<ArgumentException>()
.WithParameterName("ScheduledAt");
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenScheduledAtIsOtherOffset()
{
// UTC+2 — wrong offset (CEST, for example)
var cestTime = new DateTimeOffset(2026, 5, 10, 17, 0, 0, TimeSpan.FromHours(2));
var act = () => CreateValidEvent(cestTime);
act.Should().Throw<ArgumentException>()
.WithParameterName("ScheduledAt");
}
[Fact]
public void ScheduledAt_Offset_IsMoscowTime()
{
var evt = CreateValidEvent();
evt.ScheduledAt.Offset.Should().Be(TimeSpan.FromHours(3));
}
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenIdIsNull()
{
var act = () => new Event(null!, SampleSport, "RU", "l1", "cat", ValidScheduledAt, "A", "B");
act.Should().Throw<ArgumentNullException>().WithParameterName("Id");
}
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenSportIsNull()
{
var act = () => new Event(SampleId, null!, "RU", "l1", "cat", ValidScheduledAt, "A", "B");
act.Should().Throw<ArgumentNullException>().WithParameterName("Sport");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Constructor_ThrowsArgumentException_WhenCountryCodeIsEmptyOrWhitespace(string code)
{
var act = () => new Event(SampleId, SampleSport, code, "l1", "cat", ValidScheduledAt, "A", "B");
act.Should().Throw<ArgumentException>().WithParameterName("CountryCode");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Constructor_ThrowsArgumentException_WhenLeagueIdIsEmptyOrWhitespace(string leagueId)
{
var act = () => new Event(SampleId, SampleSport, "RU", leagueId, "cat", ValidScheduledAt, "A", "B");
act.Should().Throw<ArgumentException>().WithParameterName("LeagueId");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Constructor_ThrowsArgumentException_WhenSide1NameIsEmptyOrWhitespace(string name)
{
var act = () => new Event(SampleId, SampleSport, "RU", "l1", "cat", ValidScheduledAt, name, "B");
act.Should().Throw<ArgumentException>().WithParameterName("Side1Name");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Constructor_ThrowsArgumentException_WhenSide2NameIsEmptyOrWhitespace(string name)
{
var act = () => new Event(SampleId, SampleSport, "RU", "l1", "cat", ValidScheduledAt, "A", name);
act.Should().Throw<ArgumentException>().WithParameterName("Side2Name");
}
[Fact]
public void Category_CanBeEmptyString()
{
// Category is optional (deep breadcrumbs may not exist)
var evt = new Event(SampleId, SampleSport, "RU", "l1", string.Empty, ValidScheduledAt, "A", "B");
evt.Category.Should().Be(string.Empty);
}
[Fact]
public void Event_IsImmutable_NoSettablePublicProperties()
{
var eventType = typeof(Event);
var settableProperties = eventType.GetProperties()
.Where(p => p.CanWrite && p.GetSetMethod(nonPublic: false) is not null)
.ToList();
settableProperties.Should().BeEmpty("Event must be immutable.");
}
}
@@ -0,0 +1,59 @@
using FluentAssertions;
using Marathon.Domain.Entities;
using Marathon.Domain.Enums;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.Entities;
public sealed class OddsSnapshotTests
{
private static readonly EventId SampleEventId = new("26456117");
private static readonly DateTimeOffset SampleCapturedAt = DateTimeOffset.UtcNow;
private static readonly IReadOnlyList<Bet> OneBet =
[
new Bet(MatchScope.Instance, BetType.Win, Side.Side1, null, new OddsRate(1.85m)),
];
[Fact]
public void Constructor_CreatesInstance_WhenBetsIsNonEmpty()
{
var snapshot = new OddsSnapshot(SampleEventId, SampleCapturedAt, OddsSource.PreMatch, OneBet);
snapshot.EventId.Should().Be(SampleEventId);
snapshot.Bets.Should().HaveCount(1);
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenBetsIsEmpty()
{
var act = () => new OddsSnapshot(SampleEventId, SampleCapturedAt, OddsSource.PreMatch, []);
act.Should().Throw<ArgumentException>()
.WithParameterName("bets");
}
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenEventIdIsNull()
{
var act = () => new OddsSnapshot(null!, SampleCapturedAt, OddsSource.PreMatch, OneBet);
act.Should().Throw<ArgumentNullException>()
.WithParameterName("eventId");
}
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenBetsIsNull()
{
var act = () => new OddsSnapshot(SampleEventId, SampleCapturedAt, OddsSource.PreMatch, null!);
act.Should().Throw<ArgumentNullException>()
.WithParameterName("bets");
}
[Fact]
public void OddsSnapshot_IsImmutable_NoSettablePublicProperties()
{
var snapshotType = typeof(OddsSnapshot);
var settableProperties = snapshotType.GetProperties()
.Where(p => p.CanWrite && p.GetSetMethod(nonPublic: false) is not null)
.ToList();
settableProperties.Should().BeEmpty("OddsSnapshot must be immutable.");
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Marathon.Domain\Marathon.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,71 @@
using FluentAssertions;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.ValueObjects;
public sealed class BetScopeTests
{
[Fact]
public void MatchScope_Singleton_IsTheSameInstance()
{
MatchScope.Instance.Should().BeSameAs(MatchScope.Instance);
}
[Fact]
public void PeriodScope_CreatesInstance_WhenNumberIsPositive()
{
var scope = new PeriodScope(1);
scope.Number.Should().Be(1);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(int.MinValue)]
public void PeriodScope_ThrowsArgumentOutOfRangeException_WhenNumberIsZeroOrNegative(int number)
{
var act = () => new PeriodScope(number);
act.Should().Throw<ArgumentOutOfRangeException>()
.WithParameterName("Number");
}
[Fact]
public void BetScope_PatternMatching_WorksCorrectly()
{
BetScope match = MatchScope.Instance;
BetScope period = new PeriodScope(2);
var matchResult = match switch
{
MatchScope => "match",
PeriodScope(var n) => $"period-{n}",
_ => "other",
};
var periodResult = period switch
{
MatchScope => "match",
PeriodScope(var n) => $"period-{n}",
_ => "other",
};
matchResult.Should().Be("match");
periodResult.Should().Be("period-2");
}
[Fact]
public void PeriodScope_Equality_IsValueBased()
{
var a = new PeriodScope(1);
var b = new PeriodScope(1);
a.Should().Be(b);
}
[Fact]
public void MatchScope_And_PeriodScope_AreNotEqual()
{
BetScope match = MatchScope.Instance;
BetScope period = new PeriodScope(1);
match.Should().NotBe(period);
}
}
@@ -0,0 +1,48 @@
using FluentAssertions;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.ValueObjects;
public sealed class EventIdTests
{
[Fact]
public void Constructor_CreatesInstance_WhenValueIsNonEmpty()
{
var id = new EventId("26456117");
id.Value.Should().Be("26456117");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("\t")]
public void Constructor_ThrowsArgumentException_WhenValueIsEmptyOrWhitespace(string value)
{
var act = () => new EventId(value);
act.Should().Throw<ArgumentException>()
.WithParameterName("value");
}
[Fact]
public void Constructor_ThrowsArgumentException_WhenValueIsNull()
{
var act = () => new EventId(null!);
act.Should().Throw<ArgumentException>()
.WithParameterName("value");
}
[Fact]
public void ToString_ReturnsValue()
{
var id = new EventId("12345");
id.ToString().Should().Be("12345");
}
[Fact]
public void Equality_IsValueBased()
{
var a = new EventId("26456117");
var b = new EventId("26456117");
a.Should().Be(b);
}
}
@@ -0,0 +1,40 @@
using FluentAssertions;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.ValueObjects;
public sealed class OddsRateTests
{
[Theory]
[InlineData("1.01")]
[InlineData("1.65")]
[InlineData("10.5")]
[InlineData("100.0")]
public void Constructor_CreatesInstance_WhenValueIsGreaterThanOne(string rawValue)
{
var value = decimal.Parse(rawValue);
var rate = new OddsRate(value);
rate.Value.Should().Be(value);
}
[Theory]
[InlineData("1.0")]
[InlineData("0.99")]
[InlineData("0.0")]
[InlineData("-1.5")]
public void Constructor_ThrowsArgumentOutOfRangeException_WhenValueIsOneOrLess(string rawValue)
{
var value = decimal.Parse(rawValue);
var act = () => new OddsRate(value);
act.Should().Throw<ArgumentOutOfRangeException>()
.WithParameterName("value");
}
[Fact]
public void Equality_IsValueBased()
{
var a = new OddsRate(1.65m);
var b = new OddsRate(1.65m);
a.Should().Be(b);
}
}
@@ -0,0 +1,36 @@
using FluentAssertions;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.ValueObjects;
public sealed class OddsValueTests
{
[Theory]
[InlineData("3.5")]
[InlineData("213.5")]
[InlineData("-1.5")]
[InlineData("-0.5")]
[InlineData("0.5")]
public void Constructor_CreatesInstance_WhenValueIsNonZero(string rawValue)
{
var value = decimal.Parse(rawValue);
var oddsValue = new OddsValue(value);
oddsValue.Value.Should().Be(value);
}
[Fact]
public void Constructor_ThrowsArgumentOutOfRangeException_WhenValueIsZero()
{
var act = () => new OddsValue(0m);
act.Should().Throw<ArgumentOutOfRangeException>()
.WithParameterName("value");
}
[Fact]
public void Equality_IsValueBased()
{
var a = new OddsValue(3.5m);
var b = new OddsValue(3.5m);
a.Should().Be(b);
}
}
@@ -0,0 +1,41 @@
using FluentAssertions;
using Marathon.Domain.ValueObjects;
namespace Marathon.Domain.Tests.ValueObjects;
public sealed class SportCodeTests
{
[Fact]
public void Constructor_CreatesInstance_WhenValueIsPositive()
{
var code = new SportCode(6);
code.Value.Should().Be(6);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(int.MinValue)]
public void Constructor_ThrowsArgumentOutOfRangeException_WhenValueIsZeroOrNegative(int value)
{
var act = () => new SportCode(value);
act.Should().Throw<ArgumentOutOfRangeException>()
.WithParameterName("value");
}
[Fact]
public void ToString_ReturnsStringRepresentationOfValue()
{
var code = new SportCode(22723);
code.ToString().Should().Be("22723");
}
[Fact]
public void Equality_IsValueBased()
{
var a = new SportCode(6);
var b = new SportCode(6);
a.Should().Be(b);
a.Should().NotBeSameAs(b);
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Marathon.Infrastructure\Marathon.Infrastructure.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,8 @@
// Phases 2/3 will add real tests to this project.
namespace Marathon.Infrastructure.Tests;
public sealed class PlaceholderTest
{
[Fact]
public void Placeholder_AlwaysPasses() => Assert.True(true);
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Marathon.UI\Marathon.UI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,8 @@
// Phase 5/6 will add real tests to this project.
namespace Marathon.UI.Tests;
public sealed class PlaceholderTest
{
[Fact]
public void Placeholder_AlwaysPasses() => Assert.True(true);
}