Files
UnrealEngine/Engine/Source/Programs/UnrealCloudDDC/Jupiter/Tests/Functional/Objects/ReferencesTests.cs
2025-05-18 13:04:45 +08:00

2659 lines
116 KiB
C#

// 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<KeyValuePair<string, string?>> GetSettings()
{
return new[]
{
new KeyValuePair<string, string?>("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Scylla.ToString()),
};
}
protected override async Task SeedDbAsync(IServiceProvider provider)
{
IReferencesStore referencesStore = provider.GetService<IReferencesStore>()!;
//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<IContentIdStore>()!;
//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<IReplicationLog>()!;
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<KeyValuePair<string, string?>> GetSettings()
{
return new[]
{
new KeyValuePair<string, string?>("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:BlobIndexImplementation", UnrealCloudDDCSettings.BlobIndexImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Scylla.ToString()),
new KeyValuePair<string, string?>("Scylla:ConnectionString", "Contact Points=localhost,scylla;Default Keyspace=jupiter_cassandra"),
new KeyValuePair<string, string?>("Scylla:UseAzureCosmosDB", "true"),
new KeyValuePair<string, string?>("Scylla:UseSSL", "false"),
};
}
protected override async Task SeedDbAsync(IServiceProvider provider)
{
IReferencesStore referencesStore = provider.GetService<IReferencesStore>()!;
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<IContentIdStore>()!;
//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<IReplicationLog>()!;
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<KeyValuePair<string, string?>> GetSettings()
{
return new[]
{
new KeyValuePair<string, string?>("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Mongo.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Mongo.ToString()),
new KeyValuePair<string, string?>("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<string, string?>("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Memory.ToString()),
};
}
protected override async Task SeedDbAsync(IServiceProvider provider)
{
IReferencesStore referencesStore = provider.GetService<IReferencesStore>()!;
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<IContentIdStore>()!;
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<IReplicationLog>()!;
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<KeyValuePair<string, string?>> GetSettings()
{
return new[]
{
new KeyValuePair<string, string?>("UnrealCloudDDC:ReferencesDbImplementation", UnrealCloudDDCSettings.ReferencesDbImplementations.Memory.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ContentIdStoreImplementation", UnrealCloudDDCSettings.ContentIdStoreImplementations.Memory.ToString()),
new KeyValuePair<string, string?>("UnrealCloudDDC:ReplicationLogWriterImplementation", UnrealCloudDDCSettings.ReplicationLogWriterImplementations.Memory.ToString()),
};
}
protected override async Task SeedDbAsync(IServiceProvider provider)
{
IReferencesStore referencesStore = provider.GetService<IReferencesStore>()!;
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<IContentIdStore>()!;
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<IReplicationLog>()!;
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<JupiterStartup>()
);
_httpClient = server.CreateClient();
_server = server;
Service = _server.Services.GetService<IBlobService>()!;
ReferencesStore = _server.Services.GetService<IReferencesStore>()!;
await SeedDbAsync(server.Services);
}
protected abstract IEnumerable<KeyValuePair<string, string?>> 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<CbField> 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<string>()));
}
{
// 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<CbField> 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<string>()));
}
{
// 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<BlobUploadResponse>();
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<BlobId> 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<CbField> 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<string>());
}
}
[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<BlobId> 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<string>());
}
}
[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<BlobId> 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<BlobId> 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<CbField> 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<string>());
}
{
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<ExistCheckMultipleRefsResponse>();
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<BlobId> 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<byte> localMemory = new ReadOnlyMemory<byte>(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<byte> localMemory = new ReadOnlyMemory<byte>(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<string>()));
Assert.AreEqual(childDataObjectHash, new BlobId(node["objectAttachment"]!.GetValue<string>()));
}
}
[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<BlobId> 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<PutObjectResponse>(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<IContentIdStore>();
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<IoHash> 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<PutObjectResponse>(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<IoHash> 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<BlobId> 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<byte>());
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<byte> localMemory = new ReadOnlyMemory<byte>(roundTrippedBuffer);
CbObject cb = new CbObject(roundTrippedBuffer);
CbField? needsField = cb["needs"];
List<BlobId> 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<BlobId> 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<byte>());
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<byte> localMemory = new ReadOnlyMemory<byte>(roundTrippedBuffer);
CbObject cb = new CbObject(roundTrippedBuffer);
CbField? needsField = cb["needs"];
List<BlobId> 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<CompressedBufferUtils>()!;
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<BlobId> 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<ExistCheckMultipleRefsResponse>();
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<ExistCheckMultipleRefsResponse>();
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<BlobId> 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<ExistCheckMultipleRefsResponse>();
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<ExistCheckMultipleRefsResponse>();
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<BlobId> 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<ExistCheckMultipleRefsResponse>();
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<ProblemDetails>(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<ExistCheckMultipleRefsResponse>();
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<EnumerateBucketResponse>();
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<EnumerateBucketResponse>();
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<IReferencesStore>()!;
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<IBlobIndex>()!;
// verify that references are being tracked
{
List<BaseBlobReference> 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<BaseBlobReference> 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<GetNamespacesResponse>();
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<GetNamespacesResponse>();
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<BatchOpsResponse>(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<PutObjectResponse?>(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<PutObjectResponse>(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<BatchOpsResponse>(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<PutObjectResponse?>(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<PutObjectResponse>(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<BatchOpsResponse>(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<BatchOpsResponse>(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<IoHash>(), 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<IoHash>(), 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<PutObjectResponse?>(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<BatchOpsResponse>(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<IoHash>(), 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());
}
}
}
}