Skip to content

Commit

Permalink
Creating a new test to verify #1820
Browse files Browse the repository at this point in the history
Test isn't implementing logic of #1820 yet, just replicating an existing test to ensure the test runner passes initially.

Created in a new test class to reduce changes of merge conflicts, but extracted out shared logic from existing test class.
  • Loading branch information
aaronpowell committed Oct 17, 2023
1 parent 49ac53a commit 8cbbeb4
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 127 deletions.
11 changes: 11 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Service.Tests.Configuration;

internal static class ConfigurationEndpoints
{
// TODO: Remove the old endpoint once we've updated all callers to use the new one.
public const string CONFIGURATION_ENDPOINT = "/configuration";
public const string CONFIGURATION_ENDPOINT_V2 = "/configuration/v2";
}
144 changes: 144 additions & 0 deletions src/Service.Tests/Configuration/ConfigurationJsonBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Controllers;
using Microsoft.IdentityModel.Tokens;
using static Azure.DataApiBuilder.Config.FileSystemRuntimeConfigLoader;
using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationEndpoints;

namespace Azure.DataApiBuilder.Service.Tests.Configuration;

/// <summary>
/// Provides methods to build the json content for the configuration endpoint.
/// </summary>
internal static class ConfigurationJsonBuilder
{
public const string COSMOS_ENVIRONMENT = TestCategory.COSMOSDBNOSQL;
public const string COSMOS_DATABASE_NAME = "config_db";

public static JsonContent GetJsonContentForCosmosConfigRequest(string endpoint, string config = null, bool useAccessToken = false)
{
if (CONFIGURATION_ENDPOINT == endpoint)
{
ConfigurationPostParameters configParams = GetCosmosConfigurationParameters();
if (config is not null)
{
configParams = configParams with { Configuration = config };
}

if (useAccessToken)
{
configParams = configParams with
{
ConnectionString = "AccountEndpoint=https://localhost:8081/;",
AccessToken = GenerateMockJwtToken()
};
}

return JsonContent.Create(configParams);
}
else if (CONFIGURATION_ENDPOINT_V2 == endpoint)
{
ConfigurationPostParametersV2 configParams = GetCosmosConfigurationParametersV2();
if (config != null)
{
configParams = configParams with { Configuration = config };
}

if (useAccessToken)
{
// With an invalid access token, when a new instance of CosmosClient is created with that token, it
// won't throw an exception. But when a graphql request is coming in, that's when it throws a 401
// exception. To prevent this, CosmosClientProvider parses the token and retrieves the "exp" property
// from the token, if it's not valid, then we will throw an exception from our code before it
// initiating a client. Uses a valid fake JWT access token for testing purposes.
RuntimeConfig overrides = new(
Schema: null,
DataSource: new DataSource(DatabaseType.CosmosDB_NoSQL, "AccountEndpoint=https://localhost:8081/;", new()),
Runtime: null,
Entities: new(new Dictionary<string, Entity>()));

configParams = configParams with
{
ConfigurationOverrides = overrides.ToJson(),
AccessToken = GenerateMockJwtToken()
};
}

return JsonContent.Create(configParams);
}
else
{
throw new ArgumentException($"Unexpected configuration endpoint. {endpoint}");
}
}

private static string GenerateMockJwtToken()
{
string mySecret = "PlaceholderPlaceholder";
SymmetricSecurityKey mySecurityKey = new(Encoding.ASCII.GetBytes(mySecret));

JwtSecurityTokenHandler tokenHandler = new();
SecurityTokenDescriptor tokenDescriptor = new()
{
Subject = new ClaimsIdentity(new Claim[] { }),
Expires = DateTime.UtcNow.AddMinutes(5),
Issuer = "http://mysite.com",
Audience = "http://myaudience.com",
SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256Signature)
};

SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}

public static ConfigurationPostParameters GetCosmosConfigurationParameters()
{
RuntimeConfig configuration = ReadCosmosConfigurationFromFile();
return new(
configuration.ToJson(),
File.ReadAllText("schema.gql"),
$"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}",
AccessToken: null);
}

private static ConfigurationPostParametersV2 GetCosmosConfigurationParametersV2()
{
RuntimeConfig configuration = ReadCosmosConfigurationFromFile();
RuntimeConfig overrides = new(
Schema: null,
DataSource: new DataSource(DatabaseType.CosmosDB_NoSQL, $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", new()),
Runtime: null,
Entities: new(new Dictionary<string, Entity>()));

return new(
configuration.ToJson(),
overrides.ToJson(),
File.ReadAllText("schema.gql"),
AccessToken: null);
}

private static RuntimeConfig ReadCosmosConfigurationFromFile()
{
string cosmosFile = $"{CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{CONFIG_EXTENSION}";

string configurationFileContents = File.ReadAllText(cosmosFile);
if (!RuntimeConfigLoader.TryParseConfig(configurationFileContents, out RuntimeConfig config))
{
throw new Exception("Failed to parse configuration file.");
}

// The Schema file isn't provided in the configuration file when going through the configuration endpoint so we're removing it.
config.DataSource.Options.Remove("Schema");
return config;
}
}
129 changes: 2 additions & 127 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -36,26 +33,25 @@
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using VerifyMSTest;
using static Azure.DataApiBuilder.Config.FileSystemRuntimeConfigLoader;
using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationEndpoints;
using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationJsonBuilder;

namespace Azure.DataApiBuilder.Service.Tests.Configuration
{
[TestClass]
public class ConfigurationTests
: VerifyBase
{
private const string COSMOS_ENVIRONMENT = TestCategory.COSMOSDBNOSQL;
private const string MSSQL_ENVIRONMENT = TestCategory.MSSQL;
private const string MYSQL_ENVIRONMENT = TestCategory.MYSQL;
private const string POSTGRESQL_ENVIRONMENT = TestCategory.POSTGRESQL;
private const string POST_STARTUP_CONFIG_ENTITY = "Book";
private const string POST_STARTUP_CONFIG_ENTITY_SOURCE = "books";
private const string POST_STARTUP_CONFIG_ROLE = "PostStartupConfigRole";
private const string COSMOS_DATABASE_NAME = "config_db";
private const string CUSTOM_CONFIG_FILENAME = "custom-config.json";
private const string OPENAPI_SWAGGER_ENDPOINT = "swagger";
private const string OPENAPI_DOCUMENT_ENDPOINT = "openapi";
Expand All @@ -65,10 +61,6 @@ public class ConfigurationTests
private const int RETRY_COUNT = 5;
private const int RETRY_WAIT_SECONDS = 1;

// TODO: Remove the old endpoint once we've updated all callers to use the new one.
private const string CONFIGURATION_ENDPOINT = "/configuration";
private const string CONFIGURATION_ENDPOINT_V2 = "/configuration/v2";

/// <summary>
/// A valid REST API request body with correct parameter types for all the fields.
/// </summary>
Expand Down Expand Up @@ -2256,123 +2248,6 @@ private static async Task ExecuteGraphQLIntrospectionQueries(TestServer server,
}
}

private static JsonContent GetJsonContentForCosmosConfigRequest(string endpoint, string config = null, bool useAccessToken = false)
{
if (CONFIGURATION_ENDPOINT == endpoint)
{
ConfigurationPostParameters configParams = GetCosmosConfigurationParameters();
if (config != null)
{
configParams = configParams with { Configuration = config };
}

if (useAccessToken)
{
configParams = configParams with
{
ConnectionString = "AccountEndpoint=https://localhost:8081/;",
AccessToken = GenerateMockJwtToken()
};
}

return JsonContent.Create(configParams);
}
else if (CONFIGURATION_ENDPOINT_V2 == endpoint)
{
ConfigurationPostParametersV2 configParams = GetCosmosConfigurationParametersV2();
if (config != null)
{
configParams = configParams with { Configuration = config };
}

if (useAccessToken)
{
// With an invalid access token, when a new instance of CosmosClient is created with that token, it
// won't throw an exception. But when a graphql request is coming in, that's when it throws a 401
// exception. To prevent this, CosmosClientProvider parses the token and retrieves the "exp" property
// from the token, if it's not valid, then we will throw an exception from our code before it
// initiating a client. Uses a valid fake JWT access token for testing purposes.
RuntimeConfig overrides = new(
Schema: null,
DataSource: new DataSource(DatabaseType.CosmosDB_NoSQL, "AccountEndpoint=https://localhost:8081/;", new()),
Runtime: null,
Entities: new(new Dictionary<string, Entity>()));

configParams = configParams with
{
ConfigurationOverrides = overrides.ToJson(),
AccessToken = GenerateMockJwtToken()
};
}

return JsonContent.Create(configParams);
}
else
{
throw new ArgumentException($"Unexpected configuration endpoint. {endpoint}");
}
}

private static string GenerateMockJwtToken()
{
string mySecret = "PlaceholderPlaceholder";
SymmetricSecurityKey mySecurityKey = new(Encoding.ASCII.GetBytes(mySecret));

JwtSecurityTokenHandler tokenHandler = new();
SecurityTokenDescriptor tokenDescriptor = new()
{
Subject = new ClaimsIdentity(new Claim[] { }),
Expires = DateTime.UtcNow.AddMinutes(5),
Issuer = "http://mysite.com",
Audience = "http://myaudience.com",
SigningCredentials = new SigningCredentials(mySecurityKey, SecurityAlgorithms.HmacSha256Signature)
};

SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}

private static ConfigurationPostParameters GetCosmosConfigurationParameters()
{
RuntimeConfig configuration = ReadCosmosConfigurationFromFile();
return new(
configuration.ToJson(),
File.ReadAllText("schema.gql"),
$"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}",
AccessToken: null);
}

private static ConfigurationPostParametersV2 GetCosmosConfigurationParametersV2()
{
RuntimeConfig configuration = ReadCosmosConfigurationFromFile();
RuntimeConfig overrides = new(
Schema: null,
DataSource: new DataSource(DatabaseType.CosmosDB_NoSQL, $"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;Database={COSMOS_DATABASE_NAME}", new()),
Runtime: null,
Entities: new(new Dictionary<string, Entity>()));

return new(
configuration.ToJson(),
overrides.ToJson(),
File.ReadAllText("schema.gql"),
AccessToken: null);
}

private static RuntimeConfig ReadCosmosConfigurationFromFile()
{
string cosmosFile = $"{CONFIGFILE_NAME}.{COSMOS_ENVIRONMENT}{CONFIG_EXTENSION}";

string configurationFileContents = File.ReadAllText(cosmosFile);
if (!RuntimeConfigLoader.TryParseConfig(configurationFileContents, out RuntimeConfig config))
{
throw new Exception("Failed to parse configuration file.");
}

// The Schema file isn't provided in the configuration file when going through the configuration endpoint so we're removing it.
config.DataSource.Options.Remove("Schema");
return config;
}

/// <summary>
/// Helper used to create the post-startup configuration payload sent to configuration controller.
/// Adds entity used to hydrate authorization resolver post-startup and validate that hydration succeeds.
Expand Down
32 changes: 32 additions & 0 deletions src/Service.Tests/Configuration/LoadConfigViaEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.TestHost;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationEndpoints;
using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationJsonBuilder;

namespace Azure.DataApiBuilder.Service.Tests.Configuration;

[TestClass]
public class LoadConfigViaEndpointTests
{
[TestMethod("Testing that environment variables can be replaced at runtime not only when config is loaded."), TestCategory(TestCategory.COSMOSDBNOSQL)]
[DataRow(CONFIGURATION_ENDPOINT_V2)]
public async Task CanLoadConfigWithMissingEnvironmentVariables(string configurationEndpoint)
{
TestServer server = new(Program.CreateWebHostFromInMemoryUpdateableConfBuilder(Array.Empty<string>()));
HttpClient httpClient = server.CreateClient();

JsonContent content = GetJsonContentForCosmosConfigRequest(configurationEndpoint);

HttpResponseMessage postResult =
await httpClient.PostAsync(configurationEndpoint, content);
Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode);
}
}

0 comments on commit 8cbbeb4

Please sign in to comment.