Merge branch 'bug/3-users-json-as-school'
This commit is contained in:
@@ -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` — соседнее предупреждение при старте, не этот баг.
|
||||||
@@ -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` |
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user