This commit is contained in:
DmitriyFadin
2026-09-30 15:05:05 +03:00
commit ca18fc196d
122 changed files with 8794 additions and 0 deletions
@@ -0,0 +1,59 @@
using TrainingPlanner.Application;
namespace TrainingPlanner.Tests;
public class ExerciseImportTests
{
private readonly ExerciseImportOptions options = new() { MaxRows = 1000, MaxClipboardCharacters = 100000 };
[Fact]
public void ClipboardPreservesEmptyCellsAndExcelQuotedValues()
{
var rows = new ClipboardTableParser(options).Parse("\"Жим \"\"узкий\"\"\nсидя\"\thttps://example.com/video\r\nТяга\t\r\n\t\r\n");
Assert.Equal(2, rows.Count);
Assert.Equal("Жим \"узкий\"\nсидя", rows[0][0]);
Assert.Equal("https://example.com/video", rows[0][1]);
Assert.Equal("", rows[1][1]);
}
[Fact]
public void ClipboardHandlesLargePasteAndAllLineEndings()
{
var parser = new ClipboardTableParser(options);
Assert.Equal(500, parser.Parse(string.Join('\n', Enumerable.Repeat("Упражнение\thttps://example.com", 500))).Count);
Assert.Equal(3, parser.Parse("А\rБ\nВ\r\n").Count);
Assert.Empty(parser.Parse("\r\n\t\r\n"));
}
[Fact]
public void ClipboardRejectsUnclosedQuotesAndConfiguredLimits()
{
var parser = new ClipboardTableParser(new ExerciseImportOptions { MaxRows = 1, MaxClipboardCharacters = 20 });
Assert.Throws<InvalidOperationException>(() => parser.Parse("\"Открытая ячейка"));
Assert.Throws<InvalidOperationException>(() => parser.Parse("А\nБ"));
Assert.Throws<InvalidOperationException>(() => parser.Parse(new string('a', 21)));
}
[Fact]
public void ValidationSkipsBlankRowsAndAllowsMissingVideo()
{
var rows = new ExerciseImportValidator(options).Validate([
new(), new() { Name = " Тяга ", VideoUrl = " " },
new() { Name = "Жим", VideoUrl = " https://example.com " }]);
Assert.Equal(2, rows.Count);
Assert.Equal("Тяга", rows[0].Name);
Assert.Equal("", rows[0].VideoUrl);
Assert.Equal("https://example.com", rows[1].VideoUrl);
}
[Fact]
public void ValidationReportsOriginalRowAndRejectsUnsafeLinks()
{
var validator = new ExerciseImportValidator(options);
var error = Assert.Throws<InvalidOperationException>(() => validator.Validate([
new(), new() { Name = "Жим", VideoUrl = "javascript:alert(1)" }]));
Assert.Contains("Строка 2", error.Message);
Assert.Throws<InvalidOperationException>(() => validator.Validate([new() { VideoUrl = "https://example.com" }]));
Assert.Throws<InvalidOperationException>(() => validator.Validate([new()]));
}
}
@@ -0,0 +1,34 @@
using TrainingPlanner.Application;
using TrainingPlanner.Domain;
namespace TrainingPlanner.Tests;
public class ExerciseSearchTests
{
[Fact]
public void ExactAndPrefixMatchesComeBeforeMatchesInsideName()
{
var exercises = Names("Обратный жим", "Жим гантелей", "Жим", "Приседания");
var result = ExerciseSearch.Find(exercises, " ЖИМ ");
Assert.Equal(new[] { "Жим", "Жим гантелей", "Обратный жим" }, result.Select(exercise => exercise.Name));
}
[Fact]
public void AllWordsMustMatchIgnoringTheirOrderAndYoSpelling()
{
var result = ExerciseSearch.Find(Names("Жим гантелей лёжа", "Жим штанги лёжа", "Жим гантелей стоя"), "ЛЕЖА гант");
Assert.Single(result);
Assert.Equal("Жим гантелей лёжа", result[0].Name);
}
[Fact]
public void EmptyQueryReturnsAllExercisesAndUnknownQueryReturnsNone()
{
var exercises = Names("Тяга", "Жим");
Assert.Equal(2, ExerciseSearch.Find(exercises, " ").Count);
Assert.Empty(ExerciseSearch.Find(exercises, "Приседания"));
}
private static List<LibraryExercise> Names(params string[] names)
=> names.Select(name => new LibraryExercise { Name = name }).ToList();
}
@@ -0,0 +1,110 @@
using TrainingPlanner.Application;
using TrainingPlanner.Domain;
namespace TrainingPlanner.Tests;
public class PlanEditingTests
{
private readonly PlannerOptions options = new()
{
DefaultWeekCount = 4, DefaultSetsCount = 3, DefaultRestMinutes = 1.5m, DefaultRepetitions = "10"
};
[Fact]
public void MonthlyCopyPreservesPrescriptionAndCanBeEditedIndependently()
{
var plan = CreatePlan();
var workout = plan.Weeks[0].Workouts[0];
workout.Duration = "1 ч 15 мин";
var exercise = workout.Blocks[0].Exercises[0];
exercise.Id = 17;
exercise.Weight = "Подобрать";
exercise.Repetitions = "По 10 (1 RIR)";
exercise.WarmupSets = "1 — гриф; 2 — 40 кг";
exercise.Notes = "Без рывков";
exercise.IntensityPercent = 80;
exercise.RequiresVideo = true;
exercise.VideoUrl = "https://disk.yandex.ru/i/example";
var copy = new PlanCopier().Copy(plan, "Октябрь");
var copiedWorkout = copy.Weeks[0].Workouts[0];
var copied = copiedWorkout.Blocks[0].Exercises[0];
Assert.Equal(0, copied.Id);
Assert.Equal(workout.Duration, copiedWorkout.Duration);
Assert.Equal(exercise.WarmupSets, copied.WarmupSets);
Assert.Equal(exercise.Notes, copied.Notes);
Assert.Equal(exercise.Weight, copied.Weight);
Assert.Equal(exercise.Repetitions, copied.Repetitions);
Assert.Equal(exercise.IntensityPercent, copied.IntensityPercent);
Assert.Equal(exercise.VideoUrl, copied.VideoUrl);
Assert.True(copied.RequiresVideo);
copied.Weight = "60";
copiedWorkout.Blocks[0].SetsCount = 4;
Assert.Equal("Подобрать", exercise.Weight);
Assert.Equal(3, workout.Blocks[0].SetsCount);
}
[Fact]
public void RestIncludesLastApproachAndDoesNotMultiplyByRepetitions()
{
var workout = CreatePlan().Weeks[0].Workouts[0];
workout.Blocks[0].RestMinutes = 2;
workout.Blocks[0].Exercises[0].Repetitions = "По 10 (1 RIR)";
Assert.Equal(6m, WorkoutTotals.RestMinutes(workout));
workout.Blocks.Add(new WorkoutBlock { SetsCount = 2, RestMinutes = 1.5m });
Assert.Equal(9m, WorkoutTotals.RestMinutes(workout));
Assert.Equal(0m, WorkoutTotals.RestMinutes(new Workout()));
}
[Fact]
public void ValidationAllowsTextWeightAndRejectsSupersets()
{
var plan = CreatePlan();
var block = plan.Weeks[0].Workouts[0].Blocks[0];
block.Exercises[0].Weight = "Подобрать / собственный вес";
new PlanValidator().Validate(plan);
block.Exercises.Add(new WorkoutExercise { Name = "Второе", Repetitions = "10" });
Assert.Throws<InvalidOperationException>(() => new PlanValidator().Validate(plan));
}
[Theory]
[InlineData(0, 1.5, 80)]
[InlineData(3, -1, 80)]
[InlineData(3, 2, 101)]
[InlineData(3, 2, -1)]
public void ValidationRejectsInvalidPrescription(int sets, double rest, int intensity)
{
var plan = CreatePlan();
var block = plan.Weeks[0].Workouts[0].Blocks[0];
block.SetsCount = sets;
block.RestMinutes = (decimal)rest;
block.Exercises[0].IntensityPercent = intensity;
Assert.Throws<InvalidOperationException>(() => new PlanValidator().Validate(plan));
}
[Fact]
public void MovePreservesPrescriptionAndValidationRenumbersBlocks()
{
var plan = CreatePlan();
var workout = plan.Weeks[0].Workouts[0];
var original = workout.Blocks[0];
original.Exercises[0].WarmupSets = "Гриф";
workout.Blocks.Add(new PlanFactory(options).CreateBlock(new LibraryExercise { Name = "Тяга" }));
new BlockEditor().Move(workout, 0, 1);
new PlanValidator().Validate(plan);
Assert.Same(original, workout.Blocks[1]);
Assert.Equal(1, original.Position);
Assert.Equal("Гриф", original.Exercises[0].WarmupSets);
Assert.Throws<InvalidOperationException>(() => PlanValidator.ValidateVideoUrl("javascript:alert(1)"));
}
private TrainingPlan CreatePlan()
{
var factory = new PlanFactory(options);
var plan = factory.Create(1, "Сентябрь");
plan.Weeks[0].Workouts.Add(new Workout
{
Name = "Ноги", Blocks = [factory.CreateBlock(new LibraryExercise { Name = "Приседания" })]
});
return plan;
}
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\TrainingPlanner.Infrastructure\TrainingPlanner.Infrastructure.csproj" />
</ItemGroup>
</Project>