223 lines
8.1 KiB
C#
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();
|
|
}
|
|
}
|