// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using EpicGames.Horde.Storage; using Jupiter.Common; using Jupiter.Implementation; using Jupiter.Implementation.Blob; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Moq.Contrib.HttpClient; using Serilog; using Logger = Serilog.Core.Logger; namespace Jupiter.FunctionalTests.Storage { [TestClass] public class BlobReplicationTests { protected NamespaceId TestNamespaceName { get; } = new NamespaceId("test-namespace"); [TestMethod] public async Task ReplicateBlobFromRegionAsync() { // 2 regions with data // region A - will not have data // region B - will have data string contents = "This is a random string of content"; byte[] bytes = Encoding.ASCII.GetBytes(contents); BlobId blobIdentifier = BlobId.FromBlob(bytes); Mock handler = new Mock(); // site b has the content and will serve it handler.SetupRequest($"http://siteB.com/internal/api/v1/blobs/{TestNamespaceName}/{blobIdentifier}?allowOndemandReplication=false").ReturnsResponse(HttpStatusCode.OK, message => { message.Content = new ReadOnlyMemoryContent(bytes); } ).Verifiable(); handler.SetupRequest($"http://siteA.com/internal/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); handler.SetupRequest($"http://siteA.com/public/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); handler.SetupRequest($"http://siteB.com/internal/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); handler.SetupRequest($"http://siteB.com/public/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); IConfigurationRoot configuration = new ConfigurationBuilder() // we are not reading the base appSettings here as we want exact control over what runs in the tests .AddJsonFile("appsettings.Testing.json", true) .AddInMemoryCollection(new[] { new KeyValuePair("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Memory.ToString()), new KeyValuePair("UnrealCloudDDC:EnableOnDemandReplication", true.ToString()), }) .AddEnvironmentVariables() .Build(); Logger logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .CreateLogger(); using TestServer server = new TestServer(new WebHostBuilder() .UseConfiguration(configuration) .UseEnvironment("Testing") .ConfigureServices(collection => collection.AddSerilog(logger)) .ConfigureTestServices(collection => { collection.Configure(settings => { settings.Peers = new PeerSettings[] { new PeerSettings() { Name = "siteA", FullName = "siteA", Endpoints = new PeerEndpoints[] { new PeerEndpoints() { Url = new Uri("http://siteA.com/internal"), IsInternal = true }, new PeerEndpoints() { Url = new Uri("http://siteA.com/public") }, }.ToList() }, new PeerSettings() { Name = "siteB", FullName = "siteB", Endpoints = new PeerEndpoints[] { new PeerEndpoints() { Url = new Uri("http://siteB.com/internal"), IsInternal = true }, new PeerEndpoints() { Url = new Uri("http://siteB.com/public") }, }.ToList() }, }.ToList(); }); collection.Configure(settings => { settings.Policies = new Dictionary() { { TestNamespaceName.ToString(), new NamespacePolicy() { OnDemandReplication = true, Acls = new List() { new AclEntry() { Actions = new List() { JupiterAclAction.ReadObject, }, Claims = new List() { "*" } } } } } }; }); collection.AddSingleton(handler.CreateClientFactory()); }) .UseStartup() ); IBlobIndex? blobIndex = server.Services.GetService(); Assert.IsNotNull(blobIndex); await blobIndex.AddBlobToIndexAsync(TestNamespaceName, blobIdentifier, "siteB"); HttpClient httpClient = server.CreateClient(); HttpResponseMessage response = await httpClient!.GetAsync(new Uri($"api/v1/blobs/{TestNamespaceName}/{blobIdentifier}", UriKind.Relative)); response.EnsureSuccessStatusCode(); byte[] responseData = await response.Content.ReadAsByteArrayAsync(); CollectionAssert.AreEqual(bytes, responseData); handler.Verify(); } [TestMethod] public async Task ReplicateBlobNotPresentAsync() { // verify calling the blob endpoint when the blob is missing still returns a 404 string contents = "This is a random string of content"; byte[] bytes = Encoding.ASCII.GetBytes(contents); BlobId blobIdentifier = BlobId.FromBlob(bytes); Mock handler = new Mock(); handler.SetupRequest($"http://siteA.com/internal/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); handler.SetupRequest($"http://siteA.com/public/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); handler.SetupRequest($"http://siteB.com/internal/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); handler.SetupRequest($"http://siteB.com/public/health/live").ReturnsResponse(HttpStatusCode.OK).Verifiable(); IConfigurationRoot configuration = new ConfigurationBuilder() // we are not reading the base appSettings here as we want exact control over what runs in the tests .AddJsonFile("appsettings.Testing.json", true) .AddInMemoryCollection(new[] { new KeyValuePair("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Memory.ToString()) }) .AddEnvironmentVariables() .Build(); Logger logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .CreateLogger(); using TestServer server = new TestServer(new WebHostBuilder() .UseConfiguration(configuration) .UseEnvironment("Testing") .ConfigureServices(collection => collection.AddSerilog(logger)) .ConfigureTestServices(collection => { collection.Configure(settings => { settings.Peers = new PeerSettings[] { new PeerSettings() { Name = "siteA", FullName = "siteA", Endpoints = new PeerEndpoints[] { new PeerEndpoints() { Url = new Uri("http://siteA.com/internal"), IsInternal = true }, new PeerEndpoints() { Url = new Uri("http://siteA.com/public") }, }.ToList() }, new PeerSettings() { Name = "siteB", FullName = "siteB", Endpoints = new PeerEndpoints[] { new PeerEndpoints() { Url = new Uri("http://siteB.com/internal"), IsInternal = true }, new PeerEndpoints() { Url = new Uri("http://siteB.com/public") }, }.ToList() }, }.ToList(); }); collection.AddSingleton(handler.CreateClientFactory()); }) .UseStartup() ); HttpClient httpClient = server.CreateClient(); HttpResponseMessage response = await httpClient!.GetAsync(new Uri($"api/v1/blobs/{TestNamespaceName}/{blobIdentifier}", UriKind.Relative)); Assert.IsTrue(response.StatusCode == HttpStatusCode.NotFound); } } }