Give packs an identity so create can refuse missing deps and load in a stable order.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -143,6 +143,126 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
Assert.NotNull(response);
|
||||
var core = Assert.Single(response.Mods, pack => pack.Id == "core");
|
||||
Assert.True(core.Required);
|
||||
Assert.Equal("Базовая игра", core.Label);
|
||||
Assert.False(string.IsNullOrWhiteSpace(core.Version));
|
||||
Assert.Empty(core.Requires);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mods_LabelsCoreInTheRequestedLanguage()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
|
||||
var ru = await client.GetFromJsonAsync<ModsResponse>("/api/mods?lang=ru", TestContext.Current.CancellationToken);
|
||||
var en = await client.GetFromJsonAsync<ModsResponse>("/api/mods?lang=en", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(ru);
|
||||
Assert.NotNull(en);
|
||||
Assert.Equal("Базовая игра", Assert.Single(ru.Mods, pack => pack.Id == "core").Label);
|
||||
Assert.Equal("Core", Assert.Single(en.Mods, pack => pack.Id == "core").Label);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mods_PackWithoutManifest_UsesIdAsLabel()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
var root = await ModsDirectoryAsync(client);
|
||||
using (new TempPack(root, "t22plain"))
|
||||
{
|
||||
var response = await client.GetFromJsonAsync<ModsResponse>("/api/mods?lang=en", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(response);
|
||||
var pack = Assert.Single(response.Mods, candidate => candidate.Id == "t22plain");
|
||||
Assert.False(pack.Required);
|
||||
Assert.Equal("t22plain", pack.Label);
|
||||
Assert.Equal(string.Empty, pack.Version);
|
||||
Assert.Empty(pack.Requires);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_ReturnsResolvedPackOrder()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
var created = await CreateAsync(client, "С ванилью", ExpectedDefaultStart);
|
||||
|
||||
Assert.Equal(["core"], created.ModIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_WithUnselectedDependency_NamesTheMissingPack()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
var root = await ModsDirectoryAsync(client);
|
||||
using (new TempPack(root, "t22need", """{ "version": "1", "requires": ["t22ghost"] }"""))
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
"/api/schools",
|
||||
new
|
||||
{
|
||||
name = "Без базы",
|
||||
startDate = ExpectedDefaultStart,
|
||||
modIds = new[] { "t22need" },
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.Equal("missing-mod", problem?.Code);
|
||||
Assert.Equal("t22ghost", problem?.Missing);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_LoadsRequiredPackBeforeDependentEvenIfListedAfter()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
var root = await ModsDirectoryAsync(client);
|
||||
using (new TempPack(root, "t22base", """{ "version": "1", "requires": [] }"""))
|
||||
using (new TempPack(root, "t22addon", """{ "version": "1", "requires": ["t22base"] }"""))
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
"/api/schools",
|
||||
new
|
||||
{
|
||||
name = "Сначала зависимость",
|
||||
startDate = ExpectedDefaultStart,
|
||||
modIds = new[] { "t22addon", "t22base" },
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var created = await response.Content.ReadFromJsonAsync<SchoolResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(["core", "t22base", "t22addon"], created?.ModIds);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_WithCyclicMods_IsRejected()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
var root = await ModsDirectoryAsync(client);
|
||||
using (new TempPack(root, "t22left", """{ "requires": ["t22right"] }"""))
|
||||
using (new TempPack(root, "t22right", """{ "requires": ["t22left"] }"""))
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
"/api/schools",
|
||||
new
|
||||
{
|
||||
name = "Цикл",
|
||||
startDate = ExpectedDefaultStart,
|
||||
modIds = new[] { "t22left", "t22right" },
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("mod-cycle", await ProblemCodeAsync(response));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -418,7 +538,13 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
return problem?.Code;
|
||||
}
|
||||
|
||||
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex);
|
||||
internal sealed record SchoolResponse(
|
||||
int Id,
|
||||
string Name,
|
||||
DateTime GameTime,
|
||||
bool Running,
|
||||
byte SpeedIndex,
|
||||
IReadOnlyList<string>? ModIds = null);
|
||||
|
||||
internal sealed record SchoolsResponse(
|
||||
int MaxSchools,
|
||||
@@ -429,13 +555,58 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
|
||||
private sealed record RandomNameResponse(string Name);
|
||||
|
||||
private sealed record ProblemResponse(string? Code);
|
||||
private sealed record ProblemResponse(string? Code, string? Missing = null);
|
||||
|
||||
private sealed record StatusResponse(uint Tick, int TickRate, int Schools, int MaxSchools, int Connections);
|
||||
|
||||
private sealed record ModsResponse(IReadOnlyList<ModInfoResponse> Mods);
|
||||
|
||||
private sealed record ModInfoResponse(string Id, bool Required);
|
||||
private sealed record ModInfoResponse(
|
||||
string Id,
|
||||
bool Required,
|
||||
string Label,
|
||||
string Version,
|
||||
IReadOnlyList<string> Requires);
|
||||
|
||||
private sealed record PathResponse(string Path);
|
||||
|
||||
private static async Task<string> ModsDirectoryAsync(HttpClient client)
|
||||
{
|
||||
var payload = await client.GetFromJsonAsync<PathResponse>(
|
||||
"/api/dev/mods-directory",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
Assert.False(string.IsNullOrWhiteSpace(payload.Path));
|
||||
return payload.Path;
|
||||
}
|
||||
|
||||
private sealed class TempPack : IDisposable
|
||||
{
|
||||
public TempPack(string modsRoot, string id, string? packJsonc = null)
|
||||
{
|
||||
Path = System.IO.Path.Combine(modsRoot, id);
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path);
|
||||
if (packJsonc is not null)
|
||||
{
|
||||
File.WriteAllText(System.IO.Path.Combine(Path, "pack.jsonc"), packJsonc);
|
||||
}
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record CatalogResponse(
|
||||
IReadOnlyList<DefInfoResponse> Territories,
|
||||
|
||||
Reference in New Issue
Block a user