Skip to content

Commit bbe1851

Browse files
Fix export command (#2375)
## 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)
1 parent 90e1bb0 commit bbe1851

File tree

4 files changed

+221
-13
lines changed

4 files changed

+221
-13
lines changed

src/Cli.Tests/ExporterTests.cs

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Cli.Tests;
5+
6+
/// <summary>
7+
/// Tests for Export Command in CLI.
8+
/// </summary>
9+
[TestClass]
10+
public class ExporterTests
11+
{
12+
/// <summary>
13+
/// Tests the ExportGraphQLFromDabService method to ensure it logs correctly when the HTTPS endpoint works.
14+
/// </summary>
15+
[TestMethod]
16+
public void ExportGraphQLFromDabService_LogsWhenHttpsWorks()
17+
{
18+
// Arrange
19+
Mock<ILogger> mockLogger = new();
20+
Mock<Exporter> mockExporter = new();
21+
RuntimeConfig runtimeConfig = new(
22+
Schema: "schema",
23+
DataSource: new DataSource(DatabaseType.MSSQL, "", new()),
24+
Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)),
25+
Entities: new(new Dictionary<string, Entity>())
26+
);
27+
28+
// Setup the mock to return a schema when the HTTPS endpoint is used
29+
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, false))
30+
.Returns("schema from HTTPS endpoint");
31+
32+
// Act
33+
string result = mockExporter.Object.ExportGraphQLFromDabService(runtimeConfig, mockLogger.Object);
34+
35+
// Assert
36+
Assert.AreEqual("schema from HTTPS endpoint", result);
37+
mockLogger.Verify(logger => logger.Log(
38+
LogLevel.Information,
39+
It.IsAny<EventId>(),
40+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("schema from HTTPS endpoint.")),
41+
It.IsAny<Exception>(),
42+
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Never);
43+
}
44+
45+
/// <summary>
46+
/// Tests the ExportGraphQLFromDabService method to ensure it logs correctly when the HTTPS endpoint fails and falls back to the HTTP endpoint.
47+
/// This test verifies that:
48+
/// 1. The method attempts to fetch the schema using the HTTPS endpoint first.
49+
/// 2. If the HTTPS endpoint fails, it logs the failure and attempts to fetch the schema using the HTTP endpoint.
50+
/// 3. The method logs the appropriate messages during the process.
51+
/// 4. The method returns the schema fetched from the HTTP endpoint when the HTTPS endpoint fails.
52+
/// </summary>
53+
[TestMethod]
54+
public void ExportGraphQLFromDabService_LogsFallbackToHttp_WhenHttpsFails()
55+
{
56+
// Arrange
57+
Mock<ILogger> mockLogger = new();
58+
Mock<Exporter> mockExporter = new();
59+
RuntimeConfig runtimeConfig = new(
60+
Schema: "schema",
61+
DataSource: new DataSource(DatabaseType.MSSQL, "", new()),
62+
Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)),
63+
Entities: new(new Dictionary<string, Entity>())
64+
);
65+
66+
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, false))
67+
.Throws(new Exception("HTTPS endpoint failed"));
68+
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, true))
69+
.Returns("Fallback schema");
70+
71+
// Act
72+
string result = mockExporter.Object.ExportGraphQLFromDabService(runtimeConfig, mockLogger.Object);
73+
74+
// Assert
75+
Assert.AreEqual("Fallback schema", result);
76+
mockLogger.Verify(logger => logger.Log(
77+
LogLevel.Information,
78+
It.IsAny<EventId>(),
79+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Trying to fetch schema from DAB Service using HTTPS endpoint.")),
80+
It.IsAny<Exception>(),
81+
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);
82+
83+
mockLogger.Verify(logger => logger.Log(
84+
LogLevel.Information,
85+
It.IsAny<EventId>(),
86+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.")),
87+
It.IsAny<Exception>(),
88+
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);
89+
}
90+
91+
/// <summary>
92+
/// Tests the ExportGraphQLFromDabService method to ensure it throws an exception when both the HTTPS and HTTP endpoints fail.
93+
/// This test verifies that:
94+
/// 1. The method attempts to fetch the schema using the HTTPS endpoint first.
95+
/// 2. If the HTTPS endpoint fails, it logs the failure and attempts to fetch the schema using the HTTP endpoint.
96+
/// 3. If both endpoints fail, the method throws an exception.
97+
/// 4. The method logs the appropriate messages during the process.
98+
/// </summary>
99+
[TestMethod]
100+
public void ExportGraphQLFromDabService_ThrowsException_WhenBothHttpsAndHttpFail()
101+
{
102+
// Arrange
103+
Mock<ILogger> mockLogger = new();
104+
Mock<Exporter> mockExporter = new() { CallBase = true };
105+
RuntimeConfig runtimeConfig = new(
106+
Schema: "schema",
107+
DataSource: new DataSource(DatabaseType.MSSQL, "", new()),
108+
Runtime: new(Rest: new(), GraphQL: new(), Host: new(null, null)),
109+
Entities: new(new Dictionary<string, Entity>())
110+
);
111+
112+
// Setup the mock to throw an exception when the HTTPS endpoint is used
113+
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, false))
114+
.Throws(new Exception("HTTPS endpoint failed"));
115+
116+
// Setup the mock to throw an exception when the HTTP endpoint is used
117+
mockExporter.Setup(e => e.GetGraphQLSchema(runtimeConfig, true))
118+
.Throws(new Exception("Both HTTP and HTTPS endpoint failed"));
119+
120+
// Act & Assert
121+
// Verify that the method throws an exception when both endpoints fail
122+
Exception exception = Assert.ThrowsException<Exception>(() =>
123+
mockExporter.Object.ExportGraphQLFromDabService(runtimeConfig, mockLogger.Object));
124+
125+
Assert.AreEqual("Both HTTP and HTTPS endpoint failed", exception.Message);
126+
127+
// Verify that the correct log message is generated when attempting to use the HTTPS endpoint
128+
mockLogger.Verify(logger => logger.Log(
129+
LogLevel.Information,
130+
It.IsAny<EventId>(),
131+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Trying to fetch schema from DAB Service using HTTPS endpoint.")),
132+
It.IsAny<Exception>(),
133+
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);
134+
135+
// Verify that the correct log message is generated when falling back to the HTTP endpoint
136+
mockLogger.Verify(logger => logger.Log(
137+
LogLevel.Information,
138+
It.IsAny<EventId>(),
139+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.")),
140+
It.IsAny<Exception>(),
141+
It.IsAny<Func<It.IsAnyType, Exception, string>>()!), Times.Once);
142+
}
143+
}

src/Cli/Commands/ExportOptions.cs

+22
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.IO.Abstractions;
5+
using Azure.DataApiBuilder.Config;
46
using Azure.DataApiBuilder.Core.Generator;
7+
using Azure.DataApiBuilder.Product;
8+
using Cli.Constants;
59
using CommandLine;
10+
using Microsoft.Extensions.Logging;
11+
using static Cli.Utils;
612

713
namespace Cli.Commands
814
{
@@ -59,5 +65,21 @@ public ExportOptions(bool graphql, string outputDirectory, string? graphqlSchema
5965

6066
[Option("sampling-group-count", HelpText = "Specify the number of groups for sampling. This option is applicable only when the 'TimePartitionedSampler' mode is selected.")]
6167
public int? GroupCount { get; }
68+
69+
public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem)
70+
{
71+
logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion());
72+
bool isSuccess = Exporter.Export(this, logger, loader, fileSystem);
73+
if (isSuccess)
74+
{
75+
logger.LogInformation("Successfully exported the schema file.");
76+
return CliReturnCode.SUCCESS;
77+
}
78+
else
79+
{
80+
logger.LogError("Failed to export the graphql schema.");
81+
return CliReturnCode.GENERAL_ERROR;
82+
}
83+
}
6284
}
6385
}

src/Cli/Exporter.cs

+55-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.IO.Abstractions;
5+
using System.Runtime.CompilerServices;
56
using Azure.DataApiBuilder.Config;
67
using Azure.DataApiBuilder.Config.ObjectModel;
78
using Azure.DataApiBuilder.Core.Generator;
@@ -10,12 +11,14 @@
1011
using Microsoft.Extensions.Logging;
1112
using static Cli.Utils;
1213

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

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

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

8184
_cancellationTokenSource.Cancel();
82-
return isSuccess ? 0 : -1;
85+
return isSuccess;
8386
}
8487

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

109-
schemaText = ExportGraphQLFromDabService(runtimeConfig, logger);
112+
Exporter exporter = new();
113+
schemaText = exporter.ExportGraphQLFromDabService(runtimeConfig, logger);
110114
}
111115

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

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

124-
HttpClient client = new( // CodeQL[SM02185] Loading internal server connection
136+
try
137+
{
138+
logger.LogInformation("Trying to fetch schema from DAB Service using HTTPS endpoint.");
139+
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: false);
140+
}
141+
catch
142+
{
143+
logger.LogInformation("Failed to fetch schema from DAB Service using HTTPS endpoint. Trying with HTTP endpoint.");
144+
schemaText = GetGraphQLSchema(runtimeConfig, useFallbackURL: true);
145+
}
146+
147+
return schemaText;
148+
}
149+
150+
/// <summary>
151+
/// Retrieves the GraphQL schema from the DAB service using either the HTTPS or HTTP endpoint based on the specified fallback option.
152+
/// </summary>
153+
/// <param name="runtimeConfig">The runtime configuration containing the GraphQL path and other settings.</param>
154+
/// <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>
155+
internal virtual string GetGraphQLSchema(RuntimeConfig runtimeConfig, bool useFallbackURL = false)
156+
{
157+
HttpClient client;
158+
if (!useFallbackURL)
159+
{
160+
client = new( // CodeQL[SM02185] Loading internal server connection
125161
new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }
126162
)
163+
{
164+
BaseAddress = new Uri($"https://localhost:5001{runtimeConfig.GraphQLPath}")
165+
};
166+
}
167+
else
127168
{
128-
BaseAddress = new Uri($"https://localhost:5001{runtimeConfig.GraphQLPath}")
129-
};
169+
client = new()
170+
{
171+
BaseAddress = new Uri($"http://localhost:5000{runtimeConfig.GraphQLPath}")
172+
};
173+
}
130174

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

135179
HotChocolate.Language.DocumentNode node = response.Result;
136180

137-
schemaText = node.ToString();
138-
return schemaText;
181+
return node.ToString();
139182
}
140183

141184
private static async Task<string> ExportGraphQLFromCosmosDB(ExportOptions options, RuntimeConfig runtimeConfig, ILogger logger)

src/Cli/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static int Execute(string[] args, ILogger cliLogger, IFileSystem fileSyst
6767
(ValidateOptions options) => options.Handler(cliLogger, loader, fileSystem),
6868
(AddTelemetryOptions options) => options.Handler(cliLogger, loader, fileSystem),
6969
(ConfigureOptions options) => options.Handler(cliLogger, loader, fileSystem),
70-
(ExportOptions options) => Exporter.Export(options, cliLogger, loader, fileSystem),
70+
(ExportOptions options) => options.Handler(cliLogger, loader, fileSystem),
7171
errors => DabCliParserErrorHandler.ProcessErrorsAndReturnExitCode(errors));
7272

7373
return result;

0 commit comments

Comments
 (0)