mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-14 19:08:08 +00:00
Test infrastructure for C# Azure Function testing (#2055)
Add support for function tests and the ability to run them against either real Azure Storage or the Azurite emulator. See follow-up PR #2032 for actual usage.
This commit is contained in:
@ -17,6 +17,9 @@ wget https://aka.ms/downloadazcopy-v10-linux
|
||||
tar -xvf downloadazcopy-v10-linux
|
||||
sudo cp ./azcopy_linux_amd64_*/azcopy /usr/bin/
|
||||
|
||||
# Install Azurite
|
||||
sudo npm install -g azurite
|
||||
|
||||
# Restore rust dependencies
|
||||
echo "Restoring rust dependencies"
|
||||
cargo install cargo-audit cargo-license # requirements if you want to run ci/agent.sh
|
||||
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -275,6 +275,7 @@ jobs:
|
||||
cd src/ApiService/
|
||||
dotnet restore --locked-mode
|
||||
dotnet tool restore
|
||||
sudo npm install -g azurite
|
||||
- name: Check Formatting
|
||||
run: |
|
||||
cd src/ApiService/
|
||||
@ -286,8 +287,10 @@ jobs:
|
||||
- name: Test & Collect coverage info
|
||||
run: |
|
||||
cd src/ApiService/
|
||||
dotnet test --no-restore --collect:"XPlat Code Coverage"
|
||||
azurite --silent &
|
||||
dotnet test --no-restore --collect:"XPlat Code Coverage" --filter 'Category!=Integration'
|
||||
dotnet tool run reportgenerator -reports:Tests/TestResults/*/coverage.cobertura.xml -targetdir:coverage -reporttypes:MarkdownSummary
|
||||
kill %1
|
||||
cat coverage/*.md > $GITHUB_STEP_SUMMARY
|
||||
- name: copy artifacts
|
||||
run: |
|
||||
|
@ -38,6 +38,11 @@ public interface IServiceConfig {
|
||||
public string? OneFuzzTelemetry { get; }
|
||||
|
||||
public string OneFuzzVersion { get; }
|
||||
|
||||
// Prefix to add to the name of any tables created. This allows
|
||||
// multiple instances to run against the same storage account, which
|
||||
// is useful for things like integration testing.
|
||||
public string OneFuzzTablePrefix { get; }
|
||||
}
|
||||
|
||||
public class ServiceConfiguration : IServiceConfig {
|
||||
@ -82,4 +87,5 @@ public class ServiceConfiguration : IServiceConfig {
|
||||
public string OneFuzzVersion { get => Environment.GetEnvironmentVariable("ONEFUZZ_VERSION") ?? "0.0.0"; }
|
||||
|
||||
public string OneFuzzNodeDisposalStrategy { get => Environment.GetEnvironmentVariable("ONEFUZZ_NODE_DISPOSAL_STRATEGY") ?? "scale_in"; }
|
||||
public string OneFuzzTablePrefix => ""; // in production we never prefix the tables
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ public class Containers : IContainers {
|
||||
if (client is null)
|
||||
return null;
|
||||
|
||||
return new Uri($"{GetUrl(client.AccountName)}{container}/{name}");
|
||||
return new Uri($"{_storage.GetBlobEndpoint(client.AccountName)}{container}/{name}");
|
||||
}
|
||||
|
||||
public async Async.Task<BinaryData?> GetBlob(Container container, string name, StorageType storageType) {
|
||||
@ -93,14 +93,10 @@ public class Containers : IContainers {
|
||||
return null;
|
||||
}
|
||||
var storageKeyCredential = new StorageSharedKeyCredential(accountName, accountKey);
|
||||
var accountUrl = GetUrl(accountName);
|
||||
var accountUrl = _storage.GetBlobEndpoint(accountName);
|
||||
return new BlobServiceClient(accountUrl, storageKeyCredential);
|
||||
}
|
||||
|
||||
private static Uri GetUrl(string accountName) {
|
||||
return new Uri($"https://{accountName}.blob.core.windows.net/");
|
||||
}
|
||||
|
||||
public async Async.Task<Uri?> GetFileSasUrl(Container container, string name, StorageType storageType, BlobSasPermissions permissions, TimeSpan? duration = null) {
|
||||
var client = await FindContainer(container, storageType) ?? throw new Exception($"unable to find container: {container.ContainerName} - {storageType}");
|
||||
|
||||
@ -194,4 +190,3 @@ public class Containers : IContainers {
|
||||
return await client.GetBlobClient(name).ExistsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,9 +51,9 @@ public class Queue : IQueue {
|
||||
var accountId = _storage.GetPrimaryAccount(storageType);
|
||||
_log.Verbose($"getting blob container (account_id: {accountId})");
|
||||
var (name, key) = await _storage.GetStorageAccountNameAndKey(accountId);
|
||||
var accountUrl = new Uri($"https://{name}.queue.core.windows.net");
|
||||
var endpoint = _storage.GetQueueEndpoint(accountId);
|
||||
var options = new QueueClientOptions { MessageEncoding = QueueMessageEncoding.Base64 };
|
||||
return new QueueServiceClient(accountUrl, new StorageSharedKeyCredential(name, key), options);
|
||||
return new QueueServiceClient(endpoint, new StorageSharedKeyCredential(name, key), options);
|
||||
}
|
||||
|
||||
public async Task<bool> QueueObject<T>(string name, T obj, StorageType storageType, TimeSpan? visibilityTimeout = null, TimeSpan? timeToLive = null) {
|
||||
|
@ -13,6 +13,13 @@ public enum StorageType {
|
||||
public interface IStorage {
|
||||
public IEnumerable<string> CorpusAccounts();
|
||||
string GetPrimaryAccount(StorageType storageType);
|
||||
|
||||
public Uri GetTableEndpoint(string accountId);
|
||||
|
||||
public Uri GetQueueEndpoint(string accountId);
|
||||
|
||||
public Uri GetBlobEndpoint(string accountId);
|
||||
|
||||
public Async.Task<(string?, string?)> GetStorageAccountNameAndKey(string accountId);
|
||||
|
||||
public Async.Task<string?> GetStorageAccountNameAndKeyByName(string accountName);
|
||||
@ -142,4 +149,13 @@ public class Storage : IStorage {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public Uri GetTableEndpoint(string accountId)
|
||||
=> new($"https://{accountId}.table.core.windows.net/");
|
||||
|
||||
public Uri GetQueueEndpoint(string accountId)
|
||||
=> new($"https://{accountId}.queue.core.windows.net/");
|
||||
|
||||
public Uri GetBlobEndpoint(string accountId)
|
||||
=> new($"https://{accountId}.blob.core.windows.net/");
|
||||
}
|
||||
|
@ -15,6 +15,14 @@ namespace ApiService.OneFuzzLib.Orm {
|
||||
Task<ResultVoid<(int, string)>> Insert(T entity);
|
||||
Task<ResultVoid<(int, string)>> Delete(T entity);
|
||||
|
||||
IAsyncEnumerable<T> SearchAll();
|
||||
IAsyncEnumerable<T> SearchByPartitionKey(string partitionKey);
|
||||
IAsyncEnumerable<T> SearchByRowKey(string rowKey);
|
||||
IAsyncEnumerable<T> SearchByTimeRange(DateTimeOffset min, DateTimeOffset max);
|
||||
|
||||
// Allow using tuple to search.
|
||||
IAsyncEnumerable<T> SearchByTimeRange((DateTimeOffset min, DateTimeOffset max) range)
|
||||
=> SearchByTimeRange(range.min, range.max);
|
||||
}
|
||||
|
||||
|
||||
@ -84,11 +92,15 @@ namespace ApiService.OneFuzzLib.Orm {
|
||||
}
|
||||
|
||||
public async Task<TableClient> GetTableClient(string table, string? accountId = null) {
|
||||
// TODO: do this less often, instead of once per request:
|
||||
var tableName = _context.ServiceConfiguration.OneFuzzTablePrefix + table;
|
||||
|
||||
var account = accountId ?? _context.ServiceConfiguration.OneFuzzFuncStorage ?? throw new ArgumentNullException(nameof(accountId));
|
||||
var (name, key) = await _context.Storage.GetStorageAccountNameAndKey(account);
|
||||
var tableClient = new TableServiceClient(new Uri($"https://{name}.table.core.windows.net"), new TableSharedKeyCredential(name, key));
|
||||
await tableClient.CreateTableIfNotExistsAsync(table);
|
||||
return tableClient.GetTableClient(table);
|
||||
var endpoint = _context.Storage.GetTableEndpoint(account);
|
||||
var tableClient = new TableServiceClient(endpoint, new TableSharedKeyCredential(name, key));
|
||||
await tableClient.CreateTableIfNotExistsAsync(tableName);
|
||||
return tableClient.GetTableClient(tableName);
|
||||
}
|
||||
|
||||
public async Task<ResultVoid<(int, string)>> Delete(T entity) {
|
||||
@ -101,6 +113,19 @@ namespace ApiService.OneFuzzLib.Orm {
|
||||
return ResultVoid<(int, string)>.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<T> SearchAll()
|
||||
=> QueryAsync(null);
|
||||
|
||||
public IAsyncEnumerable<T> SearchByPartitionKey(string partitionKey)
|
||||
=> QueryAsync(Query.PartitionKey(partitionKey));
|
||||
|
||||
public IAsyncEnumerable<T> SearchByRowKey(string rowKey)
|
||||
=> QueryAsync(Query.RowKey(rowKey));
|
||||
|
||||
public IAsyncEnumerable<T> SearchByTimeRange(DateTimeOffset min, DateTimeOffset max) {
|
||||
return QueryAsync(Query.TimeRange(min, max));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -3,6 +3,20 @@ using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
|
||||
|
||||
namespace ApiService.OneFuzzLib.Orm {
|
||||
public static class Query {
|
||||
public static string PartitionKey(string partitionKey) {
|
||||
// TODO: need to escape
|
||||
return $"PartitionKey eq '{partitionKey}'";
|
||||
}
|
||||
|
||||
public static string RowKey(string rowKey) {
|
||||
// TODO: need to escape
|
||||
return $"RowKey eq '{rowKey}'";
|
||||
}
|
||||
|
||||
public static string SingleEntity(string partitionKey, string rowKey) {
|
||||
// TODO: need to escape
|
||||
return $"(PartitionKey eq '{partitionKey}') and (RowKey eq '{rowKey}')";
|
||||
}
|
||||
|
||||
public static string Or(IEnumerable<string> queries) {
|
||||
return string.Join(" or ", queries.Select(x => $"({x})"));
|
||||
@ -28,7 +42,19 @@ namespace ApiService.OneFuzzLib.Orm {
|
||||
|
||||
public static string EqualAnyEnum<T>(string property, IEnumerable<T> enums) where T : Enum {
|
||||
IEnumerable<string> convertedEnums = enums.Select(x => JsonSerializer.Serialize(x, EntityConverter.GetJsonSerializerOptions()).Trim('"'));
|
||||
return Query.EqualAny(property, convertedEnums);
|
||||
return EqualAny(property, convertedEnums);
|
||||
}
|
||||
|
||||
public static string TimeRange(DateTimeOffset min, DateTimeOffset max) {
|
||||
// NB: this uses the auto-populated Timestamp property, and will result in scanning
|
||||
// TODO: should this be inclusive at the endpoints?
|
||||
return $"Timestamp lt datetime'{max:o}' and Timestamp gt datetime'{min:o}'";
|
||||
}
|
||||
|
||||
public static string StartsWith(string property, string prefix) {
|
||||
var upperBound = prefix[..(prefix.Length - 1)] + (char)(prefix.Last() + 1);
|
||||
// TODO: escaping
|
||||
return $"{property} ge '{prefix}' and {property} lt '{upperBound}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
104
src/ApiService/Tests/Fakes/TestContext.cs
Normal file
104
src/ApiService/Tests/Fakes/TestContext.cs
Normal file
@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Microsoft.OneFuzz.Service.OneFuzzLib.Orm;
|
||||
using Async = System.Threading.Tasks;
|
||||
|
||||
namespace Tests.Fakes;
|
||||
|
||||
|
||||
// TestContext provides a minimal IOnefuzzContext implementation to allow running
|
||||
// of functions as unit or integration tests.
|
||||
public sealed class TestContext : IOnefuzzContext {
|
||||
public TestContext(ILogTracer logTracer, IStorage storage, string tablePrefix, string accountId) {
|
||||
ServiceConfiguration = new TestServiceConfiguration(tablePrefix, accountId);
|
||||
|
||||
Storage = storage;
|
||||
|
||||
RequestHandling = new RequestHandling(logTracer);
|
||||
TaskOperations = new TaskOperations(logTracer, this);
|
||||
NodeOperations = new NodeOperations(logTracer, this);
|
||||
JobOperations = new JobOperations(logTracer, this);
|
||||
NodeTasksOperations = new NodeTasksOperations(logTracer, this);
|
||||
}
|
||||
|
||||
public TestEvents Events { get; set; } = new();
|
||||
|
||||
// convenience method for test setup
|
||||
public Async.Task InsertAll(params EntityBase[] objs)
|
||||
=> Async.Task.WhenAll(
|
||||
objs.Select(x => x switch {
|
||||
Task t => TaskOperations.Insert(t),
|
||||
Node n => NodeOperations.Insert(n),
|
||||
Job j => JobOperations.Insert(j),
|
||||
NodeTasks nt => NodeTasksOperations.Insert(nt),
|
||||
_ => throw new NotImplementedException($"Need to add an TestContext.InsertAll case for {x.GetType()} entities"),
|
||||
}));
|
||||
|
||||
// Implementations:
|
||||
|
||||
IEvents IOnefuzzContext.Events => Events;
|
||||
|
||||
public IServiceConfig ServiceConfiguration { get; }
|
||||
|
||||
public IStorage Storage { get; }
|
||||
|
||||
public IRequestHandling RequestHandling { get; }
|
||||
|
||||
public ITaskOperations TaskOperations { get; }
|
||||
public IJobOperations JobOperations { get; }
|
||||
public INodeOperations NodeOperations { get; }
|
||||
public INodeTasksOperations NodeTasksOperations { get; }
|
||||
|
||||
// -- Remainder not implemented --
|
||||
|
||||
public IConfig Config => throw new System.NotImplementedException();
|
||||
|
||||
public IConfigOperations ConfigOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IContainers Containers => throw new System.NotImplementedException();
|
||||
|
||||
public ICreds Creds => throw new System.NotImplementedException();
|
||||
|
||||
public IDiskOperations DiskOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IExtensions Extensions => throw new System.NotImplementedException();
|
||||
|
||||
public IIpOperations IpOperations => throw new System.NotImplementedException();
|
||||
|
||||
|
||||
public ILogAnalytics LogAnalytics => throw new System.NotImplementedException();
|
||||
|
||||
public INodeMessageOperations NodeMessageOperations => throw new System.NotImplementedException();
|
||||
|
||||
public INotificationOperations NotificationOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IPoolOperations PoolOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IProxyForwardOperations ProxyForwardOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IProxyOperations ProxyOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IQueue Queue => throw new System.NotImplementedException();
|
||||
|
||||
public IReports Reports => throw new System.NotImplementedException();
|
||||
|
||||
public IReproOperations ReproOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IScalesetOperations ScalesetOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IScheduler Scheduler => throw new System.NotImplementedException();
|
||||
|
||||
public ISecretsOperations SecretsOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IUserCredentials UserCredentials => throw new System.NotImplementedException();
|
||||
|
||||
public IVmOperations VmOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IVmssOperations VmssOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IWebhookMessageLogOperations WebhookMessageLogOperations => throw new System.NotImplementedException();
|
||||
|
||||
public IWebhookOperations WebhookOperations => throw new System.NotImplementedException();
|
||||
|
||||
}
|
26
src/ApiService/Tests/Fakes/TestEvents.cs
Normal file
26
src/ApiService/Tests/Fakes/TestEvents.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
|
||||
using Async = System.Threading.Tasks;
|
||||
|
||||
namespace Tests.Fakes;
|
||||
|
||||
public sealed class TestEvents : IEvents {
|
||||
|
||||
public List<BaseEvent> Events { get; } = new();
|
||||
public List<EventMessage> SignalREvents { get; } = new();
|
||||
|
||||
public void LogEvent(BaseEvent anEvent) {
|
||||
Events.Add(anEvent);
|
||||
}
|
||||
|
||||
public Async.Task QueueSignalrEvent(EventMessage message) {
|
||||
SignalREvents.Add(message);
|
||||
return Async.Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Async.Task SendEvent(BaseEvent anEvent) {
|
||||
Events.Add(anEvent);
|
||||
return Async.Task.CompletedTask;
|
||||
}
|
||||
}
|
78
src/ApiService/Tests/Fakes/TestHttpRequestData.cs
Normal file
78
src/ApiService/Tests/Fakes/TestHttpRequestData.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using Azure.Core.Serialization;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
|
||||
namespace Tests.Fakes;
|
||||
|
||||
sealed class TestHttpRequestData : HttpRequestData {
|
||||
private static readonly ObjectSerializer Serializer =
|
||||
// we must use our shared JsonSerializerOptions to be able to serialize & deserialize polymorphic types
|
||||
new JsonObjectSerializer(Microsoft.OneFuzz.Service.OneFuzzLib.Orm.EntityConverter.GetJsonSerializerOptions());
|
||||
|
||||
sealed class TestServices : IServiceProvider {
|
||||
sealed class TestOptions : IOptions<WorkerOptions> {
|
||||
// WorkerOptions only has one setting: Serializer
|
||||
public WorkerOptions Value => new() { Serializer = Serializer };
|
||||
}
|
||||
|
||||
static readonly IOptions<WorkerOptions> Options = new TestOptions();
|
||||
|
||||
public object? GetService(Type serviceType) {
|
||||
if (serviceType == typeof(IOptions<WorkerOptions>)) {
|
||||
return Options;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static FunctionContext NewFunctionContext() {
|
||||
// mocking this out at the moment since there’s no way to create a subclass
|
||||
var mock = new Mock<FunctionContext>();
|
||||
var services = new TestServices();
|
||||
mock.SetupGet(fc => fc.InstanceServices).Returns(services);
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
public static TestHttpRequestData FromJson<T>(string method, T obj)
|
||||
=> new(method, Serializer.Serialize(obj));
|
||||
|
||||
public TestHttpRequestData(string method, BinaryData body)
|
||||
: base(NewFunctionContext()) {
|
||||
Method = method;
|
||||
_body = body;
|
||||
}
|
||||
|
||||
private readonly BinaryData _body;
|
||||
|
||||
public override Stream Body => _body.ToStream();
|
||||
|
||||
public override HttpHeadersCollection Headers => throw new NotImplementedException();
|
||||
|
||||
public override IReadOnlyCollection<IHttpCookie> Cookies => throw new NotImplementedException();
|
||||
|
||||
public override Uri Url => throw new NotImplementedException();
|
||||
|
||||
public override IEnumerable<ClaimsIdentity> Identities => throw new NotImplementedException();
|
||||
|
||||
public override string Method { get; }
|
||||
|
||||
public override HttpResponseData CreateResponse()
|
||||
=> new TestHttpResponseData(FunctionContext);
|
||||
}
|
||||
|
||||
sealed class TestHttpResponseData : HttpResponseData {
|
||||
public TestHttpResponseData(FunctionContext functionContext)
|
||||
: base(functionContext) { }
|
||||
public override HttpStatusCode StatusCode { get; set; }
|
||||
public override HttpHeadersCollection Headers { get; set; } = new();
|
||||
public override Stream Body { get; set; } = new MemoryStream();
|
||||
public override HttpCookies Cookies => throw new NotSupportedException();
|
||||
}
|
60
src/ApiService/Tests/Fakes/TestServiceConfiguration.cs
Normal file
60
src/ApiService/Tests/Fakes/TestServiceConfiguration.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
|
||||
namespace Tests.Fakes;
|
||||
|
||||
sealed class TestServiceConfiguration : IServiceConfig {
|
||||
public TestServiceConfiguration(string tablePrefix, string accountId) {
|
||||
OneFuzzTablePrefix = tablePrefix;
|
||||
OneFuzzFuncStorage = accountId;
|
||||
}
|
||||
|
||||
public string OneFuzzTablePrefix { get; }
|
||||
|
||||
public string? OneFuzzFuncStorage { get; }
|
||||
|
||||
// -- Remainder not implemented --
|
||||
|
||||
public LogDestination[] LogDestinations { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
|
||||
|
||||
public SeverityLevel LogSeverityLevel => throw new System.NotImplementedException();
|
||||
|
||||
public string? ApplicationInsightsAppId => throw new System.NotImplementedException();
|
||||
|
||||
public string? ApplicationInsightsInstrumentationKey => throw new System.NotImplementedException();
|
||||
|
||||
public string? AzureSignalRConnectionString => throw new System.NotImplementedException();
|
||||
|
||||
public string? AzureSignalRServiceTransportType => throw new System.NotImplementedException();
|
||||
|
||||
public string? AzureWebJobDisableHomePage => throw new System.NotImplementedException();
|
||||
|
||||
public string? AzureWebJobStorage => throw new System.NotImplementedException();
|
||||
|
||||
public string? DiagnosticsAzureBlobContainerSasUrl => throw new System.NotImplementedException();
|
||||
|
||||
public string? DiagnosticsAzureBlobRetentionDays => throw new System.NotImplementedException();
|
||||
|
||||
public string? MultiTenantDomain => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzDataStorage => throw new System.NotImplementedException();
|
||||
|
||||
|
||||
public string? OneFuzzInstance => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzInstanceName => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzKeyvault => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzMonitor => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzOwner => throw new System.NotImplementedException();
|
||||
|
||||
public string OneFuzzNodeDisposalStrategy => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzResourceGroup => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzTelemetry => throw new System.NotImplementedException();
|
||||
|
||||
public string OneFuzzVersion => throw new System.NotImplementedException();
|
||||
}
|
64
src/ApiService/Tests/Functions/_FunctionTestBase.cs
Normal file
64
src/ApiService/Tests/Functions/_FunctionTestBase.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using ApiService.OneFuzzLib.Orm;
|
||||
using Azure.Data.Tables;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Tests.Fakes;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Tests.Functions;
|
||||
|
||||
// FunctionTestBase contains shared implementations for running
|
||||
// functions against live Azure Storage or the Azurite emulator.
|
||||
//
|
||||
// To use this base class, derive an abstract class from this
|
||||
// with all the tests defined in it. Then, from that class
|
||||
// derive two non-abstract classes for XUnit to find:
|
||||
// - one for Azurite
|
||||
// - one for Azure Storage (marked with [Trait("Category", "Integration")])
|
||||
//
|
||||
// See AgentEventsTests for an example.
|
||||
public abstract class FunctionTestBase : IDisposable {
|
||||
private readonly IStorage _storage;
|
||||
|
||||
// each test will use a different table prefix so they don't interfere
|
||||
// with each other - generate a prefix like t12345678 (table names must start with letter)
|
||||
private readonly string _tablePrefix = "t" + Guid.NewGuid().ToString()[..8];
|
||||
|
||||
private readonly string _accountId;
|
||||
|
||||
protected ILogTracer Logger { get; }
|
||||
|
||||
protected TestContext CreateTestContext() => new(Logger, _storage, _tablePrefix, _accountId);
|
||||
|
||||
public FunctionTestBase(ITestOutputHelper output, IStorage storage, string accountId) {
|
||||
Logger = new TestLogTracer(output);
|
||||
_storage = storage;
|
||||
_accountId = accountId;
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
// TODO, a bit ugly, tidy this up:
|
||||
// delete any tables we created during the run
|
||||
if (_storage is Integration.AzureStorage storage) {
|
||||
var accountName = storage.AccountName;
|
||||
var accountKey = storage.AccountKey;
|
||||
if (accountName is not null && accountKey is not null) {
|
||||
// we are running against live storage
|
||||
var tableClient = new TableServiceClient(
|
||||
_storage.GetTableEndpoint(accountName),
|
||||
new TableSharedKeyCredential(accountName, accountKey));
|
||||
|
||||
var tablesToDelete = tableClient.Query(filter: Query.StartsWith("TableName", _tablePrefix));
|
||||
foreach (var table in tablesToDelete) {
|
||||
try {
|
||||
tableClient.DeleteTable(table.Name);
|
||||
Logger.Info($"cleaned up table {table.Name}");
|
||||
} catch (Exception ex) {
|
||||
// swallow any exceptions: this is a best-effort attempt to cleanup
|
||||
Logger.Exception(ex, "error deleting table at end of test");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
src/ApiService/Tests/Integration/AzureStorage.cs
Normal file
63
src/ApiService/Tests/Integration/AzureStorage.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
|
||||
using Async = System.Threading.Tasks;
|
||||
|
||||
namespace Tests.Integration;
|
||||
|
||||
// This exists solely to allow use of a fixed storage account in integration tests
|
||||
// against live Azure Storage.
|
||||
sealed class AzureStorage : IStorage {
|
||||
public static IStorage FromEnvironment() {
|
||||
var accountName = Environment.GetEnvironmentVariable("AZURE_ACCOUNT_NAME");
|
||||
var accountKey = Environment.GetEnvironmentVariable("AZURE_ACCOUNT_KEY");
|
||||
|
||||
if (accountName is null) {
|
||||
throw new Exception("AZURE_ACCOUNT_NAME must be set in environment to run integration tests");
|
||||
}
|
||||
|
||||
if (accountKey is null) {
|
||||
throw new Exception("AZURE_ACCOUNT_KEY must be set in environment to run integration tests");
|
||||
}
|
||||
|
||||
return new AzureStorage(accountName, accountKey);
|
||||
}
|
||||
|
||||
public string? AccountName { get; }
|
||||
public string? AccountKey { get; }
|
||||
|
||||
public AzureStorage(string? accountName, string? accountKey) {
|
||||
AccountName = accountName;
|
||||
AccountKey = accountKey;
|
||||
}
|
||||
|
||||
public IEnumerable<string> CorpusAccounts() {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAccounts(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public string GetPrimaryAccount(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<(string?, string?)> GetStorageAccountNameAndKey(string accountId)
|
||||
=> Async.Task.FromResult((AccountName, AccountKey));
|
||||
|
||||
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public Uri GetTableEndpoint(string accountId)
|
||||
=> new($"https://{AccountName}.table.core.windows.net/");
|
||||
|
||||
public Uri GetQueueEndpoint(string accountId)
|
||||
=> new($"https://{AccountName}.queue.core.windows.net/");
|
||||
|
||||
public Uri GetBlobEndpoint(string accountId)
|
||||
=> new($"https://{AccountName}.blob.core.windows.net/");
|
||||
}
|
42
src/ApiService/Tests/Integration/AzuriteStorage.cs
Normal file
42
src/ApiService/Tests/Integration/AzuriteStorage.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
|
||||
using Async = System.Threading.Tasks;
|
||||
|
||||
namespace Tests.Integration;
|
||||
|
||||
sealed class AzuriteStorage : IStorage {
|
||||
public Uri GetBlobEndpoint(string accountId)
|
||||
=> new($"http://127.0.0.1:10000/{accountId}");
|
||||
|
||||
public Uri GetQueueEndpoint(string accountId)
|
||||
=> new($"http://127.0.0.1:10001/{accountId}");
|
||||
|
||||
public Uri GetTableEndpoint(string accountId)
|
||||
=> new($"http://127.0.0.1:10002/{accountId}");
|
||||
|
||||
// This is the fixed account key used by Azurite (derived from devstorage emulator);
|
||||
// https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string#configure-a-connection-string-for-azurite
|
||||
const string AccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
|
||||
|
||||
public Task<(string?, string?)> GetStorageAccountNameAndKey(string accountId)
|
||||
=> Async.Task.FromResult<(string?, string?)>((accountId, AccountKey));
|
||||
|
||||
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<string> CorpusAccounts() {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAccounts(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public string GetPrimaryAccount(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using ApiService.OneFuzzLib.Orm;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Xunit;
|
||||
|
||||
@ -34,5 +35,11 @@ namespace Tests {
|
||||
excludeUpdateScheduled: true);
|
||||
Assert.Equal("((pool_id eq '3b0426d3-9bde-4ae8-89ac-4edf0d3b3618')) and ((scaleset_id eq '4c96dd6b-9bdb-4758-9720-1010c244fa4b')) and (((state eq 'free') or (state eq 'done') or (state eq 'ready'))) and (reimage_requested eq false) and (delete_requested eq false) and (not (version eq '1.2.3'))", query7);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartsWithTests() {
|
||||
var query = Query.StartsWith("prop", "prefix");
|
||||
Assert.Equal("prop ge 'prefix' and prop lt 'prefiy'", query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
62
src/ApiService/Tests/TestLogTracer.cs
Normal file
62
src/ApiService/Tests/TestLogTracer.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Tests;
|
||||
|
||||
sealed class TestLogTracer : ILogTracer {
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public TestLogTracer(ITestOutputHelper output)
|
||||
=> _output = output;
|
||||
|
||||
private readonly Dictionary<string, string> _tags = new();
|
||||
public IReadOnlyDictionary<string, string> Tags => _tags;
|
||||
|
||||
public void Critical(string message) {
|
||||
_output.WriteLine($"[Critical] {message}");
|
||||
}
|
||||
|
||||
public void Error(string message) {
|
||||
_output.WriteLine($"[Error] {message}");
|
||||
}
|
||||
|
||||
public void Event(string evt, IReadOnlyDictionary<string, double>? metrics) {
|
||||
// TODO: metrics
|
||||
_output.WriteLine($"[Event] [{evt}]");
|
||||
}
|
||||
|
||||
public void Exception(Exception ex, string message = "", IReadOnlyDictionary<string, double>? metrics = null) {
|
||||
// TODO: metrics
|
||||
_output.WriteLine($"[Error] {message} {ex}");
|
||||
}
|
||||
|
||||
public void ForceFlush() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
public void Info(string message) {
|
||||
_output.WriteLine($"[Info] {message}");
|
||||
}
|
||||
|
||||
public void Verbose(string message) {
|
||||
_output.WriteLine($"[Verbose] {message}");
|
||||
}
|
||||
|
||||
public void Warning(string message) {
|
||||
_output.WriteLine($"[Warning] {message}");
|
||||
}
|
||||
|
||||
public ILogTracer WithHttpStatus((int, string) status) {
|
||||
return this; // TODO?
|
||||
}
|
||||
|
||||
public ILogTracer WithTag(string k, string v) {
|
||||
return this; // TODO?
|
||||
}
|
||||
|
||||
public ILogTracer WithTags(IEnumerable<(string, string)>? tags) {
|
||||
return this; // TODO?
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user