Files

229 lines
8.2 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using DongfangHydro.Dashboard.Api.Data;
using DongfangHydro.Dashboard.Api.Contracts;
using DongfangHydro.Dashboard.Api.Realtime;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xunit;
namespace DongfangHydro.Dashboard.Tests;
public sealed class DashboardApiTests(DashboardApiFactory factory)
: IClassFixture<DashboardApiFactory>
{
[Fact]
public async Task Orders_SearchesSeededWaterConservancyProductNames()
{
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders?keyword=门架&page=1&pageSize=20");
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(body.GetProperty("total").GetInt32() >= 1);
Assert.Contains(
body.GetProperty("items").EnumerateArray(),
item => item.GetProperty("productName").GetString()!.Contains("门架", StringComparison.Ordinal));
}
[Fact]
public async Task OrderTree_ReturnsTheCompleteSeededHierarchy()
{
using var client = factory.CreateClient();
var orders = await client.GetFromJsonAsync<JsonElement>("/api/orders?page=1&pageSize=20");
var orderId = orders.GetProperty("items")[0].GetProperty("id").GetString();
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
Assert.Equal("order", root.GetProperty("nodeType").GetString());
Assert.Equal(66, CountNodes(root));
Assert.Equal(1, CountNodes(root, "order"));
Assert.Equal(5, CountNodes(root, "part"));
Assert.Equal(15, CountNodes(root, "component"));
Assert.Equal(45, CountNodes(root, "process"));
Assert.False(string.IsNullOrWhiteSpace(FindFirstProcess(root).GetProperty("version").GetString()));
}
[Fact]
public async Task UpdateProgress_RecalculatesTheOrderAndReturnsTheUpdatedNode()
{
using var client = factory.CreateClient();
factory.UpdateQueue.Reset();
var orders = await client.GetFromJsonAsync<JsonElement>("/api/orders?page=1&pageSize=20");
var orderId = orders.GetProperty("items")[0].GetProperty("id").GetString();
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
var process = FindFirstProcess(root);
var response = await client.PatchAsJsonAsync(
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
new
{
completedQty = process.GetProperty("requiredQty").GetInt32(),
expectedVersion = process.GetProperty("version").GetString(),
status = "done",
riskLevel = "normal",
delayDays = 0,
delayReason = string.Empty,
});
var updated = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(100, updated.GetProperty("node").GetProperty("progress").GetInt32());
Assert.Equal("done", updated.GetProperty("node").GetProperty("status").GetString());
Assert.Equal(orderId, updated.GetProperty("order").GetProperty("id").GetString());
Assert.Equal(1, factory.UpdateQueue.EnqueueCount);
}
[Fact]
public async Task UpdateProgress_RejectsARequestWithoutCompletedQuantity()
{
using var client = factory.CreateClient();
var process = await GetFirstProcessAsync(client);
var response = await client.PatchAsJsonAsync(
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
new { status = "in_progress" });
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task UpdateProgress_RejectsDoneStatusBeforeThePlannedQuantityIsComplete()
{
using var client = factory.CreateClient();
var process = await GetFirstProcessAsync(client);
var requiredQty = process.GetProperty("requiredQty").GetInt32();
var response = await client.PatchAsJsonAsync(
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
new
{
completedQty = requiredQty - 1,
expectedVersion = process.GetProperty("version").GetString(),
status = "done",
riskLevel = "normal",
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task UpdateProgress_RequiresTheClientsExpectedVersion()
{
using var client = factory.CreateClient();
var process = await GetFirstProcessAsync(client);
var response = await client.PatchAsJsonAsync(
$"/api/nodes/{process.GetProperty("id").GetString()}/progress",
new
{
completedQty = process.GetProperty("completedQty").GetInt32(),
status = process.GetProperty("status").GetString(),
riskLevel = process.GetProperty("riskLevel").GetString(),
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static int CountNodes(JsonElement node)
{
var count = 1;
if (node.TryGetProperty("children", out var children))
{
count += children.EnumerateArray().Sum(CountNodes);
}
return count;
}
private static int CountNodes(JsonElement node, string nodeType)
{
var count = node.GetProperty("nodeType").GetString() == nodeType ? 1 : 0;
if (node.TryGetProperty("children", out var children))
{
count += children.EnumerateArray().Sum(child => CountNodes(child, nodeType));
}
return count;
}
private static async Task<JsonElement> GetFirstProcessAsync(HttpClient client)
{
var orders = await client.GetFromJsonAsync<JsonElement>("/api/orders?page=1&pageSize=20");
var orderId = orders.GetProperty("items")[0].GetProperty("id").GetString();
var root = await client.GetFromJsonAsync<JsonElement>($"/api/orders/{orderId}/tree");
return FindFirstProcess(root);
}
private static JsonElement FindFirstProcess(JsonElement node)
{
if (node.GetProperty("nodeType").GetString() == "process")
{
return node;
}
foreach (var child in node.GetProperty("children").EnumerateArray())
{
var match = FindFirstProcess(child);
if (match.ValueKind != JsonValueKind.Undefined)
{
return match;
}
}
return default;
}
}
public sealed class DashboardApiFactory : WebApplicationFactory<Program>
{
private readonly string databaseName = $"dashboard-tests-{Guid.NewGuid():N}";
public RecordingDashboardUpdateQueue UpdateQueue { get; } = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
builder.ConfigureAppConfiguration((_, configuration) =>
{
configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
["Database:AutoMigrate"] = "true",
["Database:Seed"] = "true",
});
});
builder.ConfigureServices(services =>
{
services.RemoveAll<DbContextOptions<DashboardDbContext>>();
services.AddDbContext<DashboardDbContext>(options =>
options.UseInMemoryDatabase(databaseName));
services.RemoveAll<IDashboardUpdateQueue>();
services.AddSingleton<IDashboardUpdateQueue>(UpdateQueue);
});
}
}
public sealed class RecordingDashboardUpdateQueue : IDashboardUpdateQueue
{
public int EnqueueCount { get; private set; }
public ValueTask EnqueueAsync(
UpdateNodeProgressResultDto update,
CancellationToken cancellationToken)
{
EnqueueCount++;
return ValueTask.CompletedTask;
}
public void Reset()
{
EnqueueCount = 0;
}
}