krona-staff/apps/KronaStaffTerminal.Tests/AttendanceSyncTests.cs

186 lines
6.3 KiB
C#

using System.Net;
using KronaStaffTerminal.Models;
using KronaStaffTerminal.Services;
using Xunit;
namespace KronaStaffTerminal.Tests;
public class AttendanceSyncTests
{
private sealed class MockHttpMessageHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, HttpResponseMessage> HandlerFunc { get; set; } =
_ => new HttpResponseMessage(HttpStatusCode.OK);
public List<HttpRequestMessage> CapturedRequests { get; } = new();
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
CapturedRequests.Add(request);
return Task.FromResult(HandlerFunc(request));
}
}
private static TerminalState CreateConfiguredState()
{
return new TerminalState
{
Settings = new TerminalSettings
{
StaffServerUrl = "https://staff.krona.internal:8453",
StaffServerToken = "test_token_secret_123",
TerminalDeviceId = "TERM-TEST-01"
}
};
}
[Fact]
public async Task SyncPending_With200Ok_MarksRecordsAsSyncedAndSendsHeaders()
{
var mockHandler = new MockHttpMessageHandler
{
HandlerFunc = _ => new HttpResponseMessage(HttpStatusCode.OK)
};
var httpClient = new HttpClient(mockHandler);
var syncService = new AttendanceSyncService(httpClient);
var state = CreateConfiguredState();
var record1 = new AttendanceRecord
{
Id = UlidGenerator.New(),
EmployeeId = "emp-10",
EventType = "Приход",
OccurredAtUtc = DateTimeOffset.UtcNow.AddMinutes(-5),
ServerSynced = false
};
var record2 = new AttendanceRecord
{
Id = UlidGenerator.New(),
EmployeeId = "emp-10",
EventType = "Уход",
OccurredAtUtc = DateTimeOffset.UtcNow,
ServerSynced = false
};
state.Attendance.Add(record1);
state.Attendance.Add(record2);
var result = await syncService.SyncPendingAsync(state, TestContext.Current.CancellationToken);
Assert.Equal(2, result.ProcessedCount);
Assert.Equal(2, result.SuccessCount);
Assert.Equal(0, result.FailedCount);
Assert.True(record1.ServerSynced);
Assert.True(record2.ServerSynced);
// Verify headers
Assert.Equal(2, mockHandler.CapturedRequests.Count);
var req = mockHandler.CapturedRequests[0];
Assert.Equal("Bearer test_token_secret_123", req.Headers.Authorization?.ToString());
Assert.True(req.Headers.Contains("Idempotency-Key"));
Assert.True(req.Headers.Contains("X-Request-Id"));
}
[Fact]
public async Task SyncPending_With409Conflict_TreatedAsIdempotentSuccess()
{
var mockHandler = new MockHttpMessageHandler
{
HandlerFunc = _ => new HttpResponseMessage(HttpStatusCode.Conflict)
};
var httpClient = new HttpClient(mockHandler);
var syncService = new AttendanceSyncService(httpClient);
var state = CreateConfiguredState();
var record = new AttendanceRecord
{
Id = UlidGenerator.New(),
EmployeeId = "emp-15",
EventType = "Приход",
OccurredAtUtc = DateTimeOffset.UtcNow,
ServerSynced = false
};
state.Attendance.Add(record);
var result = await syncService.SyncPendingAsync(state, TestContext.Current.CancellationToken);
Assert.Equal(1, result.ProcessedCount);
Assert.Equal(1, result.SuccessCount);
Assert.Equal(0, result.FailedCount);
Assert.True(record.ServerSynced);
Assert.Null(record.ServerSyncError);
}
[Fact]
public async Task SyncPending_With500Error_RetainsRecordForRetry()
{
var mockHandler = new MockHttpMessageHandler
{
HandlerFunc = _ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
};
var httpClient = new HttpClient(mockHandler);
var syncService = new AttendanceSyncService(httpClient);
var state = CreateConfiguredState();
var record = new AttendanceRecord
{
Id = UlidGenerator.New(),
EmployeeId = "emp-20",
EventType = "Приход",
OccurredAtUtc = DateTimeOffset.UtcNow,
ServerSynced = false
};
state.Attendance.Add(record);
var result = await syncService.SyncPendingAsync(state, TestContext.Current.CancellationToken);
Assert.Equal(1, result.ProcessedCount);
Assert.Equal(0, result.SuccessCount);
Assert.Equal(1, result.FailedCount);
Assert.False(record.ServerSynced);
Assert.Equal("HTTP 500", record.ServerSyncError);
}
[Fact]
public async Task SyncPending_WithNetworkException_HaltsCurrentCycle()
{
var mockHandler = new MockHttpMessageHandler
{
HandlerFunc = _ => throw new HttpRequestException("Connection refused")
};
var httpClient = new HttpClient(mockHandler);
var syncService = new AttendanceSyncService(httpClient);
var state = CreateConfiguredState();
var record1 = new AttendanceRecord
{
Id = UlidGenerator.New(),
EmployeeId = "emp-1",
EventType = "Приход",
OccurredAtUtc = DateTimeOffset.UtcNow.AddMinutes(-2),
ServerSynced = false
};
var record2 = new AttendanceRecord
{
Id = UlidGenerator.New(),
EmployeeId = "emp-2",
EventType = "Приход",
OccurredAtUtc = DateTimeOffset.UtcNow.AddMinutes(-1),
ServerSynced = false
};
state.Attendance.Add(record1);
state.Attendance.Add(record2);
var result = await syncService.SyncPendingAsync(state, TestContext.Current.CancellationToken);
Assert.Equal(2, result.ProcessedCount);
Assert.Equal(1, result.FailedCount);
Assert.True(result.HasNetworkError);
Assert.False(record1.ServerSynced);
Assert.False(record2.ServerSynced);
// Only first was attempted because loop broke on network drop
Assert.Single(mockHandler.CapturedRequests);
}
}