APP热更新
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user