Skip to content

feat(Storage): Add filter to ListObjectOptions and integration tests for object context#11

Open
mahendra-google wants to merge 1 commit intomainfrom
object-context
Open

feat(Storage): Add filter to ListObjectOptions and integration tests for object context#11
mahendra-google wants to merge 1 commit intomainfrom
object-context

Conversation

@mahendra-google
Copy link
Copy Markdown
Owner

@mahendra-google mahendra-google commented Feb 20, 2026

This PR introduces the ability to filter ListObjects results with the introduction of Filter property in ListObjectOptions and adds comprehensive integration tests for the Object Context feature. Please see b/434726579

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @mahendra-google, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the test coverage for the Google Cloud Storage client library by introducing a suite of new integration tests specifically for the object context feature. These tests validate the behavior of object contexts across various operations, including copying, retrieving, patching, and uploading objects, ensuring the feature functions as expected in real-world scenarios.

Highlights

  • CopyObject Integration Tests: Added integration tests for CopyObject to verify object context preservation and overriding during copy operations.
  • GetObject Integration Test: Introduced an integration test for GetObject to confirm the correct retrieval of object contexts.
  • PatchObject Integration Tests: Implemented comprehensive integration tests for PatchObject covering scenarios such as clearing all, removing individual, modifying individual, and adding new object contexts.
  • UploadObject Integration Test: Included an integration test for UploadObject to ensure object contexts are correctly uploaded with new objects.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.IntegrationTests/CopyObjectTest.cs
    • Imported Google.Apis.Storage.v1.Data and System.Collections.Generic namespaces.
    • Added a test case CopyObject_WithObjectContext to verify object context is copied.
    • Added a test case ObjectContextsOverride to verify object context can be overridden during copy.
  • apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.IntegrationTests/GetObjectTest.cs
    • Imported Google.Apis.Storage.v1.Data, System.Collections.Generic namespaces, and TestHelpers static class.
    • Added a test case GetObjectContext to verify object context is retrieved correctly.
  • apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.IntegrationTests/PatchObjectTest.cs
    • Imported Google.Apis.Storage.v1.Data, System.Collections.Generic namespaces, and TestHelpers static class.
    • Added a test case ClearAllObjectContext to verify all object contexts can be cleared.
    • Added a test case RemoveIndividualObjectContext to verify individual object contexts can be removed.
    • Added a test case ModifyIndividualObjectContext to verify individual object contexts can be modified.
    • Added a test case AddObjectContext to verify new object contexts can be added.
  • apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.IntegrationTests/UpdateObjectTest.cs
    • Imported Google.Apis.Storage.v1.Data namespace and TestHelpers static class.
    • Added a test case UpdateObject_WithObjectContext to verify object contexts are updated correctly.
  • apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.IntegrationTests/UploadObjectTest.cs
    • Added a test case UploadObject_WithObjectContext to verify object contexts are uploaded correctly.
Activity
  • No specific activity (comments, reviews, progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a good set of integration tests for the new object context feature, covering copy, get, patch, update, and upload operations. The test cases are logical and cover important scenarios. I have identified a couple of areas where the test assertions can be improved for correctness and robustness. One test contains an incorrect assertion that will likely cause a runtime error, and another could be strengthened to more thoroughly validate the update functionality. My specific comments provide code suggestions to address these points.

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces new integration tests for the object context feature across various Storage operations like CopyObject, GetObject, ListObjects, PatchObject, and UploadObject. The changes primarily involve adding new test methods to verify the functionality of object contexts, including setting, retrieving, updating, and filtering by custom contexts. The new tests enhance the coverage for this feature, ensuring its correct behavior in different scenarios.

Comment on lines +273 to +276
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x =>
x.Contexts == null &&
x.Contexts.Custom == null &&
!x.Contexts.Custom.ContainsKey(filteredCustom.Keys.FirstOrDefault())).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The Where clause in ListObjectsWithoutContextKey contains a logical error. If x.Contexts is null, accessing x.Contexts.Custom will result in a NullReferenceException. Also, x.Contexts.Custom == null and !x.Contexts.Custom.ContainsKey(...) are contradictory if x.Contexts.Custom is null. The logic needs to be re-evaluated to correctly filter objects that do not contain a specific context key.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x =>
                            x.Contexts == null ||
                            x.Contexts.Custom == null ||
                            !x.Contexts.Custom.ContainsKey(filteredCustom.Keys.FirstOrDefault())).ToList();


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts == filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Where clause for filtering objects by context key-value pair uses x.Contexts == filteredContext. This comparison might not work as expected for complex objects like Object.ContextsData and Dictionary<string, ObjectCustomContextPayload> in C#. By default, == for reference types checks for reference equality, not value equality. You should compare the Custom dictionaries explicitly.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts?.Custom?.SequenceEqual(filteredContext.Custom) == true).ToList();


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts != filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the previous comment, the Where clause x.Contexts != filteredContext will perform reference inequality check, which is likely not the intended behavior for comparing object contexts. You should explicitly compare the Custom dictionaries for inequality.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts?.Custom?.SequenceEqual(filteredContext.Custom) != true).ToList();

Comment on lines +278 to +279
{
Assert.Equal(obj.Contexts.Custom, filteredCustom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The assertion Assert.Equal(obj.Contexts.Custom, filteredCustom); in ListObjectsWithoutContextKey seems incorrect given the test name "ListObjectsWithoutContextKey". This test should assert that the objects do not contain the specified context key, or that the context is null/empty, not that they are equal to filteredCustom.

                Assert.False(obj.Contexts?.Custom?.ContainsKey(filteredCustom.Keys.FirstOrDefault()) == true);

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces new integration tests for the object context feature across various Storage operations like Copy, Get, List, Patch, and Upload. The added tests cover important scenarios such as copying objects with contexts, retrieving objects with contexts, listing objects with and without context filters, patching contexts (clearing, removing individual, modifying, and adding), and uploading objects with contexts. Overall, the new tests are a valuable addition, but some assertions could be more robust or logically correct to prevent potential false positives or incorrect test outcomes.

!targetKeysSet.SetEquals(x.Contexts.Custom.Keys)).ToList();
foreach (var obj in objects)
{
Assert.Equal(obj.Contexts.Custom.Keys, filteredCustom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There appears to be a logical error in this assertion. The Where clause filters for objects where the key set is not equal (!targetKeysSet.SetEquals(...)). Therefore, asserting that obj.Contexts.Custom.Keys is equal to filteredCustom.Keys (which is targetKeysSet) contradicts the filter's intent. This assertion should likely be Assert.False or Assert.NotEqual to match the filtering logic.

Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(filteredCustom.Keys));


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts == filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Comparing x.Contexts == filteredContext directly for Object.ContextsData (a class) will perform a reference equality check, not a value equality check. This means the filter will likely not work as intended unless Object.ContextsData overrides Equals and GetHashCode for deep comparison, which is uncommon for generated data classes. You'll need to compare the properties of Contexts explicitly, for example, by comparing the Custom dictionaries' contents.

var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts == filteredContext).ToList();
foreach (var obj in objects)
{
Assert.Equal(obj.Contexts.Custom, filteredCustom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the Where clause, Assert.Equal(obj.Contexts.Custom, filteredCustom); for dictionaries will typically perform a reference equality check or an order-dependent comparison, which might not be robust for dictionary contents. It's better to explicitly compare the contents of the dictionaries, for example, by checking if they have the same keys and if the values for each key are equal.


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts != filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the previous ListObjectsFilteredByContextKeyValuePair test, comparing x.Contexts != filteredContext directly for Object.ContextsData (a class) will perform a reference inequality check, not a value inequality check. This means the filter will likely not work as intended. You'll need to compare the properties of Contexts explicitly.

var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts != filteredContext).ToList();
foreach (var obj in objects)
{
Assert.NotEqual(obj.Contexts, filteredContext);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the Where clause, Assert.NotEqual(obj.Contexts, filteredContext); for Object.ContextsData will typically perform a reference inequality check. For a robust test, you should explicitly compare the contents of the Contexts objects to ensure they are indeed different as expected by the filter.

_fixture.SingleVersionBucket, destination.Name,
_fixture.MultiVersionBucket, destination.Name);
Object fetched = _fixture.Client.GetObject(_fixture.MultiVersionBucket, destination.Name);
Assert.Equal(result.Contexts.Custom.Keys, fetched.Contexts.Custom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When comparing collections like dictionary keys, it's generally more robust to use SequenceEqual from LINQ, especially since the order of keys in a dictionary is not guaranteed. This ensures that the content of the key collections is compared, not just their references or a potentially order-dependent equality check.

Assert.True(result.Contexts.Custom.Keys.SequenceEqual(fetched.Contexts.Custom.Keys));

var obj = _fixture.Client.GetObject(_fixture.MultiVersionBucket, destination.Name);
Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current assertion only checks the count of custom contexts. To ensure the object context was correctly retrieved, you should also assert that the actual key-value pairs within obj.Contexts.Custom match those in result.Contexts.Custom. This provides a more comprehensive validation of the retrieved context data.

foreach (var obj in objects)
{
Assert.NotNull(obj.Name);
Assert.NotNull(obj.Contexts);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While Assert.NotNull(obj.Contexts); confirms the presence of contexts, it doesn't verify that the content of the contexts is as expected. For a complete test, consider adding assertions to check the actual key-value pairs within obj.Contexts.Custom against the custom dictionary used for upload.

targetKeysSet.SetEquals(x.Contexts.Custom.Keys)).ToList();
foreach (var obj in objects)
{
Assert.Equal(obj.Contexts.Custom.Keys, filteredCustom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When comparing collections like dictionary keys, it's generally more robust to use SequenceEqual from LINQ, especially since the order of keys in a dictionary is not guaranteed. This ensures that the content of the key collections is compared, not just their references or a potentially order-dependent equality check.

Assert.True(obj.Contexts.Custom.Keys.SequenceEqual(filteredCustom.Keys));

var result = _fixture.Client.UploadObject(destination, source);
Assert.Equal(destination.Name, result.Name);
Assert.Equal(destination.Bucket, result.Bucket);
Assert.Equal(destination.Contexts.Custom.Keys, result.Contexts.Custom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When comparing collections like dictionary keys, it's generally more robust to use SequenceEqual from LINQ, especially since the order of keys in a dictionary is not guaranteed. This ensures that the content of the key collections is compared, not just their references or a potentially order-dependent equality check.

Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(result.Contexts.Custom.Keys));

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature, covering create, read, update, delete, and list operations. This is a valuable addition for ensuring the feature's correctness. However, I've identified several issues within the new tests, particularly in ListObjectsTest.cs, where the filtering logic is incorrect due to improper object comparisons. Additionally, some tests rely on hardcoded strings in assertions, which makes them brittle. I've provided specific feedback and code suggestions to improve the robustness and maintainability of these tests.


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts == filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The filtering logic x.Contexts == filteredContext uses reference equality for the ContextsData object. Since the ListObjects method returns new object instances, this comparison will almost certainly always be false, even if the contents are identical. The test is therefore not functioning as intended.

To fix this, you should compare the contents of the context data, for example by comparing the Custom dictionaries. Also, the test name is misleading as it implies server-side filtering, whereas the filtering is happening on the client side.


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts != filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The filtering logic x.Contexts != filteredContext uses reference inequality. For different object instances, this will almost always be true, regardless of their content. This means the test is not correctly filtering out objects with matching context data, and is not testing what its name implies.

To fix this, the comparison should be based on the content of the context data, not the object reference.

!targetKeysSet.SetEquals(x.Contexts.Custom.Keys)).ToList();
foreach (var obj in objects)
{
Assert.Equal(obj.Contexts.Custom.Keys, filteredCustom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a logical contradiction in this test. The Where clause on line 274 filters for objects where the custom context keys are not equal to targetKeysSet. However, the assertion inside the loop then checks if the keys are equal. This will cause the test to fail, and it does not correctly verify the filtering logic.

                Assert.False(targetKeysSet.SetEquals(obj.Contexts.Custom.Keys));

var obj = _fixture.Client.GetObject(_fixture.MultiVersionBucket, destination.Name);
Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current assertion only checks if the number of custom context entries is the same. This is a weak assertion as it doesn't verify the actual content (keys and values). A more robust test would compare the fetched context with the original data to ensure they are identical.

            Assert.Equal(custom, obj.Contexts.Custom);

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds valuable integration tests for the new object context feature across various operations like copy, get, list, patch, update, and upload. The tests are generally well-written and cover important scenarios. However, I've identified a few issues in the new tests within GetObjectTest.cs and ListObjectsTest.cs. Some tests in ListObjectsTest.cs have logical flaws that cause them to be ineffective, either by passing vacuously or having contradictory assertions. My specific comments provide details for correction.


var source = GenerateData(100);
var result = _fixture.Client.UploadObject(destination, source);
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x => x.Contexts == filteredContext).ToList();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Where(x => x.Contexts == filteredContext) clause uses reference equality on Object.ContextsData instances. Because filteredContext is a new object, this comparison will always be false, resulting in an empty objects list. The test then passes without executing any assertions inside the loop. To fix this, you need to perform a value-based comparison of the context data.

Comment on lines +272 to +278
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x =>
x.Contexts != null &&
!targetKeysSet.SetEquals(x.Contexts.Custom.Keys)).ToList();
foreach (var obj in objects)
{
Assert.Equal(obj.Contexts.Custom.Keys, filteredCustom.Keys);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The assertion on line 277 contradicts the filter condition. The Where clause selects objects where the context keys do not match targetKeysSet. However, the assertion Assert.Equal(obj.Contexts.Custom.Keys, filteredCustom.Keys) checks if they do match. This test is logically flawed and will fail if it correctly selects an object that meets the filter criteria.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).Where(x =>
                            x.Contexts != null &&
                            !targetKeysSet.SetEquals(x.Contexts.Custom.Keys)).ToList();
            foreach (var obj in objects)
            {
                Assert.False(targetKeysSet.SetEquals(obj.Contexts.Custom.Keys));
            }

Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(result.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion checks the result of the UploadObject call (result) rather than the GetObject call (obj). To ensure the test is validating the behavior of GetObject as its name implies, the assertion should compare against the obj variable.

            Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(obj.Contexts.Custom.Keys));

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces new integration test cases for the object context feature across various Storage operations, including CopyObject, GetObject, ListObjects, PatchObject, and UploadObject. The new tests cover important scenarios like copying contexts, retrieving contexts, listing objects by context filters, and modifying contexts via patch and update operations. My feedback focuses on enhancing the robustness and correctness of some of these new tests, specifically by improving assertions to ensure complete comparison of object contexts (both keys and values) and correcting the construction of filter strings in list operations.


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $"contexts.{custom.Keys} = {custom.Values}";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

custom.Keys and custom.Values are collections. Directly interpolating them into the filter string will insert their type names, not the actual key or value. Since the custom dictionary is initialized with a single entry, Single() should be used to extract the key and value for the filter string.

            string filter = $"contexts.{custom.Keys.Single()} = {custom.Values.Single().Value}";


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $"-contexts.{custom.Keys}:*";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

custom.Keys is a collection. Directly interpolating it into the filter string will insert its type name, not the actual key. Since the custom dictionary is initialized with a single entry, Single() should be used to extract the key for the filter string.

            string filter = $"-contexts.{custom.Keys.Single()}:*";


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $"-contexts.{custom.Keys} = {custom.Values}";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

custom.Keys and custom.Values are collections. Directly interpolating them into the filter string will insert their type names, not the actual key or value. Since the custom dictionary is initialized with a single entry, Single() should be used to extract the key and value for the filter string.

            string filter = $"-contexts.{custom.Keys.Single()} = {custom.Values.Single().Value}";


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $"contexts.{custom.Keys}:*";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

custom.Keys is a collection. Directly interpolating it into the filter string will insert its type name, not the actual key. Since the custom dictionary is initialized with a single entry, Single() should be used to extract the key for the filter string.

            string filter = $"contexts.{custom.Keys.Single()}:*";

Comment on lines +147 to +150
if (obj.Contexts != null)
{
Assert.NotNull(obj.Contexts);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The if (obj.Contexts != null) condition is redundant here. If obj.Contexts is null, Assert.NotNull(obj.Contexts) will fail. If it's not null, the if condition is met, and Assert.NotNull on a non-null object is unnecessary.

                Assert.NotNull(obj.Contexts);

Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(obj.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion only checks if the keys of the custom contexts are equal. For a complete test of context retrieval, the values associated with these keys should also be compared.

Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(obj.Contexts.Custom.Keys));
            Assert.True(destination.Contexts.Custom.All(pair => obj.Contexts.Custom.ContainsKey(pair.Key) && obj.Contexts.Custom[pair.Key].Value == pair.Value.Value));

{
if (obj.Contexts != null)
{
Assert.NotEqual(obj.Contexts.Custom.Keys, custom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Assert.NotEqual on collections typically checks for reference equality. For asserting that the content of two collections is not equal, SequenceEqual can be used with a False assertion.

                    Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(custom.Keys));

Comment on lines +184 to +185
Assert.True(fetched.Contexts.Custom.ContainsKey(newCustom.Keys.Single()));
Assert.False(fetched.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertions correctly verify the presence of the new key and absence of the old key. To fully confirm the override behavior, it would be beneficial to also assert the value of the newly added context.

Assert.True(fetched.Contexts.Custom.ContainsKey(newCustom.Keys.Single()));
            Assert.Equal(newCustom.Values.Single().Value, fetched.Contexts.Custom[newCustom.Keys.Single()].Value);
            Assert.False(fetched.Contexts.Custom.ContainsKey(custom.Keys.Single()));

{
if (obj.Contexts != null)
{
Assert.NotEqual(obj.Contexts.Custom.Keys, custom.Keys);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Assert.NotEqual on collections typically checks for reference equality. For asserting that the content of two collections is not equal, SequenceEqual can be used with a False assertion.

                    Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(custom.Keys));

var result = _fixture.Client.UploadObject(destination, source);
Assert.Equal(destination.Name, result.Name);
Assert.Equal(destination.Bucket, result.Bucket);
Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(result.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion only verifies that the keys of the custom contexts are equal. For a complete test of context uploading, the values associated with these keys should also be checked to ensure the entire context is uploaded correctly.

Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(result.Contexts.Custom.Keys));
            Assert.True(destination.Contexts.Custom.All(pair => result.Contexts.Custom.ContainsKey(pair.Key) && result.Contexts.Custom[pair.Key].Value == pair.Value.Value));

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature. The coverage is good, spanning copy, get, list, patch, update, and upload operations. I've identified several issues within the new tests, including a few critical bugs where tests are broken due to incorrect filter string construction or flawed assertion logic. There are also some cases of flaky or brittle assertions that rely on dictionary key ordering or use Single() where it's not robust. Additionally, I've made some suggestions to improve code maintainability by removing hardcoded strings and redundant code. Please see the detailed comments for specific suggestions.


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $@"-contexts.""{custom.Keys}""=""{custom.Values}""";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There's a bug in how the filter string is constructed. custom.Keys and custom.Values are collections, and calling ToString() on them will not produce the desired key and value strings for the filter. You should use Single() to get the individual key and value.

            string filter = $@"-contexts.""{custom.Keys.Single()}""=""{custom.Values.Single().Value}""";

Comment on lines +257 to +263
foreach (var obj in objects)
{
if (obj.Contexts != null)
{
Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(custom.Keys));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The assertion logic here is incorrect. The test should verify that the object with the specified context key is excluded from the results. The current loop makes a flawed check on the objects that are returned. The correct approach is to assert that the object created for this test is not present in the result list.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
            Assert.DoesNotContain(objects, o => o.Name == destination.Name);


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $@"-contexts.""{custom.Keys}"":*";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The filter string is constructed incorrectly. custom.Keys is a collection, and its ToString() method won't produce the key string needed for the filter. You should use Single() to retrieve the key.

            string filter = $@"-contexts.""{custom.Keys.Single()}"":*";

Comment on lines +228 to +235
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
foreach (var obj in objects)
{
if (obj.Contexts != null)
{
Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(custom.Keys));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The assertion logic for this test is incorrect. The goal of a negative filter is to ensure the object matching the filter is not included in the results. The current loop iterates through the results and makes a flawed assertion about their contexts. A much simpler and more correct way to test this is to assert that the object created for this test does not exist in the list of returned objects.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
            Assert.DoesNotContain(objects, o => o.Name == destination.Name);

Comment on lines +143 to +151
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).ToList();
foreach (var obj in objects)
{
Assert.NotNull(obj.Name);
if (obj.Contexts != null)
{
Assert.NotNull(obj.Contexts);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current test logic is not very specific. It lists all objects in the bucket and performs a weak check. A more robust approach would be to specifically find the object created in the test and verify its contexts are correct. This ensures the test is precise and not affected by other objects in the bucket.

            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, prefix: destination.Name).ToList();
            var obj = Assert.Single(objects);
            Assert.NotNull(obj.Contexts);
            Assert.True(obj.Contexts.Custom.ContainsKey(custom.Keys.Single()));
            Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);

Assert.True(fetched.Contexts.Custom.ContainsKey(newCustom.Keys.Single()));
Assert.False(fetched.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Assert.Equal(newCustom.Values.Single().Value, fetched.Contexts.Custom[newCustom.Keys.Single()].Value);
Assert.False(fetched.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is a duplicate of the one on line 185. It can be removed to avoid redundancy.


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal("AbcñΩ🚀", updated.Contexts.Custom["AñΩ🚀"].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using hardcoded string literals for both the expected value and the dictionary key in the assertion makes the test harder to maintain. It's better to use the variables from the test setup (modifiedCustom) to make the assertion more readable and robust against changes in the test data.

            Assert.Equal(modifiedCustom.Values.Single().Value, updated.Contexts.Custom[modifiedCustom.Keys.Single()].Value);


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal(2, updated.Contexts.Custom.Keys.Count);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While asserting the count is 2 is a good start, the test could be made more robust by also asserting that both the original and the newly added context keys are present in the updated object. This would more thoroughly verify the patch operation's additive behavior.

            Assert.Equal(2, updated.Contexts.Custom.Keys.Count);
            Assert.True(updated.Contexts.Custom.ContainsKey(custom.Keys.Single()));
            Assert.True(updated.Contexts.Custom.ContainsKey(modifiedCustom.Keys.Single()));

Comment on lines +89 to +92
Assert.Equal(modifiedCustom.Count, updated.Contexts.Custom.Count);
Assert.Equal(modifiedCustom["AñΩ🚀"].Value, updated.Contexts.Custom["AñΩ🚀"].Value);
Assert.Equal(modifiedCustom["CAñΩ🚀"].Value, updated.Contexts.Custom["CAñΩ🚀"].Value);
Assert.Equal(modifiedCustom["CBñΩ🚀"].Value, updated.Contexts.Custom["CBñΩ🚀"].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions can be made more maintainable and concise. Instead of having a separate assertion with a hardcoded key for each item, you can iterate over the modifiedCustom dictionary. This makes the test more robust if you decide to add or change contexts in the test data.

            Assert.Equal(modifiedCustom.Count, updated.Contexts.Custom.Count);
            foreach (var pair in modifiedCustom)
            {
                Assert.Equal(pair.Value.Value, updated.Contexts.Custom[pair.Key].Value);
            }

var result = _fixture.Client.UploadObject(destination, source);
Assert.Equal(destination.Name, result.Name);
Assert.Equal(destination.Bucket, result.Bucket);
Assert.True(destination.Contexts.Custom.Keys.Single().SequenceEqual(result.Contexts.Custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using SequenceEqual to compare two strings (which is what Keys.Single() returns) is unconventional and less clear than using Assert.Equal. For better readability, it's recommended to use Assert.Equal for string comparisons.

            Assert.Equal(destination.Contexts.Custom.Keys.Single(), result.Contexts.Custom.Keys.Single());

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature. The changes are well-structured and cover various API endpoints. I've identified a few areas where the test assertions can be made more robust and resilient to future changes, along with some minor code style improvements. Overall, this is a solid contribution to improving test coverage.


var source = GenerateData(100);
_fixture.Client.UploadObject(destination, source);
string filter = $@"-contexts.""{custom.Keys.Single()}""=""{custom.Values.Single()}""";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The filter string construction is slightly incorrect. custom.Values.Single() will return an ObjectCustomContextPayload, not the string value itself. You should access the .Value property.

            string filter = $@"-contexts.""{custom.Keys.Single()}""=""{custom.Values.Single().Value}""";

Comment on lines +229 to +235
foreach (var obj in objects)
{
if (obj.Contexts != null)
{
Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(custom.Keys));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is weak. It only checks that if an object has contexts, they don't match the one from the object we created. A much stronger and simpler assertion would be to verify that the object we created is not present in the returned list at all.

            Assert.DoesNotContain(objects, obj => obj.Name == destination.Name);

Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(obj.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion checking for key sequence equality is redundant. The assertion on line 119 checks for the same number of items, and the assertion on line 121 checks that all keys and values from the original destination context exist in the fetched obj context. Together, they already confirm that the key sets are identical. Relying on key order with SequenceEqual can also make tests brittle if the service doesn't guarantee order.

Comment on lines +143 to +151
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket).ToList();
foreach (var obj in objects)
{
Assert.NotNull(obj.Name);
if (obj.Contexts != null)
{
Assert.NotNull(obj.Contexts);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertions in this test are quite weak. It iterates through all objects in the bucket but doesn't specifically check the object that was just uploaded. The check if (obj.Contexts != null) { Assert.NotNull(obj.Contexts); } is also redundant. A better test would be to find the newly created object in the list (e.g., by filtering with a prefix) and assert that its context is what you expect.

var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
foreach (var obj in objects)
{
Assert.True(obj.Contexts.Custom.Keys.Single().SequenceEqual(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using SequenceEqual to compare two strings (the single keys from the dictionaries) is unconventional and can be confusing. Assert.Equal would be more idiomatic for string comparison. Additionally, relying on Single() makes the test fragile if the test data changes to include more than one context.

                Assert.Equal(custom.Keys.Single(), obj.Contexts.Custom.Keys.Single());

Comment on lines +257 to +263
foreach (var obj in objects)
{
if (obj.Contexts != null)
{
Assert.False(obj.Contexts.Custom.Keys.SequenceEqual(custom.Keys));
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is weak. It only checks that if an object has contexts, they don't match the one from the object we created. A much stronger and simpler assertion would be to verify that the object we created is not present in the returned list at all.

            Assert.DoesNotContain(objects, obj => obj.Name == destination.Name);


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal("AbcñΩ🚀", updated.Contexts.Custom["AñΩ🚀"].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The hardcoded Unicode string "AbcñΩ🚀" makes the assertion less readable and maintainable. It would be better to use the modifiedCustom dictionary to retrieve the expected value, which makes the test's intent clearer and less prone to typos.

            Assert.Equal(modifiedCustom["AñΩ🚀"].Value, updated.Contexts.Custom["AñΩ🚀"].Value);

Comment on lines +90 to +92
Assert.Equal(modifiedCustom["AñΩ🚀"].Value, updated.Contexts.Custom["AñΩ🚀"].Value);
Assert.Equal(modifiedCustom["CAñΩ🚀"].Value, updated.Contexts.Custom["CAñΩ🚀"].Value);
Assert.Equal(modifiedCustom["CBñΩ🚀"].Value, updated.Contexts.Custom["CBñΩ🚀"].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions are repetitive. Using a loop to iterate over the expected contexts would make the test more concise and easier to maintain if more contexts are added in the future.

            foreach (var expectedContext in modifiedCustom)
            {
                Assert.Equal(expectedContext.Value.Value, updated.Contexts.Custom[expectedContext.Key].Value);
            }

Comment on lines +112 to +113
Assert.True(destination.Contexts.Custom.Keys.Single().SequenceEqual(result.Contexts.Custom.Keys.Single()));
Assert.Equal(destination.Contexts.Custom.Values.Single().Value, result.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions are a bit fragile and hard to read. Using Single() can cause tests to fail if the test data is ever updated to include more than one context. Also, using SequenceEqual to compare single strings is unconventional; Assert.Equal is more appropriate. A more robust approach would be to check the count and then assert the key and value directly.

            var expectedContext = Assert.Single(destination.Contexts.Custom);
            var actualContext = Assert.Single(result.Contexts.Custom);
            Assert.Equal(expectedContext.Key, actualContext.Key);
            Assert.Equal(expectedContext.Value.Value, actualContext.Value.Value);

EndOffset = "end",
MatchGlob = "a/*.txt"
MatchGlob = "a/*.txt",
Filter = "contexts.\"key\":*\""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filter string contexts."key":*" appears to have a typo with an extra double quote at the end. Based on the filter syntax, it should likely be contexts."key":*.

                Filter = "contexts.\"key\":*"

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature, covering various operations like copy, get, list, patch, update, and upload. The changes are well-structured. My review focuses on improving the robustness and correctness of the test assertions. I've found several instances where tests use fragile patterns like .Single() or incorrect methods for comparison like SequenceEqual on strings. I've provided specific suggestions to make the tests more reliable and maintainable.

Comment on lines +172 to +173
Assert.True(obj.Contexts.Custom.Keys.Single().SequenceEqual(custom.Keys.Single()));
Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are a couple of issues here:

  1. SequenceEqual on a string (custom.Keys.Single()) compares it character by character. This is likely not the intended comparison and is a bug in the test. You should use Assert.Equal for string comparison or check for key existence.
  2. Using .Single() makes the test fragile.

A more robust and correct way to assert this would be to get the single key-value pair from the custom dictionary and then check for its presence and correct value in the obj.Contexts.Custom dictionary.

                var customPair = Assert.Single(custom);
                Assert.True(obj.Contexts.Custom.ContainsKey(customPair.Key));
                Assert.Equal(customPair.Value.Value, obj.Contexts.Custom[customPair.Key].Value);

Comment on lines +199 to +200
Assert.True(obj.Contexts.Custom.Keys.Single().SequenceEqual(custom.Keys.Single()));
Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to another test in this file, there are issues with these assertions:

  1. SequenceEqual on a string (custom.Keys.Single()) is incorrect for comparing keys. It compares character sequences. You should check for key equality or existence.
  2. Using .Single() makes the test fragile to changes in test data.

Please refactor to use a more robust assertion pattern.

                var customPair = Assert.Single(custom);
                Assert.True(obj.Contexts.Custom.ContainsKey(customPair.Key));
                Assert.Equal(customPair.Value.Value, obj.Contexts.Custom[customPair.Key].Value);

Comment on lines +112 to +113
Assert.True(destination.Contexts.Custom.Keys.Single().SequenceEqual(result.Contexts.Custom.Keys.Single()));
Assert.Equal(destination.Contexts.Custom.Values.Single().Value, result.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are a couple of issues with these assertions:

  1. SequenceEqual on a string (custom.Keys.Single()) compares it character by character. This is a bug in the test logic. You should be checking for key equality.
  2. Using .Single() on Keys and Values collections separately makes the test fragile and harder to read.

A more robust approach is to get the single KeyValuePair from the dictionary and then check for its presence and value in the result.

            var customPair = Assert.Single(destination.Contexts.Custom);
            Assert.NotNull(result.Contexts?.Custom);
            Assert.True(result.Contexts.Custom.TryGetValue(customPair.Key, out var resultPayload));
            Assert.Equal(customPair.Value.Value, resultPayload.Value);
            Assert.Equal(1, result.Contexts.Custom.Count);

_fixture.SingleVersionBucket, destination.Name,
_fixture.MultiVersionBucket, destination.Name);
Object fetched = _fixture.Client.GetObject(_fixture.MultiVersionBucket, destination.Name);
Assert.True(result.Contexts.Custom.Keys.SequenceEqual(fetched.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The assertion result.Contexts.Custom.Keys.SequenceEqual(fetched.Contexts.Custom.Keys) is not robust. It only checks that the keys are the same and in the same order, but it doesn't verify the values of the contexts. Additionally, relying on key order for dictionaries can be brittle.

A more thorough check would be to verify the counts and then check that each key-value pair from the source exists in the destination.

            Assert.Equal(result.Contexts.Custom.Count, fetched.Contexts.Custom.Count);
            Assert.All(result.Contexts.Custom, kvp =>
            {
                Assert.True(fetched.Contexts.Custom.TryGetValue(kvp.Key, out var fetchedValue));
                Assert.Equal(kvp.Value.Value, fetchedValue.Value);
            });

Comment on lines +184 to +186
Assert.True(fetched.Contexts.Custom.ContainsKey(newCustom.Keys.Single()));
Assert.Equal(newCustom.Values.Single().Value, fetched.Contexts.Custom[newCustom.Keys.Single()].Value);
Assert.False(fetched.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using .Single() on the keys and values collections makes the test fragile. If the test data is changed in the future to include more than one item, these assertions will break. It's more robust and readable to use Assert.Single on the dictionary to get the KeyValuePair and then use its properties for assertions.

            var newCustomPair = Assert.Single(newCustom);
            var oldCustomPair = Assert.Single(custom);
            Assert.True(fetched.Contexts.Custom.ContainsKey(newCustomPair.Key));
            Assert.Equal(newCustomPair.Value.Value, fetched.Contexts.Custom[newCustomPair.Key].Value);
            Assert.False(fetched.Contexts.Custom.ContainsKey(oldCustomPair.Key));

Comment on lines +167 to +168
string filter = $@"contexts.""{custom.Keys.Single()}""=""{custom[custom.Keys.Single()].Value}""";
var options = new ListObjectsOptions { Filter = filter };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The construction of the filter string using .Single() is fragile. If the test data in the custom dictionary changes, this could break. It's better to extract the key-value pair in a more robust way to build the filter string.

            var customPair = Assert.Single(custom);
            var options = new ListObjectsOptions { Filter = $@"contexts."{customPair.Key}"=""{customPair.Value.Value}""" };

Comment on lines +194 to +195
string filter = $@"contexts.""{custom.Keys.Single()}"":*";
var options = new ListObjectsOptions { Filter = filter };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The construction of the filter string using .Single() is fragile. If the test data in the custom dictionary changes, this could break. It's better to extract the key in a more robust way to build the filter string.

            var customPair = Assert.Single(custom);
            var options = new ListObjectsOptions { Filter = $@"contexts."{customPair.Key}":*" };

Comment on lines +222 to +223
string filter = $@"-contexts.""{custom.Keys.Single()}""=""{custom.Values.Single().Value}""";
var options = new ListObjectsOptions { Filter = filter };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The construction of the filter string using .Single() is fragile. If the test data in the custom dictionary changes, this could break. It's better to extract the key-value pair in a more robust way to build the filter string.

            var customPair = Assert.Single(custom);
            var options = new ListObjectsOptions { Filter = $@"-contexts."{customPair.Key}"=""{customPair.Value.Value}""" };

Comment on lines +245 to +246
string filter = $@"-contexts.""{custom.Keys.Single()}"":*";
var options = new ListObjectsOptions { Filter = filter };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The construction of the filter string using .Single() is fragile. If the test data in the custom dictionary changes, this could break. It's better to extract the key in a more robust way to build the filter string.

            var customPair = Assert.Single(custom);
            var options = new ListObjectsOptions { Filter = $@"-contexts."{customPair.Key}":*" };


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal(modifiedCustom[modifiedCustom.Keys.Single()].Value, updated.Contexts.Custom[modifiedCustom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using .Single() on Keys and Values collections separately is fragile and less readable. A better approach is to get the single KeyValuePair from the dictionary using Assert.Single and then use its Key and Value properties. This makes the test more robust and the intent clearer.

            var modifiedPair = Assert.Single(modifiedCustom);
            Assert.Equal(modifiedPair.Value.Value, updated.Contexts.Custom[modifiedPair.Key].Value);

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature. The tests cover various scenarios including creating, getting, listing, updating, patching, and copying objects with contexts. The changes are well-structured. I've made a few suggestions to improve the robustness and clarity of some test assertions.

_fixture.SingleVersionBucket, destination.Name,
_fixture.MultiVersionBucket, destination.Name);
Object fetched = _fixture.Client.GetObject(_fixture.MultiVersionBucket, destination.Name);
Assert.True(result.Contexts.Custom.Keys.SequenceEqual(fetched.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The assertion using SequenceEqual on dictionary keys is not robust as it relies on the order of keys, which is not guaranteed. Additionally, it only validates the keys and not the values. A more reliable approach is to check for the equality of counts and then verify that all key-value pairs from the source context exist in the fetched context.

            Assert.Equal(result.Contexts.Custom.Count, fetched.Contexts.Custom.Count);
            Assert.True(result.Contexts.Custom.All(p =>
                fetched.Contexts.Custom.TryGetValue(p.Key, out var val) && val.Value == p.Value.Value));

Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Assert.True(destination.Contexts.Custom.Keys.SequenceEqual(obj.Contexts.Custom.Keys));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion using SequenceEqual is brittle as it depends on the order of keys from a dictionary, which is not guaranteed. It is also redundant because the assertions on lines 119 (count check) and 121 (key-value pair check) already provide a more robust validation of the Custom contexts. This line can be removed.

var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
foreach (var obj in objects)
{
Assert.True(obj.Contexts.Custom.Keys.Single().SequenceEqual(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While SequenceEqual works for comparing strings, Assert.Equal is more idiomatic and clearly conveys the intent of comparing two strings for equality.

                Assert.Equal(custom.Keys.Single(), obj.Contexts.Custom.Keys.Single());

var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
foreach (var obj in objects)
{
Assert.True(obj.Contexts.Custom.Keys.Single().SequenceEqual(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While SequenceEqual works for comparing strings, Assert.Equal is more idiomatic and clearly conveys the intent of comparing two strings for equality.

                Assert.Equal(custom.Keys.Single(), obj.Contexts.Custom.Keys.Single());

var result = _fixture.Client.UploadObject(destination, source);
Assert.Equal(destination.Name, result.Name);
Assert.Equal(destination.Bucket, result.Bucket);
Assert.True(destination.Contexts.Custom.Keys.Single().SequenceEqual(result.Contexts.Custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While SequenceEqual works for comparing strings, Assert.Equal is more idiomatic and clearly conveys the intent of comparing two strings for equality.

            Assert.Equal(destination.Contexts.Custom.Keys.Single(), result.Contexts.Custom.Keys.Single());

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature. The changes are well-structured and cover various scenarios including upload, get, copy, patch, update, and list operations. My review focuses on improving the robustness and clarity of the test assertions. Several tests could be improved to be more explicit about their expectations (e.g., asserting the exact number of items) and to be more resilient (e.g., by using Assert.Single and deconstruction for cleaner and safer assertions). I've also identified a few tests that were not fully isolated or could pass incorrectly under certain conditions, and I've provided suggestions to address these issues.

Comment on lines +167 to +174
string filter = $@"contexts.""{custom.Keys.Single()}""=""{custom[custom.Keys.Single()].Value}""";
var options = new ListObjectsOptions { Filter = filter };
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
foreach (var obj in objects)
{
Assert.Equal(custom.Keys.Single(), obj.Contexts.Custom.Keys.Single());
Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This part of the test has a few issues:

  1. It will pass even if no matching objects are found, as the foreach loop will simply not execute.
  2. The assertions inside the loop are brittle due to repeated calls to Single().
  3. The test isn't fully isolated as it lists all objects in the bucket and could be affected by other tests. It's better to filter the results to only the object created in this test.

The suggested change addresses these points by filtering for the specific object and using more robust assertions.

            var (key, value) = custom.Single();
            string filter = $@"contexts."{key}"="{value.Value}"";
            var options = new ListObjectsOptions { Filter = filter };
            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).Where(o => o.Name == destination.Name).ToList();

            var obj = Assert.Single(objects);
            var fetchedContext = Assert.Single(obj.Contexts.Custom);
            Assert.Equal(key, fetchedContext.Key);
            Assert.Equal(value.Value, fetchedContext.Value.Value);

Comment on lines +194 to +202
string filter = $@"contexts.""{custom.Keys.Single()}"":*";
var options = new ListObjectsOptions { Filter = filter };
var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).ToList();
foreach (var obj in objects)
{
Assert.Equal(custom.Keys.Single(), obj.Contexts.Custom.Keys.Single());
Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This part of the test has a few issues:

  1. It will pass even if no matching objects are found, as the foreach loop will simply not execute.
  2. The assertions inside the loop are brittle due to repeated calls to Single().
  3. The test isn't fully isolated as it lists all objects in the bucket and could be affected by other tests. It's better to filter the results to only the object created in this test.

The suggested change addresses these points by filtering for the specific object and using more robust assertions.

            var (key, value) = custom.Single();
            string filter = $@"contexts."{key}":*";
            var options = new ListObjectsOptions { Filter = filter };
            var objects = _fixture.Client.ListObjects(_fixture.ReadBucket, options: options).Where(o => o.Name == destination.Name).ToList();

            var obj = Assert.Single(objects);
            var fetchedContext = Assert.Single(obj.Contexts.Custom);
            Assert.Equal(key, fetchedContext.Key);
            Assert.Equal(value.Value, fetchedContext.Value.Value);


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal(2, updated.Contexts.Custom.Keys.Count);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This assertion is incomplete. It correctly checks that the number of contexts is 2, but it doesn't verify that both the original and the new contexts are present with their correct values. The test should be expanded to check for the presence and correctness of both contexts.

            Assert.Equal(2, updated.Contexts.Custom.Count);

            var (newKey, newValue) = modifiedCustom.Single();
            Assert.True(updated.Contexts.Custom.TryGetValue(newKey, out var newFetchedValue));
            Assert.Equal(newValue.Value, newFetchedValue.Value);

            var (oldKey, oldValue) = custom.Single();
            Assert.True(updated.Contexts.Custom.TryGetValue(oldKey, out var oldFetchedValue));
            Assert.Equal(oldValue.Value, oldFetchedValue.Value);

Comment on lines +186 to +188
Assert.True(fetched.Contexts.Custom.ContainsKey(newCustom.Keys.Single()));
Assert.Equal(newCustom.Values.Single().Value, fetched.Contexts.Custom[newCustom.Keys.Single()].Value);
Assert.False(fetched.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions can be made more robust and readable. Using Assert.Single can verify that there's exactly one context and deconstruct the result for cleaner assertions. This also implicitly verifies that the old context was removed.

            var (newKey, newValue) = newCustom.Single();
            var fetchedContext = Assert.Single(fetched.Contexts.Custom);
            Assert.Equal(newKey, fetchedContext.Key);
            Assert.Equal(newValue.Value, fetchedContext.Value.Value);

Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Assert.True(destination.Contexts.Custom.All(pair => obj.Contexts.Custom.ContainsKey(pair.Key) && obj.Contexts.Custom[pair.Key].Value == pair.Value.Value));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is correct, but for better readability and consistency with other tests, consider using Assert.Single to verify the context count and its contents, especially since you know there's only one item in destination.Contexts.Custom.

            var (key, value) = Assert.Single(destination.Contexts.Custom);
            var fetchedContext = Assert.Single(obj.Contexts.Custom);
            Assert.Equal(key, fetchedContext.Key);
            Assert.Equal(value.Value, fetchedContext.Value.Value);

Comment on lines +146 to +147
Assert.True(obj.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions can be simplified and made more robust by using Assert.Single to verify the number of contexts and deconstructing the key-value pair for cleaner checks.

            var (key, value) = custom.Single();
            var fetchedContext = Assert.Single(obj.Contexts.Custom);
            Assert.Equal(key, fetchedContext.Key);
            Assert.Equal(value.Value, fetchedContext.Value.Value);


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal(modifiedCustom[modifiedCustom.Keys.Single()].Value, updated.Contexts.Custom[modifiedCustom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion can be made more robust and readable. It's good practice to also assert that there is still only one context after the modification. Using Assert.Single can help with both.

            var (key, value) = modifiedCustom.Single();
            var fetchedContext = Assert.Single(updated.Contexts.Custom);
            Assert.Equal(key, fetchedContext.Key);
            Assert.Equal(value.Value, fetchedContext.Value.Value);

Comment on lines +112 to +113
Assert.Equal(destination.Contexts.Custom.Keys.Single(), result.Contexts.Custom.Keys.Single());
Assert.Equal(destination.Contexts.Custom.Values.Single().Value, result.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions can be made more robust and readable by using Assert.Single to verify there's exactly one context and deconstructing the result for cleaner checks.

            var (key, value) = Assert.Single(destination.Contexts.Custom);
            var resultContext = Assert.Single(result.Contexts.Custom);
            Assert.Equal(key, resultContext.Key);
            Assert.Equal(value.Value, resultContext.Value.Value);

@mahendra-google
Copy link
Copy Markdown
Owner Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a comprehensive set of integration tests for the new object context feature, covering upload, get, copy, patch, update, and list operations. The changes also include adding a Filter option to ListObjectsOptions to support filtering by context. The new tests are well-structured and cover various scenarios, including using non-ASCII characters. My review focuses on improving the readability and robustness of assertions in several test cases to make them clearer and more maintainable.

Comment on lines +186 to +187
Assert.True(fetched.Contexts.Custom.ContainsKey(newCustom.Keys.Single()));
Assert.Equal(newCustom.Values.Single().Value, fetched.Contexts.Custom[newCustom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions can be made more readable and robust by first extracting the single key-value pair from the newCustom dictionary. This avoids repeated calls to .Single() and separate dictionary lookups.

            var newContext = Assert.Single(newCustom);
            Assert.True(fetched.Contexts.Custom.ContainsKey(newContext.Key));
            Assert.Equal(newContext.Value.Value, fetched.Contexts.Custom[newContext.Key].Value);

Assert.Equal(_fixture.MultiVersionBucket, obj.Bucket);
Assert.NotNull(obj.Contexts);
Assert.Equal(obj.Contexts.Custom.Count, result.Contexts.Custom.Count);
Assert.True(destination.Contexts.Custom.All(pair => obj.Contexts.Custom.ContainsKey(pair.Key) && obj.Contexts.Custom[pair.Key].Value == pair.Value.Value));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This All assertion is correct but can be hard to read and debug. Since you expect a single context, consider using Assert.Single to retrieve both the expected and actual contexts, and then assert on their Key and Value properties separately. This would make the test's intent clearer and more aligned with other assertions in the test suite.

Comment on lines +146 to +147
Assert.True(obj.Contexts.Custom.ContainsKey(custom.Keys.Single()));
Assert.Equal(custom.Values.Single().Value, obj.Contexts.Custom[custom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions can be simplified for better readability and robustness. Instead of checking for the key and then fetching the value in separate steps, you can retrieve the single context from both dictionaries and compare them.

            var expectedContext = Assert.Single(custom);
            var actualContext = Assert.Single(obj.Contexts.Custom);
            Assert.Equal(expectedContext.Key, actualContext.Key);
            Assert.Equal(expectedContext.Value.Value, actualContext.Value.Value);


Object obj = new Object { Name = destination.Name, Bucket = _fixture.SingleVersionBucket, ContentType = "text/plain", Contexts = new Object.ContextsData { Custom = modifiedCustom } };
var updated = client.PatchObject(obj);
Assert.Equal(modifiedCustom[modifiedCustom.Keys.Single()].Value, updated.Contexts.Custom[modifiedCustom.Keys.Single()].Value);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is a bit dense and performs multiple dictionary lookups. For better readability and to make the test's intent clearer, consider using Assert.Single to retrieve the modified and updated contexts as KeyValuePairs, and then assert on their properties separately.

Comment on lines +112 to +115
Assert.Equal(destination.Contexts.Custom.Keys.Single(), result.Contexts.Custom.Keys.Single());
Assert.Equal(destination.Contexts.Custom.Values.Single().Value, result.Contexts.Custom[custom.Keys.Single()].Value);
Assert.NotNull(result.Contexts.Custom[custom.Keys.Single()].CreateTimeRaw);
Assert.NotNull(result.Contexts.Custom[custom.Keys.Single()].UpdateTimeRaw);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These assertions are a bit complex and perform multiple dictionary lookups. You can improve readability and robustness by using Assert.Single to get the KeyValuePair for both expected and actual contexts, and then perform assertions on their properties.

            var expectedContext = Assert.Single(destination.Contexts.Custom);
            var actualContext = Assert.Single(result.Contexts.Custom);
            Assert.Equal(expectedContext.Key, actualContext.Key);
            Assert.Equal(expectedContext.Value.Value, actualContext.Value.Value);
            Assert.NotNull(actualContext.Value.CreateTimeRaw);
            Assert.NotNull(actualContext.Value.UpdateTimeRaw);

@mahendra-google mahendra-google changed the title tests(Storage): Add new integration test cases for object context feature feat(Storage): Add filter to ListObjectOptions and integration tests for object context Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant