236 lines
7.9 KiB
C#
236 lines
7.9 KiB
C#
using Kit.Helpers.SqlHelperAbstract;
|
||
using Server.WatchBody.Core.Dal.Src.Domain;
|
||
using Server.WatchBody.Core.Dal.Src.Repository;
|
||
using Server.WatchBody.Core.Programs;
|
||
using Xunit;
|
||
|
||
namespace Server.WatchBody.Web.Tests;
|
||
|
||
/// <summary>
|
||
/// Интеграционные тесты ProgramStore (specs m03 01-lifecycle,
|
||
/// 02-reading) против docker watchbody-pg. Каждый тест — свой атлет
|
||
/// (случайный ключ), fixture-данные не пересекаются с m02.
|
||
/// </summary>
|
||
[Collection("web")]
|
||
public class ProgramStoreTests(TestApp app)
|
||
{
|
||
private (ProgramStore Store, int AthleteId) NewStore()
|
||
{
|
||
var cs = new ConnectionString
|
||
{
|
||
Type = ConnectionStringType.Postgres,
|
||
Value = app.Config.DbConnectionString,
|
||
};
|
||
var athleteId = new AthleteRepository(cs).Insert(new Athlete
|
||
{
|
||
Name = "ProgramStore test",
|
||
ApiKeyHash = Server.WatchBody.Web.Services.ApiKeyHash.Of(Guid.NewGuid().ToString()),
|
||
});
|
||
var store = new ProgramStore(new ProgramRepository(cs), new ProgramVersionRepository(cs));
|
||
return (store, athleteId);
|
||
}
|
||
|
||
// --- EnsureProgram (spec 01 #1–2) ---
|
||
|
||
[Fact]
|
||
public void EnsureProgram_CreatesOnce()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var first = store.EnsureProgram(athlete, "Программа v1");
|
||
var second = store.EnsureProgram(athlete, "Другое название");
|
||
|
||
Assert.Equal(first.Id, second.Id);
|
||
Assert.Equal("Программа v1", second.Title); // title не перезаписывается
|
||
}
|
||
|
||
// --- CreateDraft (spec 01 #3–9) ---
|
||
|
||
[Fact]
|
||
public void CreateDraft_FirstVersion_NoBasedOn()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, """{"weeks":[]}""", "user", "первая");
|
||
|
||
Assert.Equal(1, v1.Version);
|
||
Assert.Equal("draft", v1.Status);
|
||
Assert.Null(v1.BasedOnVersion);
|
||
}
|
||
|
||
[Fact]
|
||
public void CreateDraft_Second_BasedOnLatest()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
store.CreateDraft(athlete, """{"weeks":[]}""", "user", "первая");
|
||
var v2 = store.CreateDraft(athlete, """{"weeks":[1]}""", "user", "вторая");
|
||
|
||
Assert.Equal(2, v2.Version);
|
||
Assert.Equal(1, v2.BasedOnVersion);
|
||
}
|
||
|
||
[Fact]
|
||
public void CreateDraft_AuthorLlm_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
Assert.Throws<ProgramStoreException>(
|
||
() => store.CreateDraft(athlete, "{}", "llm", ""));
|
||
}
|
||
|
||
[Fact]
|
||
public void CreateDraft_AuthorRules_Accepted()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v = store.CreateDraft(athlete, "{}", "rules", "правила");
|
||
Assert.Equal("rules", v.Author);
|
||
}
|
||
|
||
[Fact]
|
||
public void CreateDraft_BrokenChain_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
Assert.Throws<ProgramStoreException>(
|
||
() => store.CreateDraft(athlete, "{}", "user", "", basedOnVersion: 5));
|
||
}
|
||
|
||
[Fact]
|
||
public void CreateDraft_EmptyBody_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
Assert.Throws<ProgramStoreException>(() => store.CreateDraft(athlete, " ", "user", ""));
|
||
}
|
||
|
||
[Fact]
|
||
public void CreateDraft_TwoDrafts_VersionsGrow()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var a = store.CreateDraft(athlete, "{}", "user", "a");
|
||
var b = store.CreateDraft(athlete, "{}", "user", "b");
|
||
Assert.Equal(1, a.Version);
|
||
Assert.Equal(2, b.Version);
|
||
Assert.All(store.ListVersions(athlete), v => Assert.Equal("draft", v.Status));
|
||
}
|
||
|
||
// --- Activate / Archive (spec 01 #10–16) ---
|
||
|
||
[Fact]
|
||
public void Activate_ShiftsPreviousToArchived()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, "{}", "user", "v1");
|
||
var v2 = store.CreateDraft(athlete, "{}", "user", "v2");
|
||
|
||
store.Activate(athlete, v1.Id);
|
||
Assert.Equal(v1.Id, store.GetActive(athlete)!.Id);
|
||
|
||
store.Activate(athlete, v2.Id);
|
||
var active = store.GetActive(athlete)!;
|
||
Assert.Equal(v2.Id, active.Id);
|
||
Assert.Equal("archived", store.GetVersion(athlete, v1.Id)!.Status);
|
||
}
|
||
|
||
[Fact]
|
||
public void Activate_FirstVersion_SingleActive()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, "{}", "user", "v1");
|
||
store.Activate(athlete, v1.Id);
|
||
Assert.Equal("active", store.GetActive(athlete)!.Status);
|
||
}
|
||
|
||
[Fact]
|
||
public void Activate_Archived_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, "{}", "user", "v1");
|
||
var v2 = store.CreateDraft(athlete, "{}", "user", "v2");
|
||
store.Activate(athlete, v1.Id);
|
||
store.Activate(athlete, v2.Id);
|
||
|
||
Assert.Throws<ProgramStoreException>(() => store.Activate(athlete, v1.Id));
|
||
}
|
||
|
||
[Fact]
|
||
public void Activate_AlreadyActive_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, "{}", "user", "v1");
|
||
store.Activate(athlete, v1.Id);
|
||
Assert.Throws<ProgramStoreException>(() => store.Activate(athlete, v1.Id));
|
||
}
|
||
|
||
[Fact]
|
||
public void Activate_ForeignVersion_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var (otherStore, otherAthlete) = NewStore();
|
||
var foreign = otherStore.CreateDraft(otherAthlete, "{}", "user", "чужая");
|
||
Assert.Throws<ProgramStoreException>(() => store.Activate(athlete, foreign.Id));
|
||
}
|
||
|
||
[Fact]
|
||
public void Archive_Active_ThenNoActive()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, "{}", "user", "v1");
|
||
store.Activate(athlete, v1.Id);
|
||
store.Archive(athlete, v1.Id);
|
||
|
||
Assert.Null(store.GetActive(athlete));
|
||
Assert.Throws<ProgramStoreException>(() => store.Archive(athlete, v1.Id)); // повтор
|
||
}
|
||
|
||
[Fact]
|
||
public void Archive_Draft_Rejected()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, "{}", "user", "v1");
|
||
Assert.Throws<ProgramStoreException>(() => store.Archive(athlete, v1.Id));
|
||
}
|
||
|
||
// --- чтение (spec 02 #1–7) ---
|
||
|
||
[Fact]
|
||
public void GetActive_NoProgram_Null()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
Assert.Null(store.GetActive(athlete));
|
||
}
|
||
|
||
[Fact]
|
||
public void GetActive_AllArchived_Null()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var v1 = store.CreateDraft(athlete, """{"x":1}""", "user", "v1");
|
||
store.Activate(athlete, v1.Id);
|
||
store.Archive(athlete, v1.Id);
|
||
Assert.Null(store.GetActive(athlete));
|
||
}
|
||
|
||
[Fact]
|
||
public void ListVersions_Ordered_NoBody_Isolated()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var (otherStore, otherAthlete) = NewStore();
|
||
for (var i = 0; i < 3; i++)
|
||
store.CreateDraft(athlete, "{\"n\":" + i + "}", "user", $"v{i + 1}");
|
||
otherStore.CreateDraft(otherAthlete, "{}", "user", "чужая");
|
||
|
||
var list = store.ListVersions(athlete);
|
||
Assert.Equal([1, 2, 3], list.Select(v => v.Version));
|
||
Assert.All(list, v => Assert.Equal(string.Empty, v.Body));
|
||
}
|
||
|
||
[Fact]
|
||
public void GetVersion_Own_WithBody_Foreign_Null()
|
||
{
|
||
var (store, athlete) = NewStore();
|
||
var (otherStore, otherAthlete) = NewStore();
|
||
var v = store.CreateDraft(athlete, """{"weeks":[2]}""", "user", "v1");
|
||
var foreign = otherStore.CreateDraft(otherAthlete, "{}", "user", "чужая");
|
||
|
||
Assert.True(System.Text.Json.JsonElement.DeepEquals(
|
||
System.Text.Json.JsonDocument.Parse("""{"weeks":[2]}""").RootElement,
|
||
System.Text.Json.JsonDocument.Parse(store.GetVersion(athlete, v.Id)!.Body).RootElement));
|
||
Assert.Null(store.GetVersion(athlete, foreign.Id));
|
||
}
|
||
}
|