// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using EpicGames.AspNet; using EpicGames.Core; using EpicGames.Horde.Storage; using EpicGames.Serialization; using Jupiter.Controllers; using Jupiter.Implementation; using Jupiter.Implementation.Blob; using Jupiter.Implementation.Objects; using Jupiter.Implementation.Replication; using Jupiter.Tests.Functional; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Serilog; using ContentId = Jupiter.Implementation.ContentId; using Logger = Serilog.Core.Logger; namespace Jupiter.FunctionalTests.References { [TestClass] // due to timing issues creating the UDTs this test can not be run with other tests, once we have deleted usage of the UDTs we can remove this [DoNotParallelize] public class ScyllaReferencesTests : ReferencesTests { public ScyllaReferencesTests() : base("scylla") { } protected override IEnumerable> GetSettings() { return new[] { new KeyValuePair("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Scylla.ToString()), new KeyValuePair("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Scylla.ToString()), new KeyValuePair("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Scylla.ToString()), new KeyValuePair("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Scylla.ToString()), }; } protected override async Task SeedDbAsync(IServiceProvider provider) { IReferencesStore referencesStore = provider.GetService()!; //verify we are using the expected store //verify we are using the expected store if (referencesStore is MemoryCachedReferencesStore memoryReferencesStore) { memoryReferencesStore.Clear(); referencesStore = memoryReferencesStore.GetUnderlyingStore(); } Assert.IsTrue(referencesStore.GetType() == typeof(ScyllaReferencesStore)); IContentIdStore contentIdStore = provider.GetService()!; //verify we are using the expected store if (contentIdStore is MemoryCachedContentIdStore memoryStore) { memoryStore.Clear(); contentIdStore = memoryStore.GetUnderlyingContentIdStore(); } Assert.IsTrue(contentIdStore.GetType() == typeof(ScyllaContentIdStore)); IReplicationLog replicationLog = provider.GetService()!; if (replicationLog is MemoryCachedReplicationLog log) { replicationLog = log.GetUnderlyingContentIdStore(); } //verify we are using the replication log writer Assert.IsTrue(replicationLog.GetType() == typeof(ScyllaReplicationLog)); await Task.CompletedTask; } } [TestClass] // due to timing issues creating the UDTs this test can not be run with other tests, once we have deleted usage of the UDTs we can remove this [DoNotParallelize] public class CassandraReferencesTests : ReferencesTests { public CassandraReferencesTests() : base("cassandra") { } protected override IEnumerable> GetSettings() { return new[] { new KeyValuePair("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Scylla.ToString()), new KeyValuePair("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Scylla.ToString()), new KeyValuePair("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Scylla.ToString()), new KeyValuePair("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Scylla.ToString()), new KeyValuePair("Scylla:ConnectionString", "Contact Points=localhost,scylla;Default Keyspace=jupiter_cassandra"), new KeyValuePair("Scylla:UseAzureCosmosDB", "true"), new KeyValuePair("Scylla:UseSSL", "false"), }; } protected override async Task SeedDbAsync(IServiceProvider provider) { IReferencesStore referencesStore = provider.GetService()!; if (referencesStore is MemoryCachedReferencesStore memoryReferencesStore) { memoryReferencesStore.Clear(); referencesStore = memoryReferencesStore.GetUnderlyingStore(); } //verify we are using the expected store Assert.IsTrue(referencesStore.GetType() == typeof(ScyllaReferencesStore)); IContentIdStore contentIdStore = provider.GetService()!; //verify we are using the expected store if (contentIdStore is MemoryCachedContentIdStore memoryStore) { memoryStore.Clear(); contentIdStore = memoryStore.GetUnderlyingContentIdStore(); } Assert.IsTrue(contentIdStore.GetType() == typeof(ScyllaContentIdStore)); IReplicationLog replicationLog = provider.GetService()!; if (replicationLog is MemoryCachedReplicationLog log) { replicationLog = log.GetUnderlyingContentIdStore(); } //verify we are using the replication log writer Assert.IsTrue(replicationLog.GetType() == typeof(ScyllaReplicationLog)); await Task.CompletedTask; } } [TestClass] public class MongoReferencesTests : ReferencesTests { public MongoReferencesTests() : base("mongo") { } protected override IEnumerable> GetSettings() { return new[] { new KeyValuePair("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Mongo.ToString()), new KeyValuePair("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Mongo.ToString()), new KeyValuePair("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Mongo.ToString()), // we do not have a mongo version of the replication log, as the mongo deployment is only intended for single servers new KeyValuePair("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Memory.ToString()), }; } protected override async Task SeedDbAsync(IServiceProvider provider) { IReferencesStore referencesStore = provider.GetService()!; if (referencesStore is MemoryCachedReferencesStore memoryReferencesStore) { memoryReferencesStore.Clear(); referencesStore = memoryReferencesStore.GetUnderlyingStore(); } //verify we are using the expected store Assert.IsTrue(referencesStore.GetType() == typeof(MongoReferencesStore)); IContentIdStore contentIdStore = provider.GetService()!; if (contentIdStore is MemoryCachedContentIdStore memoryStore) { memoryStore.Clear(); contentIdStore = memoryStore.GetUnderlyingContentIdStore(); } //verify we are using the expected store Assert.IsTrue(contentIdStore.GetType() == typeof(MongoContentIdStore)); IReplicationLog replicationLog = provider.GetService()!; if (replicationLog is MemoryCachedReplicationLog log) { replicationLog = log.GetUnderlyingContentIdStore(); } //verify we are using the replication log writer Assert.IsTrue(replicationLog.GetType() == typeof(MemoryReplicationLog)); await Task.CompletedTask; } } [TestClass] public class MemoryReferencesTests : ReferencesTests { public MemoryReferencesTests() : base("memory") { } protected override IEnumerable> GetSettings() { return new[] { new KeyValuePair("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Memory.ToString()), new KeyValuePair("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Memory.ToString()), new KeyValuePair("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Memory.ToString()), }; } protected override async Task SeedDbAsync(IServiceProvider provider) { IReferencesStore referencesStore = provider.GetService()!; if (referencesStore is MemoryCachedReferencesStore memoryReferencesStore) { memoryReferencesStore.Clear(); referencesStore = memoryReferencesStore.GetUnderlyingStore(); } //verify we are using the expected refs store Assert.IsTrue(referencesStore.GetType() == typeof(MemoryReferencesStore)); IContentIdStore contentIdStore = provider.GetService()!; if (contentIdStore is MemoryCachedContentIdStore memoryStore) { memoryStore.Clear(); contentIdStore = memoryStore.GetUnderlyingContentIdStore(); } //verify we are using the expected store Assert.IsTrue(contentIdStore.GetType() == typeof(MemoryContentIdStore)); IReplicationLog replicationLog = provider.GetService()!; if (replicationLog is MemoryCachedReplicationLog log) { replicationLog = log.GetUnderlyingContentIdStore(); } //verify we are using the replication log writer Assert.IsTrue(replicationLog.GetType() == typeof(MemoryReplicationLog)); await Task.CompletedTask; } } public abstract class ReferencesTests { protected ReferencesTests(string namespaceSuffix) { TestNamespace = new NamespaceId($"test-namespace-{namespaceSuffix}"); TestNamespaceNoOverwrite = new NamespaceId($"test-namespace-no-overwrite-{namespaceSuffix}"); NamespaceToBeDeleted = new NamespaceId($"test-namespace-delete-{namespaceSuffix}"); } private TestServer? _server; private HttpClient? _httpClient; protected IBlobService Service { get; set; } = null!; protected IReferencesStore ReferencesStore { get; set; } = null!; protected NamespaceId TestNamespace { get; init; } protected NamespaceId TestNamespaceNoOverwrite { get; init; } protected NamespaceId NamespaceToBeDeleted { get; init; } [TestInitialize] public async Task SetupAsync() { 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(GetSettings()) .AddEnvironmentVariables() .Build(); Logger logger = new LoggerConfiguration() .ReadFrom.Configuration(configuration) .CreateLogger(); TestServer server = new TestServer(new WebHostBuilder() .UseConfiguration(configuration) .UseEnvironment("Testing") .ConfigureServices(collection => collection.AddSerilog(logger)) .UseStartup() ); _httpClient = server.CreateClient(); _server = server; Service = _server.Services.GetService()!; ReferencesStore = _server.Services.GetService()!; await SeedDbAsync(server.Services); } protected abstract IEnumerable> GetSettings(); protected abstract Task SeedDbAsync(IServiceProvider provider); [TestMethod] public async Task PutGetBlobAsync() { const string objectContents = $"This is treated as a opaque blob in {nameof(PutGetBlobAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); RefId key = RefId.FromName("newBlobObject"); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedPayload = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(objectContents, roundTrippedPayload); CollectionAssert.AreEqual(data, roundTrippedBuffer); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } { BlobId attachment; { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); List fields = cb.ToList(); Assert.AreEqual(2, fields.Count); CbField payloadField = fields[0]; Assert.IsNotNull(payloadField); Assert.IsTrue(payloadField.IsBinaryAttachment()); attachment = BlobId.FromIoHash(payloadField.AsBinaryAttachment()); } { HttpResponseMessage getAttachment = await _httpClient.GetAsync(new Uri($"api/v1/blobs/{TestNamespace}/{attachment}", UriKind.Relative)); getAttachment.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getAttachment.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedString = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(objectContents, roundTrippedString); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? jsonNode = JsonNode.Parse(s); Assert.IsNotNull(jsonNode); Assert.AreEqual(objectHash, new BlobId(jsonNode["RawHash"]!.GetValue())); } { // request the object as a json response using accept instead of the format filter using HttpRequestMessage request = new(HttpMethod.Get, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); HttpResponseMessage getResponse = await _httpClient.SendAsync(request); getResponse.EnsureSuccessStatusCode(); Assert.AreEqual(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? node = JsonNode.Parse(s); Assert.IsNotNull(node); Assert.AreEqual(objectHash, new BlobId(node["RawHash"]!.ToString())); } { // request the object as a jupiter inlined payload using HttpRequestMessage request = new(HttpMethod.Get, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(CustomMediaTypeNames.JupiterInlinedPayload)); HttpResponseMessage getResponse = await _httpClient.SendAsync(request); getResponse.EnsureSuccessStatusCode(); Assert.AreEqual(CustomMediaTypeNames.JupiterInlinedPayload, getResponse.Content.Headers.ContentType?.MediaType); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedString = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(objectContents, roundTrippedString); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } } [TestMethod] public async Task PutGetBlobNewAsync() { const string objectContents = $"This is treated as a opaque blob in {nameof(PutGetBlobAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); RefId key = RefId.FromName("newBlobObject"); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedPayload = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(objectContents, roundTrippedPayload); CollectionAssert.AreEqual(data, roundTrippedBuffer); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } { BlobId attachment; { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); List fields = cb.ToList(); Assert.AreEqual(2, fields.Count); CbField payloadField = fields[0]; Assert.IsNotNull(payloadField); Assert.IsTrue(payloadField.IsBinaryAttachment()); attachment = BlobId.FromIoHash(payloadField.AsBinaryAttachment()); } { HttpResponseMessage getAttachment = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}/blobs/{attachment}", UriKind.Relative)); getAttachment.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getAttachment.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedString = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(objectContents, roundTrippedString); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? jsonNode = JsonNode.Parse(s); Assert.IsNotNull(jsonNode); Assert.AreEqual(objectHash, new BlobId(jsonNode["RawHash"]!.GetValue())); } { // request the object as a json response using accept instead of the format filter using HttpRequestMessage request = new(HttpMethod.Get, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); HttpResponseMessage getResponse = await _httpClient.SendAsync(request); getResponse.EnsureSuccessStatusCode(); Assert.AreEqual(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? node = JsonNode.Parse(s); Assert.IsNotNull(node); Assert.AreEqual(objectHash, new BlobId(node["RawHash"]!.ToString())); } { // request the object as a jupiter inlined payload using HttpRequestMessage request = new(HttpMethod.Get, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(CustomMediaTypeNames.JupiterInlinedPayload)); HttpResponseMessage getResponse = await _httpClient.SendAsync(request); getResponse.EnsureSuccessStatusCode(); Assert.AreEqual(CustomMediaTypeNames.JupiterInlinedPayload, getResponse.Content.Headers.ContentType?.MediaType); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedString = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(objectContents, roundTrippedString); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } } [TestMethod] public async Task PutGetComplexTextureAsync() { byte[] texturePayload = await File.ReadAllBytesAsync("ContentId/Payloads/UncompressedTexture_CAS_dea81b6c3b565bb5089695377c98ce0f1c13b0c3.udd"); BlobId compressedPayloadIdentifier = BlobId.FromBlob(texturePayload); ContentId uncompressedPayloadIdentifier = new ContentId("DEA81B6C3B565BB5089695377C98CE0F1C13B0C3"); CbWriter writer = new CbWriter(); writer.BeginObject(); writer.WriteBinaryAttachment("payload", uncompressedPayloadIdentifier.AsIoHash()); writer.EndObject(); byte[] data = writer.ToByteArray(); BlobId objectHash = BlobId.FromBlob(data); BucketId testBucket = new BucketId("test-bucket"); RefId refName = RefId.FromName(nameof(PutGetComplexTextureAsync)); { using ByteArrayContent content = new(texturePayload); content.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompressedBuffer); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{testBucket}/{refName}/blobs/{uncompressedPayloadIdentifier}", UriKind.Relative), content); result.EnsureSuccessStatusCode(); BlobUploadResponse? response = await result.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.IsNotNull(response.Identifier); Assert.AreNotEqual(compressedPayloadIdentifier, response.Identifier); Assert.AreEqual(uncompressedPayloadIdentifier, ContentId.FromBlobIdentifier(response.Identifier)); } { using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{testBucket}/{refName}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/{testBucket}/{refName}/blobs/{uncompressedPayloadIdentifier}", UriKind.Relative)); result.EnsureSuccessStatusCode(); Assert.AreEqual(CustomMediaTypeNames.UnrealCompressedBuffer, result.Content.Headers.ContentType!.MediaType); byte[] blobContent = await result.Content.ReadAsByteArrayAsync(); CollectionAssert.AreEqual(texturePayload, blobContent); } { // verify the compressed blob can be retrieved in the blob store HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/{testBucket}/{refName}/blobs/{compressedPayloadIdentifier}", UriKind.Relative)); result.EnsureSuccessStatusCode(); Assert.AreEqual(MediaTypeNames.Application.Octet, result.Content.Headers.ContentType!.MediaType); byte[] blobContent = await result.Content.ReadAsByteArrayAsync(); CollectionAssert.AreEqual(texturePayload, blobContent); } } [TestMethod] public async Task PutGetCompactBinaryAsync() { CbWriter writer = new CbWriter(); writer.BeginObject(); writer.WriteString("stringField", nameof(PutGetCompactBinaryAsync)); writer.EndObject(); byte[] objectData = writer.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); RefId key = RefId.FromName("newReferenceObjectCb"); using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); using HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that no blobs are missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; Assert.AreNotEqual(CbField.Empty, needsField); List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } { BucketId bucket = new BucketId("bucket"); RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespace, bucket, key, IReferencesStore.FieldFlags.IncludePayload, IReferencesStore.OperationFlags.None); Assert.IsTrue(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(objectHash, objectRecord.BlobIdentifier); Assert.IsNotNull(objectRecord.InlinePayload); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(objectData, roundTrippedBuffer); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); List fields = cb.ToList(); Assert.AreEqual(1, fields.Count); CbField stringField = fields[0]; Assert.AreEqual(new Utf8String("stringField"), stringField.Name); Assert.AreEqual(nameof(PutGetCompactBinaryAsync), stringField.AsString()); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? node = JsonNode.Parse(s); Assert.IsNotNull(node); Assert.AreEqual(nameof(PutGetCompactBinaryAsync), node["stringField"]!.GetValue()); } } [TestMethod] public async Task PutGetCompactBinaryFilteringAsync() { CbWriter writer = new CbWriter(); writer.BeginObject(); writer.WriteString("stringField", nameof(PutGetCompactBinaryFilteringAsync)); writer.EndObject(); byte[] objectData = writer.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); RefId key = RefId.FromName("newReferenceObjectCBFiltering"); using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that no blobs are missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; Assert.AreNotEqual(CbField.Empty, needsField); List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } { BucketId bucket = new BucketId("bucket"); RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespace, bucket, key, IReferencesStore.FieldFlags.None, IReferencesStore.OperationFlags.None); Assert.IsTrue(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(objectHash, objectRecord.BlobIdentifier); Assert.IsNull(objectRecord.InlinePayload); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json?fields=name", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? node = JsonNode.Parse(s); Assert.IsNotNull(node); Assert.AreEqual(nameof(PutGetCompactBinaryFilteringAsync), node["stringField"]!.GetValue()); } } [TestMethod] public async Task PutLargeCompactBinaryAsync() { if (this is MongoReferencesTests) { Assert.Inconclusive("Mongo server runs out of space in the wait queue when running this test"); } byte[] data = await File.ReadAllBytesAsync($"Objects/Payloads/lyra.cb"); BlobId objectHash = BlobId.FromBlob(data); RefId key = RefId.FromName("largeCompactBinary"); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); CbObject cb = new CbObject(await result.Content.ReadAsByteArrayAsync()); CbArray needsField = cb["needs"].AsArray(); Assert.IsNotNull(needsField); Assert.AreEqual(4924, needsField.Count); } [TestMethod] public async Task PutGetCompactBinaryHierarchyAsync() { CbWriter childObjectWriter = new CbWriter(); childObjectWriter.BeginObject(); childObjectWriter.WriteString("stringField", nameof(PutGetCompactBinaryHierarchyAsync)); childObjectWriter.EndObject(); byte[] childObjectData = childObjectWriter.ToByteArray(); BlobId childObjectHash = BlobId.FromBlob(childObjectData); CbWriter parentObjectWriter = new CbWriter(); parentObjectWriter.BeginObject(); parentObjectWriter.WriteObjectAttachment("childObject", childObjectHash.AsIoHash()); parentObjectWriter.EndObject(); byte[] parentObjectData = parentObjectWriter.ToByteArray(); BlobId parentObjectHash = BlobId.FromBlob(parentObjectData); RefId key = RefId.FromName("newCbHierarchyObject"); // this first upload should fail with the child object missing { using HttpContent requestContent = new ByteArrayContent(parentObjectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, parentObjectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that one blobs is missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; Assert.AreNotEqual(CbField.Empty, needsField); List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(1, missingBlobs.Count); Assert.AreEqual(childObjectHash, missingBlobs[0]); } // upload the child object { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, new Uri($"api/v1/objects/{TestNamespace}/{childObjectHash}", UriKind.Relative)); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(CustomMediaTypeNames.UnrealCompactBinary)); HttpContent requestContent = new ByteArrayContent(childObjectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, childObjectHash.ToString()); request.Content = requestContent; HttpResponseMessage result = await _httpClient!.SendAsync(request); result.EnsureSuccessStatusCode(); Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that one blobs is missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField value = cb["identifier"]; Assert.AreNotEqual(CbField.Empty, value); Assert.AreEqual(childObjectHash, BlobId.FromIoHash(value.AsHash())); } // since we have now uploaded the child object putting the object again should result in no missing references { using HttpContent requestContent = new ByteArrayContent(parentObjectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, parentObjectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); // check that one blobs is missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } { BucketId bucket = new BucketId("bucket"); RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespace, bucket, key, IReferencesStore.FieldFlags.IncludePayload, IReferencesStore.OperationFlags.None); Assert.IsTrue(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(parentObjectHash, objectRecord.BlobIdentifier); Assert.IsNotNull(objectRecord.InlinePayload); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(parentObjectData, roundTrippedBuffer); Assert.AreEqual(parentObjectHash, BlobId.FromBlob(roundTrippedBuffer)); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); List fields = cb.ToList(); Assert.AreEqual(1, fields.Count); CbField childObjectField = fields[0]; Assert.AreEqual(new Utf8String("childObject"), childObjectField.Name); Assert.AreEqual(childObjectHash, BlobId.FromIoHash(childObjectField.AsHash())); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? node = JsonNode.Parse(s); Assert.IsNotNull(node); Assert.AreEqual(childObjectHash.ToString().ToLower(), node["childObject"]!.GetValue()); } { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/objects/{TestNamespace}/{childObjectHash}", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(childObjectData, roundTrippedBuffer); Assert.AreEqual(childObjectHash, BlobId.FromBlob(roundTrippedBuffer)); } } [TestMethod] public async Task ExistsChecksAsync() { const string objectContents = $"This is treated as a opaque blob in {nameof(ExistsChecksAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); RefId key = RefId.FromName("newObject"); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { using HttpRequestMessage message = new(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage result = await _httpClient!.SendAsync(message); result.EnsureSuccessStatusCode(); } { using HttpRequestMessage message = new(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{RefId.FromName("missingObject")}", UriKind.Relative)); HttpResponseMessage result = await _httpClient!.SendAsync(message); Assert.AreEqual(HttpStatusCode.NotFound, result.StatusCode); } } [TestMethod] public async Task ExistsChecksMultipleAsync() { BucketId bucket = new BucketId("bucket"); RefId existingObject = RefId.FromName("existingObjectMultiple"); const string objectContents = $"This is treated as a opaque blob in {nameof(ExistsChecksMultipleAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{bucket}/{existingObject}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } RefId missingObject = RefId.FromName("missingObjectMultiple"); string queryString = $"?names={bucket}.{existingObject}&names={bucket}.{missingObject}"; { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists" + queryString, UriKind.Relative)); result.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await result.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(1, response.Missing.Count); Assert.AreEqual(bucket, response.Missing[0].Bucket); Assert.AreEqual(missingObject, response.Missing[0].Key); } } [TestMethod] public async Task PutGetObjectHierarchyAsync() { string blobContents = $"This is a string that is referenced as a blob in {nameof(PutGetObjectHierarchyAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); await Service.PutObjectAsync(TestNamespace, blobData, blobHash); string blobContentsChild = $"This string is also referenced as a blob but from a child object in {nameof(PutGetObjectHierarchyAsync)}"; byte[] dataChild = Encoding.ASCII.GetBytes(blobContentsChild); BlobId blobHashChild = BlobId.FromBlob(dataChild); await Service.PutObjectAsync(TestNamespace, dataChild, blobHashChild); CbWriter writerChild = new CbWriter(); writerChild.BeginObject(); writerChild.WriteBinaryAttachment("blob", blobHashChild.AsIoHash()); writerChild.EndObject(); byte[] childDataObject = writerChild.ToByteArray(); BlobId childDataObjectHash = BlobId.FromBlob(childDataObject); await Service.PutObjectAsync(TestNamespace, childDataObject, childDataObjectHash); CbWriter writerParent = new CbWriter(); writerParent.BeginObject(); writerParent.WriteBinaryAttachment("blobAttachment", blobHash.AsIoHash()); writerParent.WriteObjectAttachment("objectAttachment", childDataObjectHash.AsIoHash()); writerParent.EndObject(); byte[] objectData = writerParent.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); RefId key = RefId.FromName("newHierarchyObject"); using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); // check the response { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); // check that no blobs are missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } // check that actual internal representation { BucketId bucket = new BucketId("bucket"); RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespace, bucket, key, IReferencesStore.FieldFlags.IncludePayload, IReferencesStore.OperationFlags.None); Assert.IsTrue(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(objectHash, objectRecord.BlobIdentifier); Assert.IsNotNull(objectRecord.InlinePayload); } // verify attachments { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(objectData, roundTrippedBuffer); Assert.AreEqual(objectHash, BlobId.FromBlob(roundTrippedBuffer)); } { BlobId blobAttachment; BlobId objectAttachment; { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); ReadOnlyMemory localMemory = new ReadOnlyMemory(roundTrippedBuffer); CbObject cb = new CbObject(roundTrippedBuffer); Assert.AreEqual(2, cb.Count()); CbField blobAttachmentField = cb["blobAttachment"]; Assert.AreNotEqual(CbField.Empty, blobAttachmentField); blobAttachment = BlobId.FromIoHash(blobAttachmentField.AsBinaryAttachment()); CbField objectAttachmentField = cb["objectAttachment"]; Assert.AreNotEqual(CbField.Empty, objectAttachmentField); objectAttachment = BlobId.FromIoHash(objectAttachmentField.AsObjectAttachment().Hash); } { HttpResponseMessage getAttachment = await _httpClient.GetAsync(new Uri($"api/v1/blobs/{TestNamespace}/{blobAttachment}", UriKind.Relative)); getAttachment.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getAttachment.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedString = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(blobContents, roundTrippedString); Assert.AreEqual(blobHash, BlobId.FromBlob(roundTrippedBuffer)); } BlobId attachedBlobIdentifier; { HttpResponseMessage getAttachment = await _httpClient.GetAsync(new Uri($"api/v1/blobs/{TestNamespace}/{objectAttachment}", UriKind.Relative)); getAttachment.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getAttachment.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); ReadOnlyMemory localMemory = new ReadOnlyMemory(roundTrippedBuffer); CbObject cb = new CbObject(roundTrippedBuffer); Assert.AreEqual(1, cb.Count()); CbField blobField = cb["blob"]; Assert.AreNotEqual(CbField.Empty, blobField); attachedBlobIdentifier = BlobId.FromIoHash(blobField!.AsBinaryAttachment()); } { HttpResponseMessage getAttachment = await _httpClient.GetAsync(new Uri($"api/v1/blobs/{TestNamespace}/{attachedBlobIdentifier}", UriKind.Relative)); getAttachment.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getAttachment.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string roundTrippedString = Encoding.ASCII.GetString(roundTrippedBuffer); Assert.AreEqual(blobContentsChild, roundTrippedString); Assert.AreEqual(blobHashChild, BlobId.FromBlob(roundTrippedBuffer)); } } // check json representation { HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); JsonNode? node = JsonNode.Parse(s); Assert.IsNotNull(node); Assert.AreEqual(blobHash, new BlobId(node["blobAttachment"]!.GetValue())); Assert.AreEqual(childDataObjectHash, new BlobId(node["objectAttachment"]!.GetValue())); } } [TestMethod] public async Task PutPartialHierarchyAsync() { // do not submit the content of the blobs, which should be reported in the response of the put string blobContents = $"This is a string that is referenced as a blob in {nameof(PutPartialHierarchyAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); string blobContentsChild = $"This string is also referenced as a blob but from a child object in {nameof(PutPartialHierarchyAsync)}"; byte[] dataChild = Encoding.ASCII.GetBytes(blobContentsChild); BlobId blobHashChild = BlobId.FromBlob(dataChild); CbWriter writerChild = new CbWriter(); writerChild.BeginObject(); writerChild.WriteBinaryAttachment("blob", blobHashChild.AsIoHash()); writerChild.EndObject(); byte[] childDataObject = writerChild.ToByteArray(); BlobId childDataObjectHash = BlobId.FromBlob(childDataObject); await Service.PutObjectAsync(TestNamespace, childDataObject, childDataObjectHash); CbWriter writerParent = new CbWriter(); writerParent.BeginObject(); writerParent.WriteBinaryAttachment("blobAttachment", blobHash.AsIoHash()); writerParent.WriteObjectAttachment("objectAttachment", childDataObjectHash.AsIoHash()); writerParent.EndObject(); byte[] objectData = writerParent.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); RefId key = RefId.FromName("newPartialHierarchyObject"); { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(2, missingBlobs.Count); Assert.IsTrue(missingBlobs.Contains(blobHash)); Assert.IsTrue(missingBlobs.Contains(blobHashChild)); } } { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, MediaTypeNames.Application.Json); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); PutObjectResponse? response = JsonSerializer.Deserialize(s, JsonTestUtils.DefaultJsonSerializerSettings); Assert.IsNotNull(response); BlobId[] missingBlobs = response.Needs.Select(hash => new BlobId(hash.HashData)).ToArray(); Assert.AreEqual(2, missingBlobs.Length); Assert.IsTrue(missingBlobs.Contains(blobHash)); Assert.IsTrue(missingBlobs.Contains(blobHashChild)); } } } [TestMethod] public async Task PutContentIdMissingBlobAsync() { IContentIdStore? contentIdStore = _server!.Services.GetService(); Assert.IsNotNull(contentIdStore); // submit a object which contains a content id, which exists but points to a blob that does not exist string blobContents = $"This is a string that is referenced as a blob in {nameof(PutContentIdMissingBlobAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); ContentId contentId = new ContentId("0000000000000000000000AA0000000000000000"); await contentIdStore.PutAsync(TestNamespace, contentId, new BlobId[] {blobHash}, blobData.Length); CbWriter writer = new CbWriter(); writer.BeginObject(); writer.WriteBinaryAttachment("blob", contentId.AsIoHash()); writer.EndObject(); byte[] objectData = writer.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); RefId key = RefId.FromName("putContentIdMissingBlob"); { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; Assert.AreNotEqual(CbField.Empty, needsField); List missingBlobs = needsField.AsArray().Select(field => field.AsHash()).ToList(); Assert.AreEqual(1, missingBlobs.Count); Assert.AreNotEqual(blobHash.AsIoHash(), missingBlobs[0], "Refs should not be returning the mapped blob identifiers as this is unknown to the client attempting to put a new ref"); Assert.AreEqual(contentId.AsBlobIdentifier(), BlobId.FromIoHash(missingBlobs[0])); } } { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, MediaTypeNames.Application.Json); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); string s = Encoding.ASCII.GetString(roundTrippedBuffer); PutObjectResponse? response = JsonSerializer.Deserialize(s, JsonTestUtils.DefaultJsonSerializerSettings); Assert.IsNotNull(response); BlobId[] missingBlobs = response.Needs.Select(field => new BlobId(field.HashData)).ToArray(); Assert.AreEqual(1, missingBlobs.Length); Assert.AreEqual(contentId.AsBlobIdentifier(), missingBlobs[0]); } } } [TestMethod] public async Task PutMissingAttachmentComplexAsync() { string blobContents = "This is a string that is referenced as a blob but will not be uploaded"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); CbWriter writer = new CbWriter(); writer.BeginObject(); writer.WriteString("name", "randomStringContent"); writer.BeginArray("values"); writer.BeginObject(); writer.WriteInteger("rawSize", 200); writer.WriteBinaryAttachment("rawHash", blobHash.AsIoHash()); writer.EndObject(); writer.EndArray(); writer.EndObject(); byte[] objectData = writer.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); RefId key = RefId.FromName("putContentIdMissingBlobComplex"); { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; Assert.AreNotEqual(CbField.Empty, needsField); List missingBlobs = needsField.AsArray().Select(field => field.AsHash()).ToList(); Assert.AreEqual(1, missingBlobs.Count); Assert.AreEqual(blobHash.AsIoHash(), missingBlobs[0], "Expected to find missing hash even for attachments not directly in the root object"); } } } [TestMethod] public async Task PutAndFinalizeAsync() { BucketId bucket = new BucketId("bucket"); RefId key = RefId.FromName("willFinalizeObject"); // do not submit the content of the blobs, which should be reported in the response of the put string blobContents = $"This is a string that is referenced as a blob in {nameof(PutAndFinalizeAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); string blobContentsChild = $"This string is also referenced as a blob but from a child object in {nameof(PutAndFinalizeAsync)}"; byte[] dataChild = Encoding.ASCII.GetBytes(blobContentsChild); BlobId blobHashChild = BlobId.FromBlob(dataChild); CbWriter writerChild = new CbWriter(); writerChild.BeginObject(); writerChild.WriteBinaryAttachment("blob", blobHashChild.AsIoHash()); writerChild.EndObject(); byte[] childDataObject = writerChild.ToByteArray(); BlobId childDataObjectHash = BlobId.FromBlob(childDataObject); await Service.PutObjectAsync(TestNamespace, childDataObject, childDataObjectHash); CbWriter writerParent = new CbWriter(); writerParent.BeginObject(); writerParent.WriteBinaryAttachment("blobAttachment", blobHash.AsIoHash()); writerParent.WriteObjectAttachment("objectAttachment", childDataObjectHash.AsIoHash()); writerParent.EndObject(); byte[] objectData = writerParent.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(2, missingBlobs.Count); Assert.IsTrue(missingBlobs.Contains(blobHash)); Assert.IsTrue(missingBlobs.Contains(blobHashChild)); } } // check that actual internal representation { RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespace, bucket, key, IReferencesStore.FieldFlags.IncludePayload, IReferencesStore.OperationFlags.None); Assert.IsFalse(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(objectHash, objectRecord.BlobIdentifier); Assert.IsNotNull(objectRecord.InlinePayload); } // upload missing pieces { await Service.PutObjectAsync(TestNamespace, blobData, blobHash); await Service.PutObjectAsync(TestNamespace, dataChild, blobHashChild); } // finalize the object as no pieces is now missing { using HttpContent requestContent = new ByteArrayContent(Array.Empty()); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}/finalize/{objectHash}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); ReadOnlyMemory localMemory = new ReadOnlyMemory(roundTrippedBuffer); CbObject cb = new CbObject(roundTrippedBuffer); CbField? needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } } // check that actual internal representation has updated its state { RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespace, bucket, key, IReferencesStore.FieldFlags.None, IReferencesStore.OperationFlags.None); Assert.IsTrue(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(objectHash, objectRecord.BlobIdentifier); } } [TestMethod] public async Task PutNoOverwriteAsync() { BucketId bucket = new BucketId("bucket"); RefId key = RefId.FromName("willFinalizeObject"); // do not submit the content of the blobs, which should be reported in the response of the put string blobContents = $"This is a string that is referenced as a blob in {nameof(PutNoOverwriteAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); string blobContentsChild = $"This string is also referenced as a blob but from a child object in {nameof(PutNoOverwriteAsync)}"; byte[] dataChild = Encoding.ASCII.GetBytes(blobContentsChild); BlobId blobHashChild = BlobId.FromBlob(dataChild); CbWriter writerChild = new CbWriter(); writerChild.BeginObject(); writerChild.WriteBinaryAttachment("blob", blobHashChild.AsIoHash()); writerChild.EndObject(); byte[] childDataObject = writerChild.ToByteArray(); BlobId childDataObjectHash = BlobId.FromBlob(childDataObject); await Service.PutObjectAsync(TestNamespaceNoOverwrite, childDataObject, childDataObjectHash); CbWriter writerParent = new CbWriter(); writerParent.BeginObject(); writerParent.WriteBinaryAttachment("blobAttachment", blobHash.AsIoHash()); writerParent.WriteObjectAttachment("objectAttachment", childDataObjectHash.AsIoHash()); writerParent.EndObject(); byte[] objectData = writerParent.ToByteArray(); BlobId objectHash = BlobId.FromBlob(objectData); { using HttpContent requestContent = new ByteArrayContent(objectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); requestContent.Headers.Add(CommonHeaders.AllowOverwrite, "true"); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespaceNoOverwrite}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(2, missingBlobs.Count); Assert.IsTrue(missingBlobs.Contains(blobHash)); Assert.IsTrue(missingBlobs.Contains(blobHashChild)); } } // upload missing pieces { await Service.PutObjectAsync(TestNamespaceNoOverwrite, blobData, blobHash); await Service.PutObjectAsync(TestNamespaceNoOverwrite, dataChild, blobHashChild); } // finalize the object as no pieces is now missing { using HttpContent requestContent = new ByteArrayContent(Array.Empty()); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespaceNoOverwrite}/bucket/{key}/finalize/{objectHash}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, CustomMediaTypeNames.UnrealCompactBinary); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); ReadOnlyMemory localMemory = new ReadOnlyMemory(roundTrippedBuffer); CbObject cb = new CbObject(roundTrippedBuffer); CbField? needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } } // check that actual internal representation has updated its state { RefRecord objectRecord = await ReferencesStore.GetAsync(TestNamespaceNoOverwrite, bucket, key, IReferencesStore.FieldFlags.None, IReferencesStore.OperationFlags.None); Assert.IsTrue(objectRecord.IsFinalized); Assert.AreEqual(key, objectRecord.Name); Assert.AreEqual(objectHash, objectRecord.BlobIdentifier); } // attempt to put this again but with different content (still referencing the same blobs though), this should now result in a 409 (Conflict) { CbWriter writerOtherObject = new CbWriter(); writerOtherObject.BeginObject(); writerOtherObject.WriteBinaryAttachment("blobAttachment-changed", blobHash.AsIoHash()); writerOtherObject.WriteObjectAttachment("objectAttachment-changed", childDataObjectHash.AsIoHash()); writerOtherObject.EndObject(); byte[] objectDataOther = writerOtherObject.ToByteArray(); BlobId objectHashOther = BlobId.FromBlob(objectDataOther); using HttpContent requestContent = new ByteArrayContent(objectDataOther); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHashOther.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespaceNoOverwrite}/bucket/{key}.uecb", UriKind.Relative), requestContent); Assert.AreEqual(result.StatusCode, HttpStatusCode.Conflict); } } [TestMethod] public async Task GetMissingContentIdRecordAsync() { string blobContents = $"This is a blob in {nameof(GetMissingContentIdRecordAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId uncompressedHash = BlobId.FromBlob(blobData); RefId key = RefId.FromName("compressedObject"); CompressedBufferUtils bufferUtils = _server!.Services.GetService()!; using MemoryStream compressedStream = new MemoryStream(); bufferUtils.CompressContent(compressedStream, OoodleCompressorMethod.Mermaid, OoodleCompressionLevel.VeryFast, blobData); byte[] compressedBuffer = compressedStream.ToArray(); BlobId compressedHash = BlobId.FromBlob(compressedBuffer); CbObject cbObject = CbObject.Build(writer => writer.WriteBinaryAttachment("Attachment", uncompressedHash.AsIoHash())); byte[] cbObjectData = cbObject.GetView().ToArray(); BlobId cbObjectHash = BlobId.FromBlob(cbObjectData); { // upload compressed blob using HttpContent requestContent = new ByteArrayContent(compressedBuffer); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompressedBuffer); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/compressed-blobs/{TestNamespace}/{uncompressedHash}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { // upload ref using HttpContent requestContent = new ByteArrayContent(cbObjectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, cbObjectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that no blobs are missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } } { // verify we can fetch the blob properly HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(cbObjectData, roundTrippedBuffer); Assert.AreEqual(cbObjectHash, BlobId.FromBlob(roundTrippedBuffer)); } { // verify that head checks find the object using HttpRequestMessage headRequest = new HttpRequestMessage(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage getResponse = await _httpClient.SendAsync(headRequest); getResponse.EnsureSuccessStatusCode(); } { // verify that the exists check doesnt find any issue HttpResponseMessage existsResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists?names=bucket.{key}", UriKind.Relative)); existsResponse.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await existsResponse.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(0, response.Missing.Count); } { // delete the blob referenced by the compressed buffer await Service.DeleteObjectAsync(TestNamespace, compressedHash); } { // the compact binary object still exists, so this returns a success (trying to resolve the attachment will fail) HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); } { // verify that head checks fail using HttpRequestMessage headRequest = new HttpRequestMessage(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage getResponse = await _httpClient.SendAsync(headRequest); Assert.AreEqual(HttpStatusCode.NotFound, getResponse.StatusCode); } { // verify that the exists check correctly returns a missing blob HttpResponseMessage existsResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists?names=bucket.{key}", UriKind.Relative)); existsResponse.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await existsResponse.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(1, response.Missing.Count); Assert.AreEqual(key, response.Missing[0].Key); Assert.AreEqual("bucket", response.Missing[0].Bucket.ToString()); } } [TestMethod] public async Task GetMissingCompressedBufferAttachmentAsync() { CbObject cbObjectAttachment = CbObject.Build(writer => writer.WriteString("ValueField", "This field has a value")); byte[] cbAttachmentData = cbObjectAttachment.GetView().ToArray(); BlobId cbAttachmentHash = BlobId.FromBlob(cbAttachmentData); RefId key = RefId.FromName("compressedAttachedObject"); CbObject cbObject = CbObject.Build(writer => writer.WriteObjectAttachment("Attachment", cbAttachmentHash.AsIoHash())); byte[] cbObjectData = cbObject.GetView().ToArray(); BlobId cbObjectHash = BlobId.FromBlob(cbObjectData); { // upload compressed blob using HttpContent requestContent = new ByteArrayContent(cbAttachmentData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/objects/{TestNamespace}/{cbAttachmentHash}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { // upload ref using HttpContent requestContent = new ByteArrayContent(cbObjectData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, cbObjectHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that no blobs are missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } } { // verify we can fetch the blob properly HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(cbObjectData, roundTrippedBuffer); Assert.AreEqual(cbObjectHash, BlobId.FromBlob(roundTrippedBuffer)); } { // verify that head checks find the object using HttpRequestMessage headRequest = new HttpRequestMessage(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage getResponse = await _httpClient.SendAsync(headRequest); getResponse.EnsureSuccessStatusCode(); } { // verify that the exists check doesnt find any issue HttpResponseMessage existsResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists?names=bucket.{key}", UriKind.Relative)); existsResponse.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await existsResponse.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(0, response.Missing.Count); } { // delete the blob referenced by the compressed buffer await Service.DeleteObjectAsync(TestNamespace, cbAttachmentHash); } { // the compact binary object still exists, so this returns a success (trying to resolve the attachment will fail) HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); } { // verify that head checks fail using HttpRequestMessage headRequest = new HttpRequestMessage(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage getResponse = await _httpClient.SendAsync(headRequest); Assert.AreEqual(HttpStatusCode.NotFound, getResponse.StatusCode); } { // verify that the exists check correctly returns a missing blob HttpResponseMessage existsResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists?names=bucket.{key}", UriKind.Relative)); existsResponse.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await existsResponse.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(1, response.Missing.Count); Assert.AreEqual(key, response.Missing[0].Key); Assert.AreEqual("bucket", response.Missing[0].Bucket.ToString()); } } [TestMethod] public async Task GetMissingBlobRecordAsync() { string blobContents = $"This is a blob in {nameof(GetMissingBlobRecordAsync)}"; byte[] blobData = Encoding.ASCII.GetBytes(blobContents); BlobId blobHash = BlobId.FromBlob(blobData); RefId key = RefId.FromName("newReferenceObjectMissingBlobs"); using HttpContent requestContent = new ByteArrayContent(blobData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); { Assert.AreEqual(CustomMediaTypeNames.UnrealCompactBinary, result!.Content.Headers.ContentType!.MediaType); // check that no blobs are missing await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CbObject cb = new CbObject(roundTrippedBuffer); CbField needsField = cb["needs"]; List missingBlobs = needsField.AsArray().Select(field => BlobId.FromIoHash(field.AsHash())).ToList(); Assert.AreEqual(0, missingBlobs.Count); } { // verify we can fetch the blob properly HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await getResponse.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); CollectionAssert.AreEqual(blobData, roundTrippedBuffer); Assert.AreEqual(blobHash, BlobId.FromBlob(roundTrippedBuffer)); } { // verify that head checks find the object using HttpRequestMessage headRequest = new HttpRequestMessage(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage getResponse = await _httpClient.SendAsync(headRequest); getResponse.EnsureSuccessStatusCode(); } { // verify that the exists check doesnt find any issue HttpResponseMessage existsResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists?names=bucket.{key}", UriKind.Relative)); existsResponse.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await existsResponse.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(0, response.Missing.Count); } { // delete the blob await Service.DeleteObjectAsync(TestNamespace, blobHash); } { // the compact binary object still exists, so this returns a success (trying to resolve the attachment will fail) HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.uecb", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); } { // the compact binary object still exists, so this returns a success (trying to resolve the attachment will fail) HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.json", UriKind.Relative)); getResponse.EnsureSuccessStatusCode(); } { // we should now see a 404 as the blob is missing HttpResponseMessage getResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); Assert.AreEqual(HttpStatusCode.NotFound, getResponse.StatusCode); Assert.AreEqual("application/problem+json", getResponse.Content.Headers.ContentType!.MediaType); string s = await getResponse.Content.ReadAsStringAsync(); ProblemDetails? problem = JsonSerializer.Deserialize(s, JsonTestUtils.DefaultJsonSerializerSettings); Assert.IsNotNull(problem); Assert.AreEqual($"Object {blobHash} in {TestNamespace} not found", problem.Title); } { // verify that head checks fail using HttpRequestMessage headRequest = new HttpRequestMessage(HttpMethod.Head, new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); HttpResponseMessage getResponse = await _httpClient.SendAsync(headRequest); Assert.AreEqual(HttpStatusCode.NotFound, getResponse.StatusCode); } { // verify that the exists check correctly returns a missing blob HttpResponseMessage existsResponse = await _httpClient.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/exists?names=bucket.{key}", UriKind.Relative)); existsResponse.EnsureSuccessStatusCode(); ExistCheckMultipleRefsResponse? response = await existsResponse.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.AreEqual(1, response.Missing.Count); Assert.AreEqual(key, response.Missing[0].Key); Assert.AreEqual("bucket", response.Missing[0].Bucket.ToString()); } } [TestMethod] public async Task EnumerateBucketAsync() { NamespaceId ns = new NamespaceId("test-namespace-enumeration"); { const string contents = $"EnumerateBlobContents0"; byte[] data = Encoding.ASCII.GetBytes(contents); BlobId hash = BlobId.FromBlob(data); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, hash.ToString()); RefId key = RefId.FromName("object0"); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{ns}/enumerationBucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { const string contents = $"EnumerateBlobContents1"; byte[] data = Encoding.ASCII.GetBytes(contents); BlobId hash = BlobId.FromBlob(data); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, hash.ToString()); RefId key = RefId.FromName("object1"); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{ns}/enumerationBucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { // verify we can fetch the enumeration properly HttpResponseMessage response = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{ns}/enumerationBucket", UriKind.Relative)); response.EnsureSuccessStatusCode(); EnumerateBucketResponse? enumerateResponse = await response.Content.ReadFromJsonAsync(); Assert.IsNotNull(enumerateResponse); Assert.AreEqual(2, enumerateResponse.RefIds.Length); } { // verify we can fetch the enumeration properly a second time (were it should be cached) HttpResponseMessage response = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{ns}/enumerationBucket", UriKind.Relative)); response.EnsureSuccessStatusCode(); EnumerateBucketResponse? enumerateResponse = await response.Content.ReadFromJsonAsync(); Assert.IsNotNull(enumerateResponse); Assert.AreEqual(2, enumerateResponse.RefIds.Length); } } [TestMethod] public async Task DeleteObjectAsync() { const string objectContents = $"This is treated as a opaque blob in {nameof(DeleteObjectAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); RefId key = RefId.FromName("deletableObject"); // submit the object { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } IReferencesStore refStore = _server!.Services.GetService()!; RefRecord refRecord = await refStore.GetAsync(TestNamespace, new BucketId("bucket"), key, IReferencesStore.FieldFlags.None, IReferencesStore.OperationFlags.None); BlobId inlinedObjectId = refRecord.BlobIdentifier; IBlobIndex index = _server!.Services.GetService()!; // verify that references are being tracked { List blobRefs = await index.GetBlobReferencesAsync(TestNamespace, inlinedObjectId).ToListAsync(); Assert.AreEqual(1, blobRefs.Count); } // verify it is present { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // delete the object { HttpResponseMessage result = await _httpClient!.DeleteAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // ensure the object is not present anymore { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); Assert.AreEqual(HttpStatusCode.NotFound, result.StatusCode); } // delete the object again which doesn't exist anymore, but we do not return that in the api (as it causes us to need to do extra work for some backends) { HttpResponseMessage result = await _httpClient!.DeleteAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // verify that the reference tracking have been cleaned up { List blobRefs = await index.GetBlobReferencesAsync(TestNamespace, inlinedObjectId).ToListAsync(); Assert.AreEqual(0, blobRefs.Count); } } [TestMethod] public async Task DropBucketAsync() { const string BucketToDelete = "delete-bucket"; const string objectContents = $"This is treated as a opaque blob in {nameof(DropBucketAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); RefId key = RefId.FromName("deletableObjectBucket"); // submit the object into multiple buckets { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{BucketToDelete}/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } // verify it is present { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // verify it is present { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/{BucketToDelete}/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // delete the bucket { HttpResponseMessage result = await _httpClient!.DeleteAsync(new Uri($"api/v1/refs/{TestNamespace}/{BucketToDelete}", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // ensure the object is present in not deleted bucket { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // ensure the object is not present anymore { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/{BucketToDelete}/{key}.raw", UriKind.Relative)); Assert.AreEqual(HttpStatusCode.NotFound, result.StatusCode); } } [TestMethod] public async Task DeleteNamespaceAsync() { const string objectContents = $"This is treated as a opaque blob in {nameof(DeleteNamespaceAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); RefId key = RefId.FromName("deletableObjectNamespace"); // submit the object into multiple namespaces { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{NamespaceToBeDeleted}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } // verify it is present { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // verify it is present { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{NamespaceToBeDeleted}/bucket/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // delete the namespace { HttpResponseMessage result = await _httpClient!.DeleteAsync(new Uri($"api/v1/refs/{NamespaceToBeDeleted}", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // ensure the object is present in not deleted namespace { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}.raw", UriKind.Relative)); result.EnsureSuccessStatusCode(); } // ensure the object is not present anymore { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs/{NamespaceToBeDeleted}/bucket/{key}.raw", UriKind.Relative)); Assert.AreEqual(HttpStatusCode.NotFound, result.StatusCode); } // make sure the namespace is not considered a valid namespace anymore { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs", UriKind.Relative)); result.EnsureSuccessStatusCode(); Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, MediaTypeNames.Application.Json); GetNamespacesResponse? response = await result.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); CollectionAssert.DoesNotContain(response.Namespaces, NamespaceToBeDeleted); } } [TestMethod] public async Task ListNamespacesAsync() { const string objectContents = $"This is treated as a opaque blob in {nameof(ListNamespacesAsync)}"; byte[] data = Encoding.ASCII.GetBytes(objectContents); BlobId objectHash = BlobId.FromBlob(data); RefId key = RefId.FromName("notUsedObject"); using HttpContent requestContent = new ByteArrayContent(data); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, objectHash.ToString()); // submit a object to make sure a namespace is created { HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/bucket/{key}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { HttpResponseMessage result = await _httpClient!.GetAsync(new Uri($"api/v1/refs", UriKind.Relative)); result.EnsureSuccessStatusCode(); Assert.AreEqual(result!.Content.Headers.ContentType!.MediaType, MediaTypeNames.Application.Json); GetNamespacesResponse? response = await result.Content.ReadFromJsonAsync(); Assert.IsNotNull(response); Assert.IsTrue(response.Namespaces.Contains(TestNamespace)); } } [TestMethod] public async Task BatchJsonRequestAsync() { // verifies that json request against the batch endpoint will fail { using HttpContent requestContent = new StringContent("{}"); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}", UriKind.Relative), requestContent); Assert.AreEqual(HttpStatusCode.UnsupportedMediaType, result.StatusCode); } } [TestMethod] public async Task BatchErrorOperationsAsync() { // seed some data BucketId bucket = new BucketId("bucket"); RefId newBlobObjectKey = RefId.FromName("thisObjectDoesNotExist"); RefId putObjectKey = RefId.FromName("putObjectFail"); CbObject ref1 = CbObject.Empty; CbWriter getObjectOp = new CbWriter(); getObjectOp.BeginObject(); getObjectOp.WriteInteger("opId", 0); getObjectOp.WriteString("op", BatchOps.BatchOp.Operation.GET.ToString()); getObjectOp.WriteString("bucket", bucket.ToString()); getObjectOp.WriteString("key", newBlobObjectKey.ToString()); getObjectOp.EndObject(); CbWriter putObjectOp = new CbWriter(); putObjectOp.BeginObject(); putObjectOp.WriteInteger("opId", 1); putObjectOp.WriteString("op", BatchOps.BatchOp.Operation.PUT.ToString()); putObjectOp.WriteString("bucket", bucket.ToString()); putObjectOp.WriteString("key", putObjectKey.ToString()); putObjectOp.WriteObject("payload", ref1); // omit the hash for a error object1.WriteHash("payloadHash", IoHash.Compute(ref1.GetView().Span)); putObjectOp.EndObject(); CbObject[] ops = new[] { getObjectOp.ToObject(), putObjectOp.ToObject(), }; CbWriter batchRequestWriter = new CbWriter(); batchRequestWriter.BeginObject(); batchRequestWriter.BeginUniformArray("ops", CbFieldType.Object); foreach (CbObject o in ops) { batchRequestWriter.WriteObject(o); } batchRequestWriter.EndUniformArray(); batchRequestWriter.EndObject(); byte[] batchRequestData = batchRequestWriter.ToByteArray(); { using HttpContent requestContent = new ByteArrayContent(batchRequestData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); BatchOpsResponse response = CbSerializer.Deserialize(roundTrippedBuffer); Assert.AreEqual(2, response.Results.Count); BatchOpsResponse.OpResponses op0 = response.Results.First(r => r.OpId == 0); Assert.IsNotNull(op0.Response); Assert.AreEqual(404, op0.StatusCode); Assert.IsTrue(!op0.Response["title"].Equals(CbField.Empty)); Assert.AreEqual($"Object not found 29911b58b3c970ba39d9690f2dca66839dd6f5d9 in bucket bucket namespace {TestNamespace}", op0.Response["title"].AsString()); BatchOpsResponse.OpResponses op1 = response.Results.First(r => r.OpId == 1); Assert.IsNotNull(op1.Response); Assert.AreEqual(500, op1.StatusCode); Assert.IsTrue(!op1.Response["title"].Equals(CbField.Empty)); Assert.AreEqual("Missing payload for operation: 1", op1.Response["title"].AsString()); } } [TestMethod] public async Task BatchGetOperationsAsync() { // seed some data BucketId bucket = new BucketId("bucket"); RefId newBlobObjectKey = RefId.FromName("newBlobObjectBatch"); CbObject newBlobObject = CbObject.Build(writer => writer.WriteString("String", $"this-has-contents-in-{nameof(BatchGetOperationsAsync)}")); { byte[] cbObjectBytes = newBlobObject.GetView().ToArray(); BlobId blobHash = BlobId.FromBlob(cbObjectBytes); using HttpContent requestContent = new ByteArrayContent(cbObjectBytes); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{bucket}/{newBlobObjectKey}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); byte[] content = await result.Content.ReadAsByteArrayAsync(); PutObjectResponse? response = CbSerializer.Deserialize(content); Assert.IsNotNull(response); Assert.IsTrue(response.Needs.Length == 0); } byte[] blobContents = Encoding.ASCII.GetBytes($"This is a attached blob in {nameof(BatchGetOperationsAsync)}"); CbObject newReferenceObject = CbObject.Build(writer => writer.WriteBinaryAttachment("Attachment", IoHash.Compute(blobContents))); RefId newReferenceObjectKey = RefId.FromName("newReferenceObjectBatch"); { BlobId blobHash = BlobId.FromBlob(blobContents); using HttpContent requestContent = new ByteArrayContent(blobContents); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/blobs/{TestNamespace}/{blobHash}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } byte[] blobContentsMissing = Encoding.ASCII.GetBytes("This contents will not be submitted"); CbObject missingAttachmentObject = CbObject.Build(writer => writer.WriteBinaryAttachment("Attachment", IoHash.Compute(blobContentsMissing))); RefId missingAttachmentKey = RefId.FromName("blobMissingAttachment"); { BlobId blobHash = BlobId.FromBlob(blobContents); using HttpContent requestContent = new ByteArrayContent(blobContents); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/blobs/{TestNamespace}/{blobHash}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { byte[] cbObjectBytes = newReferenceObject.GetView().ToArray(); BlobId blobHash = BlobId.FromBlob(cbObjectBytes); using HttpContent requestContent = new ByteArrayContent(cbObjectBytes); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{bucket}/{newReferenceObjectKey}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); result.EnsureSuccessStatusCode(); byte[] content = await result.Content.ReadAsByteArrayAsync(); PutObjectResponse response = CbSerializer.Deserialize(content); Assert.IsTrue(response.Needs.Length == 0); } CbWriter getObjectOp = new CbWriter(); getObjectOp.BeginObject(); getObjectOp.WriteInteger("opId", 0); getObjectOp.WriteString("op", BatchOps.BatchOp.Operation.GET.ToString()); getObjectOp.WriteString("bucket", bucket.ToString()); getObjectOp.WriteString("key", newBlobObjectKey.ToString()); getObjectOp.EndObject(); CbWriter getObjectOp2 = new CbWriter(); getObjectOp2.BeginObject(); getObjectOp2.WriteInteger("opId", 1); getObjectOp2.WriteString("op", BatchOps.BatchOp.Operation.GET.ToString()); getObjectOp2.WriteString("bucket", bucket.ToString()); getObjectOp2.WriteString("key", newReferenceObjectKey.ToString()); getObjectOp2.EndObject(); CbWriter getObjectOp3 = new CbWriter(); getObjectOp3.BeginObject(); getObjectOp3.WriteInteger("opId", 2); getObjectOp3.WriteString("op", BatchOps.BatchOp.Operation.GET.ToString()); getObjectOp3.WriteString("bucket", bucket.ToString()); getObjectOp3.WriteString("key", missingAttachmentKey.ToString()); getObjectOp3.WriteBool("resolveAttachments", true); getObjectOp3.EndObject(); CbObject[] ops = new[] { getObjectOp.ToObject(), getObjectOp2.ToObject(), getObjectOp3.ToObject(), }; CbWriter batchRequestWriter = new CbWriter(); batchRequestWriter.BeginObject(); batchRequestWriter.BeginUniformArray("ops", CbFieldType.Object); foreach (CbObject o in ops) { batchRequestWriter.WriteObject(o); } batchRequestWriter.EndUniformArray(); batchRequestWriter.EndObject(); byte[] batchRequestData = batchRequestWriter.ToByteArray(); { using HttpContent requestContent = new ByteArrayContent(batchRequestData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); BatchOpsResponse response = CbSerializer.Deserialize(roundTrippedBuffer); Assert.AreEqual(3, response.Results.Count); BatchOpsResponse.OpResponses op0 = response.Results.First(r => r.OpId == 0); Assert.IsNotNull(op0.Response); Assert.AreEqual(200, op0.StatusCode); Assert.AreEqual(newBlobObject, op0.Response); BatchOpsResponse.OpResponses op1 = response.Results.First(r => r.OpId == 1); Assert.IsNotNull(op1.Response); Assert.AreEqual(200, op1.StatusCode); Assert.AreEqual(newReferenceObject, op1.Response); BatchOpsResponse.OpResponses op2 = response.Results.First(r => r.OpId == 2); Assert.IsNotNull(op2.Response); Assert.AreEqual(404, op2.StatusCode); Assert.AreNotEqual(missingAttachmentObject, op2.Response); } } [TestMethod] public async Task BatchHeadOperationsAsync() { // seed some data BucketId bucket = new BucketId("bucket"); RefId newBlobObjectKey = RefId.FromName("newBlobObjectBatchHead"); CbObject newBlobObject = CbObject.Build(writer => writer.WriteString("String", $"this-has-contents-in-{nameof(BatchHeadOperationsAsync)}")); { byte[] cbObjectBytes = newBlobObject.GetView().ToArray(); BlobId blobHash = BlobId.FromBlob(cbObjectBytes); using HttpContent requestContent = new ByteArrayContent(cbObjectBytes); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{bucket}/{newBlobObjectKey}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); byte[] content = await result.Content.ReadAsByteArrayAsync(); PutObjectResponse? response = CbSerializer.Deserialize(content); Assert.IsNotNull(response); Assert.IsTrue(response.Needs.Length == 0); } byte[] blobContents = Encoding.ASCII.GetBytes($"This is a attached blob in {nameof(BatchHeadOperationsAsync)}"); CbObject newReferenceObject = CbObject.Build(writer => writer.WriteBinaryAttachment("Attachment", IoHash.Compute(blobContents))); RefId newReferenceObjectKey = RefId.FromName("newReferenceObjectBatchHead"); { BlobId blobHash = BlobId.FromBlob(blobContents); using HttpContent requestContent = new ByteArrayContent(blobContents); requestContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Octet); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/blobs/{TestNamespace}/{blobHash}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); } { byte[] cbObjectBytes = newReferenceObject.GetView().ToArray(); BlobId blobHash = BlobId.FromBlob(cbObjectBytes); using HttpContent requestContent = new ByteArrayContent(cbObjectBytes); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{bucket}/{newReferenceObjectKey}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); byte[] content = await result.Content.ReadAsByteArrayAsync(); PutObjectResponse response = CbSerializer.Deserialize(content); Assert.IsTrue(response.Needs.Length == 0); } CbWriter getObjectOp = new CbWriter(); getObjectOp.BeginObject(); getObjectOp.WriteInteger("opId", 0); getObjectOp.WriteString("op", BatchOps.BatchOp.Operation.HEAD.ToString()); getObjectOp.WriteString("bucket", bucket.ToString()); getObjectOp.WriteString("key", newBlobObjectKey.ToString()); getObjectOp.EndObject(); CbWriter getObjectOp2 = new CbWriter(); getObjectOp2.BeginObject(); getObjectOp2.WriteInteger("opId", 1); getObjectOp2.WriteString("op", BatchOps.BatchOp.Operation.HEAD.ToString()); getObjectOp2.WriteString("bucket", bucket.ToString()); getObjectOp2.WriteString("key", newReferenceObjectKey.ToString()); getObjectOp2.EndObject(); CbObject[] ops = new[] { getObjectOp.ToObject(), getObjectOp2.ToObject(), }; CbWriter batchRequestWriter = new CbWriter(); batchRequestWriter.BeginObject(); batchRequestWriter.BeginUniformArray("ops", CbFieldType.Object); foreach (CbObject o in ops) { batchRequestWriter.WriteObject(o); } batchRequestWriter.EndUniformArray(); batchRequestWriter.EndObject(); byte[] batchRequestData = batchRequestWriter.ToByteArray(); { using HttpContent requestContent = new ByteArrayContent(batchRequestData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); BatchOpsResponse response = CbSerializer.Deserialize(roundTrippedBuffer); Assert.AreEqual(2, response.Results.Count); BatchOpsResponse.OpResponses op0 = response.Results.First(r => r.OpId == 0); Assert.IsNotNull(op0.Response); Assert.AreEqual(200, op0.StatusCode); Assert.IsTrue(!op0.Response["exists"].Equals(CbField.Empty)); Assert.IsTrue(op0.Response["exists"].AsBool()); BatchOpsResponse.OpResponses op1 = response.Results.First(r => r.OpId == 1); Assert.IsNotNull(op1.Response); Assert.AreEqual(200, op1.StatusCode); Assert.IsTrue(!op1.Response["exists"].Equals(CbField.Empty)); Assert.IsTrue(op1.Response["exists"].AsBool()); } } [TestMethod] public async Task BatchPutOperationsAsync() { BucketId bucket = new BucketId("bucket"); RefId ref0name = RefId.FromName("putRef0"); CbObject ref0 = CbObject.Build(writer => writer.WriteString("foo", "bar")); RefId ref1name = RefId.FromName("putRef1"); CbObject ref1 = CbObject.Build(writer => writer.WriteInteger("baz", 1337)); CbWriter object0 = new CbWriter(); object0.BeginObject(); object0.WriteInteger("opId", 0); object0.WriteString("op", BatchOps.BatchOp.Operation.PUT.ToString()); object0.WriteString("bucket", bucket.ToString()); object0.WriteString("key", ref0name.ToString()); object0.WriteObject("payload", ref0); object0.WriteHash("payloadHash", IoHash.Compute(ref0.GetView().Span)); object0.EndObject(); CbWriter object1 = new CbWriter(); object1.BeginObject(); object1.WriteInteger("opId", 1); object1.WriteString("op", BatchOps.BatchOp.Operation.PUT.ToString()); object1.WriteString("bucket", bucket.ToString()); object1.WriteString("key", ref1name.ToString()); object1.WriteObject("payload", ref1); object1.WriteHash("payloadHash", IoHash.Compute(ref1.GetView().Span)); object1.EndObject(); CbObject[] ops = new[] { object0.ToObject(), object1.ToObject(), }; CbWriter batchRequestWriter = new CbWriter(); batchRequestWriter.BeginObject(); batchRequestWriter.BeginUniformArray("ops", CbFieldType.Object); foreach (CbObject o in ops) { batchRequestWriter.WriteObject(o); } batchRequestWriter.EndUniformArray(); batchRequestWriter.EndObject(); byte[] batchRequestData = batchRequestWriter.ToByteArray(); { using HttpContent requestContent = new ByteArrayContent(batchRequestData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); BatchOpsResponse response = CbSerializer.Deserialize(roundTrippedBuffer); Assert.AreEqual(2, response.Results.Count); BatchOpsResponse.OpResponses op0 = response.Results.First(r => r.OpId == 0); Assert.IsNotNull(op0.Response); Assert.AreEqual(200, op0.StatusCode, $"Expected 200 response, got {op0.StatusCode} . Response: {op0.Response.ToJson()}"); Assert.IsTrue(!op0.Response["needs"].Equals(CbField.Empty)); CollectionAssert.AreEqual(Array.Empty(), op0.Response["needs"].ToArray()); BatchOpsResponse.OpResponses op1 = response.Results.First(r => r.OpId == 1); Assert.IsNotNull(op1.Response); Assert.AreEqual(200, op1.StatusCode, $"Expected 200 response, got {op1.StatusCode} . Response: {op1.Response.ToJson()}"); Assert.IsTrue(!op1.Response["needs"].Equals(CbField.Empty)); CollectionAssert.AreEqual(Array.Empty(), op1.Response["needs"].ToArray()); } } [TestMethod] public async Task BatchMixedOperationsAsync() { // seed some data BucketId bucket = new BucketId("bucket"); RefId getObjectKey = RefId.FromName("getBlobObject"); CbObject getObject = CbObject.Build(writer => writer.WriteString("String", $"this-has-contents-in-{nameof(BatchMixedOperationsAsync)}-0")); RefId putObjectKey = RefId.FromName("putBlobObject"); CbObject putObject = CbObject.Build(writer => writer.WriteString("String", $"this-has-contents-in-{nameof(BatchMixedOperationsAsync)}-1")); RefId missingObjectKey = RefId.FromName("thisKeyDoesNotExist"); { byte[] cbObjectBytes = getObject.GetView().ToArray(); BlobId blobHash = BlobId.FromBlob(cbObjectBytes); using HttpContent requestContent = new ByteArrayContent(cbObjectBytes); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); requestContent.Headers.Add(CommonHeaders.HashHeaderName, blobHash.ToString()); HttpResponseMessage result = await _httpClient!.PutAsync(new Uri($"api/v1/refs/{TestNamespace}/{bucket}/{getObjectKey}.uecb", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); byte[] content = await result.Content.ReadAsByteArrayAsync(); PutObjectResponse? response = CbSerializer.Deserialize(content); Assert.IsNotNull(response); Assert.IsTrue(response.Needs.Length == 0); } CbWriter getObjectOp = new CbWriter(); getObjectOp.BeginObject(); getObjectOp.WriteInteger("opId", 0); getObjectOp.WriteString("op", BatchOps.BatchOp.Operation.GET.ToString()); getObjectOp.WriteString("bucket", bucket.ToString()); getObjectOp.WriteString("key", getObjectKey.ToString()); getObjectOp.EndObject(); CbWriter putObjectOp = new CbWriter(); putObjectOp.BeginObject(); putObjectOp.WriteInteger("opId", 1); putObjectOp.WriteString("op", BatchOps.BatchOp.Operation.PUT.ToString()); putObjectOp.WriteString("bucket", bucket.ToString()); putObjectOp.WriteString("key", putObjectKey.ToString()); putObjectOp.WriteObject("payload", putObject); putObjectOp.WriteHash("payloadHash", IoHash.Compute(putObject.GetView().Span)); putObjectOp.EndObject(); CbWriter errorObjectOp = new CbWriter(); errorObjectOp.BeginObject(); errorObjectOp.WriteInteger("opId", 2); errorObjectOp.WriteString("op", BatchOps.BatchOp.Operation.GET.ToString()); errorObjectOp.WriteString("bucket", bucket.ToString()); errorObjectOp.WriteString("key", missingObjectKey.ToString()); errorObjectOp.EndObject(); CbWriter headObjectOp = new CbWriter(); headObjectOp.BeginObject(); headObjectOp.WriteInteger("opId", 3); headObjectOp.WriteString("op", BatchOps.BatchOp.Operation.HEAD.ToString()); headObjectOp.WriteString("bucket", bucket.ToString()); headObjectOp.WriteString("key", getObjectKey.ToString()); headObjectOp.EndObject(); CbObject[] ops = new[] { getObjectOp.ToObject(), putObjectOp.ToObject(), errorObjectOp.ToObject(), headObjectOp.ToObject(), }; CbWriter batchRequestWriter = new CbWriter(); batchRequestWriter.BeginObject(); batchRequestWriter.BeginUniformArray("ops", CbFieldType.Object); foreach (CbObject o in ops) { batchRequestWriter.WriteObject(o); } batchRequestWriter.EndUniformArray(); batchRequestWriter.EndObject(); byte[] batchRequestData = batchRequestWriter.ToByteArray(); { using HttpContent requestContent = new ByteArrayContent(batchRequestData); requestContent.Headers.ContentType = new MediaTypeHeaderValue(CustomMediaTypeNames.UnrealCompactBinary); HttpResponseMessage result = await _httpClient!.PostAsync(new Uri($"api/v1/refs/{TestNamespace}", UriKind.Relative), requestContent); result.EnsureSuccessStatusCode(); await using MemoryStream ms = new MemoryStream(); await result.Content.CopyToAsync(ms); byte[] roundTrippedBuffer = ms.ToArray(); BatchOpsResponse response = CbSerializer.Deserialize(roundTrippedBuffer); Assert.AreEqual(4, response.Results.Count); BatchOpsResponse.OpResponses getOp = response.Results.First(r => r.OpId == 0); Assert.IsNotNull(getOp.Response); Assert.AreEqual(200, getOp.StatusCode); Assert.AreEqual(getObject, getOp.Response); BatchOpsResponse.OpResponses putOp = response.Results.First(r => r.OpId == 1); Assert.IsNotNull(putOp.Response); Assert.AreEqual(200, putOp.StatusCode); Assert.IsTrue(!putOp.Response["needs"].Equals(CbField.Empty)); CollectionAssert.AreEqual(Array.Empty(), putOp.Response["needs"].ToArray()); BatchOpsResponse.OpResponses errorOp = response.Results.First(r => r.OpId == 2); Assert.IsNotNull(errorOp.Response); Assert.AreEqual(404, errorOp.StatusCode); Assert.IsTrue(!errorOp.Response["title"].Equals(CbField.Empty)); Assert.AreEqual($"Object not found cbc3db15b9c8253f6106158962325c8fd848daef in bucket bucket namespace {TestNamespace}", errorOp.Response["title"].AsString()); Assert.AreEqual(404, errorOp.Response["status"].AsInt32()); BatchOpsResponse.OpResponses headOp = response.Results.First(r => r.OpId == 3); Assert.IsNotNull(headOp.Response); Assert.AreEqual(200, headOp.StatusCode); Assert.IsTrue(!headOp.Response["exists"].Equals(CbField.Empty)); Assert.IsTrue(headOp.Response["exists"].AsBool()); } } } }