diff --git a/VERSION b/VERSION index 7cc919f925..0047786a3c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.27 \ No newline at end of file +2.0.28 diff --git a/src/VirtualClient/VirtualClient.Contracts.UnitTests/FileUploadDescriptorFactoryTests.cs b/src/VirtualClient/VirtualClient.Contracts.UnitTests/FileUploadDescriptorFactoryTests.cs index f4b797227b..f280c2e5ff 100644 --- a/src/VirtualClient/VirtualClient.Contracts.UnitTests/FileUploadDescriptorFactoryTests.cs +++ b/src/VirtualClient/VirtualClient.Contracts.UnitTests/FileUploadDescriptorFactoryTests.cs @@ -729,5 +729,629 @@ public void FileUploadDescriptorFactoryHandlesCasesWhereTheTemplateReferencesAPa Assert.AreEqual(expectedContentType, descriptor.ContentType); Assert.AreEqual(expectedFilePath, descriptor.FilePath); } + + [Test] + [TestCase(null, null, null, null)] + [TestCase("Agent01", null, null, null)] + [TestCase("Agent01", "ToolA", null, null)] + [TestCase("Agent01", "ToolA", "Scenario01", null)] + [TestCase("Agent01", "ToolA", "Scenario01", "Client")] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptor_When_Not_Timestamped_With_VirtualDirectory(string expectedAgentId, string expectedToolName, string expectedScenario, string expectedRole) + { + this.SetupDefaults(); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = this.mockFile.Object.Name; + string expectedBlobPath = string.Join('/', (new string[] { expectedAgentId, expectedToolName, expectedRole, expectedScenario }) + .Where(i => i != null)) + .ToLowerInvariant(); + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = !string.IsNullOrWhiteSpace(expectedBlobPath) + ? $"/{expectedBlobPath}/local/system/{expectedFileName}" + : $"/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + null, + expectedRole); + + string pathTemplate = "{experimentId}/{agentId}/{toolName}/{role}/{scenario}"; + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: false, pathTemplate: pathTemplate, virtualDirectory: "/local/system"); + + Assert.AreEqual(expectedExperimentId.ToLowerInvariant(), descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + + [Test] + [TestCase(null, null, null, null)] + [TestCase("Agent01", null, null, null)] + [TestCase("Agent01", "ToolA", null, null)] + [TestCase("Agent01", "ToolA", "Scenario01", null)] + [TestCase("Agent01", "ToolA", "Scenario01", "Client")] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptor_When_Timestamped_With_VirtualDirectory(string expectedAgentId, string expectedToolName, string expectedScenario, string expectedRole) + { + this.SetupDefaults(); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + string expectedBlobPath = string.Join('/', (new string[] { expectedAgentId, expectedToolName, expectedRole, expectedScenario }) + .Where(i => i != null)) + .ToLowerInvariant(); + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = !string.IsNullOrWhiteSpace(expectedBlobPath) + ? $"/{expectedBlobPath}/local/system/{expectedFileName}" + : $"/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + null, + expectedRole); + + string pathTemplate = "{experimentId}/{agentId}/{toolName}/{role}/{scenario}"; + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: true, pathTemplate: pathTemplate, virtualDirectory: "local\\system\\"); + + Assert.AreEqual(expectedExperimentId.ToLowerInvariant(), descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptorWithDefaultContentPathTemplate_Scenario_3() + { + // Default Template: + // {experimentId}/{agentId}/{toolName}/{scenario} + this.SetupDefaults(); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string expectedAgentId = "Agent01"; + string expectedToolName = "Toolkit"; + string expectedScenario = "Cycle-VegaServer"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + string expectedBlobPath = string.Join('/', (new string[] + { + expectedAgentId, + expectedToolName, + expectedScenario + }) + .Where(i => i != null)) + .ToLowerInvariant(); + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"/{expectedBlobPath}/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario); + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: true, virtualDirectory: "local/system"); + + Assert.AreEqual(expectedExperimentId, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptorWithDefaultContentPathTemplate_Scenario_4() + { + // Default Template: + // {experimentId}/{agentId}/{toolName}/{role}/{scenario} + this.SetupDefaults(); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string expectedAgentId = "Agent01"; + string expectedToolName = "Toolkit"; + string expectedRole = "Client"; + string expectedScenario = "Cycle-VegaServer"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + string expectedBlobPath = string.Join('/', (new string[] + { + expectedAgentId, + expectedToolName, + expectedRole, + expectedScenario + }) + .Where(i => i != null)) + .ToLowerInvariant(); + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"/{expectedBlobPath}/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + role: expectedRole); + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: true, virtualDirectory: "\\local\\system\\"); + + Assert.AreEqual(expectedExperimentId, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase + ( + "customcontainer", + "customcontainer", + "/" + )] + [TestCase + ( + "customcontainer/{ExperimentId}", + "customcontainer", + "/cfad01a9-8F5c-4210-841e-63210ed6a85d" + )] + [TestCase + ( + "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId}/{Scenario}", + "customcontainer", + "/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8F5c-4210-841e-63210ed6a85d/cycle-vegaserver" + )] + [TestCase + ( + "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId},{Revision}/{Scenario}", + "customcontainer", + "/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8F5c-4210-841e-63210ed6a85d,revision01/cycle-vegaserver" + )] + [TestCase + ( + "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId},{Revision}/{AgentId}/{Scenario}", + "customcontainer", + "/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8F5c-4210-841e-63210ed6a85d,revision01/642,042728166da7,37,192.168.2.155/cycle-vegaserver" + )] + [TestCase + ( + "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId},{Revision}/{AgentId}/{Scenario}/{Role}", + "customcontainer", + "/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8F5c-4210-841e-63210ed6a85d,revision01/642,042728166da7,37,192.168.2.155/cycle-vegaserver/client" + )] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptorWithCustomContentPathTemplates_WithVirtualDirectory(string contentPathTemplate, string expectedContainer, string expectedBlobPath) + { + this.SetupDefaults(); + + string expectedExperimentName = "Test_Experiment"; + string expectedExperimentId = "cfad01a9-8F5c-4210-841e-63210ed6a85d"; + string expectedExperimentDefinitionId = "645f6639-98c6-41ee-a853-b0a3ab8ce0a3"; + string expectedAgentId = "642,042728166da7,37,192.168.2.155"; + string expectedRevision = "Revision01"; + string expectedToolName = "Toolkit"; + string expectedScenario = "Cycle-VegaServer"; + string expectedRole = "Client"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"{expectedBlobPath.ToLowerInvariant().TrimEnd('/')}/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + null, + expectedRole); + + IDictionary componentMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Revision", expectedRevision }, + { "ExperimentDefinitionId", expectedExperimentDefinitionId }, + { "ExperimentName", expectedExperimentName } + }; + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, metadata: componentMetadata, timestamped: true, pathTemplate: contentPathTemplate, virtualDirectory: "/local/system/"); + + Assert.AreEqual(expectedContainer, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase + ( + "customcontainer", + "customcontainer", + "/" + )] + [TestCase + ( + "customcontainer/{ExperimentId}", + "customcontainer", + "/cfad01a9-8F5c-4210-841e-63210ed6a85d" + )] + [TestCase + ( + "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId},{Revision}/{AgentId}/{Scenario}/{Role}", + "customcontainer", + "/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8F5c-4210-841e-63210ed6a85d,revision01/642,042728166da7,37,192.168.2.155/cycle-vegaserver/client" + )] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptorWithCustomContentPathTemplatesDefinedInCommandLineMetadata_WithVirtualDirectory(string contentPathTemplate, string expectedContainer, string expectedBlobPath) + { + this.SetupDefaults(); + + string expectedExperimentName = "Test_Experiment"; + string expectedExperimentId = "cfad01a9-8F5c-4210-841e-63210ed6a85d"; + string expectedExperimentDefinitionId = "645f6639-98c6-41ee-a853-b0a3ab8ce0a3"; + string expectedAgentId = "642,042728166da7,37,192.168.2.155"; + string expectedRevision = "Revision01"; + string expectedToolName = "Toolkit"; + string expectedScenario = "Cycle-VegaServer"; + string expectedRole = "Client"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"{expectedBlobPath.ToLowerInvariant().TrimEnd('/')}/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + null, + expectedRole); + + IDictionary componentMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Revision", expectedRevision }, + { "ExperimentDefinitionId", expectedExperimentDefinitionId }, + { "ExperimentName", expectedExperimentName }, + { "ContentPathTemplate", contentPathTemplate } + }; + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, metadata: componentMetadata, timestamped: true, virtualDirectory: "local/system/"); + + Assert.AreEqual(expectedContainer, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase("contentpathtemplate")] + [TestCase("CONTENTPATHTEMPLATE")] + [TestCase("ContentPathTemplate")] + [TestCase("contentPathTemplate")] + public void FileUploadDescriptorFactoryIsNotCaseSensitiveOnContentPathTemplatesDefinedInCommandLineMetadata_WithVirtualDirectory(string contentPathTemplateParameterName) + { + this.SetupDefaults(); + + string expectedContentPathTemplate = "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId},{Revision}/{AgentId}/{Scenario}/{Role}"; + string expectedExperimentName = "Test_Experiment"; + string expectedExperimentId = "cfad01a9-8F5c-4210-841e-63210ed6a85d"; + string expectedExperimentDefinitionId = "645f6639-98c6-41ee-a853-b0a3ab8ce0a3"; + string expectedAgentId = "642,042728166da7,37,192.168.2.155"; + string expectedRevision = "Revision01"; + string expectedToolName = "Toolkit"; + string expectedScenario = "Cycle-VegaServer"; + string expectedRole = "Client"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + string expectedContainer = "customcontainer"; + string expectedBlobPath = $"/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8f5c-4210-841e-63210ed6a85d,revision01/642,042728166da7,37,192.168.2.155/cycle-vegaserver/client/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + null, + expectedRole); + + IDictionary componentMetadata = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Revision", expectedRevision }, + { "ExperimentDefinitionId", expectedExperimentDefinitionId }, + { "ExperimentName", expectedExperimentName }, + { contentPathTemplateParameterName, expectedContentPathTemplate } + }; + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, metadata: componentMetadata, timestamped: true, virtualDirectory: "local\\system"); + + Assert.AreEqual(expectedContainer, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase + ( + "customcontainer", + "customcontainer", + "/" + )] + [TestCase + ( + "customcontainer/{ExperimentId}", + "customcontainer", + "/cfad01a9-8F5c-4210-841e-63210ed6a85d" + )] + [TestCase + ( + "customcontainer/{ExperimentDefinitionId}/{ExperimentName}/{ExperimentId},{Revision}/{AgentId}/{Scenario}/{Role}", + "customcontainer", + "/645f6639-98c6-41ee-a853-b0a3ab8ce0a3/test_experiment/cfad01a9-8F5c-4210-841e-63210ed6a85d,revision01/642,042728166da7,37,192.168.2.155/cycle-vegaserver/client" + )] + public void FileUploadDescriptorFactoryCreatesTheExpectedDescriptorWithCustomContentPathTemplatesDefinedInComponentParameters_WithVirtualDirectory(string contentPathTemplate, string expectedContainer, string expectedBlobPath) + { + this.SetupDefaults(); + + string expectedExperimentName = "Test_Experiment"; + string expectedExperimentId = "cfad01a9-8F5c-4210-841e-63210ed6a85d"; + string expectedExperimentDefinitionId = "645f6639-98c6-41ee-a853-b0a3ab8ce0a3"; + string expectedAgentId = "642,042728166da7,37,192.168.2.155"; + string expectedRevision = "Revision01"; + string expectedToolName = "Toolkit"; + string expectedScenario = "Cycle-VegaServer"; + string expectedRole = "Client"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"{expectedBlobPath.ToLowerInvariant().TrimEnd('/')}/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + null, + expectedRole); + + IDictionary componentParameters = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Revision", expectedRevision }, + { "ExperimentDefinitionId", expectedExperimentDefinitionId }, + { "ExperimentName", expectedExperimentName }, + { "ContentPathTemplate", contentPathTemplate } + }; + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, parameters: componentParameters, timestamped: true, virtualDirectory: "local\\system"); + + Assert.AreEqual(expectedContainer, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase + ( + "CUSTOMCONTAINER", + "customcontainer", + "/" + )] + [TestCase + ( + "CustomContainer/ANY/other/pAtH/WiTH/MIXed/CAsinG", + "customcontainer", + "/any/other/path/with/mixed/casing" + )] + public void FileUploadDescriptorFactoryCreatesBlobPathsWithTheExpectedCasing_WithVirtualDirectory(string contentPathTemplate, string expectedContainer, string expectedBlobPath) + { + this.SetupDefaults(); + + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = $"{this.mockFile.Object.CreationTimeUtc.ToString("yyyy-MM-ddTHH-mm-ss-fffffZ")}-{this.mockFile.Object.Name}"; + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"{expectedBlobPath.ToLowerInvariant().TrimEnd('/')}/local/system/{expectedFileName}"; + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + "AnyExperimentId", + "AnyAgentId", + "AnyToolName", + "AnyScenario", + "AnyCommandLine", + "AnyRole"); + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: true, pathTemplate: contentPathTemplate, virtualDirectory: "local/system"); + + Assert.AreEqual(expectedContainer, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase("/local/system/", "local/system")] + [TestCase("/local/system", "local/system")] + [TestCase("local/system/", "local/system")] + [TestCase("local/system", "local/system")] + [TestCase("\\local\\system\\", "local/system")] + [TestCase("\\local\\system", "local/system")] + [TestCase("local\\system\\", "local/system")] + [TestCase("local\\system", "local/system")] + [TestCase("/", "")] + [TestCase("\\", "")] + [TestCase("\\", "")] + [TestCase("local", "local")] + [TestCase("local\\filename.txt", "local/filename.txt")] // system does not remove the file name if provided. + public void FileUploadDescriptorFactoryCreatesExpectedBlobPathsWhenVirtualDirectoryIsDifferentFormats(string virtualDirectory, string expectedVirtualDirectory) + { + // Default Template: + // {experimentId}/{agentId}/{toolName}/{role}/{scenario} + this.SetupDefaults(); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string expectedAgentId = "Agent01"; + string expectedToolName = "Toolkit"; + string expectedRole = "Client"; + string expectedScenario = "Cycle-VegaServer"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = this.mockFile.Object.Name; + string expectedBlobPath = string.Join('/', (new string[] + { + expectedAgentId, + expectedToolName, + expectedRole, + expectedScenario + }) + .Where(i => i != null)) + .ToLowerInvariant(); + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"/{expectedBlobPath}/{expectedVirtualDirectory}/{expectedFileName}".Replace("//", "/"); + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + role: expectedRole); + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: false, virtualDirectory: virtualDirectory); + + Assert.AreEqual(expectedExperimentId, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } + + [Test] + [TestCase("C:\\local", "local")] // remove the drive letter. + [Platform(Include = "win")] + public void FileUploadDescriptorFactoryCreatesExpectedBlobPathsWhenVirtualDirectoryHasWindowsRootedPath(string virtualDirectory, string expectedVirtualDirectory) + { + // Default Template: + // {experimentId}/{agentId}/{toolName}/{role}/{scenario} + this.SetupDefaults(); + + string expectedExperimentId = Guid.NewGuid().ToString(); + string expectedAgentId = "Agent01"; + string expectedToolName = "Toolkit"; + string expectedRole = "Client"; + string expectedScenario = "Cycle-VegaServer"; + string expectedContentType = HttpContentType.PlainText; + string expectedContentEncoding = Encoding.UTF8.WebName; + string expectedFilePath = this.mockFile.Object.FullName; + string expectedFileName = this.mockFile.Object.Name; + string expectedBlobPath = string.Join('/', (new string[] + { + expectedAgentId, + expectedToolName, + expectedRole, + expectedScenario + }) + .Where(i => i != null)) + .ToLowerInvariant(); + + // The blob path itself is lower-cased. However, the file name casing is NOT modified. + expectedBlobPath = $"/{expectedBlobPath}/{expectedVirtualDirectory}/{expectedFileName}".Replace("//", "/"); + + FileContext context = new FileContext( + this.mockFile.Object, + expectedContentType, + expectedContentEncoding, + expectedExperimentId, + expectedAgentId, + expectedToolName, + expectedScenario, + role: expectedRole); + + FileUploadDescriptor descriptor = FileUploadDescriptorFactory.CreateDescriptor(context, timestamped: false, virtualDirectory: virtualDirectory); + + Assert.AreEqual(expectedExperimentId, descriptor.ContainerName); + Assert.AreEqual(expectedFileName, descriptor.BlobName); + Assert.AreEqual(expectedBlobPath, descriptor.BlobPath); + Assert.AreEqual(expectedContentEncoding, descriptor.ContentEncoding); + Assert.AreEqual(expectedContentType, descriptor.ContentType); + Assert.AreEqual(expectedFilePath, descriptor.FilePath); + } } } diff --git a/src/VirtualClient/VirtualClient.Contracts/FileUploadDescriptorFactory.cs b/src/VirtualClient/VirtualClient.Contracts/FileUploadDescriptorFactory.cs index 0e09d28316..c197005ef3 100644 --- a/src/VirtualClient/VirtualClient.Contracts/FileUploadDescriptorFactory.cs +++ b/src/VirtualClient/VirtualClient.Contracts/FileUploadDescriptorFactory.cs @@ -43,6 +43,10 @@ public static class FileUploadDescriptorFactory ///
b9d30758-20a7-4779-826e-137c31a867e1/agent01/cps/client/cps_t16/2022-03-18T10-00-05-13813Z-cps_t16.manifest.json ///
b9d30758-20a7-4779-826e-137c31a867e1/agent01/cps/server/cps_t16/2022-03-18T10-00-06-13813Z-cps_t16.log ///
b9d30758-20a7-4779-826e-137c31a867e1/agent01/cps/server/cps_t16/2022-03-18T10-00-06-13813Z-cps_t16.manifest.json + ///

+ /// Examples (w/ 'Virtual Directory' of /local/folder): + ///
b9d30758-20a7-4779-826e-137c31a867e1/agent01/fio/fio_randwrite_496gb_12k_d32_th16/local/folder/2022-03-18T10-00-05-12765Z-fio_randwrite_496gb_12k_d32_th16.log + ///
b9d30758-20a7-4779-826e-137c31a867e1/agent01/fio/fio_randwrite_496gb_12k_d32_th16/local/folder/2022-03-18T10-00-05-12765Z-fio_randwrite_496gb_12k_d32_th16.manifest.json /// /// /// Provides context about a file to be uploaded. @@ -53,7 +57,11 @@ public static class FileUploadDescriptorFactory /// file name is not desirable. Default = true (timestamped file names). /// /// Content path template to use when uploading content to target storage resources. - public static FileUploadDescriptor CreateDescriptor(FileContext fileContext, IDictionary parameters = null, IDictionary metadata = null, bool timestamped = true, string pathTemplate = null) + /// + /// The virtual directory to store the blob under. This virtual directory is appended to the end of the qualified blob path. This allows the system to replicate + /// the local folder structure in remote storage. + /// + public static FileUploadDescriptor CreateDescriptor(FileContext fileContext, IDictionary parameters = null, IDictionary metadata = null, bool timestamped = true, string pathTemplate = null, string virtualDirectory = null) { fileContext.ThrowIfNull(nameof(fileContext)); @@ -114,13 +122,34 @@ public static FileUploadDescriptor CreateDescriptor(FileContext fileContext, IDi throw new SchemaException($"The container name in the content path template '{effectivePathTemplate}' cannot be empty string."); } + string effectiveVirtualDirectory = virtualDirectory; + if (!string.IsNullOrWhiteSpace(virtualDirectory)) + { + if (Path.IsPathRooted(virtualDirectory)) + { + effectiveVirtualDirectory = virtualDirectory.Substring(Path.GetPathRoot(virtualDirectory).Length); + } + + effectiveVirtualDirectory = effectiveVirtualDirectory.Replace('\\', '/').Trim('/'); + } + string blobPath = null; if (resolvedTemplateParts.Count() > 1) { - blobPath = $"{BlobDescriptor.SanitizeBlobPath($"/{string.Join('/', resolvedTemplateParts.Skip(1))}").ToLowerInvariant()}/{blobName}"; + blobPath = $"{BlobDescriptor.SanitizeBlobPath($"/{string.Join('/', resolvedTemplateParts.Skip(1))}").ToLowerInvariant()}"; + if (!string.IsNullOrWhiteSpace(effectiveVirtualDirectory)) + { + blobPath = $"{blobPath}/{effectiveVirtualDirectory}"; + } + + blobPath = $"{blobPath}/{blobName}"; } - else + else if (!string.IsNullOrWhiteSpace(effectiveVirtualDirectory)) { + blobPath = $"/{effectiveVirtualDirectory}/{blobName}"; + } + else + { blobPath = $"/{blobName}"; }