mirror of
https://github.com/ivuorinen/hiha-arvio.git
synced 2026-03-06 21:58:28 +00:00
feat: implement services layer with TDD (Milestone 2)
Implemented all core services following strict TDD (RED-GREEN-REFACTOR): **Services Implemented:** - EstimateService: Generate estimates with intensity-based range selection - StorageService: SQLite persistence with auto-pruning - ShakeDetectionService: Accelerometer-based shake detection **Features:** - Cryptographically secure RNG for estimate selection - Easter egg logic (>15s shake → Humorous mode) - Intensity-based range calculation (0-0.3: 20%, 0.3-0.7: 50%, 0.7+: 100%) - SQLite with auto-pruning based on MaxHistorySize - Shake detection with 1.5g threshold - Duration tracking and intensity normalization (0.0-1.0) - Event-based notifications (ShakeDataChanged) **Tests:** - EstimateService: 25 tests (RED-GREEN-REFACTOR) - StorageService: 14 tests (RED-GREEN-REFACTOR) - ShakeDetectionService: 22 tests (RED-GREEN-REFACTOR) - Integration tests: 10 tests - Total: 119 tests, all passing **Quality:** - Build: 0 warnings, 0 errors across all platforms - Coverage: 51.28% line (low due to MAUI template), 87.5% branch - All service/model code has high coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
283
tests/HihaArvio.Tests/Services/StorageServiceTests.cs
Normal file
283
tests/HihaArvio.Tests/Services/StorageServiceTests.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using FluentAssertions;
|
||||
using HihaArvio.Models;
|
||||
using HihaArvio.Services;
|
||||
using HihaArvio.Services.Interfaces;
|
||||
|
||||
namespace HihaArvio.Tests.Services;
|
||||
|
||||
public class StorageServiceTests : IDisposable
|
||||
{
|
||||
private readonly IStorageService _service;
|
||||
private readonly string _testDbPath;
|
||||
|
||||
public StorageServiceTests()
|
||||
{
|
||||
// Use unique temp file for each test to ensure isolation
|
||||
_testDbPath = Path.Combine(Path.GetTempPath(), $"test_hiha_{Guid.NewGuid()}.db");
|
||||
_service = new StorageService(_testDbPath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Clean up test database
|
||||
if (File.Exists(_testDbPath))
|
||||
{
|
||||
File.Delete(_testDbPath);
|
||||
}
|
||||
}
|
||||
|
||||
#region Settings Tests
|
||||
|
||||
[Fact]
|
||||
public async Task LoadSettingsAsync_WhenNoSettingsExist_ShouldReturnDefaultSettings()
|
||||
{
|
||||
// Act
|
||||
var settings = await _service.LoadSettingsAsync();
|
||||
|
||||
// Assert
|
||||
settings.Should().NotBeNull();
|
||||
settings.SelectedMode.Should().Be(EstimateMode.Work);
|
||||
settings.MaxHistorySize.Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveAndLoadSettings_ShouldPersistSettings()
|
||||
{
|
||||
// Arrange
|
||||
var settingsToSave = new AppSettings
|
||||
{
|
||||
SelectedMode = EstimateMode.Generic,
|
||||
MaxHistorySize = 20
|
||||
};
|
||||
|
||||
// Act
|
||||
await _service.SaveSettingsAsync(settingsToSave);
|
||||
var loadedSettings = await _service.LoadSettingsAsync();
|
||||
|
||||
// Assert
|
||||
loadedSettings.SelectedMode.Should().Be(EstimateMode.Generic);
|
||||
loadedSettings.MaxHistorySize.Should().Be(20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveSettings_MultipleTimes_ShouldOverwritePreviousSettings()
|
||||
{
|
||||
// Arrange
|
||||
var settings1 = new AppSettings { SelectedMode = EstimateMode.Work, MaxHistorySize = 10 };
|
||||
var settings2 = new AppSettings { SelectedMode = EstimateMode.Humorous, MaxHistorySize = 50 };
|
||||
|
||||
// Act
|
||||
await _service.SaveSettingsAsync(settings1);
|
||||
await _service.SaveSettingsAsync(settings2);
|
||||
var loaded = await _service.LoadSettingsAsync();
|
||||
|
||||
// Assert
|
||||
loaded.SelectedMode.Should().Be(EstimateMode.Humorous);
|
||||
loaded.MaxHistorySize.Should().Be(50);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Estimate History Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SaveEstimateAsync_ShouldPersistEstimate()
|
||||
{
|
||||
// Arrange
|
||||
var estimate = EstimateResult.Create("2 weeks", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Act
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
var history = await _service.GetHistoryAsync();
|
||||
|
||||
// Assert
|
||||
history.Should().ContainSingle();
|
||||
var saved = history.First();
|
||||
saved.Id.Should().Be(estimate.Id);
|
||||
saved.EstimateText.Should().Be("2 weeks");
|
||||
saved.Mode.Should().Be(EstimateMode.Work);
|
||||
saved.ShakeIntensity.Should().Be(0.5);
|
||||
saved.ShakeDuration.Should().Be(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistoryAsync_WhenEmpty_ShouldReturnEmptyList()
|
||||
{
|
||||
// Act
|
||||
var history = await _service.GetHistoryAsync();
|
||||
|
||||
// Assert
|
||||
history.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistoryAsync_ShouldReturnNewestFirst()
|
||||
{
|
||||
// Arrange
|
||||
var estimate1 = EstimateResult.Create("1 day", EstimateMode.Work, 0.3, TimeSpan.FromSeconds(3));
|
||||
await Task.Delay(10); // Ensure different timestamps
|
||||
var estimate2 = EstimateResult.Create("2 days", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await Task.Delay(10);
|
||||
var estimate3 = EstimateResult.Create("3 days", EstimateMode.Work, 0.7, TimeSpan.FromSeconds(7));
|
||||
|
||||
// Act
|
||||
await _service.SaveEstimateAsync(estimate1);
|
||||
await _service.SaveEstimateAsync(estimate2);
|
||||
await _service.SaveEstimateAsync(estimate3);
|
||||
var history = await _service.GetHistoryAsync();
|
||||
|
||||
// Assert
|
||||
history.Should().HaveCount(3);
|
||||
history[0].EstimateText.Should().Be("3 days"); // Newest
|
||||
history[1].EstimateText.Should().Be("2 days");
|
||||
history[2].EstimateText.Should().Be("1 day"); // Oldest
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistoryAsync_WithCount_ShouldLimitResults()
|
||||
{
|
||||
// Arrange
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var estimate = EstimateResult.Create($"{i} days", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
await Task.Delay(5); // Ensure different timestamps
|
||||
}
|
||||
|
||||
// Act
|
||||
var history = await _service.GetHistoryAsync(5);
|
||||
|
||||
// Assert
|
||||
history.Should().HaveCount(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveEstimateAsync_ShouldAutoPruneWhenExceedingMaxSize()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new AppSettings { MaxHistorySize = 5 };
|
||||
await _service.SaveSettingsAsync(settings);
|
||||
|
||||
// Act - Save 7 estimates (exceeds max of 5)
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
var estimate = EstimateResult.Create($"{i} days", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
await Task.Delay(5);
|
||||
}
|
||||
|
||||
var count = await _service.GetHistoryCountAsync();
|
||||
|
||||
// Assert - Should have pruned to max size
|
||||
count.Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveEstimateAsync_AfterPruning_ShouldKeepNewestEstimates()
|
||||
{
|
||||
// Arrange
|
||||
var settings = new AppSettings { MaxHistorySize = 3 };
|
||||
await _service.SaveSettingsAsync(settings);
|
||||
|
||||
// Act
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var estimate = EstimateResult.Create($"{i} days", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
var history = await _service.GetHistoryAsync();
|
||||
|
||||
// Assert - Should have only the newest 3
|
||||
history.Should().HaveCount(3);
|
||||
history[0].EstimateText.Should().Be("4 days"); // Newest
|
||||
history[1].EstimateText.Should().Be("3 days");
|
||||
history[2].EstimateText.Should().Be("2 days");
|
||||
// "0 days" and "1 days" should be pruned
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearHistoryAsync_ShouldRemoveAllEstimates()
|
||||
{
|
||||
// Arrange
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var estimate = EstimateResult.Create($"{i} days", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
}
|
||||
|
||||
// Act
|
||||
await _service.ClearHistoryAsync();
|
||||
var count = await _service.GetHistoryCountAsync();
|
||||
|
||||
// Assert
|
||||
count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistoryCountAsync_ShouldReturnCorrectCount()
|
||||
{
|
||||
// Arrange
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
var estimate = EstimateResult.Create($"{i} days", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
}
|
||||
|
||||
// Act
|
||||
var count = await _service.GetHistoryCountAsync();
|
||||
|
||||
// Assert
|
||||
count.Should().Be(7);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Fact]
|
||||
public async Task SaveEstimateAsync_WithSpecialCharacters_ShouldPersist()
|
||||
{
|
||||
// Arrange
|
||||
var estimate = EstimateResult.Create("when hell freezes over", EstimateMode.Humorous, 0.9, TimeSpan.FromSeconds(20));
|
||||
|
||||
// Act
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
var history = await _service.GetHistoryAsync();
|
||||
|
||||
// Assert
|
||||
history.Should().ContainSingle();
|
||||
history[0].EstimateText.Should().Be("when hell freezes over");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistoryAsync_WithCountZero_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var estimate = EstimateResult.Create("1 day", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
|
||||
// Act
|
||||
var history = await _service.GetHistoryAsync(0);
|
||||
|
||||
// Assert
|
||||
history.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetHistoryAsync_WithNegativeCount_ShouldReturnEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var estimate = EstimateResult.Create("1 day", EstimateMode.Work, 0.5, TimeSpan.FromSeconds(5));
|
||||
await _service.SaveEstimateAsync(estimate);
|
||||
|
||||
// Act
|
||||
var history = await _service.GetHistoryAsync(-1);
|
||||
|
||||
// Assert
|
||||
history.Should().BeEmpty();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user