Files
hiha-arvio/tests/HihaArvio.Tests/Integration/ServiceIntegrationTests.cs
Ismo Vuorinen dd3a4f3d97 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>
2025-11-18 12:37:20 +02:00

322 lines
11 KiB
C#

using FluentAssertions;
using HihaArvio.Models;
using HihaArvio.Services;
using HihaArvio.Services.Interfaces;
namespace HihaArvio.Tests.Integration;
/// <summary>
/// Integration tests verifying that services work together correctly.
/// </summary>
public class ServiceIntegrationTests : IDisposable
{
private readonly IEstimateService _estimateService;
private readonly IStorageService _storageService;
private readonly IShakeDetectionService _shakeDetectionService;
private readonly string _testDbPath;
public ServiceIntegrationTests()
{
_estimateService = new EstimateService();
_testDbPath = Path.Combine(Path.GetTempPath(), $"test_integration_{Guid.NewGuid()}.db");
_storageService = new StorageService(_testDbPath);
_shakeDetectionService = new ShakeDetectionService();
}
public void Dispose()
{
if (File.Exists(_testDbPath))
{
File.Delete(_testDbPath);
}
}
#region EstimateService + StorageService Integration
[Fact]
public async Task GenerateAndSaveEstimate_ShouldPersistToStorage()
{
// Arrange
var intensity = 0.75;
var duration = TimeSpan.FromSeconds(5);
var mode = EstimateMode.Work;
// Act - Generate estimate
var estimate = _estimateService.GenerateEstimate(intensity, duration, mode);
// Save to storage
await _storageService.SaveEstimateAsync(estimate);
// Load from storage
var history = await _storageService.GetHistoryAsync();
// Assert
history.Should().ContainSingle();
var saved = history.First();
saved.Id.Should().Be(estimate.Id);
saved.EstimateText.Should().Be(estimate.EstimateText);
saved.Mode.Should().Be(estimate.Mode);
saved.ShakeIntensity.Should().Be(estimate.ShakeIntensity);
saved.ShakeDuration.Should().Be(estimate.ShakeDuration);
}
[Fact]
public async Task GenerateMultipleEstimates_ShouldRespectHistoryLimit()
{
// Arrange
var settings = new AppSettings { MaxHistorySize = 3 };
await _storageService.SaveSettingsAsync(settings);
// Act - Generate and save 5 estimates
for (int i = 0; i < 5; i++)
{
var estimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Work);
await _storageService.SaveEstimateAsync(estimate);
await Task.Delay(10); // Ensure different timestamps
}
// Assert - Should only keep 3 newest
var count = await _storageService.GetHistoryCountAsync();
count.Should().Be(3);
}
[Fact]
public async Task LoadSettings_GenerateEstimate_ShouldUseCorrectMode()
{
// Arrange - Save settings with Generic mode
var settings = new AppSettings { SelectedMode = EstimateMode.Generic };
await _storageService.SaveSettingsAsync(settings);
// Act - Load settings and generate estimate
var loadedSettings = await _storageService.LoadSettingsAsync();
var estimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), loadedSettings.SelectedMode);
// Assert
estimate.Mode.Should().Be(EstimateMode.Generic);
}
#endregion
#region ShakeDetectionService + EstimateService Integration
[Fact]
public void DetectShake_GenerateEstimate_ShouldUseShakeData()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Simulate shake
_shakeDetectionService.ProcessAccelerometerReading(3.0, 2.5, 2.0);
var shakeData = _shakeDetectionService.CurrentShakeData;
// Generate estimate based on shake
var estimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
EstimateMode.Work);
// Assert
estimate.ShakeIntensity.Should().Be(shakeData.Intensity);
estimate.ShakeDuration.Should().Be(shakeData.Duration);
estimate.EstimateText.Should().NotBeNullOrEmpty();
}
[Fact]
public void DetectLongShake_GenerateEstimate_ShouldTriggerHumorousMode()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Simulate shake and manually set long duration for test
_shakeDetectionService.ProcessAccelerometerReading(2.5, 2.0, 1.5);
// Simulate 16-second shake by generating estimate with long duration
var estimate = _estimateService.GenerateEstimate(
0.5,
TimeSpan.FromSeconds(16), // Exceeds 15-second threshold
EstimateMode.Work);
// Assert - Should switch to Humorous mode (easter egg)
estimate.Mode.Should().Be(EstimateMode.Humorous);
estimate.EstimateText.Should().BeOneOf(
"5 minutes", "tomorrow", "eventually", "next quarter",
"when hell freezes over", "3 lifetimes", "Tuesday", "never", "your retirement");
}
[Fact]
public void DetectVaryingIntensity_GenerateEstimates_ShouldProduceDifferentRanges()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Test low, medium, and high intensity shakes
var estimates = new List<EstimateResult>();
// Low intensity shake
_shakeDetectionService.ProcessAccelerometerReading(1.8, 1.5, 0.5);
var lowShake = _shakeDetectionService.CurrentShakeData;
lowShake.Intensity.Should().BeLessThan(0.3);
estimates.Add(_estimateService.GenerateEstimate(lowShake.Intensity, lowShake.Duration, EstimateMode.Work));
// High intensity shake
_shakeDetectionService.Reset();
_shakeDetectionService.ProcessAccelerometerReading(4.0, 3.5, 3.0);
var highShake = _shakeDetectionService.CurrentShakeData;
highShake.Intensity.Should().BeGreaterThan(0.7);
estimates.Add(_estimateService.GenerateEstimate(highShake.Intensity, highShake.Duration, EstimateMode.Work));
// Assert - Both should generate valid estimates
estimates.Should().AllSatisfy(e =>
{
e.EstimateText.Should().NotBeNullOrEmpty();
e.Mode.Should().Be(EstimateMode.Work);
});
}
#endregion
#region Full Flow Integration
[Fact]
public async Task CompleteFlow_ShakeToEstimateToStorage_ShouldWorkEndToEnd()
{
// Arrange
var settings = await _storageService.LoadSettingsAsync();
_shakeDetectionService.StartMonitoring();
// Act - Simulate shake gesture
_shakeDetectionService.ProcessAccelerometerReading(3.0, 2.5, 2.0);
var shakeData = _shakeDetectionService.CurrentShakeData;
shakeData.IsShaking.Should().BeTrue();
// Generate estimate from shake
var estimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
settings.SelectedMode);
// Save to storage
await _storageService.SaveEstimateAsync(estimate);
// Load from storage
var history = await _storageService.GetHistoryAsync();
// Assert - Complete flow succeeded
history.Should().ContainSingle();
var saved = history.First();
saved.Id.Should().Be(estimate.Id);
saved.EstimateText.Should().Be(estimate.EstimateText);
saved.ShakeIntensity.Should().Be(shakeData.Intensity);
// Reset for next shake
_shakeDetectionService.Reset();
_shakeDetectionService.CurrentShakeData.IsShaking.Should().BeFalse();
}
[Fact]
public async Task MultipleShakes_ShouldAccumulateHistory()
{
// Arrange
_shakeDetectionService.StartMonitoring();
// Act - Simulate 3 shake gestures
for (int i = 0; i < 3; i++)
{
// Shake
_shakeDetectionService.ProcessAccelerometerReading(2.5, 2.0, 1.5);
var shakeData = _shakeDetectionService.CurrentShakeData;
// Generate estimate
var estimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
EstimateMode.Work);
// Save
await _storageService.SaveEstimateAsync(estimate);
// Reset for next shake
_shakeDetectionService.Reset();
await Task.Delay(10); // Ensure different timestamps
}
// Assert
var history = await _storageService.GetHistoryAsync();
history.Should().HaveCount(3);
// Verify ordering (newest first)
for (int i = 0; i < history.Count - 1; i++)
{
history[i].Timestamp.Should().BeOnOrAfter(history[i + 1].Timestamp);
}
}
[Fact]
public async Task ChangeMode_GenerateEstimate_ShouldUseNewMode()
{
// Arrange
var initialSettings = new AppSettings { SelectedMode = EstimateMode.Work };
await _storageService.SaveSettingsAsync(initialSettings);
// Act - Generate estimate with Work mode
var workEstimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Work);
await _storageService.SaveEstimateAsync(workEstimate);
// Change mode
var newSettings = new AppSettings { SelectedMode = EstimateMode.Humorous };
await _storageService.SaveSettingsAsync(newSettings);
// Generate estimate with new mode
var humorousEstimate = _estimateService.GenerateEstimate(0.5, TimeSpan.FromSeconds(5), EstimateMode.Humorous);
await _storageService.SaveEstimateAsync(humorousEstimate);
// Assert
var history = await _storageService.GetHistoryAsync();
history.Should().HaveCount(2);
history[0].Mode.Should().Be(EstimateMode.Humorous); // Newest
history[1].Mode.Should().Be(EstimateMode.Work); // Oldest
}
#endregion
#region Event Integration
[Fact]
public async Task ShakeEvent_ShouldTriggerEstimateGeneration()
{
// Arrange
_shakeDetectionService.StartMonitoring();
EstimateResult? generatedEstimate = null;
_shakeDetectionService.ShakeDataChanged += async (sender, shakeData) =>
{
if (shakeData.IsShaking)
{
// Generate estimate when shake detected
generatedEstimate = _estimateService.GenerateEstimate(
shakeData.Intensity,
shakeData.Duration,
EstimateMode.Work);
await _storageService.SaveEstimateAsync(generatedEstimate);
}
};
// Act - Simulate shake
_shakeDetectionService.ProcessAccelerometerReading(3.0, 2.5, 2.0);
// Give event handler time to execute
await Task.Delay(50);
// Assert
generatedEstimate.Should().NotBeNull();
var history = await _storageService.GetHistoryAsync();
history.Should().ContainSingle();
history.First().Id.Should().Be(generatedEstimate!.Id);
}
#endregion
}