Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4b0d135
feat(Storage): Enable full object checksum validation for resumable u…
mahendra-google Jan 16, 2026
fe3538c
refactor(Storage): Address review feedback from the Storage Team
mahendra-google Aug 24, 2026
2c8f193
refactor(Storage): Address review feedback from the Storage Team
mahendra-google Aug 25, 2026
b97acca
docs(Storage): Remove erroneous character from comment
mahendra-google Aug 25, 2026
1e6e369
refactor(Storage): Add new property nextVersion for the storage libra…
mahendra-google Sep 10, 2026
259cc8d
refactor(Storage): Update dependency for Google.Apis.Storage.v1 to 1.…
mahendra-google Sep 10, 2026
927cfad
refactor(Storage): Address review feedback from .NET Cloud Sdk Team
mahendra-google Sep 15, 2026
9d021de
refactor(Storage): Remove test for CustomMediaUpload with intermediat…
mahendra-google Sep 16, 2026
7d2128e
refactor(Storage); Use file-scoped namespace for new test file.
mahendra-google Sep 17, 2026
f693f0e
refactor(Storage): Revert Google.Apis.Storage.v1 dependency version f…
mahendra-google Sep 18, 2026
517f421
Merge branch 'main' into feature/enable-full-object-checksum
mahendra-google Sep 18, 2026
fb8763d
refactor(Storage): Add Google.Apis package reference with version ove…
mahendra-google Sep 18, 2026
f690b9b
refactor(Storage): Revert Google.Apis dependency version from Google…
mahendra-google Sep 18, 2026
1101712
refactor(Storage): Add Google.Apis package reference with version ove…
mahendra-google Sep 18, 2026
2c0d5dc
refactor(Storage): Fix formatting of PackageReference for Google.Apis
mahendra-google Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -299,52 +300,21 @@ public void UploadObject_InvalidHash_None()
}

[Fact]
public void UploadObject_InvalidHash_ThrowOnly()
public void UploadObject_InvalidHash_RejectAndThrow()
{
var client = StorageClient.Create();
var interceptor = new BreakUploadInterceptor();
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(interceptor);
var stream = GenerateData(50);
var name = IdGenerator.FromGuid();
var bucket = _fixture.MultiVersionBucket;
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.ThrowOnly };
Assert.Throws<UploadValidationException>(() => client.UploadObject(bucket, name, null, stream, options));
// We don't delete the object, so it's still present.
ValidateData(bucket, name, new MemoryStream(interceptor.UploadedBytes));
}

[Fact]
public void UploadObject_InvalidHash_DeleteAndThrow()
{
var client = StorageClient.Create();
var interceptor = new BreakUploadInterceptor();
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(interceptor);
var stream = GenerateData(50);
var name = IdGenerator.FromGuid();
var bucket = _fixture.MultiVersionBucket;
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.DeleteAndThrow };
Assert.Throws<UploadValidationException>(() => client.UploadObject(bucket, name, null, stream, options));
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.RejectAndThrow };
var exception = Assert.Throws<GoogleApiException>(() => client.UploadObject(bucket, name, null, stream, options));
Assert.Equal(HttpStatusCode.BadRequest, exception.HttpStatusCode);
var notFound = Assert.Throws<GoogleApiException>(() => _fixture.Client.GetObject(bucket, name));
Assert.Equal(HttpStatusCode.NotFound, notFound.HttpStatusCode);
}

[Fact]
public void UploadObject_InvalidHash_DeleteAndThrow_DeleteFails()
{
var client = StorageClient.Create();
var interceptor = new BreakUploadInterceptor();
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(interceptor);
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(new BreakDeleteInterceptor());
var stream = GenerateData(50);
var name = IdGenerator.FromGuid();
var bucket = _fixture.MultiVersionBucket;
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.DeleteAndThrow };
var ex = Assert.Throws<UploadValidationException>(() => client.UploadObject(bucket, name, null, stream, options));
Assert.NotNull(ex.AdditionalFailures);
// The deletion failed, so the uploaded object still exists.
ValidateData(bucket, name, new MemoryStream(interceptor.UploadedBytes));
}

[Fact]
public async Task UploadObjectAsync_InvalidHash_None()
{
Expand All @@ -362,53 +332,21 @@ public async Task UploadObjectAsync_InvalidHash_None()
}

[Fact]
public async Task UploadObjectAsync_InvalidHash_ThrowOnly()
public async Task UploadObjectAsync_InvalidHash_RejectAndThrow()
{
var client = StorageClient.Create();
var interceptor = new BreakUploadInterceptor();
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(interceptor);
var stream = GenerateData(50);
var name = IdGenerator.FromGuid();
var bucket = _fixture.MultiVersionBucket;
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.ThrowOnly };
await Assert.ThrowsAsync<UploadValidationException>(() => client.UploadObjectAsync(bucket, name, null, stream, options));
// We don't delete the object, so it's still present.
ValidateData(bucket, name, new MemoryStream(interceptor.UploadedBytes));
}

[Fact]
public async Task UploadObjectAsync_InvalidHash_DeleteAndThrow()
{
var client = StorageClient.Create();
var interceptor = new BreakUploadInterceptor();
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(interceptor);

var stream = GenerateData(50);
var name = IdGenerator.FromGuid();
var bucket = _fixture.MultiVersionBucket;
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.DeleteAndThrow };
await Assert.ThrowsAsync<UploadValidationException>(() => client.UploadObjectAsync(bucket, name, null, stream, options));
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.RejectAndThrow };
var exception = await Assert.ThrowsAsync<GoogleApiException>(() => client.UploadObjectAsync(bucket, name, null, stream, options));
Assert.Equal(HttpStatusCode.BadRequest, exception.HttpStatusCode);
var notFound = await Assert.ThrowsAsync<GoogleApiException>(() => _fixture.Client.GetObjectAsync(bucket, name));
Assert.Equal(HttpStatusCode.NotFound, notFound.HttpStatusCode);
}

[Fact]
public async Task UploadObjectAsync_InvalidHash_DeleteAndThrow_DeleteFails()
{
var client = StorageClient.Create();
var interceptor = new BreakUploadInterceptor();
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(interceptor);
client.Service.HttpClient.MessageHandler.AddExecuteInterceptor(new BreakDeleteInterceptor());
var stream = GenerateData(50);
var name = IdGenerator.FromGuid();
var bucket = _fixture.MultiVersionBucket;
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.DeleteAndThrow };
var ex = await Assert.ThrowsAsync<UploadValidationException>(() => client.UploadObjectAsync(bucket, name, null, stream, options));
Assert.NotNull(ex.AdditionalFailures);
// The deletion failed, so the uploaded object still exists.
ValidateData(bucket, name, new MemoryStream(interceptor.UploadedBytes));
}

[Fact]
public async Task InitiateUploadSessionAsync_NegativeLength()
{
Expand Down Expand Up @@ -488,19 +426,100 @@ public async Task InterceptAsync(HttpRequestMessage request, CancellationToken c
}
}

private class BreakDeleteInterceptor : IHttpExecuteInterceptor
[Fact]
public async Task CustomMediaUpload_ResumeAsync_WithStreamGap_FailsWithArgumentException()
{
public Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)
var client = _fixture.Client;
var bucket = _fixture.MultiVersionBucket;
var name = IdGenerator.FromGuid();

int chunk1Size = 256 * 1024;
int chunk2Size = 100;
int totalSize = chunk1Size + chunk2Size;

var fullData = GenerateData(totalSize);
byte[] fullBytes = fullData.ToArray();

var uploadUri = await client.InitiateUploadSessionAsync(bucket, name, "application/octet-stream", totalSize);

// Upload the first 256 KiB chunk directly using HTTP PUT so GCS contains bytes 0..262143
var chunk1Content = new ByteArrayContent(fullBytes, 0, chunk1Size);
chunk1Content.Headers.Add("Content-Range", $"bytes 0-{chunk1Size - 1}/{totalSize}");
chunk1Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");

var chunk1Request = new HttpRequestMessage(HttpMethod.Put, uploadUri)
{
// We only care about Delete requests
if (request.Method == HttpMethod.Delete)
{
// Ugly but effective hack: replace the generation URL parameter so that we add a leading 9,
// so the generation we try to delete is the wrong one.
request.RequestUri = new Uri(request.RequestUri.ToString().Replace("generation=", "generation=9"));
}
return Task.FromResult(0);
}
Content = chunk1Content
};
var chunk1Response = await client.Service.HttpClient.SendAsync(chunk1Request);
Assert.Equal((HttpStatusCode) 308, chunk1Response.StatusCode);

// Resume the session using a new CustomMediaUpload instance with validation enabled
var fullStream = new MemoryStream(fullBytes);
var destination = new Object { Bucket = bucket, Name = name, ContentType = "application/octet-stream" };
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.RejectAndThrow };

var uploader = (CustomMediaUpload) client.CreateObjectUploader(destination, fullStream, options);

// Execute ResumeAsync via the Google.Apis ResumableUpload
var progress = await uploader.ResumeAsync(uploadUri);

// Verify failure and exception
Assert.Equal(UploadStatus.Failed, progress.Status);
var exception = Assert.IsType<ArgumentException>(progress.Exception);
Assert.Contains("Cannot perform hash validation when resuming", exception.Message);
Assert.Equal("stream", exception.ParamName);

// Verify ThrowOnFailure() unwraps and rethrows the ArgumentException
var thrown = Assert.Throws<ArgumentException>(() => progress.ThrowOnFailure());
Assert.Same(exception, thrown);
}

[Fact]
public async Task CustomMediaUpload_ResumeSync_WithStreamGap_FailsWithArgumentException()
{
var client = _fixture.Client;
var bucket = _fixture.MultiVersionBucket;
var name = IdGenerator.FromGuid();

int chunk1Size = 256 * 1024;
int chunk2Size = 100;
int totalSize = chunk1Size + chunk2Size;

var fullData = GenerateData(totalSize);
byte[] fullBytes = fullData.ToArray();

var uploadUri = await client.InitiateUploadSessionAsync(bucket, name, "application/octet-stream", totalSize);

// Upload first 256 KiB chunk directly via HTTP
var chunk1Content = new ByteArrayContent(fullBytes, 0, chunk1Size);
chunk1Content.Headers.Add("Content-Range", $"bytes 0-{chunk1Size - 1}/{totalSize}");
chunk1Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");

var chunk1Request = new HttpRequestMessage(HttpMethod.Put, uploadUri)
{
Content = chunk1Content
};
var chunk1Response = await client.Service.HttpClient.SendAsync(chunk1Request);
Assert.Equal((HttpStatusCode) 308, chunk1Response.StatusCode);

// Resume synchronously with CustomMediaUpload
var fullStream = new MemoryStream(fullBytes);
var destination = new Object { Bucket = bucket, Name = name, ContentType = "application/octet-stream" };
var options = new UploadObjectOptions { UploadValidationMode = UploadValidationMode.RejectAndThrow };

var uploader = (CustomMediaUpload) client.CreateObjectUploader(destination, fullStream, options);

var progress = uploader.Resume(uploadUri);

Assert.Equal(UploadStatus.Failed, progress.Status);
var exception = Assert.IsType<ArgumentException>(progress.Exception);
Assert.Contains("Cannot perform hash validation when resuming", exception.Message);
Assert.Equal("stream", exception.ParamName);

// Verify ThrowOnFailure() unwraps and rethrows the ArgumentException
var thrown = Assert.Throws<ArgumentException>(() => progress.ThrowOnFailure());
Assert.Same(exception, thrown);
}

private Object GetExistingObject()
Expand Down
Loading
Loading