183 lines
7.1 KiB
C#
183 lines
7.1 KiB
C#
using System.Text;
|
||
using System.Text.Json;
|
||
using Kit.Helpers.SqlHelperAbstract;
|
||
using Server.WatchBody.Core.Dal.Src.Domain;
|
||
using Server.WatchBody.Core.Dal.Src.Repository;
|
||
using Server.WatchBody.Fetch;
|
||
using Server.WatchBody.Import;
|
||
using Xunit;
|
||
|
||
namespace Server.WatchBody.Web.Tests;
|
||
|
||
/// <summary>
|
||
/// Интеграционные тесты m07 (specs 01–03) против docker watchbody-pg
|
||
/// с фейковым Garmin (FakeGarminHandler); .fit — энкодер SDK.
|
||
/// </summary>
|
||
[Collection("web")]
|
||
public class FitFetcherTests(TestApp app)
|
||
{
|
||
private static readonly DateTime Start = new(2026, 10, 15, 18, 0, 0, DateTimeKind.Utc);
|
||
|
||
private ConnectionString NewCs() => new()
|
||
{
|
||
Type = ConnectionStringType.Postgres,
|
||
Value = app.Config.DbConnectionString,
|
||
};
|
||
|
||
private int NewAthlete(string name)
|
||
{
|
||
var cs = NewCs();
|
||
var athleteId = new AthleteRepository(cs).Insert(new Athlete
|
||
{
|
||
Name = name,
|
||
ApiKeyHash = Server.WatchBody.Web.Services.ApiKeyHash.Of(Guid.NewGuid().ToString()),
|
||
});
|
||
return athleteId;
|
||
}
|
||
|
||
private static (string Dir, string Base) NewEnv()
|
||
{
|
||
var dir = Path.Combine(Path.GetTempPath(), "wb-fetch-" + Guid.NewGuid().ToString("N")[..8]);
|
||
Directory.CreateDirectory(dir);
|
||
return (dir, "http://fake-garmin.test");
|
||
}
|
||
|
||
private static byte[] StrengthFit() =>
|
||
FitBytes(Start, "123456-20261015T180000Z");
|
||
|
||
private static byte[] FitBytes(DateTime start, string _) // serial задаётся энкодером
|
||
{
|
||
var path = Path.Combine(Path.GetTempPath(), "wb-fit-" + Guid.NewGuid().ToString("N")[..8] + ".fit");
|
||
FitParser.WriteTestFile(path, start,
|
||
records: [(0, 100), (60, 110)], laps: [(0, 0, 300)], sets: [(10, 80.0m)], serial: "777");
|
||
var bytes = File.ReadAllBytes(path);
|
||
File.Delete(path);
|
||
return bytes;
|
||
}
|
||
|
||
private FitFetcher NewFetcher(string dir, FakeGarminHandler handler, string athleteName) =>
|
||
new(NewCs(), new GarminClient("owner@example.com", "secret", "http://fake-garmin.test", handler),
|
||
dir, athleteName);
|
||
|
||
// --- spec 02: цикл ---
|
||
|
||
[Fact]
|
||
public void Fetch_StrengthOnly_DownloadedAndImported()
|
||
{
|
||
var athlete = NewAthlete("f7-" + Guid.NewGuid().ToString("N")[..8]);
|
||
var (dir, _) = NewEnv();
|
||
var handler = new FakeGarminHandler
|
||
{
|
||
Activities =
|
||
[
|
||
new GarminActivity(1001, Start, "strength_training"),
|
||
new GarminActivity(1002, Start.AddHours(-2), "cardio"), // не силовая
|
||
],
|
||
FitFiles = { [1001] = StrengthFit() },
|
||
};
|
||
|
||
var (downloaded, skipped, failed) = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
|
||
|
||
Assert.Equal((1, 0, 0), (downloaded, skipped, failed));
|
||
Assert.Equal(1, handler.DownloadCalls); // cardio не скачивается
|
||
var row = new WorkoutRepository(NewCs()).Select(new WorkoutFilter
|
||
{
|
||
AthleteId = athlete,
|
||
DateFrom = new DateOnly(2026, 10, 14),
|
||
DateTo = new DateOnly(2026, 10, 16),
|
||
}).Single(w => w.FitFileId != null);
|
||
Assert.NotNull(row.FactsJson);
|
||
Assert.Equal(105, row.AvgHr);
|
||
Assert.True(File.Exists(Path.Combine(dir, "1001.fit"))); // кэш
|
||
|
||
// повторный fetch — всё в кэше, скачиваний нет, дублей нет
|
||
var second = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
|
||
Assert.Equal((0, 1, 0), second);
|
||
Assert.Equal(1, handler.DownloadCalls);
|
||
}
|
||
|
||
[Fact]
|
||
public void Fetch_DuplicateFitFileId_FileDeletedNoSecondRow()
|
||
{
|
||
var athlete = NewAthlete("f7-dup-" + Guid.NewGuid().ToString("N")[..8]);
|
||
var (dir, _) = NewEnv();
|
||
var handler = new FakeGarminHandler
|
||
{
|
||
Activities = { new GarminActivity(2001, Start, "strength_training") },
|
||
FitFiles = { [2001] = StrengthFit() },
|
||
};
|
||
Assert.Equal((1, 0, 0), NewFetcher(dir, handler, athleteName(athlete)).FetchNew());
|
||
|
||
// тот же .fit под другим activityId (переименование в Garmin)
|
||
handler.Activities.Add(new GarminActivity(2002, Start, "strength_training"));
|
||
handler.FitFiles[2002] = StrengthFit();
|
||
|
||
var (downloaded, skipped, failed) = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
|
||
|
||
// 2001 — кэш (рубеж 1), 2002 — дубль fit_file_id (рубеж 2)
|
||
Assert.Equal((0, 2, 0), (downloaded, skipped, failed));
|
||
Assert.False(File.Exists(Path.Combine(dir, "2002.fit"))); // дубль не кэшируется
|
||
var rows = new WorkoutRepository(NewCs()).Select(new WorkoutFilter
|
||
{
|
||
AthleteId = athlete,
|
||
DateFrom = new DateOnly(2026, 10, 14),
|
||
DateTo = new DateOnly(2026, 10, 16),
|
||
}).Count(w => w.FitFileId != null);
|
||
Assert.Equal(1, rows); // второй строки нет
|
||
}
|
||
|
||
[Fact]
|
||
public void Fetch_BrokenDownload_MarkedBad_Continues()
|
||
{
|
||
var athlete = NewAthlete("f7-bad-" + Guid.NewGuid().ToString("N")[..8]);
|
||
var (dir, _) = NewEnv();
|
||
var handler = new FakeGarminHandler
|
||
{
|
||
Activities =
|
||
[
|
||
new GarminActivity(3001, Start, "strength_training"),
|
||
new GarminActivity(3002, Start.AddHours(-1), "strength_training"),
|
||
],
|
||
FitFiles = { [3001] = [1, 2, 3], [3002] = StrengthFit() },
|
||
};
|
||
|
||
var (downloaded, skipped, failed) = NewFetcher(dir, handler, athleteName(athlete)).FetchNew();
|
||
|
||
Assert.Equal((1, 0, 1), (downloaded, skipped, failed));
|
||
Assert.True(File.Exists(Path.Combine(dir, "3001.fit.bad")));
|
||
Assert.True(File.Exists(Path.Combine(dir, "3002.fit")));
|
||
}
|
||
|
||
[Fact]
|
||
public void Fetch_UnknownAthlete_Throws()
|
||
{
|
||
var (dir, _) = NewEnv();
|
||
var handler = new FakeGarminHandler
|
||
{
|
||
Activities = { new GarminActivity(4001, Start, "strength_training") },
|
||
FitFiles = { [4001] = StrengthFit() },
|
||
};
|
||
var fetcher = NewFetcher(dir, handler, "нет-такого-атлета");
|
||
|
||
Assert.Throws<FetchException>(() => fetcher.FetchNew());
|
||
Assert.Equal(0, handler.LoginCalls); // атлет проверяется до сети
|
||
}
|
||
|
||
[Fact]
|
||
public void Fetch_AuthFailure_ThrowsWithoutPassword()
|
||
{
|
||
var athlete = NewAthlete("f7-auth-" + Guid.NewGuid().ToString("N")[..8]);
|
||
var (dir, _) = NewEnv();
|
||
var handler = new FakeGarminHandler { FailLogin = true };
|
||
var fetcher = NewFetcher(dir, handler, athleteName(athlete));
|
||
|
||
var exception = Assert.Throws<GarminAuthException>(() => fetcher.FetchNew());
|
||
Assert.DoesNotContain("secret", exception.Message); // 🛡4: пароль не в тексте
|
||
Assert.Equal(1, handler.LoginCalls);
|
||
}
|
||
|
||
private string athleteName(int athleteId) =>
|
||
new AthleteRepository(NewCs()).Select(new AthleteFilter())
|
||
.First(a => a.Id == athleteId).Name;
|
||
}
|