Refactor project structure and update documentation. Replace PixiJS with plain DOM for UI rendering, enhance README with game features, and revise protocol documentation for HTTP API. Remove unused files and streamline client code for better maintainability.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The main menu's HTTP surface: list, create, delete. Tests share one AppHost, so each of them
|
||||
/// starts from an empty list rather than assuming one.
|
||||
/// </summary>
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class SchoolApiTests(AppHostFixture fixture)
|
||||
{
|
||||
private static readonly DateTime ExpectedDefaultStart = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public async Task Schools_ReportTheConfiguredLimitAndDefaultStartDate()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
var state = await GetSchoolsAsync(client);
|
||||
|
||||
Assert.Equal(6, state.MaxSchools);
|
||||
Assert.Equal(ExpectedDefaultStart, state.DefaultStartDate);
|
||||
|
||||
// Without the "Z" the browser would read the start date in its own time zone and the
|
||||
// creation form would offer the wrong hour.
|
||||
Assert.Equal(DateTimeKind.Utc, state.DefaultStartDate.Kind);
|
||||
Assert.Equal(5d, state.GameMinutesPerRealSecond);
|
||||
Assert.Empty(state.Schools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_StartsAtTheRequestedDateAndRunning()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
var created = await CreateAsync(client, "Гимназия у моря", ExpectedDefaultStart);
|
||||
|
||||
Assert.Equal("Гимназия у моря", created.Name);
|
||||
Assert.Equal(ExpectedDefaultStart, created.GameTime);
|
||||
|
||||
// Schools live from the moment they exist, whether or not anybody is inside.
|
||||
Assert.True(created.Running);
|
||||
|
||||
var state = await GetSchoolsAsync(client);
|
||||
Assert.Contains(state.Schools, school => school.Id == created.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_BeyondTheLimit_IsRejected()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
var state = await GetSchoolsAsync(client);
|
||||
for (var i = 0; i < state.MaxSchools; i++)
|
||||
{
|
||||
await CreateAsync(client, $"Школа {i + 1}", ExpectedDefaultStart);
|
||||
}
|
||||
|
||||
using var response = await PostAsync(client, "Лишняя", ExpectedDefaultStart);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
Assert.Equal("school-limit-reached", await ProblemCodeAsync(response));
|
||||
Assert.Equal(state.MaxSchools, (await GetSchoolsAsync(client)).Schools.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task CreateSchool_WithABlankName_IsRejected(string name)
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
using var response = await PostAsync(client, name, ExpectedDefaultStart);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("invalid-name", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSchool_WithAnImpossibleStartDate_IsRejected()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
using var response = await PostAsync(client, "Школа", new DateTime(1500, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.Equal("invalid-start-date", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSchool_RemovesItAndFreesASlot()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
var created = await CreateAsync(client, "На удаление", ExpectedDefaultStart);
|
||||
|
||||
using var response = await client.DeleteAsync($"/api/schools/{created.Id}", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
Assert.DoesNotContain((await GetSchoolsAsync(client)).Schools, school => school.Id == created.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSchool_ThatDoesNotExist_IsNotFound()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
|
||||
using var response = await client.DeleteAsync("/api/schools/999999", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RandomName_IsUsableAsIs()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await ResetAsync(client);
|
||||
|
||||
var suggestion = await client.GetFromJsonAsync<RandomNameResponse>(
|
||||
"/api/schools/random-name",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(suggestion);
|
||||
Assert.False(string.IsNullOrWhiteSpace(suggestion.Name));
|
||||
|
||||
var created = await CreateAsync(client, suggestion.Name, ExpectedDefaultStart);
|
||||
Assert.Equal(suggestion.Name, created.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Status_ReportsTheLoopAndTheLimit()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
|
||||
var status = await client.GetFromJsonAsync<StatusResponse>("/api/status", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(status);
|
||||
Assert.Equal(20, status.TickRate);
|
||||
Assert.Equal(6, status.MaxSchools);
|
||||
Assert.True(status.Tick > 0, "The loop should have ticked by now.");
|
||||
}
|
||||
|
||||
internal static async Task ResetAsync(HttpClient client)
|
||||
{
|
||||
var state = await GetSchoolsAsync(client);
|
||||
|
||||
foreach (var school in state.Schools)
|
||||
{
|
||||
using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<SchoolsResponse> GetSchoolsAsync(HttpClient client)
|
||||
{
|
||||
var state = await client.GetFromJsonAsync<SchoolsResponse>("/api/schools", TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
internal static async Task<SchoolResponse> CreateAsync(HttpClient client, string name, DateTime startDate)
|
||||
{
|
||||
using var response = await PostAsync(client, name, startDate);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var created = await response.Content.ReadFromJsonAsync<SchoolResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private static Task<HttpResponseMessage> PostAsync(HttpClient client, string name, DateTime startDate) =>
|
||||
client.PostAsJsonAsync(
|
||||
"/api/schools",
|
||||
new { name, startDate },
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
private static async Task<string?> ProblemCodeAsync(HttpResponseMessage response)
|
||||
{
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||
return problem?.Code;
|
||||
}
|
||||
|
||||
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex);
|
||||
|
||||
internal sealed record SchoolsResponse(
|
||||
int MaxSchools,
|
||||
DateTime DefaultStartDate,
|
||||
double GameMinutesPerRealSecond,
|
||||
IReadOnlyList<SchoolResponse> Schools);
|
||||
|
||||
private sealed record RandomNameResponse(string Name);
|
||||
|
||||
private sealed record ProblemResponse(string? Code);
|
||||
|
||||
private sealed record StatusResponse(uint Tick, int TickRate, int Schools, int MaxSchools, int Connections);
|
||||
}
|
||||
Reference in New Issue
Block a user