202 lines
7.6 KiB
C#
202 lines
7.6 KiB
C#
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);
|
|
}
|