feat: project setup and core models with TDD

MILESTONE 1 COMPLETE:

Project Setup:
- Create .NET 8 MAUI solution targeting iOS, macOS, and web
- Configure nullable reference types and warnings as errors
- Add required dependencies (MAUI 8.0.3, SQLite, CommunityToolkit.Mvvm)
- Add testing dependencies (xUnit, NSubstitute, FluentAssertions, Coverlet)
- Create comprehensive .editorconfig with C# coding standards
- Update CLAUDE.md with development commands

Core Models (TDD - 48 tests, all passing):
- EstimateMode enum (Work, Generic, Humorous modes)
- EstimateResult model with validation (intensity 0.0-1.0, non-null text)
- ShakeData model with intensity validation
- AppSettings model with defaults (Work mode, max history 10)

Build Status:
- All platforms build successfully (net8.0, iOS, macOS)
- 0 warnings, 0 errors
- Test coverage: 51.28% line, 87.5% branch (models have ~100% coverage)

Per spec: Strict TDD followed (RED-GREEN-REFACTOR), models fully validated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 12:12:36 +02:00
parent 17df545fba
commit 83ec08f14b
49 changed files with 2725 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
using FluentAssertions;
using HihaArvio.Models;
namespace HihaArvio.Tests.Models;
public class AppSettingsTests
{
[Fact]
public void AppSettings_DefaultConstructor_ShouldSetWorkModeAsDefault()
{
// Act
var settings = new AppSettings();
// Assert
settings.SelectedMode.Should().Be(EstimateMode.Work);
}
[Fact]
public void AppSettings_DefaultConstructor_ShouldSetMaxHistorySizeTo10()
{
// Act
var settings = new AppSettings();
// Assert
settings.MaxHistorySize.Should().Be(10);
}
[Theory]
[InlineData(EstimateMode.Work)]
[InlineData(EstimateMode.Generic)]
[InlineData(EstimateMode.Humorous)]
public void AppSettings_ShouldAllowModeChanges(EstimateMode mode)
{
// Arrange
var settings = new AppSettings();
// Act
settings.SelectedMode = mode;
// Assert
settings.SelectedMode.Should().Be(mode);
}
[Theory]
[InlineData(1)]
[InlineData(5)]
[InlineData(10)]
[InlineData(50)]
[InlineData(100)]
public void AppSettings_ShouldAcceptValidMaxHistorySizeValues(int size)
{
// Arrange
var settings = new AppSettings();
// Act
settings.MaxHistorySize = size;
// Assert
settings.MaxHistorySize.Should().Be(size);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-10)]
public void AppSettings_ShouldThrowForInvalidMaxHistorySize(int invalidSize)
{
// Arrange
var settings = new AppSettings();
// Act
Action act = () => settings.MaxHistorySize = invalidSize;
// Assert
act.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("*must be greater than 0*");
}
[Fact]
public void AppSettings_ShouldCreateWithDefaultValues()
{
// Act
var settings = new AppSettings();
// Assert
settings.Should().NotBeNull();
settings.SelectedMode.Should().Be(EstimateMode.Work);
settings.MaxHistorySize.Should().Be(10);
}
}

View File

@@ -0,0 +1,51 @@
using FluentAssertions;
using HihaArvio.Models;
namespace HihaArvio.Tests.Models;
public class EstimateModeTests
{
[Fact]
public void EstimateMode_ShouldHaveWorkValue()
{
// Act
var mode = EstimateMode.Work;
// Assert
mode.Should().Be(EstimateMode.Work);
((int)mode).Should().Be(0);
}
[Fact]
public void EstimateMode_ShouldHaveGenericValue()
{
// Act
var mode = EstimateMode.Generic;
// Assert
mode.Should().Be(EstimateMode.Generic);
((int)mode).Should().Be(1);
}
[Fact]
public void EstimateMode_ShouldHaveHumorousValue()
{
// Act
var mode = EstimateMode.Humorous;
// Assert
mode.Should().Be(EstimateMode.Humorous);
((int)mode).Should().Be(2);
}
[Fact]
public void EstimateMode_ShouldHaveExactlyThreeValues()
{
// Act
var values = Enum.GetValues<EstimateMode>();
// Assert
values.Should().HaveCount(3);
values.Should().Contain(new[] { EstimateMode.Work, EstimateMode.Generic, EstimateMode.Humorous });
}
}

View File

@@ -0,0 +1,147 @@
using FluentAssertions;
using HihaArvio.Models;
namespace HihaArvio.Tests.Models;
public class EstimateResultTests
{
[Fact]
public void EstimateResult_ShouldCreateWithAllProperties()
{
// Arrange
var id = Guid.NewGuid();
var timestamp = DateTimeOffset.UtcNow;
var estimateText = "2 weeks";
var mode = EstimateMode.Work;
var intensity = 0.75;
var duration = TimeSpan.FromSeconds(5);
// Act
var result = new EstimateResult
{
Id = id,
Timestamp = timestamp,
EstimateText = estimateText,
Mode = mode,
ShakeIntensity = intensity,
ShakeDuration = duration
};
// Assert
result.Id.Should().Be(id);
result.Timestamp.Should().Be(timestamp);
result.EstimateText.Should().Be(estimateText);
result.Mode.Should().Be(mode);
result.ShakeIntensity.Should().Be(intensity);
result.ShakeDuration.Should().Be(duration);
}
[Theory]
[InlineData(0.0)]
[InlineData(0.3)]
[InlineData(0.7)]
[InlineData(1.0)]
public void EstimateResult_ShouldAcceptValidIntensityValues(double intensity)
{
// Act
var result = new EstimateResult { ShakeIntensity = intensity };
// Assert
result.ShakeIntensity.Should().Be(intensity);
}
[Theory]
[InlineData(-0.1)]
[InlineData(1.1)]
[InlineData(2.0)]
public void EstimateResult_ShouldThrowForInvalidIntensity(double invalidIntensity)
{
// Act
Action act = () => _ = new EstimateResult { ShakeIntensity = invalidIntensity };
// Assert
act.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("*must be between 0.0 and 1.0*");
}
[Fact]
public void EstimateResult_ShouldThrowForNullEstimateText()
{
// Act
Action act = () => _ = new EstimateResult { EstimateText = null! };
// Assert
act.Should().Throw<ArgumentNullException>()
.WithParameterName("value");
}
[Theory]
[InlineData("")]
[InlineData(" ")]
public void EstimateResult_ShouldThrowForEmptyOrWhitespaceEstimateText(string invalidText)
{
// Act
Action act = () => _ = new EstimateResult { EstimateText = invalidText };
// Assert
act.Should().Throw<ArgumentException>()
.WithMessage("*cannot be empty or whitespace*");
}
[Fact]
public void EstimateResult_ShouldAcceptZeroDuration()
{
// Act
var result = new EstimateResult { ShakeDuration = TimeSpan.Zero };
// Assert
result.ShakeDuration.Should().Be(TimeSpan.Zero);
}
[Fact]
public void EstimateResult_ShouldGenerateUniqueIds()
{
// Arrange & Act
var result1 = EstimateResult.Create("test1", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(1));
var result2 = EstimateResult.Create("test2", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(1));
// Assert
result1.Id.Should().NotBe(result2.Id);
result1.Id.Should().NotBeEmpty();
result2.Id.Should().NotBeEmpty();
}
[Fact]
public void EstimateResult_Create_ShouldSetTimestampAutomatically()
{
// Arrange
var before = DateTimeOffset.UtcNow;
// Act
var result = EstimateResult.Create("test", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(1));
var after = DateTimeOffset.UtcNow;
// Assert
result.Timestamp.Should().BeOnOrAfter(before);
result.Timestamp.Should().BeOnOrBefore(after);
}
[Fact]
public void EstimateResult_Create_ShouldSetAllProperties()
{
// Arrange
var estimateText = "3 months";
var mode = EstimateMode.Generic;
var intensity = 0.8;
var duration = TimeSpan.FromSeconds(10);
// Act
var result = EstimateResult.Create(estimateText, mode, intensity, duration);
// Assert
result.EstimateText.Should().Be(estimateText);
result.Mode.Should().Be(mode);
result.ShakeIntensity.Should().Be(intensity);
result.ShakeDuration.Should().Be(duration);
}
}

View File

@@ -0,0 +1,106 @@
using FluentAssertions;
using HihaArvio.Models;
namespace HihaArvio.Tests.Models;
public class ShakeDataTests
{
[Fact]
public void ShakeData_ShouldCreateWithAllProperties()
{
// Arrange
var intensity = 0.65;
var duration = TimeSpan.FromSeconds(3);
var isShaking = true;
// Act
var shakeData = new ShakeData
{
Intensity = intensity,
Duration = duration,
IsShaking = isShaking
};
// Assert
shakeData.Intensity.Should().Be(intensity);
shakeData.Duration.Should().Be(duration);
shakeData.IsShaking.Should().Be(isShaking);
}
[Theory]
[InlineData(0.0)]
[InlineData(0.25)]
[InlineData(0.5)]
[InlineData(0.75)]
[InlineData(1.0)]
public void ShakeData_ShouldAcceptValidIntensityValues(double intensity)
{
// Act
var shakeData = new ShakeData { Intensity = intensity };
// Assert
shakeData.Intensity.Should().Be(intensity);
}
[Theory]
[InlineData(-0.1)]
[InlineData(-1.0)]
[InlineData(1.01)]
[InlineData(2.0)]
public void ShakeData_ShouldThrowForInvalidIntensity(double invalidIntensity)
{
// Act
Action act = () => _ = new ShakeData { Intensity = invalidIntensity };
// Assert
act.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("*must be between 0.0 and 1.0*");
}
[Fact]
public void ShakeData_ShouldAcceptZeroDuration()
{
// Act
var shakeData = new ShakeData { Duration = TimeSpan.Zero };
// Assert
shakeData.Duration.Should().Be(TimeSpan.Zero);
}
[Fact]
public void ShakeData_ShouldAcceptPositiveDuration()
{
// Arrange
var duration = TimeSpan.FromSeconds(15.5);
// Act
var shakeData = new ShakeData { Duration = duration };
// Assert
shakeData.Duration.Should().Be(duration);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ShakeData_ShouldAcceptBooleanIsShakingValues(bool isShaking)
{
// Act
var shakeData = new ShakeData { IsShaking = isShaking };
// Assert
shakeData.IsShaking.Should().Be(isShaking);
}
[Fact]
public void ShakeData_DefaultConstructor_ShouldSetDefaultValues()
{
// Act
var shakeData = new ShakeData();
// Assert
shakeData.Intensity.Should().Be(0.0);
shakeData.Duration.Should().Be(TimeSpan.Zero);
shakeData.IsShaking.Should().BeFalse();
}
}