Files
Academic-Affairs-System/tests/Jiaowu.Api.Tests/AppUpdatesControllerTests.cs
T
2026-07-29 10:34:33 +08:00

223 lines
8.1 KiB
C#

using System.IO.Compression;
using System.Reflection;
using System.Security.Claims;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Tests;
public sealed class AppUpdatesControllerTests
{
[Fact]
public void Management_actions_are_restricted_to_super_admin()
{
var method = typeof(AppUpdatesController)
.GetMethod(nameof(AppUpdatesController.UploadRelease));
var authorize = method?.GetCustomAttribute<AuthorizeAttribute>();
Assert.NotNull(authorize);
Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles);
}
[Fact]
public async Task Upload_publish_check_and_download_form_a_complete_flow()
{
await using var fixture = await AppUpdatesFixture.CreateAsync();
var content = CreateBundle("first");
var uploaded = await fixture.Controller.UploadRelease(
UploadRequest(content, "1.0.1"),
CancellationToken.None);
var created = Assert.IsType<CreatedAtActionResult>(uploaded.Result);
var release = Assert.IsType<AppUpdateReleaseItem>(created.Value);
Assert.Equal(AppUpdateReleaseStatus.Draft, release.Status);
Assert.Equal(64, release.Sha256.Length);
var publishedAction = await fixture.Controller.PublishRelease(
release.Id,
CancellationToken.None);
var publishedResult = Assert.IsType<OkObjectResult>(
publishedAction.Result);
var published = Assert.IsType<AppUpdateReleaseItem>(
publishedResult.Value);
Assert.Equal(AppUpdateReleaseStatus.Published, published.Status);
var checkAction = await fixture.Controller.GetLatest(
"android",
"1.0",
"production",
"1.0.0",
CancellationToken.None);
var checkResult = Assert.IsType<OkObjectResult>(checkAction.Result);
var check = Assert.IsType<AppUpdateCheckResponse>(checkResult.Value);
Assert.True(check.Available);
Assert.Equal("1.0.1", check.Version);
Assert.Equal(release.Sha256, check.Sha256);
Assert.Equal($"app-updates/releases/{release.Id}/bundle", check.DownloadUrl);
var downloadAction = await fixture.Controller.DownloadBundle(
release.Id,
CancellationToken.None);
var download = Assert.IsType<FileContentResult>(downloadAction);
Assert.Equal(content, download.FileContents);
Assert.True(download.EnableRangeProcessing);
var currentAction = await fixture.Controller.GetLatest(
"android",
"1.0",
"production",
"1.0.1",
CancellationToken.None);
var currentResult = Assert.IsType<OkObjectResult>(
currentAction.Result);
Assert.False(Assert.IsType<AppUpdateCheckResponse>(
currentResult.Value).Available);
}
[Fact]
public async Task Publishing_an_archived_release_performs_a_rollback()
{
await using var fixture = await AppUpdatesFixture.CreateAsync();
var first = await UploadAsync(fixture, "1.0.1", "first");
await fixture.Controller.PublishRelease(first.Id, CancellationToken.None);
var second = await UploadAsync(fixture, "1.0.2", "second");
await fixture.Controller.PublishRelease(second.Id, CancellationToken.None);
Assert.Equal(
AppUpdateReleaseStatus.Archived,
(await fixture.Db.AppUpdateReleases.FindAsync(first.Id))!.Status);
Assert.Equal(
AppUpdateReleaseStatus.Published,
(await fixture.Db.AppUpdateReleases.FindAsync(second.Id))!.Status);
await fixture.Controller.PublishRelease(first.Id, CancellationToken.None);
fixture.Db.ChangeTracker.Clear();
Assert.Equal(
AppUpdateReleaseStatus.Published,
(await fixture.Db.AppUpdateReleases.FindAsync(first.Id))!.Status);
Assert.Equal(
AppUpdateReleaseStatus.Archived,
(await fixture.Db.AppUpdateReleases.FindAsync(second.Id))!.Status);
}
[Fact]
public async Task Invalid_or_nested_web_bundles_are_rejected()
{
await using var fixture = await AppUpdatesFixture.CreateAsync();
var invalid = await fixture.Controller.UploadRelease(
UploadRequest("not a zip"u8.ToArray(), "1.0.1"),
CancellationToken.None);
Assert.IsType<ObjectResult>(invalid.Result);
var nested = CreateBundle("nested", "dist/index.html");
var nestedResult = await fixture.Controller.UploadRelease(
UploadRequest(nested, "1.0.2"),
CancellationToken.None);
var problem = Assert.IsType<ObjectResult>(nestedResult.Result);
Assert.Contains(
"根目录",
Assert.IsType<ValidationProblemDetails>(problem.Value).Detail);
}
private static async Task<AppUpdateReleaseItem> UploadAsync(
AppUpdatesFixture fixture,
string version,
string marker)
{
var action = await fixture.Controller.UploadRelease(
UploadRequest(CreateBundle(marker), version),
CancellationToken.None);
return Assert.IsType<AppUpdateReleaseItem>(
Assert.IsType<CreatedAtActionResult>(action.Result).Value);
}
private static AppUpdateUploadRequest UploadRequest(
byte[] content,
string version)
{
var stream = new MemoryStream(content);
return new AppUpdateUploadRequest
{
Bundle = new FormFile(
stream,
0,
content.Length,
"bundle",
$"jiaowu-{version}.zip"),
Platform = "android",
Channel = "production",
Version = version,
NativeVersion = "1.0",
ReleaseNotes = $"Release {version}"
};
}
private static byte[] CreateBundle(
string marker,
string indexPath = "index.html")
{
using var output = new MemoryStream();
using (var archive = new ZipArchive(
output,
ZipArchiveMode.Create,
leaveOpen: true))
{
var index = archive.CreateEntry(indexPath);
using var writer = new StreamWriter(index.Open());
writer.Write($"<!doctype html><title>{marker}</title>");
}
return output.ToArray();
}
private sealed class AppUpdatesFixture : IAsyncDisposable
{
private AppUpdatesFixture(
AppDbContext db,
AppUpdatesController controller)
{
Db = db;
Controller = controller;
}
public AppDbContext Db { get; }
public AppUpdatesController Controller { get; }
public static async Task<AppUpdatesFixture> CreateAsync()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite("Data Source=:memory:")
.Options;
var db = new AppDbContext(options);
await db.Database.OpenConnectionAsync();
await db.Database.EnsureCreatedAsync();
var controller = new AppUpdatesController(db)
{
ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim(ClaimTypes.Name, "root"),
new Claim(
ClaimTypes.Role,
SystemRoles.SuperAdmin)
],
"test"))
}
}
};
return new AppUpdatesFixture(db, controller);
}
public async ValueTask DisposeAsync() => await Db.DisposeAsync();
}
}