186 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|