mirror of
https://github.com/ivuorinen/hiha-arvio.git
synced 2026-01-26 11:24:04 +00:00
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>
284 lines
8.5 KiB
C#
284 lines
8.5 KiB
C#
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
|
|
}
|