Skip to content

Commit d61cecd

Browse files
authored
Merge pull request #227 from AzureCosmosDB/copilot/prevent-container-wipe-issue
Fix container wipe issue with duplicate source and sink settings
2 parents b5622d6 + 39bc283 commit d61cecd

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed

Core/Cosmos.DataTransfer.Core.UnitTests/RunCommandTests.cs

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,5 +336,290 @@ public void Invoke_WithInvalidSinkExtension_ThrowsException()
336336
// Should throw exception when sink extension is not found
337337
Assert.ThrowsException<InvalidOperationException>(() => handler.Invoke(new InvocationContext(parseResult)));
338338
}
339+
340+
[TestMethod]
341+
public void Invoke_WithSameCosmosSourceAndSinkWithRecreateContainer_ThrowsException()
342+
{
343+
const string cosmosExtension = "Cosmos-nosql";
344+
const string connectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test";
345+
const string database = "testDb";
346+
const string container = "testContainer";
347+
348+
IConfigurationRoot configuration = new ConfigurationBuilder()
349+
.AddInMemoryCollection(new Dictionary<string, string>
350+
{
351+
{ "Source", cosmosExtension },
352+
{ "Sink", cosmosExtension },
353+
{ "SourceSettings:ConnectionString", connectionString },
354+
{ "SourceSettings:Database", database },
355+
{ "SourceSettings:Container", container },
356+
{ "SinkSettings:ConnectionString", connectionString },
357+
{ "SinkSettings:Database", database },
358+
{ "SinkSettings:Container", container },
359+
{ "SinkSettings:RecreateContainer", "true" },
360+
})
361+
.Build();
362+
363+
var loader = new Mock<IExtensionLoader>();
364+
var sourceExtension = new Mock<IDataSourceExtension>();
365+
sourceExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
366+
loader
367+
.Setup(l => l.LoadExtensions<IDataSourceExtension>(It.IsAny<CompositionContainer>()))
368+
.Returns(new List<IDataSourceExtension> { sourceExtension.Object });
369+
370+
var sinkExtension = new Mock<IDataSinkExtension>();
371+
sinkExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
372+
loader
373+
.Setup(l => l.LoadExtensions<IDataSinkExtension>(It.IsAny<CompositionContainer>()))
374+
.Returns(new List<IDataSinkExtension> { sinkExtension.Object });
375+
376+
var handler = new RunCommand.CommandHandler(loader.Object,
377+
configuration,
378+
NullLoggerFactory.Instance);
379+
380+
var parseResult = new RootCommand().Parse(Array.Empty<string>());
381+
382+
// Should throw exception when same container is used for source and sink with RecreateContainer
383+
var exception = Assert.ThrowsException<InvalidOperationException>(() => handler.Invoke(new InvocationContext(parseResult)));
384+
Assert.IsTrue(exception.Message.Contains("same Cosmos DB container"));
385+
Assert.IsTrue(exception.Message.Contains("RecreateContainer"));
386+
}
387+
388+
[TestMethod]
389+
public void Invoke_WithSameCosmosSourceAndSinkWithoutRecreateContainer_Succeeds()
390+
{
391+
const string cosmosExtension = "Cosmos-nosql";
392+
const string connectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test";
393+
const string database = "testDb";
394+
const string container = "testContainer";
395+
396+
IConfigurationRoot configuration = new ConfigurationBuilder()
397+
.AddInMemoryCollection(new Dictionary<string, string>
398+
{
399+
{ "Source", cosmosExtension },
400+
{ "Sink", cosmosExtension },
401+
{ "SourceSettings:ConnectionString", connectionString },
402+
{ "SourceSettings:Database", database },
403+
{ "SourceSettings:Container", container },
404+
{ "SinkSettings:ConnectionString", connectionString },
405+
{ "SinkSettings:Database", database },
406+
{ "SinkSettings:Container", container },
407+
{ "SinkSettings:RecreateContainer", "false" },
408+
})
409+
.Build();
410+
411+
var loader = new Mock<IExtensionLoader>();
412+
var sourceExtension = new Mock<IDataSourceExtension>();
413+
sourceExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
414+
loader
415+
.Setup(l => l.LoadExtensions<IDataSourceExtension>(It.IsAny<CompositionContainer>()))
416+
.Returns(new List<IDataSourceExtension> { sourceExtension.Object });
417+
418+
var sinkExtension = new Mock<IDataSinkExtension>();
419+
sinkExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
420+
loader
421+
.Setup(l => l.LoadExtensions<IDataSinkExtension>(It.IsAny<CompositionContainer>()))
422+
.Returns(new List<IDataSinkExtension> { sinkExtension.Object });
423+
424+
var handler = new RunCommand.CommandHandler(loader.Object,
425+
configuration,
426+
NullLoggerFactory.Instance);
427+
428+
var parseResult = new RootCommand().Parse(Array.Empty<string>());
429+
var result = handler.Invoke(new InvocationContext(parseResult));
430+
431+
// Should succeed when RecreateContainer is false even with same container
432+
Assert.AreEqual(0, result);
433+
sourceExtension.Verify(se => se.ReadAsync(It.IsAny<IConfiguration>(), It.IsAny<ILogger>(), It.IsAny<CancellationToken>()), Times.Once);
434+
}
435+
436+
[TestMethod]
437+
public void Invoke_WithSameCosmosSourceAndSinkDifferentDatabase_Succeeds()
438+
{
439+
const string cosmosExtension = "Cosmos-nosql";
440+
const string connectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test";
441+
const string sourceDatabase = "sourceDb";
442+
const string sinkDatabase = "sinkDb";
443+
const string container = "testContainer";
444+
445+
IConfigurationRoot configuration = new ConfigurationBuilder()
446+
.AddInMemoryCollection(new Dictionary<string, string>
447+
{
448+
{ "Source", cosmosExtension },
449+
{ "Sink", cosmosExtension },
450+
{ "SourceSettings:ConnectionString", connectionString },
451+
{ "SourceSettings:Database", sourceDatabase },
452+
{ "SourceSettings:Container", container },
453+
{ "SinkSettings:ConnectionString", connectionString },
454+
{ "SinkSettings:Database", sinkDatabase },
455+
{ "SinkSettings:Container", container },
456+
{ "SinkSettings:RecreateContainer", "true" },
457+
})
458+
.Build();
459+
460+
var loader = new Mock<IExtensionLoader>();
461+
var sourceExtension = new Mock<IDataSourceExtension>();
462+
sourceExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
463+
loader
464+
.Setup(l => l.LoadExtensions<IDataSourceExtension>(It.IsAny<CompositionContainer>()))
465+
.Returns(new List<IDataSourceExtension> { sourceExtension.Object });
466+
467+
var sinkExtension = new Mock<IDataSinkExtension>();
468+
sinkExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
469+
loader
470+
.Setup(l => l.LoadExtensions<IDataSinkExtension>(It.IsAny<CompositionContainer>()))
471+
.Returns(new List<IDataSinkExtension> { sinkExtension.Object });
472+
473+
var handler = new RunCommand.CommandHandler(loader.Object,
474+
configuration,
475+
NullLoggerFactory.Instance);
476+
477+
var parseResult = new RootCommand().Parse(Array.Empty<string>());
478+
var result = handler.Invoke(new InvocationContext(parseResult));
479+
480+
// Should succeed when database is different
481+
Assert.AreEqual(0, result);
482+
sourceExtension.Verify(se => se.ReadAsync(It.IsAny<IConfiguration>(), It.IsAny<ILogger>(), It.IsAny<CancellationToken>()), Times.Once);
483+
}
484+
485+
[TestMethod]
486+
public void Invoke_WithSameCosmosSourceAndSinkDifferentContainer_Succeeds()
487+
{
488+
const string cosmosExtension = "Cosmos-nosql";
489+
const string connectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test";
490+
const string database = "testDb";
491+
const string sourceContainer = "sourceContainer";
492+
const string sinkContainer = "sinkContainer";
493+
494+
IConfigurationRoot configuration = new ConfigurationBuilder()
495+
.AddInMemoryCollection(new Dictionary<string, string>
496+
{
497+
{ "Source", cosmosExtension },
498+
{ "Sink", cosmosExtension },
499+
{ "SourceSettings:ConnectionString", connectionString },
500+
{ "SourceSettings:Database", database },
501+
{ "SourceSettings:Container", sourceContainer },
502+
{ "SinkSettings:ConnectionString", connectionString },
503+
{ "SinkSettings:Database", database },
504+
{ "SinkSettings:Container", sinkContainer },
505+
{ "SinkSettings:RecreateContainer", "true" },
506+
})
507+
.Build();
508+
509+
var loader = new Mock<IExtensionLoader>();
510+
var sourceExtension = new Mock<IDataSourceExtension>();
511+
sourceExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
512+
loader
513+
.Setup(l => l.LoadExtensions<IDataSourceExtension>(It.IsAny<CompositionContainer>()))
514+
.Returns(new List<IDataSourceExtension> { sourceExtension.Object });
515+
516+
var sinkExtension = new Mock<IDataSinkExtension>();
517+
sinkExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
518+
loader
519+
.Setup(l => l.LoadExtensions<IDataSinkExtension>(It.IsAny<CompositionContainer>()))
520+
.Returns(new List<IDataSinkExtension> { sinkExtension.Object });
521+
522+
var handler = new RunCommand.CommandHandler(loader.Object,
523+
configuration,
524+
NullLoggerFactory.Instance);
525+
526+
var parseResult = new RootCommand().Parse(Array.Empty<string>());
527+
var result = handler.Invoke(new InvocationContext(parseResult));
528+
529+
// Should succeed when container is different
530+
Assert.AreEqual(0, result);
531+
sourceExtension.Verify(se => se.ReadAsync(It.IsAny<IConfiguration>(), It.IsAny<ILogger>(), It.IsAny<CancellationToken>()), Times.Once);
532+
}
533+
534+
[TestMethod]
535+
public void Invoke_WithDifferentExtensionTypesAndRecreateContainer_Succeeds()
536+
{
537+
const string sourceExtension = "Json";
538+
const string sinkExtension = "Cosmos-nosql";
539+
540+
IConfigurationRoot configuration = new ConfigurationBuilder()
541+
.AddInMemoryCollection(new Dictionary<string, string>
542+
{
543+
{ "Source", sourceExtension },
544+
{ "Sink", sinkExtension },
545+
{ "SourceSettings:FilePath", "test.json" },
546+
{ "SinkSettings:ConnectionString", "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test" },
547+
{ "SinkSettings:Database", "testDb" },
548+
{ "SinkSettings:Container", "testContainer" },
549+
{ "SinkSettings:RecreateContainer", "true" },
550+
})
551+
.Build();
552+
553+
var loader = new Mock<IExtensionLoader>();
554+
var source = new Mock<IDataSourceExtension>();
555+
source.SetupGet(ds => ds.DisplayName).Returns(sourceExtension);
556+
loader
557+
.Setup(l => l.LoadExtensions<IDataSourceExtension>(It.IsAny<CompositionContainer>()))
558+
.Returns(new List<IDataSourceExtension> { source.Object });
559+
560+
var sink = new Mock<IDataSinkExtension>();
561+
sink.SetupGet(ds => ds.DisplayName).Returns(sinkExtension);
562+
loader
563+
.Setup(l => l.LoadExtensions<IDataSinkExtension>(It.IsAny<CompositionContainer>()))
564+
.Returns(new List<IDataSinkExtension> { sink.Object });
565+
566+
var handler = new RunCommand.CommandHandler(loader.Object,
567+
configuration,
568+
NullLoggerFactory.Instance);
569+
570+
var parseResult = new RootCommand().Parse(Array.Empty<string>());
571+
var result = handler.Invoke(new InvocationContext(parseResult));
572+
573+
// Should succeed when source and sink are different extension types
574+
Assert.AreEqual(0, result);
575+
source.Verify(se => se.ReadAsync(It.IsAny<IConfiguration>(), It.IsAny<ILogger>(), It.IsAny<CancellationToken>()), Times.Once);
576+
}
577+
578+
[TestMethod]
579+
public void Invoke_WithSameCosmosSourceAndSinkUsingAccountEndpoint_ThrowsException()
580+
{
581+
const string cosmosExtension = "Cosmos-nosql";
582+
const string accountEndpoint = "https://test.documents.azure.com:443/";
583+
const string database = "testDb";
584+
const string container = "testContainer";
585+
586+
IConfigurationRoot configuration = new ConfigurationBuilder()
587+
.AddInMemoryCollection(new Dictionary<string, string>
588+
{
589+
{ "Source", cosmosExtension },
590+
{ "Sink", cosmosExtension },
591+
{ "SourceSettings:AccountEndpoint", accountEndpoint },
592+
{ "SourceSettings:Database", database },
593+
{ "SourceSettings:Container", container },
594+
{ "SinkSettings:AccountEndpoint", accountEndpoint },
595+
{ "SinkSettings:Database", database },
596+
{ "SinkSettings:Container", container },
597+
{ "SinkSettings:RecreateContainer", "true" },
598+
})
599+
.Build();
600+
601+
var loader = new Mock<IExtensionLoader>();
602+
var sourceExtension = new Mock<IDataSourceExtension>();
603+
sourceExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
604+
loader
605+
.Setup(l => l.LoadExtensions<IDataSourceExtension>(It.IsAny<CompositionContainer>()))
606+
.Returns(new List<IDataSourceExtension> { sourceExtension.Object });
607+
608+
var sinkExtension = new Mock<IDataSinkExtension>();
609+
sinkExtension.SetupGet(ds => ds.DisplayName).Returns(cosmosExtension);
610+
loader
611+
.Setup(l => l.LoadExtensions<IDataSinkExtension>(It.IsAny<CompositionContainer>()))
612+
.Returns(new List<IDataSinkExtension> { sinkExtension.Object });
613+
614+
var handler = new RunCommand.CommandHandler(loader.Object,
615+
configuration,
616+
NullLoggerFactory.Instance);
617+
618+
var parseResult = new RootCommand().Parse(Array.Empty<string>());
619+
620+
// Should throw exception when same container is used with AccountEndpoint
621+
var exception = Assert.ThrowsException<InvalidOperationException>(() => handler.Invoke(new InvocationContext(parseResult)));
622+
Assert.IsTrue(exception.Message.Contains("same Cosmos DB container"));
623+
}
339624
}
340625
}

0 commit comments

Comments
 (0)