Skip to content

Commit 5868509

Browse files
committed
Arrange Act Assert
1 parent 782e194 commit 5868509

2 files changed

Lines changed: 96 additions & 3 deletions

File tree

Microsoft.Azure.Cosmos.Encryption.Custom/tests/Microsoft.Azure.Cosmos.Encryption.Custom.Tests/Transformation/StreamProcessorDecryptorTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ private static EncryptionOptions CreateOptions(IEnumerable<string> paths, Compre
9494
[TestMethod]
9595
public async Task Decrypt_AllPrimitiveTypesAndContainers()
9696
{
97+
// Arrange
9798
var doc = new
9899
{
99100
id = Guid.NewGuid().ToString(),
@@ -137,6 +138,7 @@ public async Task Decrypt_AllPrimitiveTypesAndContainers()
137138
[TestMethod]
138139
public async Task Decrypt_CompressedPayloads()
139140
{
141+
// Arrange
140142
var doc = new
141143
{
142144
id = Guid.NewGuid().ToString(),
@@ -151,12 +153,14 @@ public async Task Decrypt_CompressedPayloads()
151153
Assert.IsNotNull(props.CompressedEncryptedPaths);
152154
Assert.IsTrue(props.CompressedEncryptedPaths.Count > 0); // at least LargeStr is compressed
153155

156+
// Act
154157
MemoryStream output = new();
155158
DecryptionContext ctx = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
156159
output.Position = 0;
157160
using JsonDocument jd = JsonDocument.Parse(output);
158161
JsonElement root = jd.RootElement;
159162

163+
// Assert
160164
foreach (string p in paths)
161165
{
162166
Assert.IsTrue(ctx.DecryptionInfoList[0].PathsDecrypted.Contains(p));
@@ -192,15 +196,18 @@ public async Task Decrypt_Throws_OnUnknownCompressionAlgorithm()
192196
[TestMethod]
193197
public async Task Decrypt_Skips_EncryptionInfo_Block()
194198
{
199+
// Arrange
195200
// Build a document that already has _ei. (Encryptor will append another one during encryption; we want to ensure decryptor skips only the encrypted one at top-level.)
196201
var doc = new { id = "1", _ei = new { ignore = true }, SensitiveStr = "abc" };
197202
string[] paths = new[] { "/SensitiveStr" };
198203
EncryptionOptions options = CreateOptions(paths);
199204
(MemoryStream encrypted, EncryptionProperties props) = await EncryptRawAsync(doc, options);
200205

206+
// Act
201207
MemoryStream output = new();
202208
DecryptionContext ctx = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
203209

210+
// Assert
204211
output.Position = 0;
205212
using JsonDocument jd = JsonDocument.Parse(output);
206213
JsonElement root = jd.RootElement;
@@ -212,34 +219,40 @@ public async Task Decrypt_Skips_EncryptionInfo_Block()
212219
[TestMethod]
213220
public async Task Decrypt_IgnoresUnknownPropertyTypesAndMaintainsJson()
214221
{
222+
// Arrange
215223
var doc = new { id = "1", SensitiveStr = "abc", Regular = 5 };
216224
string[] paths = new[] { "/SensitiveStr" };
217225
EncryptionOptions options = CreateOptions(paths);
218226
(MemoryStream encrypted, EncryptionProperties props) = await EncryptRawAsync(doc, options);
219227

228+
// Act
220229
MemoryStream output = new();
221230
_ = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
222231
output.Position = 0;
223232
using JsonDocument jd = JsonDocument.Parse(output);
233+
// Assert
224234
JsonElement root = jd.RootElement;
225235
Assert.AreEqual(5, root.GetProperty("Regular").GetInt32());
226236
}
227237

228238
[TestMethod]
229239
public async Task Decrypt_Throws_OnUnknownEncryptionFormatVersion()
230240
{
241+
// Arrange
231242
var doc = new { id = "1", SensitiveStr = "abc" };
232243
string[] paths = new[] { "/SensitiveStr" };
233244
EncryptionOptions options = CreateOptions(paths);
234245
(MemoryStream encrypted, EncryptionProperties props) = await EncryptRawAsync(doc, options);
235246
EncryptionProperties invalid = new EncryptionProperties(999, props.EncryptionAlgorithm, props.DataEncryptionKeyId, null, props.EncryptedPaths, props.CompressionAlgorithm, props.CompressedEncryptedPaths);
247+
// Act + Assert
236248
MemoryStream output = new();
237249
await Assert.ThrowsExceptionAsync<NotSupportedException>(() => new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, invalid, new CosmosDiagnosticsContext(), CancellationToken.None));
238250
}
239251

240252
[TestMethod]
241253
public async Task Decrypt_Throws_OnInvalidBase64Ciphertext()
242254
{
255+
// Arrange
243256
var doc = new { id = "1", SensitiveStr = "abc" };
244257
string[] paths = new[] { "/SensitiveStr" };
245258
EncryptionOptions options = CreateOptions(paths);
@@ -251,6 +264,7 @@ public async Task Decrypt_Throws_OnInvalidBase64Ciphertext()
251264
string corruptedCipher = string.Concat("#", originalCipher.AsSpan(1)); // invalid base64 start
252265
jsonText = jsonText.Replace("\"SensitiveStr\":\"" + originalCipher + "\"", "\"SensitiveStr\":\"" + corruptedCipher + "\"");
253266
MemoryStream corruptedStream = new(Encoding.UTF8.GetBytes(jsonText));
267+
// Act + Assert
254268
MemoryStream output = new();
255269
await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => new StreamProcessor().DecryptStreamAsync(corruptedStream, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None));
256270
}
@@ -318,6 +332,7 @@ public async Task Decrypt_Throws_OnCompressedPayload_DestinationTooSmall()
318332
[TestMethod]
319333
public async Task Decrypt_IgnoredBlock_PartialEiSkip()
320334
{
335+
// Arrange
321336
// Force the _ei metadata object to span multiple buffer reads so Utf8JsonReader.TrySkip() returns false,
322337
// exercising the fallback isIgnoredBlock path.
323338
const int propertyCount = 250; // large to inflate _ei encrypted paths list
@@ -340,11 +355,13 @@ public async Task Decrypt_IgnoredBlock_PartialEiSkip()
340355
StreamProcessor.InitialBufferSize = 32;
341356
try
342357
{
358+
// Act
343359
MemoryStream output = new();
344360
DecryptionContext ctx = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
345361
output.Position = 0;
346362
using JsonDocument jd = JsonDocument.Parse(output, new JsonDocumentOptions { AllowTrailingCommas = true });
347363
JsonElement root = jd.RootElement;
364+
// Assert
348365
// _ei must be removed
349366
Assert.IsFalse(root.TryGetProperty(Constants.EncryptedInfo, out _), "_ei should be skipped");
350367
// spot check a few decrypted properties
@@ -362,6 +379,7 @@ public async Task Decrypt_IgnoredBlock_PartialEiSkip()
362379
[TestMethod]
363380
public async Task Decrypt_CompressedPathsPropertyNull()
364381
{
382+
// Arrange
365383
// Cover branch where EncryptionProperties.CompressedEncryptedPaths is null (as opposed to empty dictionary or populated),
366384
// exercising the null path of the null-conditional operator in: bool containsCompressed = properties.CompressedEncryptedPaths?.Count > 0;
367385
var doc = new { id = "1", SensitiveStr = "abc" };
@@ -380,9 +398,11 @@ public async Task Decrypt_CompressedPathsPropertyNull()
380398
compressedEncryptedPaths: null);
381399

382400
encrypted.Position = 0;
401+
// Act
383402
MemoryStream output = new();
384403
DecryptionContext ctx = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, propsNullCompressed, new CosmosDiagnosticsContext(), CancellationToken.None);
385404

405+
// Assert
386406
output.Position = 0;
387407
using JsonDocument jd = JsonDocument.Parse(output);
388408
Assert.AreEqual("abc", jd.RootElement.GetProperty("SensitiveStr").GetString());
@@ -392,6 +412,7 @@ public async Task Decrypt_CompressedPathsPropertyNull()
392412
[TestMethod]
393413
public async Task Decrypt_UnencryptedArrayAndBooleans()
394414
{
415+
// Arrange
395416
// Covers StartArray / EndArray / True / False switch branches where decryptPropertyName == null (no encryption for those tokens).
396417
var doc = new
397418
{
@@ -405,9 +426,11 @@ public async Task Decrypt_UnencryptedArrayAndBooleans()
405426
EncryptionOptions options = CreateOptions(paths);
406427
(MemoryStream encrypted, EncryptionProperties props) = await EncryptRawAsync(doc, options);
407428

429+
// Act
408430
MemoryStream output = new();
409431
DecryptionContext ctx = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
410432

433+
// Assert
411434
output.Position = 0;
412435
using JsonDocument jd = JsonDocument.Parse(output);
413436
JsonElement root = jd.RootElement;
@@ -429,6 +452,7 @@ public async Task Decrypt_UnencryptedArrayAndBooleans()
429452
[TestMethod]
430453
public async Task Decrypt_ForgedCipherText_TypeMarkerNull()
431454
{
455+
// Arrange
432456
// Covers TypeMarker.Null switch branch by forging a ciphertext with first byte = Null marker.
433457
var doc = new { id = "1", SensitiveStr = "abc" };
434458
string[] paths = new[] { "/SensitiveStr" };
@@ -457,10 +481,12 @@ public async Task Decrypt_ForgedCipherText_TypeMarkerNull()
457481
}
458482
forged.Position = 0;
459483

484+
// Act
460485
// Use custom encryptor that returns empty plaintext for Null marker
461486
StreamProcessor sp = new StreamProcessor { Encryptor = new NullMarkerMdeEncryptor() };
462487
MemoryStream output = new();
463488
DecryptionContext ctx = await sp.DecryptStreamAsync(forged, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
489+
// Assert
464490
output.Position = 0;
465491
using JsonDocument outDoc = JsonDocument.Parse(output);
466492
Assert.AreEqual(JsonValueKind.Null, outDoc.RootElement.GetProperty("SensitiveStr").ValueKind);
@@ -470,6 +496,7 @@ public async Task Decrypt_ForgedCipherText_TypeMarkerNull()
470496
[TestMethod]
471497
public async Task Decrypt_Throws_OnMissingDataEncryptionKey()
472498
{
499+
// Arrange
473500
// Arrange: create a valid encrypted payload
474501
var doc = new { id = "1", SensitiveStr = "abc" };
475502
string[] paths = new[] { "/SensitiveStr" };
@@ -579,6 +606,7 @@ public async Task Decrypt_ForgedUnknownTypeMarker_WritesRaw_InvalidJson()
579606
}
580607
forged.Position = 0;
581608

609+
// Act
582610
// Use a bypass encryptor to return raw bytes that are not valid JSON, exercising the default branch (WriteRawValue)
583611
StreamProcessor sp = new StreamProcessor { Encryptor = new AlwaysPlaintextMdeEncryptor("NOT_JSON") };
584612
MemoryStream output = new();
@@ -624,6 +652,7 @@ public async Task Decrypt_ForgedTypeMarkerLong_InvalidPayload_Throws()
624652
}
625653
forged.Position = 0;
626654

655+
// Act
627656
// Use encryptor that returns a plaintext that is invalid for a long serializer
628657
StreamProcessor sp = new StreamProcessor { Encryptor = new AlwaysPlaintextMdeEncryptor("abc") };
629658
MemoryStream output = new();
@@ -642,6 +671,7 @@ public async Task Decrypt_ForgedTypeMarkerLong_InvalidPayload_Throws()
642671
[TestMethod]
643672
public async Task Decrypt_CompressedContainsPathButNotCurrentProperty()
644673
{
674+
// Arrange
645675
// Scenario: compression enabled, some paths compressed, but the current decryptPropertyName is NOT in CompressedEncryptedPaths.
646676
// Covers branch: containsCompressed == true && TryGetValue == false so decompression block is skipped.
647677
var doc = new
@@ -656,11 +686,13 @@ public async Task Decrypt_CompressedContainsPathButNotCurrentProperty()
656686
Assert.IsTrue(props.CompressedEncryptedPaths.ContainsKey("/LargeStr"));
657687
Assert.IsFalse(props.CompressedEncryptedPaths.ContainsKey("/OtherStr"));
658688

689+
// Act
659690
MemoryStream output = new();
660691
DecryptionContext ctx = await new StreamProcessor().DecryptStreamAsync(encrypted, output, mockEncryptor.Object, props, new CosmosDiagnosticsContext(), CancellationToken.None);
661692
output.Position = 0;
662693
using JsonDocument jd = JsonDocument.Parse(output);
663694
JsonElement root = jd.RootElement;
695+
// Assert
664696
Assert.AreEqual(400, root.GetProperty("LargeStr").GetString().Length);
665697
Assert.AreEqual(50, root.GetProperty("OtherStr").GetString().Length);
666698
// Both should appear in decrypted paths
@@ -671,6 +703,7 @@ public async Task Decrypt_CompressedContainsPathButNotCurrentProperty()
671703
[TestMethod]
672704
public async Task Decrypt_Fuzz_Ciphertext_Length_And_TypeMarker_CrossProduct()
673705
{
706+
// Arrange
674707
// Property-style fuzzing across type markers and plaintext lengths. We don't assert per-iteration outcomes;
675708
// instead we ensure a wide set runs without catastrophic failures and that some known-good cases succeed.
676709
var doc = new { id = "1", V = "seed" };
@@ -698,6 +731,7 @@ public async Task Decrypt_Fuzz_Ciphertext_Length_And_TypeMarker_CrossProduct()
698731
StreamProcessor sp = new StreamProcessor { Encryptor = new MutablePlaintextMdeEncryptor() };
699732
MutablePlaintextMdeEncryptor mut = (MutablePlaintextMdeEncryptor)sp.Encryptor;
700733

734+
// Act
701735
for (int len = 0; len <= maxLen; len++)
702736
{
703737
for (int m = 0; m < markers.Length; m++)
@@ -784,6 +818,7 @@ public async Task Decrypt_Fuzz_Ciphertext_Length_And_TypeMarker_CrossProduct()
784818
}
785819
}
786820

821+
// Assert
787822
Assert.IsTrue(attempts > 0, "No fuzz attempts executed");
788823
Assert.IsTrue(successes > 0, "Expected at least some successful decrypt/writes during fuzzing");
789824
}

0 commit comments

Comments
 (0)