Skip to content

Commit 880c9cd

Browse files
Routing: Adds emulator repro tests for database RID in ResolvedCollectionRid
Agent-Logs-Url: https://github.com/Azure/azure-cosmos-dotnet-v3/sessions/c7d63681-8b7e-48fd-ba4d-cca94d1c546f Co-authored-by: kirankumarkolli <6880899+kirankumarkolli@users.noreply.github.com>
1 parent 90a9dca commit 880c9cd

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
//------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
//------------------------------------------------------------
4+
5+
namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests
6+
{
7+
using System;
8+
using System.Net;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.Azure.Cosmos.Routing;
12+
using Microsoft.Azure.Cosmos.Tracing;
13+
using Microsoft.Azure.Documents;
14+
using Microsoft.VisualStudio.TestTools.UnitTesting;
15+
16+
/// <summary>
17+
/// Emulator tests that exercise <see cref="Routing.CollectionCache"/> behaviour
18+
/// directly, so that edge-case paths (e.g. a database RID leaking into
19+
/// <c>ResolvedCollectionRid</c>) can be verified end-to-end against the real
20+
/// Cosmos emulator.
21+
/// </summary>
22+
[TestClass]
23+
public class CollectionCacheEmulatorTests
24+
{
25+
private CosmosClient cosmosClient;
26+
private Cosmos.Database database;
27+
private Container container;
28+
private ContainerProperties containerProperties;
29+
30+
[TestInitialize]
31+
public async Task TestInitialize()
32+
{
33+
this.cosmosClient = TestCommon.CreateCosmosClient();
34+
this.database = await this.cosmosClient.CreateDatabaseAsync(Guid.NewGuid().ToString());
35+
this.containerProperties = new ContainerProperties(
36+
id: Guid.NewGuid().ToString(),
37+
partitionKeyPath: "/pk");
38+
ContainerResponse containerResponse = await this.database.CreateContainerAsync(this.containerProperties);
39+
this.container = containerResponse.Container;
40+
}
41+
42+
[TestCleanup]
43+
public async Task TestCleanup()
44+
{
45+
if (this.database != null)
46+
{
47+
await this.database.DeleteStreamAsync();
48+
}
49+
50+
this.cosmosClient?.Dispose();
51+
}
52+
53+
/// <summary>
54+
/// Repro for the scenario where <c>ResolvedCollectionRid</c> is set to a
55+
/// database-level RID instead of a collection-level RID.
56+
///
57+
/// SCENARIO
58+
/// --------
59+
/// In some code paths (e.g. via <see cref="RenameCollectionAwareClientRetryPolicy"/>
60+
/// or a stale cache after container delete+recreate) the
61+
/// <c>DocumentServiceRequest.RequestContext.ResolvedCollectionRid</c> field can
62+
/// end up holding the database RID (4-byte form, e.g. "jy2ekg==") rather than
63+
/// a proper collection RID (8-byte form, e.g. "jy2eklxnboe="). When that
64+
/// happens, the subsequent RID-based cache look-up fails or routes to the
65+
/// wrong container.
66+
///
67+
/// FIX
68+
/// ---
69+
/// <see cref="CollectionCache.ResolveCollectionAsync"/> now:
70+
/// 1. Logs a <c>TraceWarning</c> when it detects that the pre-existing
71+
/// <c>ResolvedCollectionRid</c> is not a collection RID.
72+
/// 2. Falls back to name-based resolution in that case.
73+
/// 3. Throws <see cref="InvalidOperationException"/> at the assignment site
74+
/// if the resolved ResourceId is still not a collection RID (guards against
75+
/// a corrupt response from the server).
76+
///
77+
/// TEST
78+
/// ----
79+
/// This test injects the real database RID into
80+
/// <c>request.RequestContext.ResolvedCollectionRid</c> to simulate the bug,
81+
/// then verifies that <see cref="CollectionCache.ResolveCollectionAsync"/>
82+
/// still returns the correct <see cref="ContainerProperties"/> (with the
83+
/// proper collection RID) via the name-based fallback.
84+
/// </summary>
85+
[TestMethod]
86+
public async Task ResolveCollectionAsync_WithDatabaseRidInResolvedCollectionRid_FallsBackToNameResolutionReproTest()
87+
{
88+
// Arrange: get the real database RID from the emulator.
89+
DatabaseResponse databaseResponse = await this.database.ReadAsync();
90+
string databaseRid = databaseResponse.Resource.ResourceId;
91+
Assert.IsNotNull(databaseRid, "Database ResourceId must not be null");
92+
93+
// Sanity-check: the database RID must NOT be a collection RID.
94+
// (A database RID is 4 bytes base64-encoded; a collection RID is 8 bytes.)
95+
Assert.IsFalse(
96+
IsCollectionRid(databaseRid),
97+
$"Expected '{databaseRid}' to be a database RID, not a collection RID.");
98+
99+
// Get the container's actual collection RID so we can assert against it.
100+
ContainerResponse containerResponse = await this.container.ReadContainerAsync();
101+
string expectedCollectionRid = containerResponse.Resource.ResourceId;
102+
Assert.IsNotNull(expectedCollectionRid, "Container ResourceId must not be null");
103+
Assert.IsTrue(
104+
IsCollectionRid(expectedCollectionRid),
105+
$"Expected '{expectedCollectionRid}' to be a valid collection RID.");
106+
107+
// Build a name-based DocumentServiceRequest that simulates a read on a
108+
// document inside the container so the request goes through the
109+
// IsNameBased path of CollectionCache.ResolveCollectionAsync.
110+
string documentPath = $"dbs/{this.database.Id}/colls/{this.container.Id}/docs/someDoc";
111+
using DocumentServiceRequest request = DocumentServiceRequest.CreateFromName(
112+
OperationType.Read,
113+
documentPath,
114+
ResourceType.Document,
115+
AuthorizationTokenType.PrimaryMasterKey,
116+
null);
117+
118+
// Simulate the bug: set ResolvedCollectionRid to the database RID.
119+
request.RequestContext.ResolvedCollectionRid = databaseRid;
120+
121+
// Act: call the collection cache directly.
122+
ClientCollectionCache collectionCache =
123+
await this.cosmosClient.DocumentClient.GetCollectionCacheAsync(NoOpTrace.Singleton);
124+
125+
ContainerProperties resolved = await collectionCache.ResolveCollectionAsync(
126+
request,
127+
CancellationToken.None,
128+
NoOpTrace.Singleton);
129+
130+
// Assert: the cache must have fallen back to name-based resolution and
131+
// returned the correct collection RID — not the database RID.
132+
Assert.IsNotNull(resolved, "ResolveCollectionAsync must return a non-null ContainerProperties");
133+
Assert.AreEqual(
134+
expectedCollectionRid,
135+
resolved.ResourceId,
136+
$"Expected collection RID '{expectedCollectionRid}' but got '{resolved.ResourceId}'. " +
137+
"The cache should have fallen back to name-based resolution and returned the correct collection RID.");
138+
139+
Assert.IsTrue(
140+
IsCollectionRid(resolved.ResourceId),
141+
$"Resolved ResourceId '{resolved.ResourceId}' must be a collection RID, not a database RID.");
142+
143+
Assert.AreNotEqual(
144+
databaseRid,
145+
resolved.ResourceId,
146+
"Resolved ResourceId must not be the database RID that was injected.");
147+
}
148+
149+
/// <summary>
150+
/// Verifies that end-to-end item operations (read/write) still succeed on a
151+
/// container whose collection cache was primed with a database RID. This
152+
/// tests the full SDK retry stack rather than just the cache layer.
153+
/// </summary>
154+
[TestMethod]
155+
public async Task ItemRead_AfterDatabaseRidInjectedIntoCollectionCache_Succeeds()
156+
{
157+
// Create a test item so we have something to read back.
158+
string pk = Guid.NewGuid().ToString("N");
159+
string id = Guid.NewGuid().ToString("N");
160+
var testItem = new { id, pk };
161+
await this.container.CreateItemAsync(testItem, new Cosmos.PartitionKey(pk));
162+
163+
// Get the database RID from the emulator.
164+
DatabaseResponse databaseResponse = await this.database.ReadAsync();
165+
string databaseRid = databaseResponse.Resource.ResourceId;
166+
167+
// Warm up the collection cache so that subsequent reads use the cache
168+
// hit path; this ensures the cache is initialised before we corrupt it.
169+
ContainerResponse containerResponse = await this.container.ReadContainerAsync();
170+
string expectedCollectionRid = containerResponse.Resource.ResourceId;
171+
172+
// Build the request and inject the database RID as ResolvedCollectionRid.
173+
string documentPath = $"dbs/{this.database.Id}/colls/{this.container.Id}/docs/{id}";
174+
using DocumentServiceRequest request = DocumentServiceRequest.CreateFromName(
175+
OperationType.Read,
176+
documentPath,
177+
ResourceType.Document,
178+
AuthorizationTokenType.PrimaryMasterKey,
179+
null);
180+
181+
request.RequestContext.ResolvedCollectionRid = databaseRid;
182+
183+
ClientCollectionCache collectionCache =
184+
await this.cosmosClient.DocumentClient.GetCollectionCacheAsync(NoOpTrace.Singleton);
185+
186+
// The cache must resolve to the correct collection RID via name-based fallback.
187+
ContainerProperties resolved = await collectionCache.ResolveCollectionAsync(
188+
request,
189+
CancellationToken.None,
190+
NoOpTrace.Singleton);
191+
192+
Assert.AreEqual(expectedCollectionRid, resolved.ResourceId);
193+
194+
// The real item read through the full SDK stack must also succeed.
195+
ItemResponse<dynamic> itemResponse = await this.container.ReadItemAsync<dynamic>(
196+
id,
197+
new Cosmos.PartitionKey(pk));
198+
199+
Assert.AreEqual(HttpStatusCode.OK, itemResponse.StatusCode);
200+
}
201+
202+
/// <summary>
203+
/// Mirrors the static <c>CollectionCache.IsCollectionRid</c> helper so tests
204+
/// can assert on whether a given ResourceId is a collection RID without
205+
/// depending on the private SDK method.
206+
/// </summary>
207+
private static bool IsCollectionRid(string resourceId)
208+
{
209+
if (string.IsNullOrWhiteSpace(resourceId) ||
210+
!ResourceId.TryParse(resourceId, out ResourceId resourceIdParsed))
211+
{
212+
return false;
213+
}
214+
215+
string collectionRid = resourceIdParsed.DocumentCollectionId.ToString();
216+
return StringComparer.Ordinal.Equals(collectionRid, resourceId);
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)