-
Notifications
You must be signed in to change notification settings - Fork 332
Expand file tree
/
Copy pathSqlTestBase.cs
More file actions
701 lines (639 loc) · 33.7 KB
/
SqlTestBase.cs
File metadata and controls
701 lines (639 loc) · 33.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Azure.DataApiBuilder.Auth;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Authorization;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Resolvers;
using Azure.DataApiBuilder.Core.Resolvers.Factories;
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Core.Services.Cache;
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
using Azure.DataApiBuilder.Service.Controllers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using MySqlConnector;
using Npgsql;
using ZiggyCreatures.Caching.Fusion;
using static Azure.DataApiBuilder.Core.AuthenticationHelpers.AppServiceAuthentication;
namespace Azure.DataApiBuilder.Service.Tests.SqlTests
{
/// <summary>
/// Base class providing common test fixture for both REST and GraphQL tests.
/// </summary>
[TestClass]
public abstract class SqlTestBase
{
protected static IQueryExecutor _queryExecutor;
protected static IQueryBuilder _queryBuilder;
protected static Mock<IAuthorizationService> _authorizationService;
protected static Mock<IHttpContextAccessor> _httpContextAccessor;
protected static Mock<IAbstractQueryManagerFactory> _queryManagerFactory;
protected static Mock<IQueryEngineFactory> _queryEngineFactory;
protected static Mock<IMetadataProviderFactory> _metadataProviderFactory;
protected static DbExceptionParser _dbExceptionParser;
protected static ISqlMetadataProvider _sqlMetadataProvider;
protected static string _defaultSchemaName;
protected static string _defaultSchemaVersion;
protected static IAuthorizationResolver _authorizationResolver;
protected static WebApplicationFactory<Program> _application;
protected static ILogger<ISqlMetadataProvider> _sqlMetadataLogger;
protected static ILogger<SqlMutationEngine> _mutationEngineLogger;
protected static ILogger<IQueryEngine> _queryEngineLogger;
protected static ILogger<RestController> _restControllerLogger;
protected static GQLFilterParser _gqlFilterParser;
protected const string MSSQL_DEFAULT_DB_NAME = "master";
protected static string DatabaseName { get; set; }
protected static string DatabaseEngine { get; set; }
protected static HttpClient HttpClient { get; private set; }
/// <summary>
/// Sets up test fixture for class, only to be run once per test run.
/// This is a helper that is called from the non abstract versions of
/// this class.
/// </summary>
/// <param name="customQueries">Test specific queries to be executed on database.</param>
/// <param name="customEntities">Test specific entities to be added to database.</param>
/// <param name="isRestBodyStrict">When false, allows extraneous fields in REST request body.</param>
/// <returns></returns>
protected async static Task InitializeTestFixture(
List<string> customQueries = null,
List<string[]> customEntities = null,
bool isRestBodyStrict = true)
{
TestHelper.SetupDatabaseEnvironment(DatabaseEngine);
// Get the base config file from disk
RuntimeConfig runtimeConfig = SqlTestHelper.SetupRuntimeConfig();
// Setting the rest.request-body-strict flag as per the test fixtures.
if (!isRestBodyStrict)
{
runtimeConfig = runtimeConfig with { Runtime = runtimeConfig.Runtime with { Rest = runtimeConfig.Runtime?.Rest with { RequestBodyStrict = isRestBodyStrict } } };
}
// Add magazines entity to the config
runtimeConfig = DatabaseEngine switch
{
TestCategory.MYSQL => TestHelper.AddMissingEntitiesToConfig(
config: runtimeConfig,
entityKey: "magazine",
entityName: "magazines"),
TestCategory.DWSQL => TestHelper.AddMissingEntitiesToConfig(
config: runtimeConfig,
entityKey: "magazine",
entityName: "foo.magazines",
keyfields: ["id"]),
_ => TestHelper.AddMissingEntitiesToConfig(
config: runtimeConfig,
entityKey: "magazine",
entityName: "foo.magazines"),
};
// Add table name collision testing entity to the config
runtimeConfig = DatabaseEngine switch
{
// MySql does not handle schema the same as other DB, so this testing entity is not needed
TestCategory.MYSQL => runtimeConfig,
_ => TestHelper.AddMissingEntitiesToConfig(
config: runtimeConfig,
entityKey: "bar_magazine",
entityName: "bar.magazines",
keyfields: ["upc"])
};
// Add custom entities for the test, if any.
runtimeConfig = AddCustomEntities(customEntities, runtimeConfig);
// Generate in memory runtime config provider that uses the config that we have modified
RuntimeConfigProvider runtimeConfigProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(runtimeConfig);
_queryEngineLogger = new Mock<ILogger<IQueryEngine>>().Object;
_mutationEngineLogger = new Mock<ILogger<SqlMutationEngine>>().Object;
_restControllerLogger = new Mock<ILogger<RestController>>().Object;
SetUpSQLMetadataProvider(runtimeConfigProvider);
// Setup Mock HttpContextAccess to return user as required when calling AuthorizationService.AuthorizeAsync
_httpContextAccessor = new Mock<IHttpContextAccessor>();
_httpContextAccessor.Setup(x => x.HttpContext.User).Returns(new ClaimsPrincipal());
await ResetDbStateAsync();
// Execute additional queries, if any.
await ExecuteQueriesOnDbAsync(customQueries);
await _sqlMetadataProvider.InitializeAsync();
// Setup Mock metadata provider Factory
_metadataProviderFactory = new Mock<IMetadataProviderFactory>();
_metadataProviderFactory.Setup(x => x.GetMetadataProvider(It.IsAny<string>())).Returns(_sqlMetadataProvider);
// Setup Mock engine Factory
_queryManagerFactory = new Mock<IAbstractQueryManagerFactory>();
_queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny<DatabaseType>())).Returns(_queryBuilder);
_queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny<DatabaseType>())).Returns(_queryExecutor);
_gqlFilterParser = new(runtimeConfigProvider, _metadataProviderFactory.Object);
// sets the database name using the connection string
SetDatabaseNameFromConnectionString(runtimeConfig.DataSource.ConnectionString);
//Initialize the authorization resolver object
_authorizationResolver = new AuthorizationResolver(
runtimeConfigProvider,
_metadataProviderFactory.Object);
Mock<IFusionCache> cache = new();
DabCacheService cacheService = new(cache.Object, logger: null, _httpContextAccessor.Object);
_application = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddHttpContextAccessor();
services.AddSingleton<RuntimeConfigProvider>(sp => runtimeConfigProvider);
services.AddSingleton(_gqlFilterParser);
services.AddSingleton<IQueryEngine>(implementationFactory: serviceProvider =>
{
return new SqlQueryEngine(
_queryManagerFactory.Object,
ActivatorUtilities.GetServiceOrCreateInstance<MetadataProviderFactory>(serviceProvider),
ActivatorUtilities.GetServiceOrCreateInstance<IHttpContextAccessor>(serviceProvider),
_authorizationResolver,
_gqlFilterParser,
_queryEngineLogger,
runtimeConfigProvider,
cacheService);
});
services.AddSingleton<IMutationEngine>(implementationFactory: serviceProvider =>
{
return new SqlMutationEngine(
_queryManagerFactory.Object,
ActivatorUtilities.GetServiceOrCreateInstance<MetadataProviderFactory>(serviceProvider),
_queryEngineFactory.Object,
_authorizationResolver,
_gqlFilterParser,
ActivatorUtilities.GetServiceOrCreateInstance<IHttpContextAccessor>(serviceProvider),
runtimeConfigProvider);
});
services.AddSingleton(_sqlMetadataProvider);
services.AddSingleton(_authorizationResolver);
});
});
HttpClient = _application.CreateClient();
}
/// <summary>
/// Helper method to add test specific entities to the entity mapping.
/// </summary>
/// <param name="customEntities">List of test specific entities.</param>
private static RuntimeConfig AddCustomEntities(List<string[]> customEntities, RuntimeConfig runtimeConfig)
{
if (customEntities is not null)
{
foreach (string[] customEntity in customEntities)
{
string objectKey = customEntity[0];
string objectName = customEntity[1];
runtimeConfig = TestHelper.AddMissingEntitiesToConfig(runtimeConfig, objectKey, objectName);
}
}
return runtimeConfig;
}
/// <summary>
/// Helper method to execute all the additional queries for a test on the database.
/// </summary>
/// <param name="customQueries"></param>
/// <returns></returns>
private static async Task ExecuteQueriesOnDbAsync(List<string> customQueries)
{
if (customQueries is not null)
{
foreach (string query in customQueries)
{
await _queryExecutor.ExecuteQueryAsync<object>(query, dataSourceName: string.Empty, parameters: null, dataReaderHandler: null);
}
}
}
/// <summary>
/// Sets the database name based on the provided connection string.
/// If connection string has no database set, we set the default based on the db type.
/// </summary>
/// <param name="connectionString">connection string containing the database name.</param>
private static void SetDatabaseNameFromConnectionString(string connectionString)
{
switch (DatabaseEngine)
{
case TestCategory.MSSQL:
// use master as default name for MsSql
string sqlDbName = new SqlConnectionStringBuilder(connectionString).InitialCatalog;
DatabaseName = !string.IsNullOrEmpty(sqlDbName) ? sqlDbName : MSSQL_DEFAULT_DB_NAME;
break;
case TestCategory.POSTGRESQL:
// use username as default name for PostgreSql, if no username use empty string
NpgsqlConnectionStringBuilder npgBuilder = new(connectionString);
DatabaseName = !string.IsNullOrEmpty(npgBuilder.Database) ? npgBuilder.Database :
!string.IsNullOrEmpty(npgBuilder.Username) ? npgBuilder.Username : string.Empty;
break;
case TestCategory.MYSQL:
// no default name needed for MySql, if db name doesn't exist use empty string
string mySqlDbName = new MySqlConnectionStringBuilder(connectionString).Database;
DatabaseName = !string.IsNullOrEmpty(mySqlDbName) ? mySqlDbName : string.Empty;
break;
}
}
protected static void SetUpSQLMetadataProvider(RuntimeConfigProvider runtimeConfigProvider)
{
_sqlMetadataLogger = new Mock<ILogger<ISqlMetadataProvider>>().Object;
_queryManagerFactory = new Mock<IAbstractQueryManagerFactory>();
Mock<IHttpContextAccessor> httpContextAccessor = new();
string dataSourceName = runtimeConfigProvider.GetConfig().DefaultDataSourceName;
IFileSystem fileSystem = new FileSystem();
Mock<ILogger<RuntimeConfigValidator>> loggerValidator = new();
RuntimeConfigValidator runtimeConfigValidator = new(runtimeConfigProvider, fileSystem, loggerValidator.Object);
switch (DatabaseEngine)
{
case TestCategory.POSTGRESQL:
Mock<ILogger<PostgreSqlQueryExecutor>> pgQueryExecutorLogger = new();
_queryBuilder = new PostgresQueryBuilder();
_defaultSchemaName = "public";
_dbExceptionParser = new PostgreSqlDbExceptionParser(runtimeConfigProvider);
_queryExecutor = new PostgreSqlQueryExecutor(
runtimeConfigProvider,
_dbExceptionParser,
pgQueryExecutorLogger.Object,
httpContextAccessor.Object);
_queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny<DatabaseType>())).Returns(_queryBuilder);
_queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny<DatabaseType>())).Returns(_queryExecutor);
_sqlMetadataProvider =
new PostgreSqlMetadataProvider(
runtimeConfigProvider,
runtimeConfigValidator,
_queryManagerFactory.Object,
_sqlMetadataLogger,
dataSourceName);
break;
case TestCategory.MSSQL:
Mock<ILogger<QueryExecutor<SqlConnection>>> msSqlQueryExecutorLogger = new();
_queryBuilder = new MsSqlQueryBuilder();
_defaultSchemaName = "dbo";
_dbExceptionParser = new MsSqlDbExceptionParser(runtimeConfigProvider);
_queryExecutor = new MsSqlQueryExecutor(
runtimeConfigProvider,
_dbExceptionParser,
msSqlQueryExecutorLogger.Object,
httpContextAccessor.Object);
_queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny<DatabaseType>())).Returns(_queryBuilder);
_queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny<DatabaseType>())).Returns(_queryExecutor);
_sqlMetadataProvider =
new MsSqlMetadataProvider(
runtimeConfigProvider,
runtimeConfigValidator,
_queryManagerFactory.Object,
_sqlMetadataLogger,
dataSourceName);
break;
case TestCategory.MYSQL:
Mock<ILogger<MySqlQueryExecutor>> mySqlQueryExecutorLogger = new();
_queryBuilder = new MySqlQueryBuilder();
_defaultSchemaName = "mysql";
_dbExceptionParser = new MySqlDbExceptionParser(runtimeConfigProvider);
_queryExecutor = new MySqlQueryExecutor(
runtimeConfigProvider,
_dbExceptionParser,
mySqlQueryExecutorLogger.Object,
httpContextAccessor.Object);
_queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny<DatabaseType>())).Returns(_queryBuilder);
_queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny<DatabaseType>())).Returns(_queryExecutor);
_sqlMetadataProvider =
new MySqlMetadataProvider(
runtimeConfigProvider,
runtimeConfigValidator,
_queryManagerFactory.Object,
_sqlMetadataLogger,
dataSourceName);
break;
case TestCategory.DWSQL:
Mock<ILogger<MsSqlQueryExecutor>> DwSqlQueryExecutorLogger = new();
_queryBuilder = new DwSqlQueryBuilder();
_defaultSchemaName = "dbo";
_dbExceptionParser = new MsSqlDbExceptionParser(runtimeConfigProvider);
_queryExecutor = new MsSqlQueryExecutor(
runtimeConfigProvider,
_dbExceptionParser,
DwSqlQueryExecutorLogger.Object,
httpContextAccessor.Object);
_queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny<DatabaseType>())).Returns(_queryBuilder);
_queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny<DatabaseType>())).Returns(_queryExecutor);
_sqlMetadataProvider =
new MsSqlMetadataProvider(
runtimeConfigProvider,
runtimeConfigValidator,
_queryManagerFactory.Object,
_sqlMetadataLogger,
dataSourceName);
break;
}
}
protected static async Task ResetDbStateAsync()
{
await _queryExecutor.ExecuteQueryAsync<object>(
File.ReadAllText($"DatabaseSchema-{DatabaseEngine}.sql"),
dataSourceName: string.Empty,
parameters: null,
dataReaderHandler: null);
}
/// <summary>
/// Sends raw SQL query to database engine to retrieve expected result in JSON format.
/// </summary>
/// <param name="queryText">raw database query, typically a SELECT</param>
/// <returns>string in JSON format</returns>
protected static async Task<string> GetDatabaseResultAsync(
string queryText,
bool expectJson = true)
{
string result;
if (expectJson)
{
using JsonDocument sqlResult =
await _queryExecutor.ExecuteQueryAsync(
queryText,
parameters: null,
_queryExecutor.GetJsonResultAsync<JsonDocument>,
string.Empty);
result = sqlResult is not null ?
sqlResult.RootElement.ToString() :
new JsonArray().ToString();
}
else
{
JsonArray resultArray =
await _queryExecutor.ExecuteQueryAsync(
queryText,
parameters: null,
_queryExecutor.GetJsonArrayAsync,
string.Empty);
using JsonDocument sqlResult = resultArray is not null ? JsonDocument.Parse(resultArray.ToJsonString()) : null;
result = sqlResult is not null ? sqlResult.RootElement.ToString() : null;
}
return result;
}
/// <summary>
/// Does the setup required to perform a test of the REST Api for both
/// MsSql and Postgress. Shared setup logic eliminates some code duplication
/// between MsSql and Postgress.
/// </summary>
/// <param name="primaryKeyRoute">string represents the primary key route</param>
/// <param name="queryString">string represents the query string provided in URL</param>
/// <param name="entityNameOrPath">string represents the name/path of the entity</param>
/// <param name="sqlQuery">string represents the query to be executed</param>
/// <param name="operationType">The operation type to be tested.</param>
/// <param name="requestBody">string represents JSON data used in mutation operations</param>
/// <param name="exceptionExpected">bool represents if we expect an exception</param>
/// <param name="expectedErrorMessage">string represents the error message in the JsonResponse</param>
/// <param name="expectedStatusCode">int represents the returned http status code</param>
/// <param name="expectedSubStatusCode">enum represents the returned sub status code</param>
/// <param name="expectedLocationHeader">The expected location header in the response (if any)</param>
/// <param name="isExpectedErrorMsgSubstr">When set to true, will look for a substring 'expectedErrorMessage'
/// in the actual error message to verify the test result. This is helpful when the actual error message is dynamic and changes
/// on every single run of the test.</param>
/// <param name="clientRoleHeader">The custom role in whose context the request will be executed.</param>
/// <returns></returns>
protected static async Task SetupAndRunRestApiTest(
string primaryKeyRoute,
string queryString,
string entityNameOrPath,
string sqlQuery,
EntityActionOperation operationType = EntityActionOperation.Read,
string restPath = "api",
IHeaderDictionary headers = null,
string requestBody = null,
bool exceptionExpected = false,
string expectedErrorMessage = "",
HttpStatusCode expectedStatusCode = HttpStatusCode.OK,
SupportedHttpVerb? restHttpVerb = null,
string expectedSubStatusCode = "BadRequest",
string expectedLocationHeader = null,
string expectedAfterQueryString = "",
bool paginated = false,
int verifyNumRecords = -1,
bool expectJson = true,
bool isExpectedErrorMsgSubstr = false,
string clientRoleHeader = null)
{
// Create the rest endpoint using the path and entity name.
string restEndPoint = restPath + "/" + entityNameOrPath;
// Append primaryKeyRoute to the endpoint if it is not empty.
if (!string.IsNullOrEmpty(primaryKeyRoute))
{
restEndPoint = restEndPoint + "/" + primaryKeyRoute;
}
// Append queryString to the endpoint if it is not empty.
if (!string.IsNullOrEmpty(queryString))
{
restEndPoint = restEndPoint + queryString;
}
// Use UnsafeRelaxedJsonEscaping to be less strict about what is encoded.
// For eg. Without using this encoder, quotation mark (") will be encoded as
// \u0022 rather than \". And single quote(') will be encoded as \u0027 rather
// than being left unescaped.
// More details can be found here:
// https://docs.microsoft.com/en-us/dotnet/api/system.text.encodings.web.javascriptencoder.unsaferelaxedjsonescaping
JsonSerializerOptions options = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
// Get the httpMethod based on the operation to be executed.
HttpMethod httpMethod = SqlTestHelper.GetHttpMethodFromOperation(operationType, restHttpVerb);
// Create the request to be sent to the engine.
HttpRequestMessage request;
if (!string.IsNullOrEmpty(requestBody))
{
JsonElement requestBodyElement = JsonDocument.Parse(requestBody).RootElement.Clone();
request = new(httpMethod, restEndPoint)
{
Content = JsonContent.Create(requestBodyElement, options: options)
};
}
else
{
request = new(httpMethod, restEndPoint);
}
// Add headers to the request if any.
if (headers is not null)
{
foreach ((string key, StringValues value) in headers)
{
request.Headers.Add(key, value.ToString());
}
}
if (clientRoleHeader is not null)
{
request.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, clientRoleHeader);
// Detect runtime auth provider once per call
RuntimeConfigProvider configProvider = _application.Services.GetRequiredService<RuntimeConfigProvider>();
string provider = configProvider.GetConfig().Runtime.Host.Authentication.Provider;
if (string.Equals(provider, nameof(EasyAuthType.AppService), StringComparison.OrdinalIgnoreCase))
{
// AppService EasyAuth principal with this role
request.Headers.Add(
AuthenticationOptions.CLIENT_PRINCIPAL_HEADER,
AuthTestHelper.CreateAppServiceEasyAuthToken(
roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE,
additionalClaims:
[
new AppServiceClaim
{
Typ = AuthenticationOptions.ROLE_CLAIM_TYPE,
Val = clientRoleHeader
}
]));
}
else
{
// Static Web Apps principal as before
request.Headers.Add(
AuthenticationOptions.CLIENT_PRINCIPAL_HEADER,
AuthTestHelper.CreateStaticWebAppsEasyAuthToken(
addAuthenticated: true,
specificRole: clientRoleHeader));
}
}
// Send request to the engine.
HttpResponseMessage response = await HttpClient.SendAsync(request);
// if an exception is expected we generate the correct error
// The expected result should be a Query that confirms the result state
// of the Operation performed for the test. However:
// Initial DELETE request results in 204 no content, no exception thrown.
// Subsequent DELETE requests result in 404, which result in an exception.
string expected;
if ((operationType is EntityActionOperation.Delete ||
operationType is EntityActionOperation.Upsert ||
operationType is EntityActionOperation.UpsertIncremental ||
operationType is EntityActionOperation.Update ||
operationType is EntityActionOperation.UpdateIncremental)
&& response.StatusCode == HttpStatusCode.NoContent
)
{
expected = string.Empty;
}
else
{
if (exceptionExpected)
{
expected = JsonSerializer.Serialize(RestController.ErrorResponse(
expectedSubStatusCode.ToString(),
expectedErrorMessage,
expectedStatusCode).Value,
options);
}
else
{
string baseUrl = HttpClient.BaseAddress.ToString() + restPath + "/" + entityNameOrPath;
if (!string.IsNullOrEmpty(queryString))
{
// Parse query string with AspNetCore components for consistent URI encoding.
baseUrl = QueryHelpers.AddQueryString(uri: baseUrl, queryString: QueryHelpers.ParseQuery(queryString));
}
string dbResult = await GetDatabaseResultAsync(sqlQuery, expectJson);
// For FIND requests, null result signifies an empty result set
dbResult = (operationType is EntityActionOperation.Read && dbResult is null) ? "[]" : dbResult;
expected = $"{{\"{SqlTestHelper.jsonResultTopLevelKey}\":" +
$"{FormatExpectedValue(dbResult)}{ExpectedNextLinkIfAny(paginated, baseUrl, $"{expectedAfterQueryString}")}}}";
}
}
// Verify the expected and actual response are identical.
await SqlTestHelper.VerifyResultAsync(
expected: expected,
request: request,
response: response,
exceptionExpected: exceptionExpected,
httpMethod: httpMethod,
expectedLocationHeader: expectedLocationHeader,
verifyNumRecords: verifyNumRecords,
isExpectedErrorMsgSubstr: isExpectedErrorMsgSubstr);
}
/// <summary>
/// Helper function formats the expected value to match actual response format.
/// </summary>
/// <param name="expected">The expected response.</param>
/// <returns>Formatted expected response.</returns>
private static string FormatExpectedValue(string expected)
{
return string.IsNullOrWhiteSpace(expected) ? string.Empty : (!Equals(expected[0], '[')) ? $"[{expected}]" : expected;
}
/// <summary>
/// Helper function will return the expected NextLink if one is
/// required, and an empty string otherwise.
/// </summary>
/// <param name="paginated">Bool representing if the nextLink is needed.</param>
/// <param name="baseUrl">The base Url.</param>
/// <param name="queryString">The query string to add to the url.</param>
/// <returns></returns>
private static string ExpectedNextLinkIfAny(bool paginated, string baseUrl, string queryString)
{
return paginated ? $",\"nextLink\":\"{baseUrl}{queryString}\"" : string.Empty;
}
/// <summary>
/// Read the data property of the GraphQLController result
/// </summary>
/// <param name="query"></param>
/// <param name="queryName"></param>
/// <param name="httpClient"></param>
/// <param name="isAuthenticated"></param>
/// <param name="variables">Variables to be included in the GraphQL request. If null, no variables property is included in the request, to pass an empty object provide an empty dictionary</param>
/// <param name="clientRoleHeader"></param>
/// <param name="expectsError"></param>
/// <returns>string in JSON format</returns>
protected virtual async Task<JsonElement> ExecuteGraphQLRequestAsync(
string query,
string queryName,
bool isAuthenticated,
Dictionary<string, object> variables = null,
string clientRoleHeader = null,
bool expectsError = false)
{
RuntimeConfigProvider configProvider = _application.Services.GetService<RuntimeConfigProvider>();
string authToken = null;
if (isAuthenticated)
{
string provider = configProvider.GetConfig().Runtime.Host.Authentication.Provider;
if (string.Equals(provider, nameof(EasyAuthType.AppService), StringComparison.OrdinalIgnoreCase))
{
authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(
roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE,
additionalClaims: !string.IsNullOrEmpty(clientRoleHeader)
?
[
new AppServiceClaim
{
Typ = AuthenticationOptions.ROLE_CLAIM_TYPE,
Val = clientRoleHeader
}
]
: null);
}
else
{
authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(
addAuthenticated: true,
specificRole: clientRoleHeader);
}
}
return await GraphQLRequestExecutor.PostGraphQLRequestAsync(
HttpClient,
configProvider,
queryName,
query,
variables,
authToken,
clientRoleHeader: clientRoleHeader);
}
[TestCleanup]
public void CleanupAfterEachTest()
=> TestHelper.UnsetAllDABEnvironmentVariables();
}
}