Skip to content

Commit

Permalink
Fix export command (#2375)
Browse files Browse the repository at this point in the history
## Why make this change?

- Closes #1542 
- With .Net deciding to remove HTTPS endpoint support by default,
`export` command started to fail as it uses HTTPS endpoint.

## What is this change?

- Added a fallback HTTP url, which will be used when HTTPS endpoint
fails to fetch the graphql schema.

## How was this tested?

- [X] Unit Tests
- [X] Manual Tests

## Sample Request(s)
command:
`dab export --graphql -o ./testgqlschemaexport`


![image](https://github.com/user-attachments/assets/2bd4bf55-5899-407f-b44b-c35a9ecc49e9)
  • Loading branch information
abhishekkumams authored Sep 25, 2024
1 parent 90e1bb0 commit bbe1851
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 13 deletions.
143 changes: 143 additions & 0 deletions src/Cli.Tests/ExporterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Cli.Tests;

/// <summary>
/// Tests for Export Command in CLI.
/// </summary>
[TestClass]
public class ExporterTests
{
/// <summary>
/// Tests the ExportGraphQLFromDabService method to ensure it logs correctly when the HTTPS endpoint works.
/// </summary>
[TestMethod]
public void ExportGraphQLFromDabService_LogsWhenHttpsWorks()
{
// Arrange
Mock<ILogger> mockLogger = new();
Mock<Exporter> mockExporter = new();
RuntimeConfig runtimeConfig = new(
Schema: "schema",
DataSource: new DataSource(DatabaseType.MSSQL, "", new()),
Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)),
Entities: new(new Dictionary<string, Entity>())
);

// Setup the mock to return a schema when the HTTPS endpoint is used
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, false))
.Returns("schema from HTTPS endpoint");

// Act
string result = mockExporter.Object.ExportGraphQLFromDabService(runtimeConfig, mockLogger.Object);

// Assert
Assert.AreEqual("schema from HTTPS endpoint", result);
mockLogger.Verify(logger => logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("schema from HTTPS endpoint.")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Never);
}

/// <summary>
/// Tests the ExportGraphQLFromDabService method to ensure it logs correctly when the HTTPS endpoint fails and falls back to the HTTP endpoint.
/// This test verifies that:
/// 1. The method attempts to fetch the schema using the HTTPS endpoint first.
/// 2. If the HTTPS endpoint fails, it logs the failure and attempts to fetch the schema using the HTTP endpoint.
/// 3. The method logs the appropriate messages during the process.
/// 4. The method returns the schema fetched from the HTTP endpoint when the HTTPS endpoint fails.
/// </summary>
[TestMethod]
public void ExportGraphQLFromDabService_LogsFallbackToHttp_WhenHttpsFails()
{
// Arrange
Mock<ILogger> mockLogger = new();
Mock<Exporter> mockExporter = new();
RuntimeConfig runtimeConfig = new(
Schema: "schema",
DataSource: new DataSource(DatabaseType.MSSQL, "", new()),
Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)),
Entities: new(new Dictionary<string, Entity>())
);

mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, false))
.Throws(new Exception("HTTPS endpoint failed"));
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, true))
.Returns("Fallback schema");

// Act
string result = mockExporter.Object.ExportGraphQLFromDabService(runtimeConfig, mockLogger.Object);

// Assert
Assert.AreEqual("Fallback schema", result);
mockLogger.Verify(logger => logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Trying to fetch schema from DAB Service using HTTPS endpoint.")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);

mockLogger.Verify(logger => logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);
}

/// <summary>
/// Tests the ExportGraphQLFromDabService method to ensure it throws an exception when both the HTTPS and HTTP endpoints fail.
/// This test verifies that:
/// 1. The method attempts to fetch the schema using the HTTPS endpoint first.
/// 2. If the HTTPS endpoint fails, it logs the failure and attempts to fetch the schema using the HTTP endpoint.
/// 3. If both endpoints fail, the method throws an exception.
/// 4. The method logs the appropriate messages during the process.
/// </summary>
[TestMethod]
public void ExportGraphQLFromDabService_ThrowsException_WhenBothHttpsAndHttpFail()
{
// Arrange
Mock<ILogger> mockLogger = new();
Mock<Exporter> mockExporter = new() { CallBase = true };
RuntimeConfig runtimeConfig = new(
Schema: "schema",
DataSource: new DataSource(DatabaseType.MSSQL, "", new()),
Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)),
Entities: new(new Dictionary<string, Entity>())
);

// Setup the mock to throw an exception when the HTTPS endpoint is used
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, false))
.Throws(new Exception("HTTPS endpoint failed"));

// Setup the mock to throw an exception when the HTTP endpoint is used
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, true))
.Throws(new Exception("Both HTTP and HTTPS endpoint failed"));

// Act & Assert
// Verify that the method throws an exception when both endpoints fail
Exception exception = Assert.ThrowsException<Exception>(() =>
mockExporter.Object.ExportGraphQLFromDabService(runtimeConfig, mockLogger.Object));

Assert.AreEqual("Both HTTP and HTTPS endpoint failed", exception.Message);

// Verify that the correct log message is generated when attempting to use the HTTPS endpoint
mockLogger.Verify(logger => logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Trying to fetch schema from DAB Service using HTTPS endpoint.")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);

// Verify that the correct log message is generated when falling back to the HTTP endpoint
mockLogger.Verify(logger => logger.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);
}
}
22 changes: 22 additions & 0 deletions src/Cli/Commands/ExportOptions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO.Abstractions;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Core.Generator;
using Azure.DataApiBuilder.Product;
using Cli.Constants;
using CommandLine;
using Microsoft.Extensions.Logging;
using static Cli.Utils;

namespace Cli.Commands
{
Expand Down Expand Up @@ -59,5 +65,21 @@ public ExportOptions(bool graphql, string outputDirectory, string? graphqlSchema

[Option("sampling-group-count", HelpText = "Specify the number of groups for sampling. This option is applicable only when the 'TimePartitionedSampler' mode is selected.")]
public int? GroupCount { get; }

public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
bool isSuccess = Exporter.Export(this, logger, loader, fileSystem);
if (isSuccess)
{
logger.LogInformation("Successfully exported the schema file.");
return CliReturnCode.SUCCESS;
}
else
{
logger.LogError("Failed to export the graphql schema.");
return CliReturnCode.GENERAL_ERROR;
}
}
}
}
67 changes: 55 additions & 12 deletions src/Cli/Exporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System.IO.Abstractions;
using System.Runtime.CompilerServices;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Generator;
Expand All @@ -10,12 +11,14 @@
using Microsoft.Extensions.Logging;
using static Cli.Utils;

// This assembly is used to create dynamic proxy objects at runtime for the purpose of mocking dependencies in the tests.
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
namespace Cli
{
/// <summary>
/// Provides functionality for exporting GraphQL schemas, either by generating from a Azure Cosmos DB database or fetching from a GraphQL API.
/// </summary>
internal static class Exporter
internal class Exporter
{
private const int COSMOS_DB_RETRY_COUNT = 1;
private const int DAB_SERVICE_RETRY_COUNT = 5;
Expand All @@ -31,13 +34,13 @@ internal static class Exporter
/// <param name="loader">The loader for runtime configuration files.</param>
/// <param name="fileSystem">The file system abstraction for handling file operations.</param>
/// <returns>Returns 0 if the export is successful, otherwise returns -1.</returns>
public static int Export(ExportOptions options, ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
public static bool Export(ExportOptions options, ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
{
// Attempt to locate the runtime configuration file based on CLI options
if (!TryGetConfigFileBasedOnCliPrecedence(loader, options.Config, out string runtimeConfigFile))
{
logger.LogError("Failed to find the config file provided, check your options and try again.");
return -1;
return false;
}

// Load the runtime configuration from the file
Expand All @@ -47,7 +50,7 @@ public static int Export(ExportOptions options, ILogger logger, FileSystemRuntim
replaceEnvVar: true) || runtimeConfig is null)
{
logger.LogError("Failed to read the config file: {0}.", runtimeConfigFile);
return -1;
return false;
}

// Do not retry if schema generation logic is running
Expand Down Expand Up @@ -79,7 +82,7 @@ public static int Export(ExportOptions options, ILogger logger, FileSystemRuntim
}

_cancellationTokenSource.Cancel();
return isSuccess ? 0 : -1;
return isSuccess;
}

/// <summary>
Expand All @@ -106,7 +109,8 @@ private static async Task ExportGraphQL(ExportOptions options, RuntimeConfig run
_ = ConfigGenerator.TryStartEngineWithOptions(startOptions, loader, fileSystem);
}, _cancellationToken);

schemaText = ExportGraphQLFromDabService(runtimeConfig, logger);
Exporter exporter = new();
schemaText = exporter.ExportGraphQLFromDabService(runtimeConfig, logger);
}

// Write the schema content to a file
Expand All @@ -115,27 +119,66 @@ private static async Task ExportGraphQL(ExportOptions options, RuntimeConfig run
logger.LogInformation("Schema file exported successfully at {0}", options.OutputDirectory);
}

private static string ExportGraphQLFromDabService(RuntimeConfig runtimeConfig, ILogger logger)
/// <summary>
/// Fetches the GraphQL schema from the DAB service, attempting to use the HTTPS endpoint first.
/// If the HTTPS endpoint fails, it falls back to using the HTTP endpoint.
/// Logs the process of fetching the schema and any fallback actions taken.
/// </summary>
/// <param name="runtimeConfig">The runtime config object containing the GraphQL path.</param>
/// <param name="logger">The logger instance used to log information and errors during the schema fetching process.</param>
/// <returns>The GraphQL schema as a string.</returns>
internal string ExportGraphQLFromDabService(RuntimeConfig runtimeConfig, ILogger logger)
{
string schemaText;
// Fetch the schema from the GraphQL API
logger.LogInformation("Fetching schema from GraphQL API.");

HttpClient client = new( // CodeQL[SM02185] Loading internal server connection
try
{
logger.LogInformation("Trying to fetch schema from DAB Service using HTTPS endpoint.");
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: false);
}
catch
{
logger.LogInformation("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.");
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: true);
}

return schemaText;
}

/// <summary>
/// Retrieves the GraphQL schema from the DAB service using either the HTTPS or HTTP endpoint based on the specified fallback option.
/// </summary>
/// <param name="runtimeConfig">The runtime configuration containing the GraphQL path and other settings.</param>
/// <param name="useFallbackURL">A boolean flag indicating whether to use the fallback HTTP endpoint. If false, the method attempts to use the HTTPS endpoint.</param>
internal virtual string GetGraphQLSchema(RuntimeConfig runtimeConfig, bool useFallbackURL = false)
{
HttpClient client;
if (!useFallbackURL)
{
client = new( // CodeQL[SM02185] Loading internal server connection
new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }
)
{
BaseAddress = new Uri($"https://localhost:5001{runtimeConfig.GraphQLPath}")
};
}
else
{
BaseAddress = new Uri($"https://localhost:5001{runtimeConfig.GraphQLPath}")
};
client = new()
{
BaseAddress = new Uri($"http://localhost:5000{runtimeConfig.GraphQLPath}")
};
}

IntrospectionClient introspectionClient = new();
Task<HotChocolate.Language.DocumentNode> response = introspectionClient.DownloadSchemaAsync(client);
response.Wait();

HotChocolate.Language.DocumentNode node = response.Result;

schemaText = node.ToString();
return schemaText;
return node.ToString();
}

private static async Task<string> ExportGraphQLFromCosmosDB(ExportOptions options, RuntimeConfig runtimeConfig, ILogger logger)
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst
(ValidateOptions options) => options.Handler(cliLogger, loader, fileSystem),
(AddTelemetryOptions options) => options.Handler(cliLogger, loader, fileSystem),
(ConfigureOptions options) => options.Handler(cliLogger, loader, fileSystem),
(ExportOptions options) => Exporter.Export(options, cliLogger, loader, fileSystem),
(ExportOptions options) => options.Handler(cliLogger, loader, fileSystem),
errors => DabCliParserErrorHandler.ProcessErrorsAndReturnExitCode(errors));

return result;
Expand Down

0 comments on commit bbe1851

Please sign in to comment.