using System.Security.Claims; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using RadicadeServer.API.Studio; using RadicadeServer.DB; using Xunit; namespace RadicadeServer.Tests; [Collection(DbFactoryCollection.Name)] public class StudioCloudBuildTests { private sealed class TestDbFactory : IDbContextFactory { private readonly DbContextOptions options = TestDbOptions.Create() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; public RecDBContext CreateDbContext() => new(options); public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => Task.FromResult(CreateDbContext()); } private static (StudioAPI Api, StudioBuildWorker Worker) CreateController(long accountId = 2, long? roomOwnerId = null) { var identity = new ClaimsIdentity("TestAuth"); identity.AddClaim(new Claim("userId", accountId.ToString())); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accountId.ToString())); var worker = new StudioBuildWorker( NullLogger.Instance, new ConfigurationBuilder().Build()); var dbFactory = new TestDbFactory(); using (var db = dbFactory.CreateDbContext()) { db.Rooms.Add(new RadicadeServer.RecNet.Rooms.RoomData { RoomId = 99, Name = "Build Test Room", CreatorAccountId = roomOwnerId ?? accountId }); db.SaveChanges(); } var api = new StudioAPI(NullLogger.Instance, worker, dbFactory) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) } } }; return (api, worker); } private static async Task WaitForBuild(StudioAPI api, string cloudBuildId) { for (var attempt = 0; attempt < 100; attempt++) { var response = Assert.IsType(await api.CloudBuild(cloudBuildId)); var build = System.Text.Json.JsonSerializer.Deserialize( System.Text.Json.JsonSerializer.Serialize(response.Value)); Assert.NotNull(build); if (build!.CompletedAt != null) return build; await Task.Delay(20); } throw new TimeoutException("The queued local build did not report a result."); } [Fact] public async Task CreateQueuesBuildAndReturnsTheWorkerFailureForMissingUnityConfiguration() { var (controller, worker) = CreateController(); await worker.StartAsync(CancellationToken.None); try { var response = Assert.IsType(await controller.CreateCloudBuild(99, 109, 1, autoPublish: true, buildBranch: null)); var json = System.Text.Json.JsonSerializer.SerializeToElement(response.Value); Assert.True(json.GetProperty("Success").GetBoolean()); Assert.Null(json.GetProperty("Error").GetString()); var queued = json.GetProperty("Value"); var cloudBuildId = queued.GetProperty("CloudBuildId").GetString(); Assert.False(string.IsNullOrWhiteSpace(cloudBuildId)); Assert.Equal(System.Text.Json.JsonValueKind.Null, queued.GetProperty("CompletedAt").ValueKind); Assert.Equal(2, queued.GetProperty("CreatedByAccountId").GetInt32()); var completed = await WaitForBuild(controller, cloudBuildId!); Assert.True(completed.CompletedAt >= completed.StartedAt); Assert.Contains("Radicade:Studio:UnityPath", completed.Error); Assert.Empty(completed.UnityAssetId); var list = Assert.IsType(await controller.CloudBuildsForRoom(99, 109, 0, 30)); var listJson = System.Text.Json.JsonSerializer.SerializeToElement(list.Value); Assert.Contains(listJson.GetProperty("Results").EnumerateArray(), item => item.GetProperty("CloudBuildId").GetString() == cloudBuildId); } finally { await worker.StopAsync(CancellationToken.None); worker.Dispose(); } } [Fact] public async Task CreateAcceptsStudioUrlEncodedPostFormFields() { var (controller, worker) = CreateController(); await worker.StartAsync(CancellationToken.None); try { var context = controller.HttpContext; context.Request.ContentType = "application/x-www-form-urlencoded"; context.Request.Body = new MemoryStream(System.Text.Encoding.UTF8.GetBytes( "roomId=99&subRoomId=109&subRoomDataSaveId=1&autoPublish=true&buildBranch=main")); context.Request.ContentLength = context.Request.Body.Length; var response = Assert.IsType(await controller.CreateCloudBuild()); var json = System.Text.Json.JsonSerializer.SerializeToElement(response.Value); Assert.True(json.GetProperty("Success").GetBoolean()); var createdId = json.GetProperty("Value").GetProperty("CloudBuildId").GetString()!; var completed = await WaitForBuild(controller, createdId); Assert.Contains("Radicade:Studio:UnityPath", completed.Error); var page = Assert.IsType(await controller.CloudBuildsForRoom(99, 109, 0, 100)); var pageJson = System.Text.Json.JsonSerializer.SerializeToElement(page.Value); Assert.Contains(pageJson.GetProperty("Results").EnumerateArray(), build => build.GetProperty("CloudBuildId").GetString() == createdId); } finally { await worker.StopAsync(CancellationToken.None); worker.Dispose(); } } [Fact] public async Task CreateRejectsMissingSaveReference() { var (controller, worker) = CreateController(); var result = Assert.IsType(await controller.CreateCloudBuild(99, 109, 0)); Assert.NotNull(result.Value); worker.Dispose(); } [Fact] public async Task CreateRejectsAccountsWithoutRoomBuildPermission() { var (controller, worker) = CreateController(accountId: 2, roomOwnerId: 7); var result = Assert.IsType(await controller.CreateCloudBuild(99, 109, 1)); Assert.NotNull(result); worker.Dispose(); } }