mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-14 11:08:06 +00:00
Implement info
Function in C# (#2061)
Adds implementation of the `info` function. Added support for blobs in the function integration test stuff. Simplified usage of integration test classes by removing the account name parameter.
This commit is contained in:
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -280,10 +280,11 @@ jobs:
|
||||
run: |
|
||||
cd src/ApiService/
|
||||
dotnet format --verify-no-changes --no-restore
|
||||
- name: Build Service
|
||||
run: |
|
||||
cd src/ApiService/
|
||||
dotnet build -warnaserror --configuration Release
|
||||
|
||||
# note that Test comes before Build:
|
||||
# Test will build things in Debug mode, Build will build them in Release
|
||||
# Build comes second in case Test would do something (unspecified) that
|
||||
# would somehow alter the binaries that we want to package.
|
||||
- name: Test & Collect coverage info
|
||||
run: |
|
||||
cd src/ApiService/
|
||||
@ -292,12 +293,30 @@ jobs:
|
||||
dotnet tool run reportgenerator -reports:Tests/TestResults/*/coverage.cobertura.xml -targetdir:coverage -reporttypes:MarkdownSummary
|
||||
kill %1
|
||||
cat coverage/*.md > $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Build Service
|
||||
run: |
|
||||
version=$(./src/ci/get-version.sh)
|
||||
|
||||
cd src/ApiService/
|
||||
|
||||
# Store GitHub RunID and SHA to be read by the 'info' function
|
||||
echo ${GITHUB_RUN_ID} | tee ApiService/onefuzzlib/build.id
|
||||
echo ${GITHUB_SHA} | tee ApiService/onefuzzlib/git.version
|
||||
|
||||
# stamp the build with version
|
||||
# note that version might have a suffix of '-{sha}' from get-version.sh
|
||||
if [[ "$version" =~ '-' ]]; then
|
||||
# if it has a suffix, split it into two parts
|
||||
dotnet build -warnaserror --configuration Release /p:VersionPrefix=${version%-*} /p:VersionSuffix=${version#*-}
|
||||
else
|
||||
dotnet build -warnaserror --configuration Release /p:VersionPrefix=${version}
|
||||
fi
|
||||
|
||||
- name: copy artifacts
|
||||
run: |
|
||||
./src/ci/get-version.sh > src/deployment/VERSION
|
||||
cd src/ApiService/ApiService/
|
||||
echo ${GITHUB_RUN_ID} | tee onefuzzlib/build.id
|
||||
echo ${GITHUB_SHA} | tee onefuzzlib/git.version
|
||||
mv az-local.settings.json bin/Release/net6.0/local.settings.json
|
||||
cd bin/Release/net6.0/
|
||||
zip -r api-service-net.zip .
|
||||
|
@ -44,5 +44,11 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
</None>
|
||||
<EmbeddedResource Include="onefuzzlib/build.id" Condition="Exists('onefuzzlib/build.id')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="onefuzzlib/git.version" Condition="Exists('onefuzzlib/git.version')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
62
src/ApiService/ApiService/Info.cs
Normal file
62
src/ApiService/ApiService/Info.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
|
||||
namespace Microsoft.OneFuzz.Service;
|
||||
|
||||
public class Info {
|
||||
private readonly IOnefuzzContext _context;
|
||||
private readonly IEndpointAuthorization _auth;
|
||||
private readonly Lazy<Async.Task<InfoResponse>> _response;
|
||||
|
||||
public Info(IEndpointAuthorization auth, IOnefuzzContext context) {
|
||||
_context = context;
|
||||
_auth = auth;
|
||||
|
||||
// TODO: this isn’t actually shared between calls at the moment,
|
||||
// this needs to be placed into a class that can be registered into the
|
||||
// DI container and shared amongst instances.
|
||||
//
|
||||
// However, we need to be careful about auth and caching between different
|
||||
// credentials.
|
||||
_response = new Lazy<Async.Task<InfoResponse>>(async () => {
|
||||
var config = _context.ServiceConfiguration;
|
||||
|
||||
var resourceGroup = _context.Creds.GetBaseResourceGroup();
|
||||
var subscription = _context.Creds.GetSubscription();
|
||||
var region = await _context.Creds.GetBaseRegion();
|
||||
|
||||
var asm = Assembly.GetExecutingAssembly();
|
||||
var gitVersion = ReadResource(asm, "ApiService.onefuzzlib.git.version");
|
||||
var buildId = ReadResource(asm, "ApiService.onefuzzlib.build.id");
|
||||
|
||||
return new InfoResponse(
|
||||
ResourceGroup: resourceGroup,
|
||||
Subscription: subscription,
|
||||
Region: region,
|
||||
Versions: new Dictionary<string, InfoVersion> { { "onefuzz", new(gitVersion, buildId, config.OneFuzzVersion) } },
|
||||
InstanceId: await _context.Containers.GetInstanceId(),
|
||||
InsightsAppid: config.ApplicationInsightsAppId,
|
||||
InsightsInstrumentationKey: config.ApplicationInsightsInstrumentationKey);
|
||||
}, LazyThreadSafetyMode.PublicationOnly);
|
||||
}
|
||||
|
||||
private static string ReadResource(Assembly asm, string resourceName) {
|
||||
using var r = asm.GetManifestResourceStream(resourceName);
|
||||
if (r is null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
using var sr = new StreamReader(r);
|
||||
return sr.ReadToEnd();
|
||||
}
|
||||
|
||||
private async Async.Task<HttpResponseData> GetResponse(HttpRequestData req)
|
||||
=> await RequestHandling.Ok(req, await _response.Value);
|
||||
|
||||
// [Function("Info")]
|
||||
public Async.Task<HttpResponseData> Run([HttpTrigger("GET")] HttpRequestData req)
|
||||
=> _auth.CallIfUser(req, GetResponse);
|
||||
}
|
@ -19,6 +19,20 @@ public record BoolResult(
|
||||
bool Result
|
||||
) : BaseResponse();
|
||||
|
||||
public record InfoResponse(
|
||||
string ResourceGroup,
|
||||
string Region,
|
||||
string Subscription,
|
||||
IReadOnlyDictionary<string, InfoVersion> Versions,
|
||||
Guid? InstanceId,
|
||||
string? InsightsAppid,
|
||||
string? InsightsInstrumentationKey
|
||||
) : BaseResponse();
|
||||
|
||||
public record InfoVersion(
|
||||
string Git,
|
||||
string Build,
|
||||
string Version);
|
||||
|
||||
public class BaseResponseConverter : JsonConverter<BaseResponse> {
|
||||
public override BaseResponse? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
|
@ -106,6 +106,7 @@ public class Program {
|
||||
.AddScoped<INodeMessageOperations, NodeMessageOperations>()
|
||||
.AddScoped<IRequestHandling, RequestHandling>()
|
||||
.AddScoped<IOnefuzzContext, OnefuzzContext>()
|
||||
.AddScoped<IEndpointAuthorization, EndpointAuthorization>()
|
||||
|
||||
.AddSingleton<ICreds, Creds>()
|
||||
.AddSingleton<IServiceConfig, ServiceConfiguration>()
|
||||
|
@ -39,10 +39,10 @@ public interface IServiceConfig {
|
||||
|
||||
public string OneFuzzVersion { get; }
|
||||
|
||||
// Prefix to add to the name of any tables created. This allows
|
||||
// Prefix to add to the name of any tables & containers 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 string OneFuzzStoragePrefix { get; }
|
||||
}
|
||||
|
||||
public class ServiceConfiguration : IServiceConfig {
|
||||
@ -87,5 +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
|
||||
public string OneFuzzStoragePrefix => ""; // in production we never prefix the tables
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
using Azure;
|
||||
using System.Threading;
|
||||
using Azure;
|
||||
using Azure.ResourceManager;
|
||||
using Azure.Storage;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Sas;
|
||||
|
||||
|
||||
namespace Microsoft.OneFuzz.Service;
|
||||
|
||||
|
||||
@ -32,12 +32,23 @@ public class Containers : IContainers {
|
||||
private IStorage _storage;
|
||||
private ICreds _creds;
|
||||
private ArmClient _armClient;
|
||||
private readonly IServiceConfig _config;
|
||||
|
||||
public Containers(ILogTracer log, IStorage storage, ICreds creds) {
|
||||
public Containers(ILogTracer log, IStorage storage, ICreds creds, IServiceConfig config) {
|
||||
_log = log;
|
||||
_storage = storage;
|
||||
_creds = creds;
|
||||
_armClient = creds.ArmClient;
|
||||
_config = config;
|
||||
|
||||
_getInstanceId = new Lazy<Async.Task<Guid>>(async () => {
|
||||
var blob = await GetBlob(new Container("base-config"), "instance_id", StorageType.Config);
|
||||
if (blob == null) {
|
||||
throw new Exception("Blob Not Found");
|
||||
}
|
||||
|
||||
return Guid.Parse(blob.ToString());
|
||||
}, LazyThreadSafetyMode.PublicationOnly);
|
||||
}
|
||||
|
||||
public async Async.Task<Uri?> GetFileUrl(Container container, string name, StorageType storageType) {
|
||||
@ -72,9 +83,11 @@ public class Containers : IContainers {
|
||||
// # Secondary accounts, if they exist, are preferred for containers and have
|
||||
// # increased IOP rates, this should be a slight optimization
|
||||
|
||||
var containerName = _config.OneFuzzStoragePrefix + container.ContainerName;
|
||||
|
||||
var containers = _storage.GetAccounts(storageType)
|
||||
.Reverse()
|
||||
.Select(async account => (await GetBlobService(account))?.GetBlobContainerClient(container.ContainerName));
|
||||
.Select(async account => (await GetBlobService(account))?.GetBlobContainerClient(containerName));
|
||||
|
||||
foreach (var c in containers) {
|
||||
var client = await c;
|
||||
@ -86,7 +99,7 @@ public class Containers : IContainers {
|
||||
}
|
||||
|
||||
private async Async.Task<BlobServiceClient?> GetBlobService(string accountId) {
|
||||
_log.Info($"getting blob container (account_id: {accountId}");
|
||||
_log.Info($"getting blob container (account_id: {accountId})");
|
||||
var (accountName, accountKey) = await _storage.GetStorageAccountNameAndKey(accountId);
|
||||
if (accountName == null) {
|
||||
_log.Error("Failed to get storage account name");
|
||||
@ -133,14 +146,8 @@ public class Containers : IContainers {
|
||||
await client.UploadBlobAsync(name, new BinaryData(data));
|
||||
}
|
||||
|
||||
//TODO: get this ones on startup and cache (and make this method un-accessible to everyone else)
|
||||
public async Async.Task<Guid> GetInstanceId() {
|
||||
var blob = await GetBlob(new Container("base-config"), "instance_id", StorageType.Config);
|
||||
if (blob == null) {
|
||||
throw new System.Exception("Blob Not Found");
|
||||
}
|
||||
return System.Guid.Parse(blob.ToString());
|
||||
}
|
||||
public Async.Task<Guid> GetInstanceId() => _getInstanceId.Value;
|
||||
private readonly Lazy<Async.Task<Guid>> _getInstanceId;
|
||||
|
||||
public Uri? GetContainerSasUrlService(
|
||||
BlobContainerClient client,
|
||||
|
@ -3,7 +3,25 @@ using Microsoft.Azure.Functions.Worker.Http;
|
||||
|
||||
namespace Microsoft.OneFuzz.Service;
|
||||
|
||||
public class EndpointAuthorization {
|
||||
public interface IEndpointAuthorization {
|
||||
Async.Task<HttpResponseData> CallIfAgent(
|
||||
HttpRequestData req,
|
||||
Func<HttpRequestData, Async.Task<HttpResponseData>> method)
|
||||
=> CallIf(req, method, allowAgent: true);
|
||||
|
||||
Async.Task<HttpResponseData> CallIfUser(
|
||||
HttpRequestData req,
|
||||
Func<HttpRequestData, Async.Task<HttpResponseData>> method)
|
||||
=> CallIf(req, method, allowUser: true);
|
||||
|
||||
Async.Task<HttpResponseData> CallIf(
|
||||
HttpRequestData req,
|
||||
Func<HttpRequestData, Async.Task<HttpResponseData>> method,
|
||||
bool allowUser = false,
|
||||
bool allowAgent = false);
|
||||
}
|
||||
|
||||
public class EndpointAuthorization : IEndpointAuthorization {
|
||||
private readonly IOnefuzzContext _context;
|
||||
private readonly ILogTracer _log;
|
||||
|
||||
@ -11,9 +29,6 @@ public class EndpointAuthorization {
|
||||
_context = context;
|
||||
_log = log;
|
||||
}
|
||||
public async Async.Task<HttpResponseData> CallIfAgent(HttpRequestData req, Func<HttpRequestData, Async.Task<HttpResponseData>> method) {
|
||||
return await CallIf(req, method, allowAgent: true);
|
||||
}
|
||||
|
||||
public async Async.Task<HttpResponseData> CallIf(HttpRequestData req, Func<HttpRequestData, Async.Task<HttpResponseData>> method, bool allowUser = false, bool allowAgent = false) {
|
||||
var tokenResult = await _context.UserCredentials.ParseJwtToken(req);
|
||||
|
@ -93,7 +93,7 @@ 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 tableName = _context.ServiceConfiguration.OneFuzzStoragePrefix + table;
|
||||
|
||||
var account = accountId ?? _context.ServiceConfiguration.OneFuzzFuncStorage ?? throw new ArgumentNullException(nameof(accountId));
|
||||
var (name, key) = await _context.Storage.GetStorageAccountNameAndKey(account);
|
||||
|
@ -10,10 +10,12 @@ 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);
|
||||
public TestContext(ILogTracer logTracer, IStorage storage, ICreds creds, string storagePrefix) {
|
||||
ServiceConfiguration = new TestServiceConfiguration(storagePrefix);
|
||||
|
||||
Storage = storage;
|
||||
Creds = creds;
|
||||
Containers = new Containers(logTracer, Storage, Creds, ServiceConfiguration);
|
||||
|
||||
RequestHandling = new RequestHandling(logTracer);
|
||||
TaskOperations = new TaskOperations(logTracer, this);
|
||||
@ -44,6 +46,8 @@ public sealed class TestContext : IOnefuzzContext {
|
||||
public IServiceConfig ServiceConfiguration { get; }
|
||||
|
||||
public IStorage Storage { get; }
|
||||
public ICreds Creds { get; }
|
||||
public IContainers Containers { get; }
|
||||
|
||||
public IRequestHandling RequestHandling { get; }
|
||||
|
||||
@ -60,10 +64,6 @@ public sealed class TestContext : IOnefuzzContext {
|
||||
|
||||
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();
|
||||
|
56
src/ApiService/Tests/Fakes/TestCreds.cs
Normal file
56
src/ApiService/Tests/Fakes/TestCreds.cs
Normal file
@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Core;
|
||||
using Azure.Identity;
|
||||
using Azure.ResourceManager;
|
||||
using Azure.ResourceManager.Resources;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Tests.Fakes;
|
||||
|
||||
class TestCreds : ICreds {
|
||||
|
||||
private readonly Guid _subscriptionId;
|
||||
private readonly string _resourceGroup;
|
||||
private readonly string _region;
|
||||
|
||||
public TestCreds(Guid subscriptionId, string resourceGroup, string region) {
|
||||
_subscriptionId = subscriptionId;
|
||||
_resourceGroup = resourceGroup;
|
||||
_region = region;
|
||||
}
|
||||
|
||||
public ArmClient ArmClient => null!;
|
||||
// we have to return something in some test cases, even if it isn’t used
|
||||
|
||||
public Task<string> GetBaseRegion() => Task.FromResult(_region);
|
||||
|
||||
public string GetBaseResourceGroup() => _resourceGroup;
|
||||
|
||||
public string GetSubscription() => _subscriptionId.ToString();
|
||||
|
||||
public DefaultAzureCredential GetIdentity() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public string GetInstanceName() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Uri GetInstanceUrl() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ResourceGroupResource GetResourceGroupResource() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ResourceIdentifier GetResourceGroupResourceIdentifier() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Guid GetScalesetPrincipalId() {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
44
src/ApiService/Tests/Fakes/TestEndpointAuthorization.cs
Normal file
44
src/ApiService/Tests/Fakes/TestEndpointAuthorization.cs
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
|
||||
enum RequestType {
|
||||
NoAuthorization,
|
||||
User,
|
||||
Agent,
|
||||
}
|
||||
|
||||
sealed class TestEndpointAuthorization : IEndpointAuthorization {
|
||||
private readonly RequestType _type;
|
||||
private readonly IOnefuzzContext _context;
|
||||
|
||||
public TestEndpointAuthorization(RequestType type, IOnefuzzContext context) {
|
||||
_type = type;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<HttpResponseData> CallIf(
|
||||
HttpRequestData req,
|
||||
Func<HttpRequestData, Task<HttpResponseData>> method,
|
||||
bool allowUser = false,
|
||||
bool allowAgent = false) {
|
||||
|
||||
if ((_type == RequestType.User && allowUser) ||
|
||||
(_type == RequestType.Agent && allowAgent)) {
|
||||
return method(req);
|
||||
}
|
||||
|
||||
return _context.RequestHandling.NotOk(
|
||||
req,
|
||||
new Error(
|
||||
ErrorCode.UNAUTHORIZED,
|
||||
new string[] { "Unrecognized agent" }
|
||||
),
|
||||
"token verification",
|
||||
HttpStatusCode.Unauthorized
|
||||
);
|
||||
}
|
||||
}
|
@ -44,6 +44,9 @@ sealed class TestHttpRequestData : HttpRequestData {
|
||||
public static TestHttpRequestData FromJson<T>(string method, T obj)
|
||||
=> new(method, Serializer.Serialize(obj));
|
||||
|
||||
public static TestHttpRequestData Empty(string method)
|
||||
=> new(method, new BinaryData(Array.Empty<byte>()));
|
||||
|
||||
public TestHttpRequestData(string method, BinaryData body)
|
||||
: base(NewFunctionContext()) {
|
||||
Method = method;
|
||||
|
@ -1,29 +1,30 @@
|
||||
using Microsoft.ApplicationInsights.DataContracts;
|
||||
using System;
|
||||
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 sealed class TestServiceConfiguration : IServiceConfig {
|
||||
public TestServiceConfiguration(string tablePrefix) {
|
||||
OneFuzzStoragePrefix = tablePrefix;
|
||||
}
|
||||
|
||||
public string OneFuzzTablePrefix { get; }
|
||||
public string OneFuzzStoragePrefix { get; }
|
||||
|
||||
public string? OneFuzzFuncStorage { get; }
|
||||
public string? OneFuzzFuncStorage { get; } = "UNUSED_ACCOUNT_ID"; // test implementations do not use this
|
||||
|
||||
public string OneFuzzVersion => "9999.0.0"; // very big version to pass any >= checks
|
||||
|
||||
public string? ApplicationInsightsAppId { get; set; } = "TestAppInsightsAppId";
|
||||
|
||||
public string? ApplicationInsightsInstrumentationKey { get; set; } = "TestAppInsightsInstrumentationKey";
|
||||
|
||||
// -- 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();
|
||||
|
||||
@ -39,8 +40,6 @@ sealed class TestServiceConfiguration : IServiceConfig {
|
||||
|
||||
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();
|
||||
@ -53,7 +52,9 @@ sealed class TestServiceConfiguration : IServiceConfig {
|
||||
|
||||
public string OneFuzzNodeDisposalStrategy => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzResourceGroup => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzTelemetry => throw new System.NotImplementedException();
|
||||
|
||||
public string? OneFuzzDataStorage => throw new NotImplementedException();
|
||||
|
||||
public string? OneFuzzResourceGroup => throw new NotImplementedException();
|
||||
}
|
||||
|
@ -13,17 +13,17 @@ namespace Tests.Functions;
|
||||
[Trait("Category", "Integration")]
|
||||
public class AzureStorageAgentEventsTest : AgentEventsTestsBase {
|
||||
public AzureStorageAgentEventsTest(ITestOutputHelper output)
|
||||
: base(output, Integration.AzureStorage.FromEnvironment(), "UNUSED") { }
|
||||
: base(output, Integration.AzureStorage.FromEnvironment()) { }
|
||||
}
|
||||
|
||||
public class AzuriteAgentEventsTest : AgentEventsTestsBase {
|
||||
public AzuriteAgentEventsTest(ITestOutputHelper output)
|
||||
: base(output, new Integration.AzuriteStorage(), "devstoreaccount1") { }
|
||||
: base(output, new Integration.AzuriteStorage()) { }
|
||||
}
|
||||
|
||||
public abstract class AgentEventsTestsBase : FunctionTestBase {
|
||||
public AgentEventsTestsBase(ITestOutputHelper output, IStorage storage, string accountId)
|
||||
: base(output, storage, accountId) { }
|
||||
public AgentEventsTestsBase(ITestOutputHelper output, IStorage storage)
|
||||
: base(output, storage) { }
|
||||
|
||||
// shared helper variables (per-test)
|
||||
readonly Guid jobId = Guid.NewGuid();
|
||||
|
65
src/ApiService/Tests/Functions/InfoTests.cs
Normal file
65
src/ApiService/Tests/Functions/InfoTests.cs
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Tests.Fakes;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
using Async = System.Threading.Tasks;
|
||||
|
||||
namespace Tests.Functions;
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
public class AzureStorageInfoTest : InfoTestBase {
|
||||
public AzureStorageInfoTest(ITestOutputHelper output)
|
||||
: base(output, Integration.AzureStorage.FromEnvironment()) { }
|
||||
}
|
||||
|
||||
public class AzuriteInfoTest : InfoTestBase {
|
||||
public AzuriteInfoTest(ITestOutputHelper output)
|
||||
: base(output, new Integration.AzuriteStorage()) { }
|
||||
}
|
||||
|
||||
public abstract class InfoTestBase : FunctionTestBase {
|
||||
public InfoTestBase(ITestOutputHelper output, IStorage storage)
|
||||
: base(output, storage) { }
|
||||
|
||||
[Fact]
|
||||
public async Async.Task TestInfo_WithoutAuthorization_IsRejected() {
|
||||
var auth = new TestEndpointAuthorization(RequestType.NoAuthorization, Context);
|
||||
var func = new Info(auth, Context);
|
||||
|
||||
var result = await func.Run(TestHttpRequestData.Empty("GET"));
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Async.Task TestInfo_WithAgentCredentials_IsRejected() {
|
||||
var auth = new TestEndpointAuthorization(RequestType.Agent, Context);
|
||||
var func = new Info(auth, Context);
|
||||
|
||||
var result = await func.Run(TestHttpRequestData.Empty("GET"));
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Async.Task TestInfo_WithUserCredentials_Succeeds() {
|
||||
// store the instance ID in the expected location:
|
||||
// for production this is done by the deploy script
|
||||
var instanceId = Guid.NewGuid().ToString();
|
||||
var containerClient = GetContainerClient("base-config");
|
||||
await containerClient.CreateAsync();
|
||||
await containerClient.GetBlobClient("instance_id").UploadAsync(new BinaryData(instanceId));
|
||||
|
||||
var auth = new TestEndpointAuthorization(RequestType.User, Context);
|
||||
var func = new Info(auth, Context);
|
||||
|
||||
var result = await func.Run(TestHttpRequestData.Empty("GET"));
|
||||
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
||||
|
||||
// the instance ID should be somewhere in the result,
|
||||
// indicating it was read from the blob
|
||||
Assert.Contains(instanceId, BodyAsString(result));
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ApiService.OneFuzzLib.Orm;
|
||||
using Azure.Data.Tables;
|
||||
using Azure.Storage;
|
||||
using Azure.Storage.Blobs;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.OneFuzz.Service;
|
||||
using Tests.Fakes;
|
||||
@ -22,19 +25,33 @@ namespace Tests.Functions;
|
||||
public abstract class FunctionTestBase : IDisposable {
|
||||
private readonly IStorage _storage;
|
||||
|
||||
// each test will use a different table prefix so they don't interfere
|
||||
// each test will use a different prefix for storage (tables, blobs) 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 _storagePrefix = "t" + Guid.NewGuid().ToString()[..8];
|
||||
|
||||
private readonly Guid _subscriptionId = Guid.NewGuid();
|
||||
private readonly string _resourceGroup = "FakeResourceGroup";
|
||||
private readonly string _region = "fakeregion";
|
||||
|
||||
protected ILogTracer Logger { get; }
|
||||
|
||||
protected TestContext Context { get; }
|
||||
|
||||
public FunctionTestBase(ITestOutputHelper output, IStorage storage, string accountId) {
|
||||
private readonly BlobServiceClient _blobClient;
|
||||
protected BlobContainerClient GetContainerClient(string container)
|
||||
=> _blobClient.GetBlobContainerClient(_storagePrefix + container);
|
||||
|
||||
public FunctionTestBase(ITestOutputHelper output, IStorage storage) {
|
||||
Logger = new TestLogTracer(output);
|
||||
_storage = storage;
|
||||
|
||||
Context = new TestContext(Logger, _storage, _tablePrefix, accountId);
|
||||
var creds = new TestCreds(_subscriptionId, _resourceGroup, _region);
|
||||
Context = new TestContext(Logger, _storage, creds, _storagePrefix);
|
||||
|
||||
// set up blob client for test purposes:
|
||||
var (accountName, accountKey) = _storage.GetStorageAccountNameAndKey("").Result; // for test impls this is always sync
|
||||
var endpoint = _storage.GetBlobEndpoint("");
|
||||
_blobClient = new BlobServiceClient(endpoint, new StorageSharedKeyCredential(accountName, accountKey));
|
||||
}
|
||||
|
||||
protected static string BodyAsString(HttpResponseData data) {
|
||||
@ -44,18 +61,36 @@ public abstract class FunctionTestBase : IDisposable {
|
||||
}
|
||||
|
||||
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;
|
||||
var (accountName, accountKey) = _storage.GetStorageAccountNameAndKey("").Result; // sync for test impls
|
||||
if (accountName is not null && accountKey is not null) {
|
||||
// we are running against live storage
|
||||
var tableClient = new TableServiceClient(
|
||||
_storage.GetTableEndpoint(accountName),
|
||||
// clean up any tables & blobs that this test created
|
||||
|
||||
CleanupTables(_storage.GetTableEndpoint(accountName),
|
||||
new TableSharedKeyCredential(accountName, accountKey));
|
||||
|
||||
var tablesToDelete = tableClient.Query(filter: Query.StartsWith("TableName", _tablePrefix));
|
||||
CleanupBlobs(_storage.GetBlobEndpoint(accountName),
|
||||
new StorageSharedKeyCredential(accountName, accountKey));
|
||||
}
|
||||
}
|
||||
private void CleanupBlobs(Uri endpoint, StorageSharedKeyCredential creds) {
|
||||
var blobClient = new BlobServiceClient(endpoint, creds);
|
||||
|
||||
var containersToDelete = blobClient.GetBlobContainers(prefix: _storagePrefix);
|
||||
foreach (var container in containersToDelete.Where(c => c.IsDeleted != true)) {
|
||||
try {
|
||||
blobClient.DeleteBlobContainer(container.Name);
|
||||
Logger.Info($"cleaned up container {container.Name}");
|
||||
} catch (Exception ex) {
|
||||
// swallow any exceptions: this is a best-effort attempt to cleanup
|
||||
Logger.Exception(ex, "error deleting container at end of test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupTables(Uri endpoint, TableSharedKeyCredential creds) {
|
||||
var tableClient = new TableServiceClient(endpoint, creds);
|
||||
|
||||
var tablesToDelete = tableClient.Query(filter: Query.StartsWith("TableName", _storagePrefix));
|
||||
foreach (var table in tablesToDelete) {
|
||||
try {
|
||||
tableClient.DeleteTable(table.Name);
|
||||
@ -67,5 +102,3 @@ public abstract class FunctionTestBase : IDisposable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,23 +33,13 @@ sealed class AzureStorage : IStorage {
|
||||
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 IEnumerable<string> GetAccounts(StorageType storageType) {
|
||||
if (AccountName != null) {
|
||||
yield return AccountName;
|
||||
}
|
||||
}
|
||||
|
||||
public Uri GetTableEndpoint(string accountId)
|
||||
@ -60,4 +50,16 @@ sealed class AzureStorage : IStorage {
|
||||
|
||||
public Uri GetBlobEndpoint(string accountId)
|
||||
=> new($"https://{AccountName}.blob.core.windows.net/");
|
||||
|
||||
public IEnumerable<string> CorpusAccounts() {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public string GetPrimaryAccount(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
@ -8,21 +8,26 @@ 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 GetBlobEndpoint(string _accountId)
|
||||
=> new($"http://127.0.0.1:10000/devstoreaccount1");
|
||||
|
||||
public Uri GetQueueEndpoint(string accountId)
|
||||
=> new($"http://127.0.0.1:10001/{accountId}");
|
||||
public Uri GetQueueEndpoint(string _accountId)
|
||||
=> new($"http://127.0.0.1:10001/devstoreaccount1");
|
||||
|
||||
public Uri GetTableEndpoint(string accountId)
|
||||
=> new($"http://127.0.0.1:10002/{accountId}");
|
||||
public Uri GetTableEndpoint(string _accountId)
|
||||
=> new($"http://127.0.0.1:10002/devstoreaccount1");
|
||||
|
||||
// This is the fixed account key used by Azurite (derived from devstorage emulator);
|
||||
// This is the fixed name & 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 AccountName = "devstoreaccount1";
|
||||
const string AccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
|
||||
|
||||
public Task<(string?, string?)> GetStorageAccountNameAndKey(string accountId)
|
||||
=> Async.Task.FromResult<(string?, string?)>((accountId, AccountKey));
|
||||
public Task<(string?, string?)> GetStorageAccountNameAndKey(string _accountId)
|
||||
=> Async.Task.FromResult<(string?, string?)>((AccountName, AccountKey));
|
||||
|
||||
public IEnumerable<string> GetAccounts(StorageType storageType) {
|
||||
yield return AccountName;
|
||||
}
|
||||
|
||||
public Task<string?> GetStorageAccountNameAndKeyByName(string accountName) {
|
||||
throw new System.NotImplementedException();
|
||||
@ -32,10 +37,6 @@ sealed class AzuriteStorage : IStorage {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAccounts(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public string GetPrimaryAccount(StorageType storageType) {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
Reference in New Issue
Block a user