Skip to content

Commit 79cdad1

Browse files
committed
Implemented upsert entity functionality
1 parent 52e00a0 commit 79cdad1

9 files changed

Lines changed: 194 additions & 33 deletions

CloudStub.AzureDataTables/TableClientStub.cs

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,137 @@ public override async Task<Response> AddEntityAsync<T>(T entity, CancellationTok
178178

179179
public override Response UpsertEntity<T>(T entity, TableUpdateMode mode = TableUpdateMode.Merge, CancellationToken cancellationToken = default)
180180
{
181+
if (entity == null)
182+
throw new ArgumentNullException("entity")
183+
{
184+
Source = "Azure.Data.Tables"
185+
};
186+
187+
if (entity.PartitionKey == null)
188+
throw new ArgumentNullException("PartitionKey")
189+
{
190+
Source = "Azure.Data.Tables"
191+
};
192+
if (entity.RowKey == null)
193+
throw new ArgumentNullException("RowKey")
194+
{
195+
Source = "Azure.Data.Tables"
196+
};
197+
if (!Enum.IsDefined(mode))
198+
throw new ArgumentException($"Unexpected value for mode: {mode}")
199+
{
200+
Source = "Azure.Data.Tables"
201+
};
202+
181203
cancellationToken.ThrowIfCancellationRequested();
182-
throw new NotImplementedException();
204+
205+
if (entity.PartitionKey.Contains((char)0) || entity.RowKey.Contains((char)0))
206+
throw TableStubResponseFactory.InvalidUriException(
207+
HttpStatusCode.BadRequest,
208+
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\">\r\n<HTML><HEAD><TITLE>Bad Request</TITLE>\r\n<META HTTP-EQUIV=\"Content-Type\" Content=\"text/html; charset=us-ascii\"></HEAD>\r\n<BODY><h2>Bad Request - Invalid URL</h2>\r\n<hr><p>HTTP Error 400. The request URL is invalid.</p>\r\n</BODY></HTML>\r\n",
209+
new InvlaidUrlResponseHeaders
210+
{
211+
{ "Connection", "close" },
212+
{ "Content-Length", "324" }
213+
}
214+
);
215+
216+
if (entity.PartitionKey.Contains('/') || entity.PartitionKey.Contains('\\') || entity.RowKey.Contains('/') || entity.RowKey.Contains('\\'))
217+
throw TableStubResponseFactory.JsonRequestFailedException(
218+
HttpStatusCode.BadRequest,
219+
"InvalidInput",
220+
"Bad Request - Error in query syntax.",
221+
new DefaultResponseHeaders(headers => headers.Remove("Cache-Control"))
222+
);
223+
224+
if (entity.PartitionKey.Any(_IsInvalidUriCharacter) || entity.RowKey.Any(_IsInvalidUriCharacter))
225+
throw TableStubResponseFactory.InvalidUriException(
226+
HttpStatusCode.BadRequest,
227+
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\"http://www.w3.org/TR/html4/strict.dtd\"><HTML><HEAD><TITLE>Bad Request</TITLE><META HTTP-EQUIV=\"Content-Type\" Content=\"text/html; charset=us-ascii\"></HEAD><BODY><h2>Bad Request - Invalid URL</h2><hr><p>HTTP Error 400. The request URL is invalid.</p></BODY></HTML>",
228+
new InvlaidUrlResponseHeaders
229+
{
230+
{ "Content-Length", "312" }
231+
}
232+
);
233+
234+
if (entity.PartitionKey.Any(TableRowStub.IsReservedKeyCharacter) || entity.RowKey.Any(TableRowStub.IsReservedKeyCharacter))
235+
throw TableStubResponseFactory.JsonRequestFailedException(
236+
HttpStatusCode.BadRequest,
237+
"OutOfRangeInput",
238+
"One of the request inputs is out of range."
239+
);
240+
241+
var mappedEntity = new ValidatedTableRowStub<T>(entity);
242+
if (mappedEntity.NotSupportedDateTimeValue != null)
243+
throw new NotSupportedException($"DateTime {mappedEntity.NotSupportedDateTimeValue} has a Kind of {mappedEntity.NotSupportedDateTimeValue?.Kind}. Azure SDK requires it to be UTC. You can call DateTime.SpecifyKind to change Kind property value to DateTimeKind.Utc.")
244+
{
245+
Source = "Azure.Data.Tables"
246+
};
247+
248+
cancellationToken.ThrowIfCancellationRequested();
249+
250+
if (mappedEntity.IsPartitionKeyExceedingMaxLength || mappedEntity.IsRowKeyExceedingMaxLength || mappedEntity.IsStringPropertyExceedingMaxLength || mappedEntity.IsBinaryPropertyExceedingMaxLength)
251+
throw TableStubResponseFactory.JsonRequestFailedException(
252+
HttpStatusCode.BadRequest,
253+
"PropertyValueTooLarge",
254+
"The property value exceeds the maximum allowed size (64KB). If the property value is a string, it is UTF-16 encoded and the maximum number of characters should be 32K or less.",
255+
new DefaultResponseHeaders()
256+
);
257+
if (mappedEntity.InvalidDateTimeProperty != null)
258+
throw TableStubResponseFactory.JsonRequestFailedException(
259+
HttpStatusCode.BadRequest,
260+
"OutOfRangeInput",
261+
$"The '{mappedEntity.InvalidDateTimeProperty?.Key}' parameter of value '{mappedEntity.InvalidDateTimeProperty?.Value:MM/dd/yyyy HH:mm:ss}' is out of range.",
262+
new DefaultResponseHeaders(headers => headers.Remove("Cache-Control"))
263+
);
264+
265+
using (_tableServiceClientStub.Tables.ReadLock())
266+
{
267+
if (!_tableServiceClientStub.Tables.TryGetValue(_tableName, out var tableItem))
268+
throw TableStubResponseFactory.JsonRequestFailedException(
269+
HttpStatusCode.NotFound,
270+
"TableNotFound",
271+
"The table specified does not exist.",
272+
new DefaultResponseHeaders(headers => headers.Remove("Cache-Control"))
273+
);
274+
275+
using (tableItem.WriteLock())
276+
{
277+
if (!tableItem.TryGetValue(entity.PartitionKey, out var tablePartition))
278+
{
279+
tablePartition = new TablePartitionStub();
280+
tableItem.Add(entity.PartitionKey, tablePartition);
281+
}
282+
283+
if (!tablePartition.TryGetValue(entity.RowKey, out var existingEntity))
284+
tablePartition.Add(entity.RowKey, mappedEntity);
285+
else
286+
switch (mode)
287+
{
288+
case TableUpdateMode.Merge:
289+
foreach (var existingEntityProperty in existingEntity)
290+
if (!mappedEntity.ContainsKey(existingEntityProperty.Key))
291+
mappedEntity.Add(existingEntityProperty.Key, existingEntityProperty.Value);
292+
293+
tablePartition[entity.RowKey] = mappedEntity;
294+
break;
295+
296+
case TableUpdateMode.Replace:
297+
tablePartition[entity.RowKey] = mappedEntity;
298+
break;
299+
300+
default:
301+
throw new InvalidOperationException($"Unhandled '{mode}' mode.");
302+
}
303+
}
304+
}
305+
306+
return TableStubResponseFactory.NoContentResponse(
307+
new NoContentResponseHeaders()
308+
{
309+
{ "ETag", mappedEntity.ETag }
310+
}
311+
);
183312
}
184313

185314
public override async Task<Response> UpsertEntityAsync<T>(T entity, TableUpdateMode mode = TableUpdateMode.Merge, CancellationToken cancellationToken = default)
@@ -211,7 +340,11 @@ public override Response UpdateEntity<T>(T entity, ETag ifMatch, TableUpdateMode
211340
{
212341
Source = "Azure.Data.Tables"
213342
};
214-
343+
if (!Enum.IsDefined(mode))
344+
throw new ArgumentException($"Unexpected value for mode: {mode}")
345+
{
346+
Source = "Azure.Data.Tables"
347+
};
215348

216349
cancellationToken.ThrowIfCancellationRequested();
217350

@@ -318,7 +451,7 @@ public override Response UpdateEntity<T>(T entity, ETag ifMatch, TableUpdateMode
318451
break;
319452

320453
default:
321-
throw new NotImplementedException($"Unhandled '{mode}' table update mode.");
454+
throw new InvalidOperationException($"Unhandled '{mode}' mode.");
322455
}
323456
}
324457
}

CloudStub.AzureDataTables/Tests/Table/Async/TableClientUpdateEntityMergeTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ public async Task UpdateEntityMergeAsync_WhenETagIsDefault_ThrowsException()
6868
Assert.Equal("Azure.Data.Tables", exception.Source);
6969
}
7070

71+
[Fact]
72+
public async Task UpdateEntityMergeAsync_WhenUpdateModeIsNotSupported_ThrowsException()
73+
{
74+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => CloudTable.UpdateEntityAsync(new TableEntity("partition-key", "row-key"), ETag.All, (TableUpdateMode)(-1)));
75+
Assert.Equal(new ArgumentException("Unexpected value for mode: -1").Message, exception.Message);
76+
}
77+
7178
[Fact]
7279
public async Task UpdateEntityMergeAsync_WhenETagsIsWildcard_MergesEntity()
7380
{

CloudStub.AzureDataTables/Tests/Table/Async/TableClientUpdateEntityReplaceTests.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public async Task UpdateEntityReplaceAsync_WhenEntityIsNull_ThrowsException()
5050
}
5151

5252
[Fact]
53-
public async Task UpdateEntityReplaceAsync_WhenETagIsMissing_ThrowsException()
53+
public async Task UpdateEntityReplaceAsync_WhenETagIsDefault_ThrowsException()
5454
{
5555
var exception = await Assert.ThrowsAsync<ArgumentException>(
5656
"ifMatch",
@@ -68,6 +68,13 @@ public async Task UpdateEntityReplaceAsync_WhenETagIsMissing_ThrowsException()
6868
Assert.Equal("Azure.Data.Tables", exception.Source);
6969
}
7070

71+
[Fact]
72+
public async Task UpdateEntityReplaceAsync_WhenUpdateModeIsNotSupported_ThrowsException()
73+
{
74+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => CloudTable.UpdateEntityAsync(new TableEntity("partition-key", "row-key"), ETag.All, (TableUpdateMode)(-1)));
75+
Assert.Equal(new ArgumentException("Unexpected value for mode: -1").Message, exception.Message);
76+
}
77+
7178
[Fact]
7279
public async Task UpdateEntityReplaceAsync_WhenETagsIsWildcard_ReplacesEntity()
7380
{

CloudStub.AzureDataTables/Tests/Table/Async/TableClientUpsertEntityMergeTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ public async Task UpsertEntityMergeAsync_WhenEntityIsNull_ThrowsException()
4747
}
4848

4949
[Fact]
50-
public async Task TableOperation_WhenUpdateModeIsNotSupported_ThrowsException()
50+
public async Task UpsertEntityMergeAsync_WhenUpdateModeIsNotSupported_ThrowsException()
5151
{
52-
var exception = await Assert.ThrowsAsync<ArgumentNullException>("entity", () => CloudTable.UpsertEntityAsync<TableEntity>(null, (TableUpdateMode)(-1)));
53-
Assert.Equal(new ArgumentNullException("entity").Message, exception.Message);
52+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => CloudTable.UpsertEntityAsync(new TableEntity("partition-key", "row-key"), (TableUpdateMode)(-1)));
53+
Assert.Equal(new ArgumentException("Unexpected value for mode: -1").Message, exception.Message);
5454
}
5555

5656
[Fact]

CloudStub.AzureDataTables/Tests/Table/Async/TableClientUpsertEntityReplaceTests.cs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace CloudStub.AzureDataTables.Tests.Table.Async
1212
public class TableClientUpsertEntityReplaceTests : BaseTableCloudStubTests
1313
{
1414
[Fact]
15-
public async Task UpsertEntityReplace_WhenTableDoesNotExist_ThrowsException()
15+
public async Task UpsertEntityReplaceAsync_WhenTableDoesNotExist_ThrowsException()
1616
{
1717
await Assertions.JsonResponseThrowsAsync(
1818
() => CloudTable.UpsertEntityAsync(
@@ -40,21 +40,21 @@ await Assertions.JsonResponseThrowsAsync(
4040
}
4141

4242
[Fact]
43-
public async Task UpsertEntityReplace_WhenEntityIsNull_ThrowsException()
43+
public async Task UpsertEntityReplaceAsync_WhenEntityIsNull_ThrowsException()
4444
{
4545
var exception = await Assert.ThrowsAsync<ArgumentNullException>("entity", () => CloudTable.UpsertEntityAsync<TableEntity>(null, TableUpdateMode.Replace));
4646
Assert.Equal(new ArgumentNullException("entity").Message, exception.Message);
4747
}
4848

4949
[Fact]
50-
public async Task UpsertEntityReplace_WhenUpdateModeIsNotSupported_ThrowsException()
50+
public async Task UpsertEntityReplaceAsync_WhenUpdateModeIsNotSupported_ThrowsException()
5151
{
52-
var exception = await Assert.ThrowsAsync<ArgumentNullException>("entity", () => CloudTable.UpsertEntityAsync<TableEntity>(null, (TableUpdateMode)(-1)));
53-
Assert.Equal(new ArgumentNullException("entity").Message, exception.Message);
52+
var exception = await Assert.ThrowsAsync<ArgumentException>(() => CloudTable.UpsertEntityAsync(new TableEntity("partition-key", "row-key"), (TableUpdateMode)(-1)));
53+
Assert.Equal(new ArgumentException("Unexpected value for mode: -1").Message, exception.Message);
5454
}
5555

5656
[Fact]
57-
public async Task UpsertEntityReplace_WhenEntityDoesNotExist_InsertsEntity()
57+
public async Task UpsertEntityReplaceAsync_WhenEntityDoesNotExist_InsertsEntity()
5858
{
5959
var tableEntity = new TableEntity
6060
{
@@ -84,7 +84,7 @@ public async Task UpsertEntityReplace_WhenEntityDoesNotExist_InsertsEntity()
8484
}
8585

8686
[Fact]
87-
public async Task UpsertEntityReplace_WhenEntityHasOtherProperties_InsertsEntity()
87+
public async Task UpsertEntityReplaceAsync_WhenEntityHasOtherProperties_InsertsEntity()
8888
{
8989
var testEntity = new TestEntity
9090
{
@@ -124,7 +124,7 @@ public async Task UpsertEntityReplace_WhenEntityHasOtherProperties_InsertsEntity
124124
}
125125

126126
[Fact]
127-
public async Task UpsertEntityReplace_InsertOrReplaceOperation_RepleacesEntity()
127+
public async Task UpsertEntityReplaceAsync_InsertOrReplaceOperation_RepleacesEntity()
128128
{
129129
var testEntity = new TestEntity
130130
{
@@ -168,7 +168,7 @@ public async Task UpsertEntityReplace_InsertOrReplaceOperation_RepleacesEntity()
168168
}
169169

170170
[Fact]
171-
public async Task UpsertEntityReplace_WhenDynamicEntityHasNullProperties_TheyAreIgnored()
171+
public async Task UpsertEntityReplaceAsync_WhenDynamicEntityHasNullProperties_TheyAreIgnored()
172172
{
173173
await CloudTable.CreateAsync();
174174

@@ -198,7 +198,7 @@ await CloudTable.UpsertEntityAsync(
198198
}
199199

200200
[Fact]
201-
public async Task UpsertEntityReplace_WhenDynamicEntityHasNullProperties_TheyAreRemovedWhenEntityAlreadyExists()
201+
public async Task UpsertEntityReplaceAsync_WhenDynamicEntityHasNullProperties_TheyAreRemovedWhenEntityAlreadyExists()
202202
{
203203
await CloudTable.CreateAsync();
204204
await CloudTable.AddEntityAsync(new TableEntity(
@@ -238,7 +238,7 @@ await CloudTable.UpsertEntityAsync(
238238
}
239239

240240
[Fact]
241-
public async Task UpsertEntityReplace_InsertOrReplaceOperationWhenPartitionKeyIsNull_ThrowsException()
241+
public async Task UpsertEntityReplaceAsync_InsertOrReplaceOperationWhenPartitionKeyIsNull_ThrowsException()
242242
{
243243
await CloudTable.CreateAsync();
244244

@@ -254,7 +254,7 @@ public async Task UpsertEntityReplace_InsertOrReplaceOperationWhenPartitionKeyIs
254254
}
255255

256256
[Theory, MemberData(nameof(TableOperationTestData.InvalidKeyTestData), MemberType = typeof(TableOperationTestData))]
257-
public async Task UpsertEntityReplace_WhenPartitionKeyIsInvalid_ThrowsException(string partitionKey)
257+
public async Task UpsertEntityReplaceAsync_WhenPartitionKeyIsInvalid_ThrowsException(string partitionKey)
258258
{
259259
var testEntity = new TableEntity
260260
{
@@ -364,7 +364,7 @@ await Assertions.JsonResponseThrowsAsync(
364364
}
365365

366366
[Fact]
367-
public async Task UpsertEntityReplace_WhenPartitionKeyExceedsLimit_ThrowsException()
367+
public async Task UpsertEntityReplaceAsync_WhenPartitionKeyExceedsLimit_ThrowsException()
368368
{
369369
await CloudTable.CreateAsync();
370370

@@ -388,7 +388,7 @@ await Assertions.JsonResponseThrowsAsync(
388388
}
389389

390390
[Fact]
391-
public async Task UpsertEntityReplace_WhenRowKeyIsNull_ThrowsException()
391+
public async Task UpsertEntityReplaceAsync_WhenRowKeyIsNull_ThrowsException()
392392
{
393393
await CloudTable.CreateAsync();
394394

@@ -401,7 +401,7 @@ public async Task UpsertEntityReplace_WhenRowKeyIsNull_ThrowsException()
401401
}
402402

403403
[Theory, MemberData(nameof(TableOperationTestData.InvalidKeyTestData), MemberType = typeof(TableOperationTestData))]
404-
public async Task UpsertEntityReplace_WhenRowKeyIsInvalid_ThrowsException(string rowKey)
404+
public async Task UpsertEntityReplaceAsync_WhenRowKeyIsInvalid_ThrowsException(string rowKey)
405405
{
406406
var testEntity = new TestEntity
407407
{
@@ -511,7 +511,7 @@ await Assertions.JsonResponseThrowsAsync(
511511
}
512512

513513
[Fact]
514-
public async Task UpsertEntityReplace_WhenRowKeyExceedsLimit_ThrowsException()
514+
public async Task UpsertEntityReplaceAsync_WhenRowKeyExceedsLimit_ThrowsException()
515515
{
516516
await CloudTable.CreateAsync();
517517

@@ -535,7 +535,7 @@ await Assertions.JsonResponseThrowsAsync(
535535
}
536536

537537
[Theory, MemberData(nameof(TableOperationTestData.InvalidStringData), MemberType = typeof(TableOperationTestData))]
538-
public async Task UpsertEntityReplace_WhenStringPropertyIsInvalid_ThrowsException(string stringPropValue)
538+
public async Task UpsertEntityReplaceAsync_WhenStringPropertyIsInvalid_ThrowsException(string stringPropValue)
539539
{
540540
await CloudTable.CreateAsync();
541541

@@ -560,7 +560,7 @@ await Assertions.JsonResponseThrowsAsync(
560560
}
561561

562562
[Theory, MemberData(nameof(TableOperationTestData.InvalidBinaryData), MemberType = typeof(TableOperationTestData))]
563-
public async Task UpsertEntityReplace_WhenBinaryPropertyIsInvalid_ThrowsException(byte[] binaryPropValue)
563+
public async Task UpsertEntityReplaceAsync_WhenBinaryPropertyIsInvalid_ThrowsException(byte[] binaryPropValue)
564564
{
565565
await CloudTable.CreateAsync();
566566

@@ -585,7 +585,7 @@ await Assertions.JsonResponseThrowsAsync(
585585
}
586586

587587
[Theory, MemberData(nameof(TableOperationTestData.InvalidDateTimeData), MemberType = typeof(TableOperationTestData))]
588-
public async Task UpsertEntityReplace_WhenDateTimePropertyIsInvalid_ThrowsException(DateTime dateTimePropValue)
588+
public async Task UpsertEntityReplaceAsync_WhenDateTimePropertyIsInvalid_ThrowsException(DateTime dateTimePropValue)
589589
{
590590
await CloudTable.CreateAsync();
591591

CloudStub.AzureDataTables/Tests/Table/Sync/TableClientUpdateEntityMergeTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ public void UpdateEntityMerge_WhenETagIsDefault_ThrowsException()
6666
Assert.Equal("Azure.Data.Tables", exception.Source);
6767
}
6868

69+
[Fact]
70+
public void UpdateEntityMerge_WhenUpdateModeIsNotSupported_ThrowsException()
71+
{
72+
var exception = Assert.Throws<ArgumentException>(() => CloudTable.UpdateEntity(new TableEntity("partition-key", "row-key"), ETag.All, (TableUpdateMode)(-1)));
73+
Assert.Equal(new ArgumentException("Unexpected value for mode: -1").Message, exception.Message);
74+
}
75+
6976
[Fact]
7077
public void UpdateEntityMerge_WhenETagsIsWildcard_MergesEntity()
7178
{

0 commit comments

Comments
 (0)