VEU.WathBody/3-app/2-cli/Server.WatchBody.Web.Tests/FitFetcherTests.cs

183 lines
7.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}