Merge branch 'bug/3-users-json-as-school'
ci / server (push) Failing after 3m49s
ci / client (push) Successful in 19s

This commit is contained in:
Leonid Pershin
2026-08-20 12:48:53 +03:00
6 changed files with 102 additions and 2 deletions
+37
View File
@@ -0,0 +1,37 @@
# Баг 3. SchoolStore принимает users.json за сейв школы
## Симптом
После первого входа в логе при старте сервера:
```
warn: HSchool.Server.Game.SchoolStore[0]
Save D:\Github\h-school\src\HSchool.Server\saves\users.json has id 0; ids start at 1. Leaving the file in place.
```
Ожидали, что список игроков рядом со школами не попадает в загрузку сейвов.
## Причина
Фаза 38 положила `saves/users.json` в тот же каталог, что и `{id}.json`. `SchoolStore.LoadAll` обходит все `*.json`, пропуская только `index.json`, `*.people.json` и `*.timetable.json`. У документа пользователей нет поля `id`, десериализация в `SchoolSave` даёт `Id = 0`, срабатывает предупреждение. Файл на месте остаётся, карточки школы из него нет.
## Путь
Пропускать `users.json` так же, как `index.json`. Не сужать обход до `{digits}.json` — это другой контракт сканера.
## Задачи
- [x] `LoadAll` не читает `users.json` как сейв
- [x] Тест: каталог с `1.json` и `users.json` возвращает одну школу
## Тест, без которого не закрыт
`HSchool.Server.Tests`: `LoadAll` рядом с валидным `{id}.json` и `users.json` (документ пользователей, без `id`) возвращает только школу.
```bash
dotnet test tests/HSchool.Server.Tests --filter FullyQualifiedName~SchoolStore
```
## Стоп
Форму `users.json` и HTTP сессии не трогать. `wwwroot` / `UseFileServer` — соседнее предупреждение при старте, не этот баг.
+1
View File
@@ -11,3 +11,4 @@
| --- | --- | --- | | --- | --- | --- |
| [1. Окно входа](01-login-layout.md) | ✅ | Форма входа прижата к углу, поле пароля на всю ширину | | [1. Окно входа](01-login-layout.md) | ✅ | Форма входа прижата к углу, поле пароля на всю ширину |
| [2. Запуск AppHost](02-client-endpoint-proxy.md) | ✅ | `run-aspire` падает: proxy + Port=TargetPort=5173 у client | | [2. Запуск AppHost](02-client-endpoint-proxy.md) | ✅ | `run-aspire` падает: proxy + Port=TargetPort=5173 у client |
| [3. users.json как сейв](03-users-json-as-school.md) | ✅ | При старте: `Save …/users.json has id 0; ids start at 1` |
+1
View File
@@ -119,6 +119,7 @@ internal sealed class SchoolStore
{ {
var fileName = Path.GetFileName(path); var fileName = Path.GetFileName(path);
if (string.Equals(fileName, IndexFileName, StringComparison.OrdinalIgnoreCase) if (string.Equals(fileName, IndexFileName, StringComparison.OrdinalIgnoreCase)
|| string.Equals(fileName, UserStore.FileName, StringComparison.OrdinalIgnoreCase)
|| fileName.EndsWith(".people.json", StringComparison.OrdinalIgnoreCase) || fileName.EndsWith(".people.json", StringComparison.OrdinalIgnoreCase)
|| fileName.EndsWith(".timetable.json", StringComparison.OrdinalIgnoreCase)) || fileName.EndsWith(".timetable.json", StringComparison.OrdinalIgnoreCase))
{ {
+2 -2
View File
@@ -8,7 +8,7 @@ internal sealed record UserRecord(string Name);
/// <summary>Persistent user list beside school saves.</summary> /// <summary>Persistent user list beside school saves.</summary>
internal sealed class UserStore internal sealed class UserStore
{ {
private const string UsersFileName = "users.json"; internal const string FileName = "users.json";
private static readonly JsonSerializerOptions Json = new() private static readonly JsonSerializerOptions Json = new()
{ {
@@ -25,7 +25,7 @@ internal sealed class UserStore
public UserStore(SchoolStore schools, ILogger<UserStore> logger) public UserStore(SchoolStore schools, ILogger<UserStore> logger)
{ {
_logger = logger; _logger = logger;
_path = Path.Combine(schools.DirectoryPath, UsersFileName); _path = Path.Combine(schools.DirectoryPath, FileName);
Load(); Load();
} }
@@ -15,6 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\HSchool.Server\HSchool.Server.csproj" /> <ProjectReference Include="..\..\src\HSchool.Server\HSchool.Server.csproj" />
<ProjectReference Include="..\..\src\HSchool.People\HSchool.People.csproj" /> <ProjectReference Include="..\..\src\HSchool.People\HSchool.People.csproj" />
<ProjectReference Include="..\..\src\HSchool.Simulation\HSchool.Simulation.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -0,0 +1,60 @@
using HSchool.Server.Game;
using HSchool.Simulation;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace HSchool.Server.Tests;
public class SchoolStoreTests
{
[Fact]
public void LoadAll_SkipsUsersJsonBesideASchoolSave()
{
var directory = Directory.CreateTempSubdirectory("h-school-store-");
try
{
File.WriteAllText(
Path.Combine(directory.FullName, UserStore.FileName),
"""{ "users": [ { "name": "Player" } ] }""");
File.WriteAllText(
Path.Combine(directory.FullName, "1.json"),
"""
{
"format": 3,
"id": 1,
"name": "North",
"gameTime": "2012-03-31T06:00:00Z",
"running": false,
"speedIndex": 0
}
""");
var store = CreateStore(directory.FullName);
var saves = store.LoadAll();
var save = Assert.Single(saves);
Assert.Equal(1, save.Id);
Assert.Equal("North", save.Name);
}
finally
{
directory.Delete(recursive: true);
}
}
private static SchoolStore CreateStore(string directory)
{
var options = Options.Create(new SimulationOptions { SavesDirectory = directory });
return new SchoolStore(options, new StubHost(), NullLogger<SchoolStore>.Instance);
}
private sealed class StubHost : IHostEnvironment
{
public string ApplicationName { get; set; } = "HSchool.Server.Tests";
public string EnvironmentName { get; set; } = "Development";
public string ContentRootPath { get; set; } = Path.GetTempPath();
public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
}
}